diff --git a/.github/ISSUE_TEMPLATES/feature.md b/.github/ISSUE_TEMPLATES/feature.md new file mode 100644 index 0000000000..3cc5678c46 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/feature.md @@ -0,0 +1 @@ +Please describe the feature need! \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 366972c165..062bf71d89 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@master with: - dotnet-version: 3.1.102 + dotnet-version: 5.0.100-rc.2.20479.15 - name: Build All run: .\build-all.ps1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..ae5d4fdacc --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [dev, rel-1.0, rel-1.1, rel-2.0, rel-2.1, rel-2.2, rel-2.3, rel-2.4, rel-2.5, rel-2.6, rel-2.7, rel-2.8, rel-2.9, rel-3.0, rel-3.1, rel-3.2] + pull_request: + # The branches below must be a subset of the branches above + branches: [dev] + schedule: + - cron: '0 8 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['csharp', 'javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index 1f14485237..5b8dfe9c11 100644 --- a/.gitignore +++ b/.gitignore @@ -303,4 +303,8 @@ modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/Logs/ /templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json /templates/app/angular/package-lock.json /modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json +abp-build-config.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/Pages/Test/ +# Identity Server temp signature file +tempkey.jwk \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..a0572030a2 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json index adc808ed7e..0d66f1288d 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json @@ -8,6 +8,7 @@ "FrameworkDocumentation": "框架文档", "OfficialBlog": "官方博客", "CommercialHomePage": "商业版首页", - "CommercialSupportWebSite": "商业版支持网站" + "CommercialSupportWebSite": "商业版支持网站", + "CommunityWebSite": "ABP社区网站" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ar.json index 96661aebeb..871e994106 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ar.json @@ -41,6 +41,16 @@ "ApplicationType": "نوع التطبيق", "Target": "استهداف", "TargetFilter": "استهداف", + "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": "EntityFramework Core", + "NugetPackageTarget.MongoDB": "قاعدة بيانات مونجو", "Edit": "تعديل", "Delete": "حذف", "Refresh": "تحديث", @@ -54,8 +64,8 @@ "CreateAModule": "إنشاء وحدة", "CreateANugetPackage": "أنشاء حزمة Nuget", "AddNew": "اضف جديد", - "PackageAlreadyExist{0}": "", - "ModuleAlreadyExist{0}": "", + "PackageAlreadyExist{0}": "حزمة \"{0}\" تمت إضافتها من قبل.", + "ModuleAlreadyExist{0}": "وحدة \"{0}\" مضافة بالفعل.", "ClearCache": "مسح ذاكرة التخزين المؤقت", "SuccessfullyCleared": "تم محوه بنجاح", "Menu:NpmPackages": "حزم NPM", @@ -93,7 +103,7 @@ "MasterModules": "الوحدات الرئيسية", "OrganizationName": "اسم المنظمة", "CreationDate": "تاريخ الإنشاء", - "LicenseStart": "تاريخ بدء الرخصة", + "LicenseStartDate": "تاريخ بدء الترخيص", "LicenseEndDate": "تاريخ إنتهاء الرخصة", "OrganizationNamePlaceholder": "اسم المنظمة ...", "TotalQuestionCountPlaceholder": "إجمالي عدد الأسئلة ...", @@ -156,6 +166,31 @@ "TotalQuestionMustBeGreaterWarningMessage": "يجب أن يكون TotalQuestionCount أكبر من RemainingQuestionCount!", "QuestionCountsMustBeGreaterThanZero": "يجب أن يكون TotalQuestionCount و RemainingQuestionCount صفرًا أو أكبر من الصفر!", "UnlimitedQuestionCount": "عدد أسئلة غير محدود", - "Notes": "ملاحظات" + "Notes": "ملاحظات", + "Menu:Community": "المجتمع", + "Menu:Articles": "مقالات", + "Wait": "انتظر", + "Approve": "موافقة", + "Reject": "رفض", + "Details": "تفاصيل", + "Url": "عنوان الموقع", + "Title": "عنوان", + "ContentSource": "مصدر المحتوى", + "Status": "الحالة", + "ReadArticle": "إقرأ المقال", + "ArticleHasBeenWaiting": "تم إيقاف المقال", + "ArticleHasBeenApproved": "تم الموافقة على المقال", + "ArticleHasBeenRejected": "تم رفض المقال", + "Permission:Community": "المجتمع", + "Permission:CommunityArticle": "مقال", + "Link": "رابط", + "Enum:ContentSource:0": "Github", + "Enum:ContentSource:1": "خارجى", + "Enum:Status:0": "انتظار", + "Enum:Status:1": "مرفوض", + "Enum:Status:2": "تمت الموافقة", + "Summary": "مُلخص", + "AuthorName": "اسم المؤلف", + "CoverImage": "صورة الغلاف" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/zh-Hans.json index bc78a54119..f5cbeb0213 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/zh-Hans.json @@ -3,6 +3,12 @@ "texts": { "Permission:Organizations": "组织", "Permission:Manage": "管理组织", + "Permission:DiscountRequests": "折扣请求", + "Permission:DiscountManage": "管理折扣请求", + "Permission:Disable": "禁用", + "Permission:Enable": "启动", + "Permission:EnableSendEmail": "启用发送邮件", + "Permission:SendEmail": "发送邮件", "Permission:NpmPackages": "NPM包", "Permission:NugetPackages": "Nuget包", "Permission:Maintenance": "维护", @@ -13,8 +19,13 @@ "Permission:Edit": "编辑", "Permission:Delete": "删除", "Permission:Create": "创建", + "Permission:Accounting": "账单", + "Permission:Accounting:Quotation": "报价", + "Permission:Accounting:Invoice": "发票", "Menu:Organizations": "组织", + "Menu:Accounting": "账单", "Menu:Packages": "包", + "Menu:DiscountRequests": "折扣请求", "NpmPackageDeletionWarningMessage": "该NPM包将会被删除. 你确定吗?", "NugetPackageDeletionWarningMessage": "该Nuget包将会被删除. 你确定吗?", "ModuleDeletionWarningMessage": "该模块将会被删除. 你确定吗?", @@ -24,6 +35,7 @@ "NameFilter": "名称", "CreationTime": "创建时间", "IsPro": "是否为专业版", + "ShowOnModuleList": "展示模块列表", "EfCoreConfigureMethodName": "配置方法法", "IsProFilter": "是否为专业版", "ApplicationType": "应用程序类型", @@ -53,6 +65,7 @@ "CreateANugetPackage": "创建Nuget包", "AddNew": "新建", "PackageAlreadyExist{0}": "\"{0}\"已经被添加.", + "ModuleAlreadyExist{0}": "\"{0}\" 模块已经添加.", "ClearCache": "清除缓存", "SuccessfullyCleared": "清除成功", "Menu:NpmPackages": "NPM包", @@ -63,6 +76,7 @@ "Organizations": "组织", "LongName": "完整名称", "LicenseType": "授权类型", + "MissingLicenseTypeField": "许可证类型字段是必需的!", "LicenseStartTime": "授权开始时间", "LicenseEndTime": "授权结束时间", "AllowedDeveloperCount": "允许的开发人员数量", @@ -74,14 +88,112 @@ "AddDeveloper": "添加开发者", "Create": "创建", "UserNotFound": "用户不存在", - "{0}WillBeRemovedFromMembers": "{0} 将从成员中删除", + "{0}WillBeRemovedFromDevelopers": "{0} 将从开发者中移除, 你确定吗?", + "{0}WillBeRemovedFromOwners": "{0} 将从所有者中移除, 你确定吗?", "Computers": "计算机", "UniqueComputerId": "计算机唯一ID", "LastSeenDate": "上次查看日期", "{0}Computer{1}WillBeRemovedFromRecords": "计算机 {0} ({1}) 将从记录中删除", "OrganizationDeletionWarningMessage": "组织将被删除", + "DeletingLastOwnerWarningMessage": "一个组织必须至少有一个所有者!因此你不能删除此所有者", "This{0}AlreadyExistInThisOrganization": "该组织中已经存在此 {0}", "AreYouSureYouWantToDeleteAllComputers": "您确定要删除所有计算机吗?", - "DeleteAll": "删除所有" + "DeleteAll": "删除所有", + "DoYouWantToCreateNewUser": "你想要创建一个新用户吗", + "MasterModules": "主模块", + "OrganizationName": "组织名称", + "CreationDate": "创建时间", + "LicenseStartDate": "许可开始时间", + "LicenseEndDate": "许可结束时间", + "OrganizationNamePlaceholder": "组织名称...", + "TotalQuestionCountPlaceholder": "问题总数...", + "RemainingQuestionCountPlaceholder": "剩下的问题数...", + "LicenseTypePlaceholder": "许可类型...", + "CreationDatePlaceholder": "创建时间...", + "LicenseStartDatePlaceholder": "许可开始时间...", + "LicenseEndDatePlaceholder": "许可结束时间...", + "UsernameOrEmail": "用户名或邮箱", + "UsernameOrEmailPlaceholder": "用户名或邮箱...", + "Member": "成员", + "PurchaseOrderNo": "订单编号", + "QuotationDate": "报价日期", + "CompanyName": "公司名称", + "CompanyAddress": "公司地址", + "Price": "价格", + "DiscountText": "折扣文字", + "DiscountQuantity": "折扣数量", + "DiscountPrice": "折扣价格", + "Quotation": "报价单", + "ExtraText": "额外文字", + "ExtraAmount": "额外金额", + "DownloadQuotation": "下载报价单", + "Invoice": "发票", + "TaxNumber": "税号", + "InvoiceNumber": "发票数字", + "InvoiceDate": "发票日期", + "InvoiceNote": "发票说明", + "Quantity": "数量", + "AddProduct": "添加产品", + "AddProductWarning": "你需要添加产品", + "TotalPrice": "总价", + "Generate": "生成", + "MissingQuantityField": "数量字段为必填项!", + "MissingPriceField": "价格字段为必填项!", + "CodeUsageStatus": "状态", + "Country": "国家", + "DeveloperCount": "开发者数量", + "RequestCode": "请求码", + "WebSite": "网站", + "GithubUsername": "Github用户名", + "PhoneNumber": "手机号", + "ProjectDescription": "产品描述", + "Referrer": "推荐人", + "DiscountRequests": "折扣请求", + "Copylink": "拷贝链接", + "Disable": "禁用", + "Enable": "启用", + "EnableSendEmail": "启用发送邮件", + "SendEmail": "发送邮件", + "SuccessfullyDisabled": "禁用成功", + "SuccessfullyEnabled": "启用成功", + "EmailSent": "邮件发送", + "SuccessfullySent": "发送成功", + "SuccessfullyDeleted": "删除成功", + "DiscountRequestDeletionWarningMessage": "折扣成功将被删除", + "BusinessType": "商业类型", + "TotalQuestionCount": "问题总数", + "RemainingQuestionCount": "剩余问题数", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount必须大于RemainingQuestionCount!", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount和RemainingQuestionCount必须为0或大于0", + "UnlimitedQuestionCount": "无限的问题计数", + "Notes": "笔记", + "Menu:Community": "社区", + "Menu:Articles": "文章", + "Wait": "等待", + "Approve": "批准", + "Reject": "拒绝", + "Details": "详情", + "Url": "Url", + "Title": "标题", + "ContentSource": "内容源", + "Status": "状态", + "ReadArticle": "阅读文章", + "ArticleHasBeenWaiting": "文章还在等待", + "ArticleHasBeenApproved": "文章已被批准", + "ArticleHasBeenRejected": "文章已被拒绝", + "Permission:Community": "社区", + "Permission:CommunityArticle": "文字", + "Link": "链接", + "Enum:ContentSource:0": "Github", + "Enum:ContentSource:1": "外部", + "Enum:Status:0": "等待中", + "Enum:Status:1": "已拒绝", + "Enum:Status:2": "已批准", + "Summary": "概要", + "AuthorName": "作者名称", + "CoverImage": "封面图片", + "RemoveCacheConfirmationMessage": "你确定要删除\"{0}\" 文章缓存?", + "SuccessfullyRemoved": "清除成功", + "RemoveCache": "删除缓存" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json new file mode 100644 index 0000000000..b780988f7d --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json @@ -0,0 +1,31 @@ +{ + "culture": "ar", + "texts": { + "Volo.AbpIo.Domain:010004": "تم الوصول إلى الحد الأقصى لعدد الأعضاء!", + "Volo.AbpIo.Domain:010005": "تم الوصول إلى الحد الأقصى لعدد المالكين!", + "Volo.AbpIo.Domain:010006": "هذا المستخدم هو بالفعل مالك في هذه المنظمة!", + "Volo.AbpIo.Domain:010007": "هذا المستخدم هو بالفعل مطور في هذه المنظمة!", + "Volo.AbpIo.Domain:010008": "لا يمكن أن يكون عدد المطورين المسموح لهم أقل من عدد المطورين الحاليين!", + "Volo.AbpIo.Domain:010009": "لا يمكن أن يكون عدد المطورين المسموح لهم أقل من 0!", + "Volo.AbpIo.Domain:010010": "تم تجاوز الحد الأقصى لعدد عناوين mac!", + "Volo.AbpIo.Domain:010011": "لا يمكن أن تحتوي الرخصة الشخصية على أكثر من مطور واحد!", + "Volo.AbpIo.Domain:010012": "لا يمكن تمديد الترخيص بعد شهر واحد من انتهاء صلاحية الترخيص!", + "Volo.AbpIo.Domain:020001": "تعذر حذف حزمة NPM هذه لأن \"{NugetPackages}\" حزم Nuget تعتمد على هذه الحزمة.", + "Volo.AbpIo.Domain:020002": "تعذر حذف حزمة NPM هذه لأن \"{Modules}\" الوحدات النمطية تستخدم هذه الحزمة.", + "Volo.AbpIo.Domain:020003": "تعذر حذف حزمة NPM هذه لأن \"{Modules}\" الوحدات النمطية تستخدم هذه الحزمة و \"{NugetPackages}\" حزم Nuget تعتمد على هذه الحزمة.", + "Volo.AbpIo.Domain:020004": "تعذر حذف حزمة Nuget هذه لأن \"{Modules}\" الوحدات النمطية تستخدم هذه الحزمة.", + "WantToLearn?": "تريد أن تتعلم؟", + "ReadyToGetStarted?": "على استعداد للبدء؟", + "JoinOurCommunity": "انضم إلى مجتمعنا", + "GetStartedUpper": "دعنا نبدأ", + "ForkMeOnGitHub": "GitHub أعطنى تنبيه على", + "Features": "خصائص", + "GetStarted": "دعنا نبدأ", + "Documents": "وثائق", + "Community": "مجتمع", + "ContributionGuide": "دليل المساهمة", + "Blog": "المدونة", + "Commercial": "تجارى", + "SeeDocuments": "انظر للوثائق" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index d6f334f7cd..a916cd82d9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -27,6 +27,7 @@ "Blog": "Blog", "Commercial": "Commercial", "MyAccount": "My account", - "SeeDocuments": "See Documents" + "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 986d1fdc2c..79493b537e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json @@ -26,6 +26,8 @@ "ContributionGuide": "贡献指南", "Blog": "博客", "Commercial": "商业版", - "SeeDocuments": "查看文档" + "MyAccount": "我的账户", + "SeeDocuments": "查看文档", + "Samples": "示例" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json new file mode 100644 index 0000000000..7219cde546 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json @@ -0,0 +1,35 @@ +{ + "culture": "ar", + "texts": { + "OrganizationManagement": "إدارة المنظمة", + "OrganizationList": "قائمة المنظمات", + "Volo.AbpIo.Commercial:010003": "لست مالك هذه المنظمة!", + "OrganizationNotFoundMessage": "لا توجد أى منظمة!", + "DeveloperCount": "المحجوز / إجمالى المطورين", + "QuestionCount": "المتبقى / إجمالى الأسئلة", + "Unlimited": "غير محدود", + "Owners": "مُلاّك", + "AddMember": "تسجيل عضو", + "AddOwner": "تسجيل مالك", + "AddDeveloper": "تسجيل مطور", + "UserName": "اسم المستخدم", + "Name": "الاسم", + "EmailAddress": "البريد الإلكترونى", + "Developers": "مطورون", + "LicenseType": "نوع الرخصة", + "Manage": "إدارة", + "StartDate": "تاريخ البداية", + "EndDate": "تاريخ الإنتهاء", + "Modules": "Modules", + "LicenseExtendMessage": "تم تمديد تاريخ انتهاء الترخيص الخاص بك إلى {0}", + "LicenseUpgradeMessage": "تمت ترقية ترخيصك إلى {0}", + "LicenseAddDeveloperMessage": "تمت إضافة {0} من المطورين إلى ترخيصك", + "Volo.AbpIo.Commercial:010004": "لا يمكن العثور على المستخدم المحدد! يجب أن يكون المستخدم قد سجل بالفعل.", + "MyOrganizations": "منظماتى", + "ApiKey": "مفتاح API", + "UserNameNotFound": "لا يوجد مستخدم باسم {0}", + "SuccessfullyAddedToNewsletter": "شكرا لك على الاشتراك في النشرة الإخبارية لدينا!", + "MyProfile": "ملفي", + "EmailNotValid": "من فضلك أدخل بريد أليكترونى صحيح." + } +} \ 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 e80457377f..0147e71a05 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json @@ -6,6 +6,8 @@ "Volo.AbpIo.Commercial:010003": "你不是该组织的所有者!", "OrganizationNotFoundMessage": "找不到任何组织!", "DeveloperCount": "开发者数量", + "QuestionCount": "剩余 / 问题总数", + "Unlimited": "无限制", "Owners": "所有者", "AddMember": "添加成员", "AddOwner": "添加所有者", @@ -25,6 +27,9 @@ "Volo.AbpIo.Commercial:010004": "不能找到指定的用户! 用户必须已经注册.", "MyOrganizations": "我的组织", "ApiKey": "API key", - "UserNameNotFound": "没有用户名为{0}的用户" + "UserNameNotFound": "没有用户名为{0}的用户", + "SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻通讯!", + "MyProfile": "我的资料", + "EmailNotValid": "请输入有效的电子邮件地址" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json new file mode 100644 index 0000000000..7f28003917 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json @@ -0,0 +1,68 @@ +{ + "culture": "ar", + "texts": { + "Permission:CommunityArticle": "مقالة المجتمع", + "Permission:Edit": "تعديل", + "Waiting": "انتظار", + "Approved": "تمت الموافقة", + "Rejected": "مرفوض", + "Wait": "انتظر", + "Approve": "موافق", + "Reject": "رفض", + "ReadArticle": "إقرأ المقال", + "Status": "الحالة", + "ContentSource": "مصدر المحتوى", + "Details": "التفاصيل", + "Url": "عنوان الموقع", + "Title": "العنوان", + "CreationTime": "وقت الإنشاء", + "Save": "حفظ", + "SameUrlAlreadyExist": "نفس عنوان الموقع موجود بالفعل إذا كنت تريد إضافة هذه المقالة ، يجب عليك تغيير عنوانها!", + "UrlIsNotValid": "عنوان الموقع غير صحيح.", + "UrlNotFound": "عنوان الموقع غير موجود.", + "UrlContentNotFound": "محتوى الموقع غير موجود.", + "Summary": "الخلاصة", + "MostRead": "الأكثر قراءة", + "Latest": "آخر", + "ContributeAbpCommunity": "المساهمة في مجتمع ABP", + "SubmitYourArticle": "إرسال مقالتك", + "ContributionGuide": "دليل المساهمة", + "BugReport": "الإبلاغ عن خطأ", + "SeeAllArticles": "انظر جميع المقالات", + "WelcomeToABPCommunity!": "مرحبًا بك في مجتمع ABP!", + "MyProfile": "ملفى", + "MyOrganizations": "منظماتي", + "EmailNotValid": "من فضلك أدخل بريد أليكترونى صحيح.", + "FeatureRequest": "طلب خاصية جديدة", + "CreateArticleTitleInfo": "عنوان المقال الذي سيظهر في قائمة المقالات.", + "CreateArticleUrlInfo": "الأصل GitHub / عنوان URL خارجي للمقالة.", + "CreateArticleSummaryInfo": "ملخص قصير للمقال ليتم عرضه في قائمة المقالات.", + "CreateArticleCoverInfo": "لإنشاء مقال فعال ، أضف صورة غلاف. لن يتم قبول سوى صور بنسبة عرض إلى ارتفاع تبلغ 9:16!", + "ThisExtensionIsNotAllowed": "امتداد الملف غير مسموح.", + "TheFileIsTooLarge": "مسار الملف طويل جداً", + "GoToTheArticle": "الذهاب للمقال", + "Contribute": "ساهم", + "OverallProgress": "التقدم الكلى", + "Done": "تم", + "Open": "افتح", + "Closed": "أُغلق", + "LatestQuestionOnThe": "آخر سؤال فى", + "Stackoverflow": "Stackoverflow", + "Votes": "تصويتات", + "Answer": "إجابة", + "Views": "مشاهدة", + "Answered": "تمت الإجابة عنه", + "WaitingForYourAnswer": "بانتظار إجابتك", + "Asked": "تم السؤال", + "AllQuestions": "كل الأسئلة", + "NextVersion": "الإصدار القادم", + "MilestoneErrorMessage": "تعذر الحصول على تفاصيل المعالم الحالية من Github.", + "QuestionItemErrorMessage": "تعذر الحصول على أحدث تفاصيل السؤال من Stackoverflow.", + "Oops": "عفواً!", + "CreateArticleSuccessMessage": "تم تقديم المقال بنجاح. سيتم نشره بعد مراجعة من إدارة الموقع.", + "ChooseCoverImage": "اختر صورة الغلاف...", + "CoverImage": "صورة الغلاف", + "ShareYourExperiencesWithTheABPFramework": "شارك خبراتك مع إطار عمل برنامج ABP!", + "Optional": "اختيارى" + } +} \ 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 286346a5b5..87db8c931a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -82,6 +82,9 @@ "SeeAllAnnouncements": "See All Announcements", "LatestBlogPost": "Latest Blog Post", "Edit": "Edit", - "ProfileImageChange": "Change the profile image" + "ProfileImageChange": "Change the profile image", + "BlogItemErrorMessage": "Could not get the latest blog post details from ABP.", + "PlannedReleaseDate": "Planned release date", + "CommunityArticleRequestErrorMessage": "Could not get the latest article request from Github." } } 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 307ff066a7..fa793c91a0 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json @@ -1,64 +1,89 @@ { - "culture": "zh-Hans", - "texts": { - "Permission:CommunityArticle": "社区文章", - "Permission:Edit": "修改", - "Waiting": "等待中", - "Approved": "已批准", - "Rejected": "已拒绝", - "Wait": "等待", - "Approve": "批准", - "Reject": "拒绝", - "ReadArticle": "阅读文章", - "Status": "状态", - "ContentSource": "内容来源", - "Details": "详情", - "Url": "url", - "Title": "标题", - "CreationTime": "创建时间", - "Save": "保存", - "SameUrlAlreadyExist": "url已存在,如果你想要添加这篇文章,你需要更改url!", - "UrlIsNotValid": "Url无效.", - "UrlNotFound" : "Url未找到.", - "UrlContentNotFound": "Url内容未找到.", - "Summary": "摘要", - "MostRead": "阅读最多", - "Latest": "最新", - "ContributeAbpCommunity": "为ABP社区做贡献", - "SubmitYourArticle": "提交你的文章", - "ContributionGuide": "贡献指南", - "BugReport": "Bug报告", - "SeeAllArticles": "查看所有的文章", - "WelcomeToABPCommunity!": "欢迎来到ABP社区!", - "MyProfile": "我的资料", - "MyOrganizations": "我的组织", - "EmailNotValid": "请输入有效的电子邮箱地址.", - "FeatureRequest": "功能请求", - "CreateArticleTitleInfo": "文章标题显示在文章列表中.", - "CreateArticleUrlInfo": "文章的原始GitHub/外部URL.", - "CreateArticleSummaryInfo": "文章的简短摘要将显示在文章列表中.", - "CreateArticleCoverInfo": "为了创建有效的文章,请添加封面图. 仅支持16:9的图片!", - "ThisExtensionIsNotAllowed": "不允许此扩展名.", - "TheFileIsTooLarge": "文件过大.", - "GoToTheArticle": "转到文章", - "Contribute": "贡献", - "OverallProgress": "总体流程", - "Done": "完成", - "Open": "打开", - "Closed": "关闭", - "LatestQuestionOnThe": "有关的最新问题", - "Stackoverflow": "Stackoverflow", - "Votes": "票数", - "Answer": "回答", - "Views": "观看次数", - "Answered": "已回答", - "WaitingForYourAnswer": "等待你的回答", - "Asked": "提问", - "AllQuestions": "所有的问题", - "NextVersion": "下一个版本", - "MilestoneErrorMessage": "无法从Github获取当前的里程碑详细信息.", - "QuestionItemErrorMessage": "无法从Stackoverflow获取最新的问题详细信息.", - "Oops": "哎呀!" - } + "culture": "zh-Hans", + "texts": { + "Permission:CommunityArticle": "社区文章", + "Permission:Edit": "修改", + "Waiting": "等待中", + "Approved": "已批准", + "Rejected": "已拒绝", + "Wait": "等待", + "Approve": "批准", + "Reject": "拒绝", + "ReadArticle": "阅读文章", + "Status": "状态", + "ContentSource": "内容来源", + "Details": "详情", + "Url": "url", + "Title": "标题", + "CreationTime": "创建时间", + "Save": "保存", + "SameUrlAlreadyExist": "url已存在,如果你想要添加这篇文章,你需要更改url!", + "UrlIsNotValid": "Url无效.", + "UrlNotFound": "Url未找到.", + "UrlContentNotFound": "Url内容未找到.", + "Summary": "摘要", + "MostRead": "阅读最多", + "Latest": "最新", + "ContributeAbpCommunity": "为ABP社区做贡献", + "SubmitYourArticle": "提交你的文章", + "ContributionGuide": "贡献指南", + "BugReport": "Bug报告", + "SeeAllArticles": "查看所有的文章", + "WelcomeToABPCommunity!": "欢迎来到ABP社区!", + "MyProfile": "我的资料", + "MyOrganizations": "我的组织", + "EmailNotValid": "请输入有效的电子邮箱地址.", + "FeatureRequest": "功能请求", + "CreateArticleTitleInfo": "文章标题显示在文章列表中.", + "CreateArticleUrlInfo": "文章的原始GitHub/外部URL.", + "CreateArticleSummaryInfo": "文章的简短摘要将显示在文章列表中.", + "CreateArticleCoverInfo": "为了创建有效的文章,请添加封面图. 仅支持16:9的图片!", + "ThisExtensionIsNotAllowed": "不允许此扩展名.", + "TheFileIsTooLarge": "文件过大.", + "GoToTheArticle": "转到文章", + "Contribute": "贡献", + "OverallProgress": "总体流程", + "Done": "完成", + "Open": "打开", + "Closed": "关闭", + "LatestQuestionOnThe": "有关的最新问题", + "Stackoverflow": "Stackoverflow", + "Votes": "票数", + "Answer": "回答", + "Views": "观看次数", + "Answered": "已回答", + "WaitingForYourAnswer": "等待你的回答", + "Asked": "提问", + "AllQuestions": "所有的问题", + "NextVersion": "下一个版本", + "MilestoneErrorMessage": "无法从Github获取当前的里程碑详细信息.", + "QuestionItemErrorMessage": "无法从Stackoverflow获取最新的问题详细信息.", + "Oops": "哎呀!", + "CreateArticleSuccessMessage": "文章提交成功. 网站管理员审核通过后将被发布.", + "ChooseCoverImage": "选项一张封面图片", + "CoverImage": "封面图片", + "ShareYourExperiencesWithTheABPFramework": "分享你的ABP Framework经验!", + "Optional": "可选的", + "UpdateUserWebSiteInfo": "示例: https://johndoe.com", + "UpdateUserTwitterInfo": "示例: johndoe", + "UpdateUserGithubInfo": "示例: johndoe", + "UpdateUserLinkedinInfo": "示例: https://www.linkedin.com/...", + "UpdateUserCompanyInfo": "示例v: Volosoft", + "UpdateUserJobTitleInfo": "示例: Software Developer", + "UserName": "用户名", + "Company": "公司", + "PersonalWebsite": "个人网站", + "RegistrationDate": "注册时间", + "Social": "社交", + "Biography": "传记", + "HasNoPublishedArticlesYet": "尚未发表文章", + "Author": "作者", + "LatestGithubAnnouncements": "最新的Github公告", + "SeeAllAnnouncements": "查看所有公告", + "LatestBlogPost": "最新博客文章", + "Edit": "修改", + "ProfileImageChange": "更改资料图片", + "BlogItemErrorMessage": "无法从ABP获取最新的博客文章详细信息.", + "PlannedReleaseDate": "计划发布日期" } - \ No newline at end of file +} \ 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 new file mode 100644 index 0000000000..a5d83f9807 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json @@ -0,0 +1,161 @@ +{ + "culture": "ar", + "texts": { + "GetStarted": "ابدأ - قوالب بدء التشغيل", + "Create": "إنشاء", + "NewProject": "مشروع جديد", + "DirectDownload": "تحميل مباشر", + "ProjectName": "اسم المشروع", + "ProjectType": "نوع المشروع", + "DatabaseProvider": "مزود قاعدة البيانات", + "NTier": "N- الطبقة", + "IncludeUserInterface": "تشمل واجهة المستخدم", + "CreateNow": "إنشاء جديد", + "TheStartupProject": "مشروع بدء التشغيل", + "Tutorial": "الدورة التعليمية", + "UsingCLI": "باستخدام CLI", + "SeeDetails": "انظر التفاصيل", + "AbpShortDescription": "ABP Framework بنية أساسية كاملة لإنشاء تطبيقات ويب حديثة باتباع أفضل ممارسات واتفاقيات تطوير البرامج.", + "SourceCodeUpper": "شفرة المصدر", + "LatestReleaseLogs": "أحدث سجلات الإصدار", + "Infrastructure": "البنية التحتية", + "Architecture": "الهيكلة", + "Modular": "معياري", + "DontRepeatYourself": "لا تكرر نفسك", + "DeveloperFocused": "تركيز المطور", + "FullStackApplicationInfrastructure": "البنية التحتية الكاملة للتطبيق.", + "DomainDrivenDesign": "Domain Driven Design", + "DomainDrivenDesignExplanation": "تم تصميمه وتطويره بناءً على أنماط ومبادئ DDD. يوفر نموذجًا متعدد الطبقات لتطبيقك.", + "Authorization": "تفويض", + "AuthorizationExplanation": "إذن متقدم مع نظام أذونات مستخدم ودور ودقيق. مبني على مكتبة هوية Microsoft.", + "MultiTenancy": "متعدد الايجار", + "MultiTenancyExplanationShort": "جعلت تطبيقات SaaS سهلة! متعدد الإيجارات المتكاملة من قاعدة البيانات إلى واجهة المستخدم.", + "CrossCuttingConcerns": "اهتمامات مشتركة", + "CrossCuttingConcernsExplanationShort": "بنية تحتية كاملة للتفويض والتحقق من الصحة ومعالجة الاستثناءات والتخزين المؤقت وتسجيل التدقيق وإدارة المعاملات والمزيد.", + "BuiltInBundlingMinification": "تجميع وتصغير مدمج", + "BuiltInBundlingMinificationExplanation": "لا حاجة لاستخدام أدوات خارجية للتجميع والتصغير. تقدم ABP طريقة أبسط وديناميكية وقوية ومتكاملة ومدمجة!", + "VirtualFileSystem": "نظام الملفات الافتراضي", + "VirtualFileSystemExplanation": "قم بتضمين العروض والنصوص والأنماط والصور ... في الحزم / المكتبات وإعادة استخدامها في تطبيقات مختلفة.", + "Theming": "تصميم", + "ThemingExplanationShort": "استخدم وتخصيص سمة واجهة المستخدم القياسية القائمة على التمهيد أو قم بإنشاء سمة خاصة بك.", + "BootstrapTagHelpersDynamicForms": "أدوات مساعدة علامة التمهيد والنماذج الديناميكية", + "BootstrapTagHelpersDynamicFormsExplanation": "بدلاً من كتابة التفاصيل المتكررة لمكونات التمهيد يدويًا ، استخدم مساعدي علامات ABP لتبسيطها والاستفادة من التحسس. أنشئ نماذج واجهة مستخدم بسرعة استنادًا إلى نموذج C # باستخدام مساعد علامة النموذج الديناميكي.", + "HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies", + "HTTPAPIsDynamicProxiesExplanation": "كشف خدمات التطبيقات تلقائيًا على أنها واجهات برمجة تطبيقات HTTP بنمط REST ، واستهلكها باستخدام وكلاء JavaScript و C # الديناميكيين.", + "CompleteArchitectureInfo": "هندسة معمارية حديثة لإنشاء حلول برمجية قابلة للصيانة.", + "DomainDrivenDesignBasedLayeringModelExplanation": "يساعدك على تنفيذ بنية طبقات تستند إلى DDD وبناء قاعدة رمز قابلة للصيانة.", + "DomainDrivenDesignBasedLayeringModelExplanationCont": "يوفر قوالب بدء التشغيل ، والملخصات ، والفئات الأساسية ، والخدمات ، والوثائق والأدلة لمساعدتك على تطوير تطبيقك بناءً على أنماط ومبادئ DDD.", + "MicroserviceCompatibleModelExplanation": "تم تصميم إطار العمل الأساسي ووحدات ما قبل الإنشاء مع مراعاة بنية الخدمات المصغرة.", + "MicroserviceCompatibleModelExplanationCont": "يوفر البنية التحتية وعمليات الدمج والعينات والوثائق لتنفيذ حلول الخدمات المصغرة بشكل أسهل ، في حين أنه لا يجلب تعقيدًا إضافيًا إذا كنت تريد تطبيقًا مترابطًا.", + "ModularInfo": "يوفر ABP نظام وحدة نمطية يسمح لك بتطوير وحدات تطبيق قابلة لإعادة الاستخدام ، والربط بأحداث دورة حياة التطبيق ، والتعبير عن التبعيات بين الأجزاء الأساسية في نظامك.", + "PreBuiltModulesThemes": "الوحدات والسمات المبنية مسبقًا", + "PreBuiltModulesThemesExplanation": "الوحدات والسمات مفتوحة المصدر والتجارية جاهزة للاستخدام في تطبيق عملك.", + "NuGetNPMPackages": "حزم NuGet و NPM", + "NuGetNPMPackagesExplanation": "توزع كحزم NuGet و NPM. سهل التركيب والترقية.", + "ExtensibleReplaceable": "قابل للتوسيع / قابل للاستبدال", + "ExtensibleReplaceableExplanation": "تم تصميم جميع الخدمات والوحدات النمطية في الاعتبار قابلية التوسع. يمكنك استبدال الخدمات والصفحات والأنماط والمكونات.", + "CrossCuttingConcernsExplanation2": "اجعل قاعدة الشفرة أصغر حتى تتمكن من التركيز على الكود الخاص بنشاطك التجاري.", + "CrossCuttingConcernsExplanation3": "لا ترسل وقتًا لتنفيذ متطلبات التطبيق المشتركة في مشاريع متعددة.", + "AuthenticationAuthorization": "المصادقة والتفويض", + "ExceptionHandling": "معالجة الاستثناء", + "Validation": "التحقق من الصحة", + "DatabaseConnection": "اتصال قاعدة البيانات", + "TransactionManagement": "إدارة المعاملات", + "AuditLogging": "تدوين التسجيل", + "Caching": "التخزين المؤقت", + "Multitenancy": "التعددية", + "DataFiltering": "تصفية البيانات", + "ConventionOverConfiguration": "اتفاقية على التكوين", + "ConventionOverConfigurationExplanation": "تنفذ ABP اصطلاحات التطبيق الشائعة افتراضيًا باستخدام الحد الأدنى من التكوين أو صفر.", + "ConventionOverConfigurationExplanationList1": "التسجيل التلقائى للخدمات المعروفة بحقن التبعية.", + "ConventionOverConfigurationExplanationList2": "يعرض خدمات التطبيقات على أنها واجهات برمجة تطبيقات HTTP من خلال اصطلاحات التسمية.", + "ConventionOverConfigurationExplanationList3": "يقوم بإنشاء وكلاء عميل HTTP ديناميكي لـ C # و JavaScript.", + "ConventionOverConfigurationExplanationList4": "يوفر المستودعات الافتراضية للكيانات الخاصة بك.", + "ConventionOverConfigurationExplanationList5": "إدارة وحدة العمل حسب طلب الويب أو طريقة خدمة التطبيق.", + "ConventionOverConfigurationExplanationList6": "ينشر إنشاء وتحديث وحذف الأحداث للكيانات الخاصة بك.", + "BaseClasses": "الفئات الأساسية", + "BaseClassesExplanation": "فئات أساسية مسبقة الصنع لأنماط التطبيق الشائعة.", + "DeveloperFocusedExplanation": "ABP للمطورين.", + "DeveloperFocusedExplanationCont": "يهدف إلى تبسيط عملية تطوير البرامج اليومية مع عدم تقييدك من كتابة رمز منخفض المستوى.", + "SeeAllFeatures": "انظر جميع الميزات", + "CLI_CommandLineInterface": "CLI (واجهة سطر الأوامر)", + "CLI_CommandLineInterfaceExplanation": "يتضمن CLI لمساعدتك على أتمتة إنشاء مشاريع جديدة وإضافة وحدات نمطية جديدة.", + "StartupTemplates": "قوالب بدء التشغيل", + "StartupTemplatesExplanation": "توفر قوالب بدء التشغيل المختلفة حلاً مهيئًا بالكامل لبدء التطوير السريع.", + "BasedOnFamiliarTools": "بناءاً على أدوات معروفة", + "BasedOnFamiliarToolsExplanation": "مصمم ومتكامل مع الأدوات الشائعة التي تعرفها بالفعل. منحنى تعليمي منخفض ، سهل التكيف ، تنمية مريحة.", + "ORMIndependent": "ORM مستقل", + "ORMIndependentExplanation": "الإطار الأساسي هو ORM / قاعدة البيانات مستقل ويمكن أن يعمل مع أي مصدر بيانات. يتوفر مقدمو Entity Framework Core و MongoDB بالفعل.", + "Features": "استكشف ميزات إطار عمل ABP", + "ABPCLI": "ABP CLI", + "Modularity": "النمطية", + "BootstrapTagHelpers": "مساعدي علامة التمهيد", + "DynamicForms": "شاشات ديناميكية", + "BundlingMinification": "التجميع والتصغير", + "BackgroundJobs": "وظائف الخلفية", + "DDDInfrastructure": "البنية التحتية DDD", + "DomainDrivenDesignInfrastructure": "البنية التحتية للتصميم يحركها المجال", + "AutoRESTAPIs": "واجهات برمجة تطبيقات Auto REST", + "DynamicClientProxies": "وكلاء العميل الديناميكي", + "DistributedEventBus": "حافلة الحدث الموزعة", + "DistributedEventBusWithRabbitMQIntegration": "حافلة الحدث الموزعة مع تكامل RabbitMQ", + "TestInfrastructure": "اختبار البنية التحتية", + "AuditLoggingEntityHistories": "سجل التدقيق وتاريخ الكيان", + "ObjectToObjectMapping": "مطابقة كائن إلى كائن", + "EmailSMSAbstractions": "التجريد من البريد الإلكتروني والرسائل النصية القصيرة", + "EmailSMSAbstractionsWithTemplatingSupport": "تجريدات البريد الإلكتروني والرسائل النصية القصيرة مع دعم القوالب", + "Localization": "التعريب", + "SettingManagement": "إدارة الإعدادات", + "ExtensionMethods": "طرق التمديد", + "ExtensionMethodsHelpers": "طرق التمديد والمساعدين", + "AspectOrientedProgramming": "البرمجة الموجهة", + "DependencyInjection": "حقن التبعية", + "DependencyInjectionByConventions": "حقن التبعية بالاتفاقيات", + "ABPCLIExplanation": "ABP CLI (واجهة سطر الأوامر) هي أداة سطر أوامر لإجراء بعض العمليات الشائعة للحلول المستندة إلى ABP.", + "ModularityExplanation": "يوفر ABP بنية أساسية كاملة لبناء وحدات التطبيق الخاصة بك والتي قد تحتوي على كيانات وخدمات وتكامل قواعد البيانات وواجهات برمجة التطبيقات ومكونات واجهة المستخدم وما إلى ذلك ..", + "MultiTenancyExplanation": "لا يدعم إطار عمل ABP تطوير التطبيقات متعددة المستأجرين فحسب ، بل يجعل الكود الخاص بك في الغالب غير مدرك للتعددية المستأجرة.", + "MultiTenancyExplanation2": "يمكن تحديد المستأجر الحالي تلقائيًا ، وعزل بيانات المستأجرين المختلفين عن بعضهم البعض.", + "MultiTenancyExplanation3": "يدعم قاعدة بيانات واحدة وقاعدة بيانات لكل مستأجر ونهج مختلطة.", + "MultiTenancyExplanation4": "أنت تركز على رمز عملك وتدع إطار العمل للتعامل مع عقود الإيجار المتعددة نيابة عنك.", + "BootstrapTagHelpersExplanation": "بدلاً من كتابة التفاصيل المتكررة لمكونات التمهيد يدويًا ، استخدم مساعدي علامات ABP لتبسيطها والاستفادة من التحسس. يمكنك بالتأكيد استخدام Bootstrap متى احتجت إليه.", + "DynamicFormsExplanation": "يمكن لمساعدي النموذج الديناميكي وعلامات الإدخال إنشاء النموذج الكامل من فئة C # كنموذج.", + "AuthenticationAuthorizationExplanation": "خيارات المصادقة والترخيص الغنية المدمجة في ASP.NET Core Identity & IdentityServer4. يوفر نظام إذن موسع ومفصل.", + "CrossCuttingConcernsExplanation": "لا تكرر نفسك لتنفيذ كل هذه الأشياء الشائعة مرارًا وتكرارًا. ركز على رمز عملك ودع برنامج ABP يقوم بأتمتة ذلك من خلال الاتفاقيات.", + "DatabaseConnectionTransactionManagement": "اتصال قاعدة البيانات وإدارة المعاملات", + "CorrelationIdTracking": "تتبع معرف الارتباط", + "BundlingMinificationExplanation": "تقدم ABP نظام تجميع وتقليل بسيط وديناميكي وقوي ونمطي ومدمج.", + "VirtualFileSystemnExplanation": "يتيح نظام الملفات الظاهري إدارة الملفات غير الموجودة فعليًا على نظام الملفات (القرص). يتم استخدامه بشكل أساسي لتضمين ملفات (js ، css ، image ، cshtml ...) في التجميعات واستخدامها مثل الملفات الفعلية في وقت التشغيل.", + "ThemingExplanation": "يسمح نظام Theming بتطوير سمة التطبيق والوحدات النمطية الخاصة بك بشكل مستقل عن طريق تحديد مجموعة من المكتبات الأساسية المشتركة والتخطيطات ، بناءً على أحدث إطار عمل Bootstrap.", + "DomainDrivenDesignInfrastructureExplanation": "بنية تحتية كاملة لإنشاء تطبيقات متعددة الطبقات بناءً على أنماط ومبادئ التصميم المدفوع بالمجال ؛", + "Specification": "تخصيص", + "Repository": "مخزن", + "DomainService": "خدمة المجال", + "ValueObject": "كائن القيمة", + "ApplicationService": "خدمة التطبيق", + "DataTransferObject": "كائن نقل البيانات", + "AggregateRootEntity": "تجميع الجذر والكيان", + "AutoRESTAPIsExplanation": "يمكن لـ ABP تكوين خدمات التطبيقات الخاصة بك تلقائيًا كوحدات تحكم API حسب الاصطلاح.", + "DynamicClientProxiesExplanation": "استهلك واجهات برمجة التطبيقات الخاصة بك بسهولة من عملاء JavaScript و C#.", + "DistributedEventBusWithRabbitMQIntegrationExplanation": "انشر واستهلك الأحداث الموزعة بسهولة باستخدام ناقل الأحداث الموزع المدمج مع توفر تكامل RabbitMQ.", + "TestInfrastructureExplanation": "تم تطوير إطار العمل واختبار الوحدة والتكامل في الاعتبار. يوفر لك الفئات الأساسية لتسهيل الأمر. تأتي قوالب بدء التشغيل مع تكوين مسبق للاختبار.", + "AuditLoggingEntityHistoriesExplanation": "تسجيل تدقيق مدمج للتطبيقات المهمة للأعمال. الطلب والخدمة وتسجيل التدقيق على مستوى الطريقة وتاريخ الكيان مع تفاصيل مستوى الملكية.", + "EmailSMSAbstractionsWithTemplatingSupportExplanation": "تفصل تجريدات IEmailSender و ISmsSender منطق التطبيق الخاص بك عن البنية التحتية. يسمح نظام قوالب البريد الإلكتروني المتقدم بإنشاء قوالب البريد الإلكتروني وتعريبها واستخدامها بسهولة عند الحاجة.", + "LocalizationExplanation": "يسمح نظام التعريب بإنشاء موارد في ملفات JSON عادية واستخدامها لتعريب واجهة المستخدم الخاصة بك. وهو يدعم السيناريوهات المتقدمة مثل الوراثة والإضافات وتكامل JavaScript بينما يتوافق تمامًا مع نظام توطين AspNet Core.", + "SettingManagementExplanation": "حدد إعدادات التطبيق الخاص بك واحصل على القيم في وقت التشغيل بناءً على التكوين الحالي والمستأجر والمستخدم.", + "ExtensionMethodsHelpersExplanation": "لا تكرر نفسك حتى مع أجزاء التعليمات البرمجية البسيطة. الإضافات والمساعدات للأنواع القياسية تجعل التعليمات البرمجية أكثر وضوحًا وسهولة في الكتابة.", + "AspectOrientedProgrammingExplanation": "يوفر بنية تحتية مريحة لإنشاء وكلاء ديناميكيين وتنفيذ البرمجة الموجهة نحو الجانب. اعترض أي فئة وقم بتنفيذ التعليمات البرمجية الخاصة بك قبل وبعد تنفيذ كل طريقة.", + "DependencyInjectionByConventionsExplanation": "لا حاجة لتسجيل فصولك في حقن التبعية يدويًا. يسجل تلقائيًا أنواع الخدمات الشائعة حسب الاصطلاح. بالنسبة لأنواع الخدمات الأخرى ، يمكنك استخدام الواجهات والسمات لتسهيل الأمر ووضعه في مكانه.", + "DataFilteringExplanation": "تحديد واستخدام عوامل تصفية البيانات التي يتم تطبيقها تلقائيًا عند الاستعلام عن الكيانات من قاعدة البيانات. يتم توفير مرشحات Soft Delete & MultiTenant خارج الصندوق عند تنفيذ واجهات بسيطة.", + "PublishEvents": "نشر الأحداث", + "HandleEvents": "معالجة الأحداث", + "AndMore": "وأكثر من ذلك ...", + "Code": "شفرة", + "Result": "النتيجة", + "SeeTheDocumentForMoreInformation": "راجع الـ {0} الوثيقة لمزيد من المعلومات", + "IndexPageHeroSection": "فتح المصدرتطبيق الويب
إطار
لـ asp.net core", + "UiFramework": "إطار عمل واجهة المستخدم", + "EmailAddress": "البريد الإلكترونى", + "Mobile": "المحمول", + "ReactNative": "React Native" + } +} \ 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 fa7b216bd0..cb4a8d076c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -93,6 +93,7 @@ "DynamicForms": "Dynamic Forms", "BundlingMinification": "Bundling & Minification", "BackgroundJobs": "Background Jobs", + "BackgroundJobsExplanation": "Define simple classes to execute jobs in background as queued. Use 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", @@ -102,6 +103,7 @@ "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", @@ -156,6 +158,32 @@ "UiFramework": "UI Framework", "EmailAddress": "Email address", "Mobile": "Mobile", - "ReactNative": "React Native" + "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." } } 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 7bd02b44ac..4dcb0cad51 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json @@ -89,10 +89,11 @@ "Features": "功能", "ABPCLI": "ABP CLI", "Modularity": "模块化", - "BootstrapTagHelpers": "Bootstrap Tag Helpers", + "BootstrapTagHelpers": "Bootstrap 标签助手", "DynamicForms": "动态表单", "BundlingMinification": "Bundling & Minification", "BackgroundJobs": "后台作业", + "BackgroundJobsExplanation": "定义简单的类以在队列中在后台执行作业. 使用内置的作业管理器或集成你自己的作业管理器. Hangfire & RabbitMQ集成已经可用.", "DDDInfrastructure": "DDD基础设施", "DomainDrivenDesignInfrastructure": "Domain Driven Design基础设施", "AutoRESTAPIs": "自动REST APIs", @@ -102,6 +103,7 @@ "TestInfrastructure": "测试基础设施", "AuditLoggingEntityHistories": "审计日志和实体历史", "ObjectToObjectMapping": "对象映射", + "ObjectToObjectMappingExplanation": "对象到对象映射 抽象AutoMapper集成.", "EmailSMSAbstractions": "电子邮件和短信抽象", "EmailSMSAbstractionsWithTemplatingSupport": "具有模板支持的电子邮件和短信抽象", "Localization": "本土化", @@ -112,7 +114,7 @@ "DependencyInjection": "依赖注入", "DependencyInjectionByConventions": "依据约定的依赖注入", "ABPCLIExplanation": "ABP CLI(命令行界面)是用于对ABP解决方案执行常见操作的命令行工具.", - "ModularityExplanation": "ABP提供了一个完整的基础设施来构建你自己的应用程序模块,这些模块可能具有实体,服务,数据库集成,API,UI组件等.", //TODO: strong "your own application modules",- + "ModularityExplanation": "ABP提供了一个完整的基础设施来构建你自己的应用程序模块,这些模块可能具有实体,服务,数据库集成,API,UI组件等.", "MultiTenancyExplanation": "ABP框架不仅支持开发多租户应用程序,而且使你的代码几乎无需知道多租户.", "MultiTenancyExplanation2": "可以自动确定当前租户,将不同租户的数据相互隔离.", "MultiTenancyExplanation3": "支持单一数据库,或每个租户单独数据库或者混合方式.", @@ -154,6 +156,34 @@ "SeeTheDocumentForMoreInformation": "查看{0} 文档获得更多信息", "IndexPageHeroSection": "asp.net core的开源Web应用程序
框架
", "UiFramework": "UI框架", - "EmailAddress": "电子邮件地址" + "EmailAddress": "电子邮件地址", + "Mobile": "移动", + "ReactNative": "React Native", + "Strong": "强大的", + "Complete": "完整的", + "BasedLayeringModel": "基于分层模型", + "Microservice": "微服务", + "Compatible": "兼容", + "MeeTTheABPCommunityInfo": "我们的使命是创建一个环境使开发人员可以在文章,教程,案例研究等方面互相帮助并结识志趣相投的人.", + "JoinTheABPCommunityInfo": "参与充满活力的社区并成为ABP框架的贡献者", + "AllArticles": "所有的文章", + "SubmitYourArticle": "提交你的文章", + "DynamicClientProxyDocument": "参阅JavaScript & C#的动态客户端代理文档.", + "EmailSMSAbstractionsDocument": "参阅emailing 和 SMS sending 文档获得更多信息.", + "CreateProjectWizard": "此向导让你从启动模板创建一个新项目,该启动模板已正确配置为可以快速启动你的项目.", + "TieredOption": "创建一个分层解决方案,其中Web和Http API层在物理上是分离的. 如果没有选中则创建一个不那么复杂且适合大多数场景的分层解决方案.", + "SeparateIdentityServerOption": "将服务器端分离为两个应用程序:第一个应用程序用于身份服务器,第二个应用程序用于服务器端HTTP API.", + "UseslatestPreVersion": "使用最新的预发布版本", + "ReadTheDocumentation": "阅读文档", + "Documentation": "文档", + "GettingStartedTutorial": "入门教程", + "ApplicationDevelopmentTutorial": "应用程序开发教程", + "TheStartupTemplate": "启动模板", + "InstallABPCLIInfo": "ABP CLI是使用ABP框架启动新解决方案的最快方式. 使用命令行窗口安装ABP CLI:", + "DifferentLevelOfNamespaces": "你可以使用不同级别的名称空间. 例如 BookStore, Acme.BookStore 或 Acme.Retail.BookStore", + "ABPCLIExamplesInfo": "new命令创建一个 分层的MVC应用程序 使用 Entity Framework Core 做为数据库提供程序. 它还有其他选项. 示例:", + "SeeCliDocumentForMoreInformation": "参阅 ABP CLI 文档 获得更多选项或选择上方的 \"直接下载\" 标签.", + "Optional": "可选的", + "LocalFrameworkRef": "保留框架包的本地项目引用." } } \ No newline at end of file diff --git a/build/common.ps1 b/build/common.ps1 index 18019a154e..0afe55f120 100644 --- a/build/common.ps1 +++ b/build/common.ps1 @@ -16,7 +16,9 @@ $solutionPaths = @( "../modules/tenant-management", "../modules/audit-logging", "../modules/background-jobs", - "../modules/account" + "../modules/account", + "../modules/cms-kit", + "../modules/blob-storing-database" ) if ($full -eq "-f") @@ -29,6 +31,8 @@ if ($full -eq "-f") "../modules/blogging", "../templates/module/aspnet-core", "../templates/app/aspnet-core", + "../templates/console", + "../templates/wpf", "../abp_io/AbpIoLocalization" ) }else{ diff --git a/common.props b/common.props index a22af0116f..9f6b677e72 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ - - latest - 3.2.0 + + latest + 4.0.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/.vscode/settings.json b/docs/en/.vscode/settings.json new file mode 100644 index 0000000000..d04572b96c --- /dev/null +++ b/docs/en/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "grammarly.userWords": [ + "api", + "apiName", + "cli", + "defaultProject", + "formatter", + "md", + "monorepo", + "ngsw", + "npx", + "pwa", + "rootNamespace" + ] +} \ No newline at end of file diff --git a/docs/en/API/Auto-API-Controllers.md b/docs/en/API/Auto-API-Controllers.md index 77819c48c9..e139c16d20 100644 --- a/docs/en/API/Auto-API-Controllers.md +++ b/docs/en/API/Auto-API-Controllers.md @@ -75,17 +75,48 @@ Configure(options => Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. -* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. +* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **kebab-case**. If your application service class name is 'ReadingBookAppService' then it becomes only '/reading-book'. * If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. * If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. * Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; * Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. * Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. - * Converting the result to **camelCase**. + * Converting the result to **kebab-case**. * If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. * Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. * If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). +#### Customizing the Route Calculation + +`IConventionalRouteBuilder` is used to build the route. It is implemented by the `ConventionalRouteBuilder` by default and works as explained above. You can replace/override this service to customize the route calculation strategy. + +#### Version 3.x Style Route Calculation + +The route calculation was different before the version 4.0. It was using camelCase conventions, while the ABP Framework version 4.0+ uses kebab-case. If you use the old route calculation strategy, follow one of the approaches; + +* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: + +````csharp +options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.UseV3UrlStyle = true; + }); +```` + +This approach effects only the controllers for the `BookStoreApplicationModule`. + +* Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: + +```csharp +Configure(options => +{ + options.UseV3UrlStyle = true; +}); +``` + +Setting it globally effects all the modules in a modular application. + ## Service Selection Creating conventional HTTP API controllers are not unique to application services actually. diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md index 94a0222243..23ec336acf 100644 --- a/docs/en/API/Dynamic-CSharp-API-Clients.md +++ b/docs/en/API/Dynamic-CSharp-API-Clients.md @@ -162,4 +162,4 @@ context.Services.AddHttpClientProxies( Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. -> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies (see the related section above). \ No newline at end of file +> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies. See the *IHttpClientProxy Interface* section above. \ No newline at end of file diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md index 9cfec614cb..f48ff9cb5b 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -445,6 +445,68 @@ These methods are used to convert Entities to DTOs and vice verse. They uses the * `MapToEntityAsync(TCreateInput)` is used to create an entity from `TCreateInput`. * `MapToEntityAsync(TUpdateInput, TEntity)` is used to update an existing entity from `TUpdateInput`. +## Miscellaneous + +### Working with Streams + +`Stream` object itself is not serializable. So, you may have problems if you directly use `Stream` as the parameter or the return value for your application service. ABP Framework provides a special type, `IRemoteStreamContent` to be used to get or return streams in the application services. + +**Example: Application Service Interface that can be used to get and return streams** + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Content; + +namespace MyProject.Test +{ + public interface ITestAppService : IApplicationService + { + Task Upload(Guid id, IRemoteStreamContent streamContent); + Task Download(Guid id); + } +} +```` + +**Example: Application Service Implementation that can be used to get and return streams** + +````csharp +using System; +using System.IO; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Content; + +namespace MyProject.Test +{ + public class TestAppService : ApplicationService, ITestAppService + { + public Task Download(Guid id) + { + var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.OpenOrCreate); + return Task.FromResult( + (IRemoteStreamContent) new RemoteStreamContent(fs) { + ContentType = "application/octet-stream" + } + ); + } + + public async Task Upload(Guid id, IRemoteStreamContent streamContent) + { + using (var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.Create)) + { + await streamContent.GetStream().CopyToAsync(fs); + await fs.FlushAsync(); + } + } + } +} +```` + +`IRemoteStreamContent` is compatible with the [Auto API Controller](API/Auto-API-Controllers.md) and [Dynamic C# HTTP Proxy](API/Dynamic-CSharp-API-Clients.md) systems. + ## Lifetime Lifetime of application services are [transient](Dependency-Injection.md) and they are automatically registered to the dependency injection system. diff --git a/docs/en/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md index 9929b6d24d..c7cc8a9e53 100644 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ b/docs/en/AspNet-Boilerplate-Migration-Guide.md @@ -364,9 +364,9 @@ ABP Framework doesn't have the same service. Instead, use `ICurrentUser` and `IC ABP Framework extends the [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing the authorization system to be usable in the [application services](Application-Services.md) too. -#### AbpAutorize vs Autorize +#### AbpAuthorize vs Authorize -Use the standard `[Autorize]` and `[AllowAnonymous]` attributes instead of ASP.NET Boilerplate's custom `[AbpAutorize]` and `[AbpAllowAnonymous]` attributes. +Use the standard `[Authorize]` and `[AllowAnonymous]` attributes instead of ASP.NET Boilerplate's custom `[AbpAuthorize]` and `[AbpAllowAnonymous]` attributes. #### IPermissionChecker vs IAuthorizationService @@ -672,9 +672,9 @@ It was like `abp.localization.localize(...)` in the ASP.NET Boilerplate. ### Navigation vs Menu -In ASP.NET you create a class deriving from the `NavigationProvider` to define your menu elements. Menu items has `requiredPermissionName` attributes to restrict access to a menu element. Menu items were static and your class is executed only one time. +In ASP.NET Boilerplate you create a class deriving from the `NavigationProvider` to define your menu elements. Menu items has `requiredPermissionName` attributes to restrict access to a menu element. Menu items were static and your class is executed only one time. -Int the ABP Framework you need to create a class implements the `IMenuContributor` interface. Your class is executed whenever the menu needs to be rendered. So, you can conditionally add menu items. +In the ABP Framework you need to create a class implements the `IMenuContributor` interface. Your class is executed whenever the menu needs to be rendered. So, you can conditionally add menu items. As an example, this is the menu contributor of the tenant management module: diff --git a/docs/en/Best-Practices/Application-Services.md b/docs/en/Best-Practices/Application-Services.md index 3332ce08ea..ff8afe81d2 100644 --- a/docs/en/Best-Practices/Application-Services.md +++ b/docs/en/Best-Practices/Application-Services.md @@ -213,6 +213,11 @@ This method votes a question and returns the current score of the question. * **Do** always get all the related entities from repositories to perform the operations on them. * **Do** call repository's Update/UpdateAsync method after updating an entity. Because, not all database APIs support change tracking & auto update. +#### Handle files + +* **Do not** use any web components like `IFormFile` or `Stream` in the application services. If you want to serve a file you can use `byte[]`. +* **Do** use a `Controller` to handle file uploading then pass the `byte[]` of the file to the application service method. + #### Using Other Application Services * **Do not** use other application services of the same module/application. Instead; diff --git a/docs/en/Blob-Storing.md b/docs/en/Blob-Storing.md index 23b4f80560..f477e6178a 100644 --- a/docs/en/Blob-Storing.md +++ b/docs/en/Blob-Storing.md @@ -20,7 +20,7 @@ The ABP Framework has already the following storage provider implementations; * [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). -* [Ninio](Blob-Storing-Minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/). +* [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://min.io/). 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/2020-09-17 v3_2_Preview/POST.md b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/POST.md new file mode 100644 index 0000000000..d789e22977 --- /dev/null +++ b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/POST.md @@ -0,0 +1,262 @@ +# ABP Framework & ABP Commercial 3.2 RC With The New Blazor UI + +We are extremely excited today to release the [ABP Framework](https://abp.io/) Release Candidate (and the [ABP Commercial](https://commercial.abp.io/), as always). This release includes an early preview version of the **Blazor UI** for the ABP.IO Platform. + +## The Blazor UI + +While the Blazor UI **should be considered as experimental** for now, it is possible to start to develop your application today. + +### Fundamental Services + +Currently, implemented some important framework features; + +* **Authentication** through the MVC backend using the OpenId Connect authorization code flow. So, all the current login options (login, register, forgot password, external/social logins...) are supported. +* **Authorization**, using the ABP Framework **permissions** as well as the standard authorization system. +* **Localization** just works like the MVC UI. +* **Basic Theme** with top main menu. +* **Dynamic C# HTTP API proxies**, so you can directly consume your backend API by injecting the application service interfaces. +* Some other **fundamental services** like `ISettingProvider`, `IFeatureChecker`, `ICurrentUser`... + +Also, the standard .net services are already available, like caching, logging, validation and much more. Since the ABP Framework is layered itself, all the non MVC UI related features are already usable for the Blazor UI. + +### Pre-Built Modules + +Some modules have been implemented; + +* **Identity** module is pre-installed and provides **user, role and permission management**. +* **Profile management** page is implemented to allow to change password and personal settings. + +### About the Blazorise Library + +We've selected the [Blazorise](https://blazorise.com/) as a fundamental UI library for the Blazor UI. It already supports different HTML/CSS frameworks (like Bootstrap, Bulma, Ant Design...) and significantly increases the developer productivity. + +![blazorise-github](blazorise-github.png) + +We also have a good news: **[Mladen Macanović](https://github.com/stsrki)**, the creator of the Blazorise, is **joining to the core ABP Framework team** in the next weeks. We are excited to work with him to bring the power of these two successful projects together. + +### The Tutorial + +We've **updated** the [web application development tutorial](https://docs.abp.io/en/abp/3.2/Tutorials/Part-1?UI=Blazor) for the **Blazor UI**. You can start to develop applications today! The **source code** of the BookStore application developed with this tutorial is [here](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore). + +### Get started with the Blazor UI + +If you want to try the Blazor UI today, follow the instructions below. + +#### Upgrade the ABP CLI + +Install the latest [ABP CLI](https://docs.abp.io/en/abp/3.2/CLI) preview version: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 3.2.0-rc.2 +```` + +#### Create a new Solution + +Then you can create a new solution using the *abp new* command: + +````bash +abp new AbpBlazorDemo -u blazor --preview +```` + +Also specify the `-t app-pro` parameter if you are an ABP Commercial user. + +> See the ABP CLI documentation for the additional options, like MongoDB database or separated authentication server. + +#### Open the Solution + +Open the generated solution using the latest Visual Studio 2019. You will see a solution structure like the picture below: + +![visual-studio-solution-with-blazor](visual-studio-solution-with-blazor.png) + +#### Run the Application + +* Run the `DbMigrator` project to create the database and seed the initial data. +* Run the `HttpApi.Host` project for the server side. +* Run the `Blazor` project to start the Blazor UI. + +Use `admin` as the username and `1q2w3E*` as the password to login to the application. + +Here, a screenshot from the role management page of the Blazor UI: + +![blazor-role-management](blazor-role-management.png) + +## What's New with the ABP Framework 3.2 + +Beside the Blazor UI, there are a lot of issues have been closed with [the milestone 3.2](https://github.com/abpframework/abp/milestone/39?closed=1). I will highlight some of the major features and changes released with this version. + +### MongoDB ACID Transactions + +[MongoDB integration](https://docs.abp.io/en/abp/3.2/MongoDB) now supports multi-document transactions that comes with the MongoDB 4.x. + +We've **disabled transactions** for solutions use the MongoDB, inside the `YourProjectMongoDbModule.cs` file in the MongoDB project. If your MongoDB server **supports transactions**, you should manually enable it in this class: + +```csharp +Configure(options => +{ + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; +}); +``` + +> Or you can delete this code since this is already the default behavior. + +#### Upgrade Notes + +If you are upgrading an existing solution and your MongoDB server doesn't support transactions, please disable it: + +```csharp +Configure(options => +{ + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; +}); +``` + +See the [Unit Of Work document](https://docs.abp.io/en/abp/3.2/Unit-Of-Work) to learn more about UOW and transactions. + +Also, add [this file](https://github.com/abpframework/abp/blob/rel-3.2/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.MongoDB/MongoDb/MongoDbMyProjectNameDbSchemaMigrator.cs) into your MongoDB project (remember to change `MongoDbMyProjectNameDbSchemaMigrator` and `IMyProjectNameDbSchemaMigrator` with your own project name). + +#### Integration Tests + +> Transactions are also **disabled for automated integration tests** coming with the application startup template, since the [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library (we use in the test projects) has a problem with the transactions. We've sent a [Pull Request](https://github.com/Mongo2Go/Mongo2Go/pull/101) to fix it and will enable the transactions again when they merge & release it. +> +> If you are upgrading an existing solution and using MongoDB, please disable transactions for the test projects just as described above. + +### Kafka Integration for the Distributed Event Bus + +ABP Framework's [distributed event system](https://docs.abp.io/en/abp/3.2/Distributed-Event-Bus) has been [integrated to RabbitMQ](https://docs.abp.io/en/abp/3.2/Distributed-Event-Bus-RabbitMQ-Integration) before. By the version 3.2, it has a Kafka integration package, named [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka). + +See the [Kafka integration documentation](https://docs.abp.io/en/abp/3.2/Distributed-Event-Bus-Kafka-Integration) to learn how to install and configure it. + +### Host Features + +[ABP Feature System](https://docs.abp.io/en/abp/3.2/Features) allows you to define features in your application. Then you can enable/disable a feature dynamically on the runtime. It is generally used in a [multi-tenant](https://docs.abp.io/en/abp/3.2/Multi-Tenancy) system to restrict features for tenants, so you can charge extra money for some features in a SaaS application. + +In some cases, you may want to use the same features in the host side (host is you as you are managing the tenants). For this case, we've added a "**Manage Host Features**" button to the Tenant Management page so you can open a modal dialog to select the features for the host side. + +![host-features](host-features.png) + +### AbpHttpClientBuilderOptions + +ABP Framework provides a system to dynamically create C# proxies to consume HTTP APIs from your client applications. `AbpHttpClientBuilderOptions` is a new option class to configure the `HttpClient`s used by the proxy system. + +**Example: Use the [Polly](https://github.com/App-vNext/Polly) library to retry up to 3 times for a failed HTTP request** + +````csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + PreConfigure(options => + { + options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => + { + clientBuilder.AddTransientHttpErrorPolicy(policyBuilder => + policyBuilder.WaitAndRetryAsync( + 3, + i => TimeSpan.FromSeconds(Math.Pow(2, i)) + ) + ); + }); + }); +} +```` + +See the issue [#5304](https://github.com/abpframework/abp/issues/5304) for the details. + +### ABP Build Command + +We are using **mono repository** approach and the [abp repository](https://github.com/abpframework/abp) has tens of solutions and hundreds of projects (the framework, modules, tooling, templates...) with all of them are referencing to each other. + +It gets a significant time to build the whole repository for every Git push. To **optimize** this process, we've created the **abp build** command in the [ABP CLI](https://docs.abp.io/en/abp/3.2/CLI): + +````bash +abp build +```` + +We will use this command to build the abp repository or a solution inside it. However it is available to everyone in case of need. + +> **Most of the people will not need it**. If you need it, see the [ABP CLI](https://docs.abp.io/en/abp/3.2/CLI) document to learn all the details and options. + +### Other Features, Improvements and Changes + +* Introduced the `DynamicRangeAttribute` that can be used to determine the range values on runtime, just like the `DynamicStringLengthAttribute` was introduced before. +* Improved the feature management modal for multi-tenant applications to group features on the UI and show hierarchically. +* Added `--skip-cli-version-check` option to ABP CLI to improve the performance by bypassing the online version check. +* Angular UI now redirect to MVC UI (the authentication server side) for profile management page, if the authorization code flow is used (which is the default). +* Account module profile management page is now extensible. You can implement the `IProfileManagementPageContributor` interface and register it using the `ProfileManagementPageOptions` class. +* Improvements and optimizations for the [Angular service proxy generation](https://blog.abp.io/abp/Introducing-the-Angular-Service-Proxy-Generation). + +And a lot of minor improvements and bug fixes. You can see [the milestone 3.2](https://github.com/abpframework/abp/milestone/39?closed=1) for all issues & PRs closed with this version. + +## What's New with the ABP Commercial 3.2 + +### Breaking Changes + +The new *profile picture management* feature uses the [BLOB storing](https://docs.abp.io/en/abp/3.2/Blob-Storing) system, so it needs a Storage Provider. The new **startup template comes with the [Database BLOB Provider](https://docs.abp.io/en/abp/3.2/Blob-Storing-Database) pre-installed**. You can change it if you want to use another BLOB provider (like Azure, AWS or a simple file system). + +**Existing solutions must configure a BLOB provider** after upgrading to the version 3.2. Follow the [BLOB Storing document](https://docs.abp.io/en/abp/3.2/Blob-Storing#blob-storage-providers) to configure the provider yourself. + +### The Blazor UI + +The **experimental** Blazor UI is also available for the ABP Commercial. The [Lepton Theme](https://commercial.abp.io/themes) hasn't been implemented with this initial preview, however we are working on it with the highest priority. + +You can use the [ABP Suite](https://docs.abp.io/en/commercial/latest/abp-suite/index) or the following ABP CLI command to create a new solution with the Blazor UI: + +````bash +abp new AbpBlazorDemo -u blazor -t app-pro --preview +```` + +Please try it and provide feedback to us. Thanks in advance. + +> See the instructions in the *Get started with the Blazor UI* section above to properly create and run your application. + +### File Management Angular UI + +Angular UI for the [File Management](https://commercial.abp.io/modules/Volo.FileManagement) module is available with the version 3.2. You can add it to your solution using the ABP Suite. + +![file-management-module-angular](file-management-module-angular.png) + +### Profile Picture Management + +We've added profile picture management for the account module, so a user can select one of the options below for her profile picture; + +* Use the default placeholder as the avatar. +* Use [Gravatar](https://gravatar.com/) service to get the picture matching the email address of the user. +* Upload a file as the profile picture. + +![account-profile-picture](account-profile-picture.jpg) + +### Two Factor Authentication Features + +Created [features](https://docs.abp.io/en/abp/3.2/Features) and [settings](https://docs.abp.io/en/abp/3.2/Settings) to disable, enable or force to use 2FA on login for the tenants and users. + +### Upgrading the ABP Suite + +You can use the following command to upgrade the ABP Suite to the latest preview version: + +```` +abp suite update --preview +```` + +## Other News + +### The ABP Community + +**ABP Community** web site is constantly being improved and new articles are added. We will add "**commenting**" and "**rating**" features to the articles soon to increase the interactivity between the people. + +![abp-community-20200917](abp-community-20200917.png) + +If you have something to share with the ABP community or want to follow the project progress, please check the **[community.abp.io](https://community.abp.io/)**! + +### CMS Kit Project + +We are silently working on a project, named [CMS Kit](https://github.com/abpframework/abp/tree/dev/modules/cms-kit), for a few months. CMS Kit is a set of reusable CMS (Content Management System) components based on the ABP Framework. Some of the components currently being developed: + +* **Comments**; Allows users to comment under something (a blog post, a document, an image... etc). +* **Reactions**; Allows users to give reactions to something (a comment, a picture... etc.) using simple emoji icons. +* **Rating**; Allows users to rate some content from 1 to 5. +* **Newsletter**; Allows you to put a newsletter box to your web site to collect emails from users. +* **Contact**; Put a form to get message from the web site visitors. + +There are more planned components like articles, tags, votes, favorites, portfolio, image gallery, FAQ... etc. We will document and deploy these components when they get matured and ready to use. Some of them will be open source & free while some of them are paid (included in the [ABP Commercial](https://commercial.abp.io/) license). + +## Feedback + +Please try the ABP Framework 3.2.0 RC and [provide feedback](https://github.com/abpframework/abp/issues/new) to help us to release a more stable version. The planned release date for the [3.2.0 final](https://github.com/abpframework/abp/milestone/43) version is October 01. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/abp-community-20200917.png b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/abp-community-20200917.png new file mode 100644 index 0000000000..1a3997a6ab Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/abp-community-20200917.png differ diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/account-profile-picture.jpg b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/account-profile-picture.jpg new file mode 100644 index 0000000000..1b57ce4025 Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/account-profile-picture.jpg differ diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/blazor-role-management.png b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/blazor-role-management.png new file mode 100644 index 0000000000..e00d66e9d0 Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/blazor-role-management.png differ diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/blazorise-github.png b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/blazorise-github.png new file mode 100644 index 0000000000..1c8490f605 Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/blazorise-github.png differ diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/file-management-module-angular.png b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/file-management-module-angular.png new file mode 100644 index 0000000000..e370c0d5bc Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/file-management-module-angular.png differ diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/host-features.png b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/host-features.png new file mode 100644 index 0000000000..c803f8599b Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/host-features.png differ diff --git a/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/visual-studio-solution-with-blazor.png b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/visual-studio-solution-with-blazor.png new file mode 100644 index 0000000000..c4c018e842 Binary files /dev/null and b/docs/en/Blog-Posts/2020-09-17 v3_2_Preview/visual-studio-solution-with-blazor.png differ diff --git a/docs/en/Blog-Posts/2020-10-01 v3_2_Release_Stable/POST.md b/docs/en/Blog-Posts/2020-10-01 v3_2_Release_Stable/POST.md new file mode 100644 index 0000000000..75ea442f0d --- /dev/null +++ b/docs/en/Blog-Posts/2020-10-01 v3_2_Release_Stable/POST.md @@ -0,0 +1,47 @@ +# ABP Framework 3.2 Final Has Been Released + +ABP Framework & ABP Commercial 3.2 have been released today. + +Since all the new features are already explained in details with the [3.2 RC Announcement Post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI) for **new feature and changes** you may need to do for your solution while upgrading to the version 3.2. + +## Creating New Solutions + +You can create a new solution with the ABP Framework version 3.2 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 +```` + +After the update command, check [the RC blog post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI) to learn if you need to make any changes in your solution. + +> You may want to see the new [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). + +## About the Next Versions + +The next two versions (3.3 & 4.0) will be mostly related to completing the Blazor UI features and upgrading the ABP Framework & ecosystem to the .NET 5.0. + +The ultimate goal is to complete the version 4.0 with a stable Blazor UI with the fundamental features implemented and publish it just after the Microsoft lunches .NET 5 in this November. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/POST.md b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/POST.md new file mode 100644 index 0000000000..4e19a294c1 --- /dev/null +++ b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/POST.md @@ -0,0 +1,271 @@ +# ABP Framework 3.3 RC Has Been Published + +We have released the [ABP Framework](https://abp.io/) (and the [ABP Commercial](https://commercial.abp.io/)) `3.3.0-rc.1` today. This blog post introduces the new features and important changes in the new version. + +## Get Started with the 3.3 RC.1 + +If you want to try the version `3.3.0-rc.1` today, follow the steps below; + +1) **Upgrade** the ABP CLI to the version `3.3.0-rc.1` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 3.3.0-rc.1 +```` + +**or install** if you haven't installed before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 3.3.0-rc.1 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/3.3/CLI) for all the available options. + +> You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the Preview checkbox. + +## What's new with the ABP Framework 3.3 + +### The Blazor UI + +We had released an experimental early preview version of the Blazor UI with the [previous version](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-3.2-RC-With-The-New-Blazor-UI). In this version, we've completed most of the fundamental infrastructure features and the application modules (like identity and tenant management). + +It currently has almost the same functionalities as the other UI types (Angular & MVC / Razor Pages). + +**Example screenshot**: User management page of the Blazor UI + +![abp-blazor-ui](abp-blazor-ui.png) + +> We've adapted the [Lepton Theme](https://commercial.abp.io/themes) for the ABP Commercial, see the related section below. + +We are still working on the fundamentals. So, the next version may introduce breaking changes of the Blazor UI. We will work hard to keep them with the minimal effect on your application code. + +#### Breaking Changes on the Blazor UI + +There are some breaking changes with the Blazor UI. If you've built an application and upgrade it, your application might not properly work. See [the migration guide](https://docs.abp.io/en/abp/3.3/Migration-Guides/BlazorUI-3_3) for the changes you need to do after upgrading your application. + +### Automatic Validation for AntiForgery Token for HTTP APIs + +Starting with the version 3.3, all your HTTP API endpoints are **automatically protected** against CSRF attacks, unless you disable it for your application. So, no configuration needed, just upgrade the ABP Framework. + +[See the documentation](https://docs.abp.io/en/abp/3.3/CSRF-Anti-Forgery) to if you want to understand why you need it and how ABP Framework solves the problem. + +### Rebus Integration Package for the Distributed Event Bus + +[Rebus](https://github.com/rebus-org/Rebus) describes itself as "Simple and lean service bus implementation for .NET". There are a lot of integration packages like RabbitMQ and Azure Service Bus for the Rebus. The new [Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) package allows you to use the Rebus as the [distributed event bus](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus) for the ABP Framework. + +See [the documentation](https://docs.abp.io/en/abp/3.3/Distributed-Event-Bus-Rebus-Integration) to learn how to use Rebus with the ABP Framework. + +### Async Repository LINQ Extension Methods + +You have a problem when you want to use **Async LINQ Extension Methods** (e.g. `FirstOrDefaultAsync(...)`) in your **domain** and **application** layers. These async methods are **not included in the standard LINQ extension methods**. Those are defined by the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) NuGet package (see [the code](https://github.com/dotnet/efcore/blob/main/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs)). To be able to use these `async` methods, you need to reference to the `Microsoft.EntityFrameworkCore` package. + +If you don't want to depend on the EF Core in your business layer, then ABP Framework provides the `IAsyncQueryableExecuter` service to execute your queries asynchronously without depending on the EF Core package. You can see [the documentation](https://docs.abp.io/en/abp/latest/Repositories#option-3-iasyncqueryableexecuter) to get more information about this service. + +ABP Framework version 3.3 takes this one step further and allows you to directly execute the async LINQ extension methods on the `IRepository` interface. + +**Example: Use `CountAsync` and `FirstOrDefaultAsync` methods on the repositories** + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace MyCompanyName.MyProjectName +{ + public class BookAppService : ApplicationService, IBookAppService + { + private readonly IRepository _bookRepository; + + public BookAppService(IRepository bookRepository) + { + _bookRepository = bookRepository; + } + + public async Task DemoAsync() + { + var countAll = await _bookRepository + .CountAsync(); + + var count = await _bookRepository + .CountAsync(x => x.Name.Contains("A")); + + var book1984 = await _bookRepository + .FirstOrDefaultAsync(x => x.Name == "1984"); + } + } +} +```` + +All the standard LINQ methods are supported: *AllAsync, AnyAsync, AverageAsync, ContainsAsync, CountAsync, FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, LongCountAsync, MaxAsync, MinAsync, SingleAsync, SingleOrDefaultAsync, SumAsync, ToArrayAsync, ToListAsync*. + +This approach still has a limitation. You need to execute the extension method directly on the repository object. For example, the below usage is **not supported**: + +````csharp +var count = await _bookRepository.Where(x => x.Name.Contains("A")).CountAsync(); +```` + +This is because the object returned from the `Where` method is not a repository object, it is a standard `IQueryable`. In such cases, you can still use the `IAsyncQueryableExecuter`: + +````csharp +var count = await AsyncExecuter.CountAsync( + _bookRepository.Where(x => x.Name.Contains("A")) +); +```` + +`AsyncExecuter` has all the standard extension methods, so you don't have any restriction here. See [the repository documentation](https://docs.abp.io/en/abp/latest/Repositories#iqueryable-async-operations) for all the options you have. + +> ABP Framework does its best to not depend on the EF Core and still be able to use the async LINQ extension methods. However, there is no problem to depend on the EF Core for your application, you can add the `Microsoft.EntityFrameworkCore` NuGet package and use the native methods. + +### Stream Support for the Application Service Methods + +[Application services](https://docs.abp.io/en/abp/latest/Application-Services) are consumed by clients and the parameters and return values (typically [Data Transfer Objects](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)). In case of the client is a remote application, then these objects should be serialized & deserialized. + +Until the version 3.3, we hadn't suggest to use the `Stream` in the application service contracts, since it is not serializable/deserializable. However, with the version 3.3, ABP Framework properly supports this scenario by introducing the new `IRemoteStreamContent` interface. + +Example: An application service that can get or return streams + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Content; + +namespace MyProject.Test +{ + public interface ITestAppService : IApplicationService + { + Task Upload(Guid id, IRemoteStreamContent streamContent); + Task Download(Guid id); + } +} +```` + +The implementation can be as shown below: + +````csharp +using System; +using System.IO; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Services; +using Volo.Abp.Content; + +namespace MyProject.Test +{ + public class TestAppService : ApplicationService, ITestAppService + { + public Task Download(Guid id) + { + var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.OpenOrCreate); + return Task.FromResult( + (IRemoteStreamContent) new RemoteStreamContent(fs) { + ContentType = "application/octet-stream" + } + ); + } + + public async Task Upload(Guid id, IRemoteStreamContent streamContent) + { + using (var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.Create)) + { + await streamContent.GetStream().CopyToAsync(fs); + await fs.FlushAsync(); + } + } + } +} +```` + +> This is just a demo code. Do it better in your production code :) + +Thanks to [@alexandru-bagu](https://github.com/alexandru-bagu) for the great contribution! + +### Other Changes + +* Upgraded all the .NET Core / ASP.NET Core related packages to the version 3.1.8. If you have additional dependencies to the .NET Core / ASP.NET Core related packages, we suggest you to updates your packages to the version 3.1.8 to have the latest bug and security fixes published by Microsoft. +* The blogging module now uses the [BLOB Storing](https://docs.abp.io/en/abp/latest/Blob-Storing) system to store images & files of the blog posts. If you are using this module, then you need to manually migrate the local files to the BLOB Storing system after the upgrade. +* The Angular UI is now redirecting to the profile management page of the MVC UI instead of using its own UI, if you've configured the authorization code flow (which is default since the version 3.2.0). + +## What's new with the ABP Commercial 3.3 + +### The Blazor UI + +We have good news for the ABP Commercial Blazor UI too. We have implemented the [Lepton Theme](https://commercial.abp.io/themes) integration, so it is now available with the Blazor UI. Also, implemented most of the fundamental [modules](https://commercial.abp.io/modules). + +**A screenshot from the ABP Commercial startup template with the Blazor UI** + +![abp-commercial-blazor-ui](abp-commercial-blazor-ui.png) + +There are still missing features and modules. However, we are working on it to have a more complete version in the next release. + +#### Breaking Changes on the Blazor UI + +There are some breaking changes with the Blazor UI. If you've built an application and upgrade it, your application might not properly work. See the [ABP Commercial Blazor UI v 3.3 Migration Guide](https://docs.abp.io/en/commercial/3.3/migration-guides/blazor-ui-3_3) for the changes you need to do after upgrading your application. + +#### Known Issues + +When you create a new project, profile management doesn't work, you get an exception because it can't find the `/libs/cropperjs/css/cropper.min.css` file. To fix the issue; + +* Add `"@volo/account": "^3.3.0-rc.1"` to the `package.json` in the `.Host` project. +* Run `yarn` (or `npm install`), then `gulp` on a command line terminal in the root folder of the `.Host` project. + +### Multi-Tenant Social Logins + +[Account module](https://commercial.abp.io/modules/Volo.Account.Pro) now supports to manage the social/external logins in the UI. You can **enable/disable** and **set options** in the settings page. It also supports to use **different credentials for the tenants** and it is also **configured on the runtime**. + +![abp-commercial-setting-account-external-logins](abp-commercial-setting-account-external-logins.png) + +### Linked Accounts + +Linked user system allows you to link other accounts (including account in a different tenant) with your account, so you can switch between different accounts with a single-click. It is practical since you no longer need to logout and login again with entering the credentials of the target account. + +To manage the linked accounts, go to the profile management page from the user menu; + +![abp-commercial-linked-users](abp-commercial-linked-users.png) + +### Paypal & Stripe Integrations + +The [Payment Module](https://commercial.abp.io/modules/Volo.Payment) was supporting PayU and 2Checkout providers until the version 3.3. It's now integrated to PayPal and Stripe. See the [technical documentation](https://docs.abp.io/en/commercial/latest/modules/payment) to learn how to use it. + +### ABP Suite Improvements + +We've done a lot of small improvements for the [ABP Suite](https://commercial.abp.io/tools/suite). Some of the enhancements are; + +* Show the previously installed modules as *installed* on the module list. +* Switch between the latest stable, the latest [preview](https://docs.abp.io/en/abp/latest/Previews) and the latest [nightly build](https://docs.abp.io/en/abp/latest/Nightly-Builds) versions of the ABP related packages. +* Moved the file that stores the *previously created entities* into the solution folder to allow you to store it in your source control system. + +### Others + +* Added an option to the Account Module to show reCAPTCHA on the login & the registration forms. + +Besides the new features introduced in this post, we've done a lot of small other enhancements and bug fixes to provide a better development experience and increase the developer productivity. + +## New Articles + +The core ABP Framework team & the community continue to publish new articles on the [ABP Community](https://community.abp.io/) web site. The recently published articles are; + +* [Replacing Email Templates and Sending Emails](https://community.abp.io/articles/replacing-email-templates-and-sending-emails-jkeb8zzh) (by [@EngincanV](https://community.abp.io/members/EngincanV)) +* [How to Add Custom Properties to the User Entity](https://community.abp.io/articles/how-to-add-custom-property-to-the-user-entity-6ggxiddr) (by [@berkansasmaz](https://community.abp.io/members/berkansasmaz)) +* [Using the AdminLTE Theme with the ABP Framework MVC / Razor Pages UI](https://community.abp.io/articles/using-the-adminlte-theme-with-the-abp-framework-mvc-razor-pages-ui-gssbhb7m) (by [@mucahiddanis](https://community.abp.io/members/mucahiddanis)) +* [Using DevExtreme Angular Components With the ABP Framework](https://community.abp.io/articles/using-devextreme-angular-components-with-the-abp-framework-x5nyvj3i) (by [@bunyamin](https://community.abp.io/members/bunyamin)) + +It is appreciated if you want to [submit an article](https://community.abp.io/articles/submit) related to the ABP Framework. + +## About the Next Release + +The next version will be `4.0.0`. We are releasing a major version, since we will move the ABP Framework to .NET 5.0. We see that for most of the applications this will not be a breaking change and we hope you easily upgrade to it. + +The planned 4.0.0-rc.1 (Release Candidate) version date is **November 11**, just after the Microsoft releases the .NET 5.0 final. The planned 4.0.0 final release date is **November 26**. + +Follow the [GitHub milestones](https://github.com/abpframework/abp/milestones) for all the planned ABP Framework version release dates. + +## Feedback + +Please check out the ABP Framework 3.3.0 RC and [provide feedback](https://github.com/abpframework/abp/issues/new) to help us to release a more stable version. The planned release date for the [3.3.0 final](https://github.com/abpframework/abp/milestone/44) version is October 27th. diff --git a/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-blazor-ui.png b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-blazor-ui.png new file mode 100644 index 0000000000..ea61f2a488 Binary files /dev/null and b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-blazor-ui.png differ diff --git a/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-blazor-ui.png b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-blazor-ui.png new file mode 100644 index 0000000000..8da48af76e Binary files /dev/null and b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-blazor-ui.png differ diff --git a/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-linked-users.png b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-linked-users.png new file mode 100644 index 0000000000..ef957aecd5 Binary files /dev/null and b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-linked-users.png differ diff --git a/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-setting-account-external-logins.png b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-setting-account-external-logins.png new file mode 100644 index 0000000000..8be08af686 Binary files /dev/null and b/docs/en/Blog-Posts/2020-10-15 v3_3_Preview/abp-commercial-setting-account-external-logins.png differ diff --git a/docs/en/Blog-Posts/2020-10-27 v3_3_Release_Stable/POST.md b/docs/en/Blog-Posts/2020-10-27 v3_3_Release_Stable/POST.md new file mode 100644 index 0000000000..4f7507cbfd --- /dev/null +++ b/docs/en/Blog-Posts/2020-10-27 v3_3_Release_Stable/POST.md @@ -0,0 +1,47 @@ +# ABP Framework & ABP Commercial 3.3 Final Have Been Released + +ABP Framework & ABP Commercial 3.3.0 have been released today. + +Since all the new features are already explained in details with the [3.3 RC Announcement Post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-v3.3-RC-Have-Been-Released), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-v3.3-RC-Have-Been-Released) for **new feature and changes** you may need to do for your solution while upgrading to the version 3.3. + +## Creating New Solutions + +You can create a new solution with the ABP Framework version 3.3 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. After the update command, check [the RC blog post](https://blog.abp.io/abp/ABP-Framework-ABP-Commercial-v3.3-RC-Have-Been-Released) to learn if you need to make any changes in your solution. + +> You may want to see the new [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). + +## About the Next Version: 4.0 + +The next version will be 4.0 and it will be mostly related to completing the Blazor UI features and upgrading the ABP Framework & ecosystem to the .NET 5.0. + +The goal is to complete the version 4.0 with a stable Blazor UI with the fundamental features implemented and publish it just after the Microsoft lunches .NET 5 in this November. The planned 4.0 preview release date is November 11th. \ No newline at end of file diff --git a/docs/en/CLI-BuildCommand.md b/docs/en/CLI-BuildCommand.md new file mode 100644 index 0000000000..d9620aa621 --- /dev/null +++ b/docs/en/CLI-BuildCommand.md @@ -0,0 +1,67 @@ +# Build Command + +Building a .NET project is hard when the project references a project reference outside of the solution or even from a different GIT repository. This command builds a GIT repository and it's depending repositories or a single .NET solution File. In order ```build``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain one of; + +* A .NET solution file (*.sln) +* abp-build-config.json + +When the executing directory (or ```--working-directory``` parameter's directory) contains a .NET solution file, ```build``` command builds all the projects in the related solution file and all project references on building project files recursively. + +When the executing directory (or ```--working-directory``` parameter's directory) contains a ```abp-build-config.json```, ```build``` command builds all changed projects form its last build and all project references on building project files recursively. + +# Build Command Config + +```abp-build-config.json``` contains properties below; + +* ```Name```: Name of the GIT repository. This can be friendly name of your GIT repository or any other unique string for the repository. +* ```RootPath```: Root path of the repository which contains ```.git``` folder. +* ```DependingRepositories```: Depending repository list of a repository. Each depending repository item contains same fields as a repository. +* ```IgnoredDirectories```: Relative directory paths to ignore while building a GIT repository. + +A sample ```abp-build-config.json``` looks like for a Windows OS; + +````json +{ + "Name": "main-repository", + "RootPath": "D:\\GitHub\\main-repository", + "DependingRepositories": [{ + "Name": "module-repository", + "RootPath": "D:\\GitHub\\module-repository" + }], + "IgnoredDirectories": [ + "utils" + ] +} +```` + +# Build Status + +ABP CLI stores a build status file for builds using the repository friendly names and current branch names in the; + +* ```%USERPROFILE%\.abp\build\``` for Windows. +* ```$HOME/.abp/build/``` for Linux/macOS. + +and uses this file when building same repository next time and only builds affected projects and decreases the total build time. A sample build status file content looks like; + +````json +{ + "RepositoryName": "main-repository", + "BranchName": "dev", + "CommitId": "84ecde8ba275aeeb14d24a87ad46a1e941adf8ba", + "SucceedProjects": [{ + "CsProjPath": "D:\\GitHub\\main-repository\\BookStore\\BookStore.Web.csproj", + "CommitId": "84ecde8ba275aeeb14d24a87ad46a1e941adf8ba" + }], + "DependingRepositories": [{ + "RepositoryName": "module-repository", + "BranchName": "dev", + "CommitId": "0598b8e45af9507fc9ba8abf304e78fc7d434e04", + "SucceedProjects": [{ + "CsProjPath": "D:\\GitHub\\module-repository\\identity-module\Identity\\Identity.Web.csproj", + "CommitId": "0598b8e45af9507fc9ba8abf304e78fc7d434e04" + }], + "DependingRepositories": [] + }] +} +```` + diff --git a/docs/en/CLI.md b/docs/en/CLI.md index 8ea20ffd57..8713bc9019 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -40,6 +40,7 @@ Here, the list of all available commands before explaining their details: * **`translate`**: Simplifies to translate localization files when you have multiple JSON [localization](Localization.md) files in a source control repository. * **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. * **`logout`**: Logouts from your computer if you've authenticated before. +* **`build`**: Builds a GIT repository and depending repositories or a single .NET solution. ### help @@ -179,7 +180,8 @@ abp add-module Volo.Blogging * `--solution` or `-s`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory. * `--skip-db-migrations`: For EF Core database provider, it automatically adds a new code first migration (`Add-Migration`) and updates the database (`Update-Database`) if necessary. Specify this option to skip this operation. * `-sp` or `--startup-project`: Relative path to the project folder of the startup project. Default value is the current folder. -* `--with-source-code`: Add source code of the module instead of NuGet/NPM packages. +* `--with-source-code`: Downloads the source code of the module to your solution folder and uses local project references instead of NuGet/NPM packages. +* `--add-to-solution-file`: Adds the downloaded module to your solution file, so you will also see the projects of the module when you open the solution on a IDE. (only available when `--with-source-code` is used.) ### get-source @@ -360,3 +362,31 @@ Logs you out by removing the session token from your computer. abp logout ``` +### build + +This command builds a GIT repository and it's depending repositories or a single .NET solution File. In order ```build``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain one of; + +* A .NET solution file (*.sln) +* abp-build-config.json (suggested to add this to .gitignore) + +Usage: + +````bash +abp build [options] +```` + +Example: + +``` +abp build --build-name "prod" --dotnet-build-arguments "\"--no-dependencies\"" +``` + +#### Options + +* ```--working-directory``` or ```-w```: Specifies the working directory. This option is useful when the command is executed outside of a GIT repository or when executing directory doesn't contain a .NET solution file. +* ```--build-name``` or ```-n```: Specifies a name for the build. This option is useful when same repository is used for more than one different builds. +* ```--dotnet-build-arguments``` or ```-a```: Arguments to pass ```dotnet build``` when building project files. This parameter must be passed like ```"\"{params}\""``` . +* ```--force``` or ```-f```: Forces to build projects even they are not changed from the last successful build. + +For more details, see [build command documentation](CLI-BuildCommand.md). + diff --git a/docs/en/CSRF-Anti-Forgery.md b/docs/en/CSRF-Anti-Forgery.md new file mode 100644 index 0000000000..ad838e3548 --- /dev/null +++ b/docs/en/CSRF-Anti-Forgery.md @@ -0,0 +1,119 @@ +# CSRF/XSRF & Anti Forgery System + +"*Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious web site, email, blog, instant message, or program causes a user’s web browser to perform an unwanted action on a trusted site for which the user is currently authenticated*" ([OWASP](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet)). + +**ABP Framework completely automates CSRF preventing** and works out of the box without any configuration. Read this documentation only if you want to understand it better or need to customize. + +## The Problem + +ASP.NET Core [provides infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery) to prevent CSRF attacks by providing a system to **generate** and **validate antiforgery tokens**. However, the standard implementation has a few drawbacks; + +Antiforgery token validation is only **enabled for razor pages by default** and not enabled for **HTTP APIs**. You need to enable it yourself for the Controllers. You can use the `[ValidateAntiForgeryToken]` attribute for a specific API Controller/Action or the `[AutoValidateAntiforgeryToken]` attribute to prevent attacks globally. + +Once you enable it; + +* You need to manually add an HTTP header, named `RequestVerificationToken` to every **AJAX request** made in your application. You should care about obtaining the token, saving in the client side and adding to the HTTP header on every HTTP request. +* All your clients, including **non-browser clients**, should care about obtaining and sending the antiforgery token in every request. In fact, non-browser clients has no CSRF risk and should not care about this. + +Especially, the second point is a pain for your clients and unnecessarily consumes your server resources. + +> You can read more about the ASP.NET Core antiforgery system in its own [documentation](https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery). + +## The Solution + +ABP Framework provides `[AbpValidateAntiForgeryToken]` and `[AbpAutoValidateAntiforgeryToken]` attributes, just like the attributes explained above. `[AbpAutoValidateAntiforgeryToken]` is already added to the global filters, so you should do nothing to enable it for your application. + +ABP Framework also automates the following infrastructure; + +* Server side sets a **special cookie**, named `XSRF-TOKEN` by default, that is used make the antiforgery token value available to the browser. This is **done automatically** (by the [application configuration](Application-Configuration.md) endpoint). Nothing to do in the client side. +* In the client side, it reads the token from the cookie and sends it in the **HTTP header** (named `RequestVerificationToken` by default). This is implemented for all the supported UI types. +* Server side validates the antiforgery token **only for same and cross site requests** made by the browser. It bypasses the validation for non-browser clients. + +That's all. The systems works smoothly. + +## Configuration / Customization + +### AbpAntiForgeryOptions + +`AbpAntiForgeryOptions` is the main [options class](Options.md) to configure the ABP Antiforgery system. It has the following properties; + +* `TokenCookie`: Can be used to configure the cookie details. This cookie is used to store the antiforgery token value in the client side, so clients can read it and sends the value as the HTTP header. Default cookie name is `XSRF-TOKEN`, expiration time is 10 years (yes, ten years! It should be a value longer than the authentication cookie max life time, for the security). +* `AuthCookieSchemaName`: The name of the authentication cookie used by your application. Default value is `Identity.Application` (which becomes `AspNetCore.Identity.Application` on runtime). The default value properly works with the ABP startup templates. **If you change the authentication cookie name, you also must change this.** +* `AutoValidate`: The single point to enable/disable the ABP automatic antiforgery validation system. Default value is `true`. +* `AutoValidateFilter`: A predicate that gets a type and returns a boolean. ABP uses this predicate to check a controller type. If it returns false for a controller type, the controller is excluded from the automatic antiforgery token validation. +* `AutoValidateIgnoredHttpMethods`: A list of HTTP Methods to ignore on automatic antiforgery validation. Default value: "GET", "HEAD", "TRACE", "OPTIONS". These HTTP Methods are safe to skip antiforgery validation since they don't change the application state. + +If you need to change these options, do it in the `ConfigureServices` method of your [module](Module-Development-Basics.md). + +**Example: Configuring the AbpAntiForgeryOptions** + +```csharp +Configure(options => +{ + options.TokenCookie.Expiration = TimeSpan.FromDays(365); + options.AutoValidateIgnoredHttpMethods.Remove("GET"); + options.AutoValidateFilter = + type => !type.Namespace.StartsWith("MyProject.MyIgnoredNamespace"); +}); +``` + +This configuration; + +* Sets the antiforgery token expiration time to ~1 year. +* Enables antiforgery token validation for GET requests too. +* Ignores the controller types in the specified namespace. + +### AntiforgeryOptions + +`AntiforgeryOptions` is the standard [options class](Options.md) of the ASP.NET Core. **You can find all the information about this class in its [own documentation](https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery)**. + +`HeaderName` option is especially important for the ABP Framework point of view. Default value of this value is `RequestVerificationToken` and the clients uses this name while sending the token value in the header. So, if you change this option, you should also arrange your clients to align the change. If you don't have a good reason, leave it as default. + +### AbpValidateAntiForgeryToken Attribute + +If you disable the automatic validation or want to perform the validation for an endpoint that is not validated by default (for example, an endpoint with HTTP GET Method), you can use the `[AbpValidateAntiForgeryToken]` attribute for a **controller type or method** (action). + +**Example: Add `[AbpValidateAntiForgeryToken]` to a HTTP GET method** + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.AntiForgery; + +namespace MyCompanyName.MyProjectName.Controllers +{ + [Route("api/products")] + public class ProductController : AbpController + { + [HttpGet] + [AbpValidateAntiForgeryToken] + public async Task GetAsync() + { + //TODO: ... + } + } +} +``` + +### Angular UI + +Angular supports CSRF Token out of box, but the token header name is `X-XSRF-TOKEN`. Since ABP Framework follows the ASP.NET Core conventions, it changes this value to `RequestVerificationToken` in the core package. + +You don't need to make anything unless you need to change the `AntiforgeryOptions.HeaderName` as explained before. If you change it, remember to change the header name for the Angular application too. To do that, add an import declaration for the `HttpClientXsrfModule` into your root module. + +**Example: Change the header name to *MyCustomHeaderName*** + +```typescript +@NgModule({ + // ... + imports: [ + //... + HttpClientXsrfModule.withOptions({ + cookieName: 'XSRF-TOKEN', + headerName: 'MyCustomHeaderName' + }) + ], +}) +export class AppModule {} +``` diff --git a/docs/en/Caching.md b/docs/en/Caching.md index bb11b2b45a..042f13ffe0 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -1,154 +1,167 @@ # Caching -ABP framework extends ASP.NET Core's distributed caching system. +ABP Framework extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). -## Volo.Abp.Caching Package +## Installation -> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. +> This package is already installed by default with the [application startup template](Startup-Templates/Application.md). So, most of the time, you don't need to install it manually. -Volo.Abp.Caching is the core package of the caching system. Install it to your project using the package manager console (PMC): +[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching) is the main package of the caching system. You can install it a project using the add-package command of the [ABP CLI](CLI.md): ``` -Install-Package Volo.Abp.Caching +abp add-package Volo.Abp.Caching ``` -Then you can add **AbpCachingModule** dependency to your module: +You need to run this command on a command line terminal in a folder containing a `csproj` file (see [other options](https://abp.io/package-detail/Volo.Abp.Caching) to install). -```c# -using Volo.Abp.Modularity; -using Volo.Abp.Caching; +## Usage -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpCachingModule))] - public class MyModule : AbpModule - { - //... - } -} -``` - -## `IDistributedCache` Interface +### `IDistributedCache` Interface -ASP.NET Core defines the `IDistributedCache` interface to get/set cache values. But it has some difficulties: +ASP.NET Core defines the `IDistributedCache` interface to get/set the cache values. But it has some difficulties: * It works with **byte arrays** rather than .NET objects. So, you need to **serialize/deserialize** the objects you need to cache. * It provides a **single key pool** for all cache items, so; * You need to care about the keys to distinguish **different type of objects**. - * You need to care about the cache items of **different tenants** (see [multi-tenancy](Multi-Tenancy.md)). + * You need to care about the cache items of **different tenants** in a [multi-tenant](Multi-Tenancy.md) system. > `IDistributedCache` is defined in the `Microsoft.Extensions.Caching.Abstractions` package. That means it is not only usable for ASP.NET Core applications, but also available to **any type of applications**. -> Default implementation of the `IDistributedCache` interface is the `MemoryDistributedCache` which works **in-memory**. See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to **Redis** or another cache provider. +> Default implementation of the `IDistributedCache` interface is the `MemoryDistributedCache` which works **in-memory**. See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](Redis-Cache.md) document if you want to use the Redis as the distributed cache server. See [ASP.NET Core's distributed caching document](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more information. -## `IDistributedCache` Interface +### `IDistributedCache` Interface ABP framework defines the generic `IDistributedCache` interface in the [Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching/) package. `TCacheItem` is the type of the object stored in the cache. `IDistributedCache` solves the difficulties explained above; * It internally **serializes/deserializes** the cached objects. Uses **JSON** serialization by default, but can be overridden by replacing the `IDistributedCacheSerializer` service in the [dependency injection](Dependency-Injection.md) system. -* It automatically adds a **cache name** prefix to the cache keys based on the object type stored in the cache. Default cache name is the full name of the cache item class (`CacheItem` postfix is removed if your cache item class ends with it). You can use the `CacheName` attribute on the cache item class to set the cache name. -* It automatically adds the **current tenant id** to the cache key to distinguish cache items for different tenants (only works if your application is [multi-tenant](Multi-Tenancy.md)). Define `IgnoreMultiTenancy` attribute on the cache item class to disable this if you want to share the cached objects among all tenants in a multi-tenant application. -* Allows to define a **global cache key prefix** per application, so different applications can use their isolated key pools in a shared distributed cache source. +* It automatically adds a **cache name** prefix to the cache keys based on the object type stored in the cache. Default cache name is the full name of the cache item class (`CacheItem` postfix is removed if your cache item class ends with it). You can use the **`CacheName` attribute** on the cache item class to set the cache name. +* It automatically adds the **current tenant id** to the cache key to distinguish cache items for different tenants (if your application is [multi-tenant](Multi-Tenancy.md)). Define `IgnoreMultiTenancy` attribute on the cache item class to disable this if you want to share the cached objects among all tenants in a multi-tenant application. +* Allows to define a **global cache key prefix** per application, so different applications can use their isolated key pools in a shared distributed cache server. +* It **can tolerate errors** wherever possible and bypasses the cache. This is useful when you have temporary problems on the cache server. +* It has methods like `GetManyAsync` and `SetManyAsync` which significantly improve the performance on **batch operations**. -### Usage - -An example class to store an item in the cache: +**Example: Store Book names and prices in the cache** ````csharp -public class BookCacheItem +namespace MyProject { - public string Name { get; set; } + public class BookCacheItem + { + public string Name { get; set; } - public float Price { get; set; } + public float Price { get; set; } + } } ```` -You can inject and use the `IDistributedCache` service to get/set `BookCacheItem` objects. - -Example usage: +You can inject and use the `IDistributedCache` service to get/set `BookCacheItem` objects: ````csharp -public class BookService : ITransientDependency -{ - private readonly IDistributedCache _cache; - - public BookService(IDistributedCache cache) - { - _cache = cache; - } - - public async Task GetAsync(Guid bookId) - { - return await _cache.GetOrAddAsync( - bookId.ToString(), //Cache key - async () => await GetBookFromDatabaseAsync(bookId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - } +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; - private Task GetBookFromDatabaseAsync(Guid bookId) +namespace MyProject +{ + public class BookService : ITransientDependency { - //TODO: get from database + private readonly IDistributedCache _cache; + + public BookService(IDistributedCache cache) + { + _cache = cache; + } + + public async Task GetAsync(Guid bookId) + { + return await _cache.GetOrAddAsync( + bookId.ToString(), //Cache key + async () => await GetBookFromDatabaseAsync(bookId), + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) + } + ); + } + + private Task GetBookFromDatabaseAsync(Guid bookId) + { + //TODO: get from database + } } } ```` -* This sample service uses the `GetOrAddAsync()` method to get a book item from the cache. +* This sample service uses the `GetOrAddAsync()` method to get a book item from the cache. `GetOrAddAsync` is an additional method that was added by the ABP Framework to the standard ASP.NET Core distributed cache methods. * If the book was not found in the cache, it calls the factory method (`GetBookFromDatabaseAsync` in this case) to retrieve the book item from the original source. * `GetOrAddAsync` optionally gets a `DistributedCacheEntryOptions` which can be used to set the lifetime of the cached item. -Other methods of the `IDistributedCache` are same as ASP.NET Core's `IDistributedCache` interface, so you can refer [it's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). +`IDistributedCache` supports the same methods of the ASP.NET Core's standard `IDistributedCache` interface, so you can refer [it's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). -## `IDistributedCache` Interface +### `IDistributedCache` Interface -`IDistributedCache` interface assumes that the type of your cache key is `string` (so, you need to manually convert your key to string if you need to use a different kind of cache key). `IDistributedCache` can be used when your cache key type is not `string`. +`IDistributedCache` interface assumes that the type of your **cache key** is `string` (so, you need to manually convert your key to string if you need to use a different kind of cache key). While this is not a big deal, `IDistributedCache` can be used when your cache key type is not `string`. -### Usage - -An example class to store an item in the cache: +**Example: Store Book names and prices in the cache** ````csharp -public class BookCacheItem +using Volo.Abp.Caching; + +namespace MyProject { - public string Name { get; set; } + [CacheName("Books")] + public class BookCacheItem + { + public string Name { get; set; } - public float Price { get; set; } + public float Price { get; set; } + } } ```` -Example usage (assumes that your cache key type is `Guid`): +* This example uses the `CacheName` attribute for the `BookCacheItem` class to set the cache name. -````csharp -public class BookService : ITransientDependency -{ - private readonly IDistributedCache _cache; +You can inject and use the `IDistributedCache` service to get/set `BookCacheItem` objects: - public BookService(IDistributedCache cache) - { - _cache = cache; - } +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; - public async Task GetAsync(Guid bookId) - { - return await _cache.GetOrAddAsync( - bookId, //Guid type used as the cache key - async () => await GetBookFromDatabaseAsync(bookId), - () => new DistributedCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) - } - ); - } - private Task GetBookFromDatabaseAsync(Guid bookId) +namespace MyProject +{ + public class BookService : ITransientDependency { - //TODO: get from database + private readonly IDistributedCache _cache; + + public BookService(IDistributedCache cache) + { + _cache = cache; + } + + public async Task GetAsync(Guid bookId) + { + return await _cache.GetOrAddAsync( + bookId, //Guid type used as the cache key + async () => await GetBookFromDatabaseAsync(bookId), + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) + } + ); + } + private Task GetBookFromDatabaseAsync(Guid bookId) + { + //TODO: get from database + } } } ```` @@ -156,6 +169,8 @@ public class BookService : ITransientDependency * This sample service uses the `GetOrAddAsync()` method to get a book item from the cache. * Since cache explicitly implemented as using `Guid` as cache key, `Guid` value passed to `_cache_GetOrAddAsync()` method. +#### Complex Types as the Cache Key + `IDistributedCache` internally uses `ToString()` method of the key object to convert it to a string. If you need to use a complex object as the cache key, you need to override `ToString` method of your class. An example class that is used as a cache key: @@ -192,7 +207,66 @@ public class BookService : ITransientDependency } ```` -### DistributedCacheOptions +## Configuration + +### AbpDistributedCacheOptions + +`AbpDistributedCacheOptions` is the main [options class](Options.md) to configure the caching. + +**Example: Set the cache key prefix for the application** + +```csharp +Configure(options => +{ + options.KeyPrefix = "MyApp1"; +}); +``` + +> Write that code inside the `ConfigureServices` method of your [module class](Module-Development-Basics.md). + +#### Available Options + +* `HideErrors` (`bool`, default: `true`): Enables/disables hiding the errors on writing/reading values from the cache server. +* `KeyPrefix` (`string`, default: `null`): If your cache server is shared by multiple applications, you can set a prefix for the cache keys for your application. In this case, different applications can not overwrite each other's cache items. +* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): Used to set default distributed cache options (like `AbsoluteExpiration` and `SlidingExpiration`) used when you don't specify the options while saving cache items. Default value uses the `SlidingExpiration` as 20 minutes. + +## Error Handling + +When you design a cache for your objects, you typically try to get the value from cache first. If not found in the cache, you query the object from the **original source**. It may be located in a **database** or may require to perform an HTTP call to a remote server. + +In most cases, you want to **tolerate the cache errors**; If you get error from the cache server you don't want to cancel the operation. Instead, you silently hide (and log) the error and **query from the original source**. This is what the ABP Framework does by default. + +ABP's Distributed Cache [handle](Exception-Handling.md), log and hide errors by default. There is an option to change this globally (see the options below). + +In addition, all of the `IDistributedCache` (and `IDistributedCache`) methods have an optional `hideErrors` parameter, which is `null` by default. The global value is used if this parameter left as `null`, otherwise you can decide to hide or throw the exceptions for individual method calls. + +## Batch Operations + +ABP's distributed cache interfaces provide methods to perform batch get/set methods those improves the performance when you want to get or set multiple cache items in a single method call. + +* `SetManyAsync` and `SetMany` methods can be used to set multiple values to the cache. +* `GetManyAsync` and `GetMany` methods can be used to retrieve multiple values from the cache. + +> These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](Redis-Cache.md). If the provider doesn't support, it fallbacks to `SetAsync` and `GetAsync` methods (called once for each item). + +## Advanced Topics + +### Unit Of Work Level Cache + +Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect. + +`IDistributedCache<..>` methods gets an optional parameter, named `considerOuw`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**. + +### IDistributedCacheSerializer + +`IDistributedCacheSerializer` service is used to serialize and deserialize the cache items. Default implementation is the `Utf8JsonDistributedCacheSerializer` class that uses `IJsonSerializer` service to convert objects to [JSON](Json.md) and vice verse. Then it uses UTC8 encoding to convert the JSON string to a byte array which is accepted by the distributed cache. + +You can [replace](Dependency-Injection.md) this service by your own implementation if you want to implement your own serialization logic. + +### IDistributedCacheKeyNormalizer + +`IDistributedCacheKeyNormalizer` is implemented by the `DistributedCacheKeyNormalizer` class by default. It adds cache name, application cache prefix and current tenant id to the cache key. If you need a more advanced key normalization, you can [replace](Dependency-Injection.md) this service by your own implementation. -TODO +## See Also +* [Redis Cache](Redis-Cache.md) \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md new file mode 100644 index 0000000000..049171731b --- /dev/null +++ b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/POST.md @@ -0,0 +1,477 @@ +# Replacing Email Templates and Sending Emails + +## Introduction + +Hi, in this step by step article, we will send an email by using standard email template and then we will replace the standard email template with our new created template, thanks to [Text Templating System](https://docs.abp.io/en/abp/latest/Text-Templating#replacing-the-existing-templates) and [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). Let's start by explaining what these systems do. + +* ABP framework provides a strong and flexible [Text Templating System](https://docs.abp.io/en/abp/latest/Text-Templating). So, we can use the text templating system to create dynamic email contents on a template and a model. + +* In this article, we will use `StandardEmailTemplates.Message` as standard email template. Then we will create a new template and replace the standard email template with our new template by using [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). + +* The `Virtual File System` makes it possible to manage files that do not physically exist on the file system. That means we can override `StandardEmailTemplates.Message` template by changing it's path with our new template's path. + +## Creating the Solution + +> ABP Framework offers startup templates to get into the business faster. + +In this article, I will create a new startup template and perform the operations on this template. But if you already have a project you don't need to create a new startup template, you can implement the following steps to your existing project. (These steps can be applied to any project. (MVC, Angular etc.)) + +> If you have already a project you can skip this section. + +Before starting to development, we will create a solution named `TemplateReplace` (or whatever you want). We can create a new startup template by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) : + +````bash +abp new TemplateReplace +```` + +Our project boilerplate will be ready after the download is finished. Then, open the solution in the Visual Studio (or your favorite IDE). + +Run the `TemplateReplace.DbMigrator` application as below to create the database and seed initial data (which creates the admin user, admin role, permissions etc.). + +![db-migrator-1](db-migrator-1.jpg) + +* Right click to `TemplateReplace.DbMigrator` and choose the `Debug`. + +![db-migrator-2](db-migrator-2.jpg) + +* After that, click the `Start new instance` option to start the database migrations. + +![db-migrator-3](db-migrator-3.jpg) + +Then we can run the `TemplateReplace.Web` project to see our application working. + +> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ + +## Starting the Development + +First thing we need to do is, creating a email service to sending emails. ABP Framework provides `IEmailSender` service that is used to send emails. + +### Step - 1 + +Create an `Emailing` folder in the `TemplateReplace.Domain` project and add a class named `EmailService` inside of it. + +```csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.TextTemplating; + +namespace TemplateReplace.Emailing +{ + public class EmailService : ITransientDependency + { + private readonly IEmailSender _emailSender; + private readonly ITemplateRenderer _templateRenderer; + + public EmailService(IEmailSender emailSender, ITemplateRenderer templateRenderer) + { + _emailSender = emailSender; + _templateRenderer = templateRenderer; + } + + public async Task SendAsync(string targetEmail) + { + var emailBody = await _templateRenderer.RenderAsync( + StandardEmailTemplates.Message, + new + { + message = "ABP Framework provides IEmailSender service that is used to send emails." + } + ); + + await _emailSender.SendAsync( + targetEmail, + "Subject", + emailBody + ); + } + } +} +``` + +* To create an email content, we need to inject `ITemplateRenderer` and use the `RenderAsync` method to render a template. + +* We've used `StandardEmailTemplates.Message` as standart email template. This provides us a standard and simple message template to send mails. + +* The resulting email body should be like shown below: + +```html + + + + + + + ABP Framework provides IEmailSender service that is used to send emails. + + +``` + +### Step - 2 (Configuring Email Settings) + +* Now, we need to configure some email settings by following [settings documentation](https://docs.abp.io/en/abp/latest/Settings#setting-values-in-the-application-configuration). For achieve this, open the `appsettings.json` file under `TemplateReplace.Web` and configure your email settings in **settings** section like below. + +![appsettings.json](settings.jpg) + +* Here, I used Google's SMTP settings to send emails via Gmail. You can change these setting values by your need. + +> **Note:** If you want to use Google's SMTP server settings and send emails via Gmail, you should confirm [this](https://myaccount.google.com/u/0/lesssecureapps). + +### Step - 3 + +* After that we need to open `TemplateReplaceDomainModule.cs` file and change its contents as below to sending real-time emails. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using TemplateReplace.MultiTenancy; +using Volo.Abp.AuditLogging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.Emailing; +using Volo.Abp.FeatureManagement; +using Volo.Abp.Identity; +using Volo.Abp.IdentityServer; +using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.PermissionManagement.IdentityServer; +using Volo.Abp.SettingManagement; +using Volo.Abp.TenantManagement; + +namespace TemplateReplace +{ + [DependsOn( + typeof(TemplateReplaceDomainSharedModule), + typeof(AbpAuditLoggingDomainModule), + typeof(AbpBackgroundJobsDomainModule), + typeof(AbpFeatureManagementDomainModule), + typeof(AbpIdentityDomainModule), + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(AbpIdentityServerDomainModule), + typeof(AbpPermissionManagementDomainIdentityServerModule), + typeof(AbpSettingManagementDomainModule), + typeof(AbpTenantManagementDomainModule), + typeof(AbpEmailingModule) + )] + public class TemplateReplaceDomainModule : AbpModule + { + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var settingManager = context.ServiceProvider.GetService(); + //encrypts the password on set and decrypts on get + settingManager.SetGlobalAsync(EmailSettingNames.Smtp.Password, "your_password"); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.IsEnabled = MultiTenancyConsts.IsEnabled; + }); + + // #if DEBUG + // context.Services.Replace(ServiceDescriptor.Singleton()); + // #endif + } + } +} + +``` + +* `NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the standard log system, rather than actually sending the emails. This class can be useful especially in development time where you generally don't want to send real emails. Therefore ABP framework defined this by default. But in our case we want to send real emails, so we must remove these lines or we must take it to the comment line. + +* `Abp.Mailing.Smtp.Password` must be an encrypted value. Therefore we used `SettingManager` in here to set the password. It internally **encrypts** the values on set and **decrypts** on get. + +* After all these steps, whenever we want to send an email, we can do it by using our `EmailService` class. We can inject this class and invoke the `SendAsync` method to sending email where its needed. + +After sending the email we should see the template like below. + +![email-message](message.jpg) + +### Step - 4 (Defining New Template) + +* So far we've sent mail by using standard email template of ABP. But we may want to replace the email template with the new one. We can achieve this by following the `Text Templating` [documentation](https://docs.abp.io/en/abp/latest/Text-Templating#replacing-the-existing-templates). + +* In this article, I will create a email template by using free template generator named **Bee**. You can reach the free templates from [here](https://beefree.io/templates/free/). + +* When we find a template for our purpose, we can hover the link and click the **get started** button to edit the template. (I chose a template named "gdpr".) + +* Here, you can edit your template as below. (You can delete or add sections, edit texts, and so on.) + +![bee](bee.gif) + +> **Note:** After editing our template, we need to export it to reach our created template's content. You can see the **export** button top-right of the template editing page. + +* After choosing and editing our free template, we can create a new **email template** in our project. For this, create a folder named `Templates` under `Emailing` folder in `TemplateReplace.Domain` and add `EmailTemplate.tpl` file inside of it. And copy-paste the below content or your template's content. + +```tpl + + + + + + + + + + + + + + + + + + + +``` + +* Then we need to make the template file as "Embedded Resource". We can do this as below. + +* First right click to **EmailTemplate.tpl** and choose `Properties`. + +![embedded-resource](embedded-resource.jpg) + +* Then be sure about build action is **Embedded resource**. + +![embedded-resource-2](embedded-resource-2.jpg) + +### Step - 4 (Replacing the Email Template) + +* To replace the current email template with our new email template, we need to override it. To achieve this, create a class named `EmailTemplateDefinitionProvider` under `Emailing` folder in `TemplateReplace.Domain` and fill it with the below content. + +```csharp +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.TextTemplating; + +namespace TemplateReplace.Emailing +{ + public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider, ITransientDependency + { + public override void Define(ITemplateDefinitionContext context) + { + var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Message); + + emailLayoutTemplate + .WithVirtualFilePath( + "/Emailing/Templates/EmailTemplate.tpl", + isInlineLocalized: true + ); + } + } +} +``` + +* In here we've created a template definition provider class that gets the email layout template and change the virtual file path for the template. + +* This approach allows us to locate templates in any folder instead of the folder defined by the depended module. For more detail, check the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). + +### Step - 5 + +* Lastly, we need to configure the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). To do this open your `TemplateReplaceDomainModule.cs` in `TemplateReplace.Domain` and update the content as below. + +```csharp +using TemplateReplace.MultiTenancy; +using Volo.Abp.AuditLogging; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.Emailing; +using Volo.Abp.FeatureManagement; +using Volo.Abp.Identity; +using Volo.Abp.IdentityServer; +using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.PermissionManagement.IdentityServer; +using Volo.Abp.SettingManagement; +using Volo.Abp.TenantManagement; +using Volo.Abp.VirtualFileSystem; + +namespace TemplateReplace +{ + [DependsOn( + typeof(TemplateReplaceDomainSharedModule), + typeof(AbpAuditLoggingDomainModule), + typeof(AbpBackgroundJobsDomainModule), + typeof(AbpFeatureManagementDomainModule), + typeof(AbpIdentityDomainModule), + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(AbpIdentityServerDomainModule), + typeof(AbpPermissionManagementDomainIdentityServerModule), + typeof(AbpSettingManagementDomainModule), + typeof(AbpTenantManagementDomainModule), + typeof(AbpEmailingModule) + )] + public class TemplateReplaceDomainModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.IsEnabled = MultiTenancyConsts.IsEnabled; + }); + + //Add this configuration + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + } + } +} + +``` + +* And now when we send a new email, we should see our newly defined template as the message like below. + +![email-last](email-last.jpg) + +## Text Template Management + +* Generally, more than one e-mail is required in applications. We create email templates for **"password changes"** or **"welcome"** etc in our applications. In such cases, it is necessary to create different templates for each mail. ABP Commercial allows us to perform these operations on UI in a simple way. Text Template Management provides UI to easily create and manage email templates. + +![template-definitions](template-definitions.png) + +* ABP Commercial's [Text Template Management](https://commercial.abp.io/modules/Volo.TextTemplateManagement) module is really fascinating. It makes it super easy to stores and edits template contents. We can list all templates on a page, editing them, localizing them, and so on. + +![inline-content](inline-content.png) + +* ABP Commercial's text template management module, allows us to modify a template through the UI. + +* I highly recommend you to [check it out](https://commercial.abp.io/modules/Volo.TextTemplateManagement). + +## References + +* [Text Templating](https://docs.abp.io/en/abp/latest/Text-Templating) +* [Emailing](https://docs.abp.io/en/abp/latest/Emailing) +* [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System) \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/bee.gif b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/bee.gif new file mode 100644 index 0000000000..15a3a01e65 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/bee.gif differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-1.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-1.jpg new file mode 100644 index 0000000000..8a0c4c345b Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-1.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-2.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-2.jpg new file mode 100644 index 0000000000..4adc9614c3 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-2.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-3.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-3.jpg new file mode 100644 index 0000000000..16b365b8a5 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/db-migrator-3.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/email-last.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/email-last.jpg new file mode 100644 index 0000000000..898d75568f Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/email-last.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/embedded-resource-2.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/embedded-resource-2.jpg new file mode 100644 index 0000000000..1abdba48cb Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/embedded-resource-2.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/embedded-resource.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/embedded-resource.jpg new file mode 100644 index 0000000000..b03739e6c0 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/embedded-resource.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/inline-content.png b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/inline-content.png new file mode 100644 index 0000000000..cbbf188f37 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/inline-content.png differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/message.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/message.jpg new file mode 100644 index 0000000000..fe80c2ac0c Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/message.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/settings.jpg b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/settings.jpg new file mode 100644 index 0000000000..e60fdcc2db Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/settings.jpg differ diff --git a/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/template-definitions.png b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/template-definitions.png new file mode 100644 index 0000000000..aaf46b353e Binary files /dev/null and b/docs/en/Community-Articles/2020-09-09-Replacing-Email-Template-and-Sending-Emails/template-definitions.png differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md new file mode 100644 index 0000000000..b286bb509d --- /dev/null +++ b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md @@ -0,0 +1,122 @@ +# How to Setup Azure Active Directory and Integrate Abp Angular Application + +This guide demonstrates how to register an application to Azure Active Directory and integrate AzureAD to an ABP angular application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. + +## Authentication Flow + +Abp angular applications use **Authentication Code with PKCE** (specs [here](https://tools.ietf.org/html/rfc7636)) which is the most suitable flow for spa applications by the time this article is written since implicit flow is deprecated. + +The most common question is; + +> Where to put OpenId connection code in angular project? + +The answer is, **you don't**. Abp angular application is integrated with backend code (HttpApi.Host project) where it loads the configurations, **permissions** etc. For none-tiered angular applications, **HttpApi.Host** project also has IdentityServer4 embedded; also serving as **Authorization Server**. Angular application authentication flow is shown below. + +auth-diagram + +> What if I want Azure AD as my authorization server and not IdentityServer? + +This means your application will be using AzureAD user store for authentication. By registering both angular app and HttpApi to AzureAD, authentication might work but **authorization won't**. Users need to be registered to Abp identity system for auditing, permissions etc. So the flow should be 3rd party registration. + +## Setting up OpenId Connection + +Lets start with adding OpenId connection. Open the **HttpApiHostModule.cs** and update the **ConfigureAuthentication** method as below: + +```csharp +JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); +JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + +context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "NonTieredAngular"; + options.JwtBackChannelHandler = new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + }) + .AddOpenIdConnect("AzureOpenId", "Azure AD OpenId", options => + { + options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; + options.ClientId = configuration["AzureAd:ClientId"]; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.CallbackPath = configuration["AzureAd:CallbackPath"]; + options.ClientSecret = configuration["AzureAd:ClientSecret"]; + options.RequireHttpsMetadata = false; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + options.Scope.Add("email"); + }); +``` + +> If you are using tiered (separate identity server) application, open **IdentityServerModule.cs** and add the OpenIdConnection manually to **ConfigureServices** method as following: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + + context.Services.AddAuthentication() + .AddOpenIdConnect("AzureOpenId", "Azure AD OpenId", options => + { + ... // Same configuration above + }); +``` + +Now we can add AzureAD settings. Open the **appsettings.json** located in **HttpApi.Host** project (or **IdentityServer** project if you are using tiered application). Add the following; + +```csharp +"AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Domain": "domain.onmicrosoft.com", + "CallbackPath": "/signin-azuread-oidc", + "ClientSecret": "" +}, +``` + +Keep on mind that **App.SelfUrl** + **AzureAd.CallbackPath** will be used in AzureAD app registration. We'll update the AzureAd settings after registering the application in Azure Portal. + +## Setting up Azure Active Directory + +Navigate to Manage Azure Active Directory in [azure portal](https://portal.azure.com/). Go to **App registrations** on left side menu and hit New registration. + +![azure-app-registration](azure-app-registration.jpg) + +Enter a name for your application and **App.SelfUrl** + **AzureAd.CallbackPath** as redirect uri then register. + +azure-app-register + +Now navigate to **Authentication** on the left menu and enable **ID tokens**. + +![azure-app-authentication](azure-app-authentication.jpg) + +We also need to set a client secret. Navigate to **Certificates & secrets** menu on the left and create a new client secret. + +![azure-app-secret](azure-app-secret.jpg) + +**Copy** the secret and update your appsettings.json **AzureAd.ClientSecret** field. + +![azure-app-overview](azure-app-overview.jpg) + +Also update your **AzureAd.TenantId** and **AzureAd.ClientId** fields with the information located under **Overview** menu. And that's all. + +Next time you hit login, you should be seeing login screen enabled Azure AD like below. + +![app-login](app-login.JPG) + +# FAQ + +* I am getting errors when trying to login to AzureAD. + * You can check [this article FAQ](https://community.abp.io/articles/how-to-use-the-azure-active-directory-authentication-for-mvc-razor-page-applications-4603b9cf) which covers many errors. + + +* But I don't want my users to see default login screen. I want my users to login **only** from AzureAD. + * You can **mimic** this behaviour by customizing the login page and instantly trigger Azure AD provider click. For more info, you can check [this article](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd). diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/app-login.JPG b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/app-login.JPG new file mode 100644 index 0000000000..94500a4f15 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/app-login.JPG differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/auth-diagram.jpeg b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/auth-diagram.jpeg new file mode 100644 index 0000000000..dec4a66a17 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/auth-diagram.jpeg differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-authentication.jpg b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-authentication.jpg new file mode 100644 index 0000000000..5061118aa3 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-authentication.jpg differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-overview.jpg b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-overview.jpg new file mode 100644 index 0000000000..11053062e2 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-overview.jpg differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-register.JPG b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-register.JPG new file mode 100644 index 0000000000..68c180cea0 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-register.JPG differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-registration.jpg b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-registration.jpg new file mode 100644 index 0000000000..12f4eb36e2 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-registration.jpg differ diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-secret.jpg b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-secret.jpg new file mode 100644 index 0000000000..9cf2009eda Binary files /dev/null and b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/azure-app-secret.jpg differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/Using-DevExtremeAngularComponents-With-The-ABP-Framework.md b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/Using-DevExtremeAngularComponents-With-The-ABP-Framework.md new file mode 100644 index 0000000000..c20b796073 --- /dev/null +++ b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/Using-DevExtremeAngularComponents-With-The-ABP-Framework.md @@ -0,0 +1,317 @@ +# Using DevExtreme Angular Components With the ABP Framework + +In this article, I will show you how to integrate the [DevExpress Angular components](https://js.devexpress.com/Documentation/Guide/Angular_Components/DevExtreme_Angular_Components/) to a project created using the ABP Framework startup templates. Then I will use the [DataGrid](https://js.devexpress.com/Documentation/Guide/Widgets/DataGrid/Overview/) component to show a list of users on the UI. + +## Create the Project + +Let's create a new web application with the Angular UI using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI#new): + +```shell +abp new DevExtremeAngular -u angular +``` + +> For detail information about how to generate and start up a project, please refer to the [official docs](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No). For the scope of this post, we will not go into details of the backend applications. + +## Running the Solution + +### The Server Side + +Server side contains multiple projects in the solution: + +![A screenshot showing DevExtremeAngular.DbMigrator and DevExtremeAngular.HttpApi.Host](project-setup.png) + +Run following projects in order; + +* Run `DevExtremeAngular.DbMigrator` to create the database and seed the initial data. +* Run `DevExtremeAngular.HttpApi.Host` project to make the backend API up & running. + +### The Angular Application + +Open a command line terminal and navigate to `angular` folder then run `yarn` or `npm install` based on which package you are using. + +After installation process is done, you can start your angular project by running `yarn start` or `npm start`. This command should serve the application and open the application in your default browser. If it doesn't open, you can navigate to http://localhost:4200 in your browser: + +![A screenshot showing localhost is running](localhost-running.png) + +You can login to the application by using following credentials: + +> _Default admin username is **admin** and password is **1q2w3E\***_ + +![A screenshot showing login page](login-screen.png) + +After successful login, you should be redirected to home page. + +## Install DevExtreme + +You can follow [the guide](https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Add_DevExtreme_to_an_Angular_CLI_Application/) provided by **DevExtreme** team or apply the following steps. + +* `npm install devextreme devextreme-angular` or `yarn add devextreme devextreme-angular` +* Import given two following styles in `angular.json` file: + +```javascript + // ... + "styles": [ + // ... + "src/styles.scss", + + "node_modules/devextreme/dist/css/dx.common.css", + "node_modules/devextreme/dist/css/dx.light.css" + ] +``` + +* Add `dx-viewport` to classes of `body` in `index.html` + +```html + + +
+
+ +``` + +After completing these steps, you need to restart the angular application. + +## Create a lazy Angular Module for DevExtreme Demo + +Let's create a module which will be loaded lazily. + +Open up a terminal and navigate to `angular` to run following command. + +```shell +ng g m dev-extreme --route dev-extreme --module app +``` + +...or with `npx`, if you do not have `angular-cli` installed... + +```shell +npx ng g m dev-extreme --route dev-extreme --module app +``` + +Your terminal should log the following output: + +```shell +CREATE src/app/dev-extreme/dev-extreme-routing.module.ts (361 bytes) +CREATE src/app/dev-extreme/dev-extreme.module.ts (379 bytes) +CREATE src/app/dev-extreme/dev-extreme.component.scss (0 bytes) +CREATE src/app/dev-extreme/dev-extreme.component.html (26 bytes) +CREATE src/app/dev-extreme/dev-extreme.component.spec.ts (655 bytes) +CREATE src/app/dev-extreme/dev-extreme.component.ts (295 bytes) +UPDATE src/app/app-routing.module.ts (362 bytes) +``` + +The Angular CLI has created a module and configured it to lazy-load at `/dev-extreme` path. + +The last step to be able to see our newly created module in the browser, open `route.provider.ts` and edit the array being added into the routes. + +```typescript + // ... + routes.add([ + { + path: '/', + name: '::Menu:Home', + iconClass: 'fas fa-home', + order: 1, + layout: eLayoutType.application, + }, + { + path: '/dev-extreme', + name: 'Dev Extreme', + order: 2, + layout: eLayoutType.application, + }, + ]); + // ... +``` + +After completing the steps above, you should be able to see `Dev Extreme` on the header and when you click on it, you should be redirected to `/dev-extreme` page and see the following message on the screen. + +![A screenshot showing a page that says dev-extreme works!](dev-extreme-page.png) + +## Display users on the dev-extreme page + +For this demo, we will list users on the screen. We already have `admin` as our first user. + +Let's add couple of more to the list in `Administration -> Identity Management -> Users` page. + +![A screenshot showing users page after adding couple of users](users.png) + +Now we are ready to fetch our users and display them on `/dev-extreme` page. + +Firstly, let's create a service for our component. + +Navigate to the `dev-extreme` folder and run following command. If you run this command at the root, the service will be generated next to `app.module.ts` + +```shell +ng g s dev-extreme +``` + +Following files should be created + +```shell +CREATE src/app/dev-extreme/dev-extreme.service.spec.ts (378 bytes) +CREATE src/app/dev-extreme/dev-extreme.service.ts (139 bytes) +``` + +Let's import and inject `IdentityService` as dependency in `dev-extreme.service.ts`. After then, let's create a stream called `users$` to retrieve the users. + +`identityService.getUsers` returns `ABP.PagedResponse` which contains two fields, `items` and `totalCount`. We are only interested in `items` for now. + +When we apply the steps described above, the final version of `dev-extreme.service` should be as follows + +```typescript +import { Injectable } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { IdentityService } from '@abp/ng.identity'; + +@Injectable({ + providedIn: 'root', +}) +export class DevExtremeService { + users$ = this.service.getUsers().pipe(map((result) => result.items)); + + constructor(private service: IdentityService) {} +} +``` + +Now we can simply inject `DevExtremeService` as public and utilize `users$` stream in `dev-extreme.component.ts` as follows: + +```typescript +import { Component } from '@angular/core'; +import { DevExtremeService } from './dev-extreme.service'; + +@Component({ + selector: 'app-dev-extreme', + templateUrl: './dev-extreme.component.html', + styleUrls: ['./dev-extreme.component.scss'], +}) +export class DevExtremeComponent { + constructor(public service: DevExtremeService) {} +} +``` + +And use it within `dev-extreme.component.html` + +```html + +
    +
  • + {{ user.name }} +
  • +
+
+``` + +This should list names of the users on the screen + +![A screenshot showing list users on the dev extreme page](users-on-dev-extreme.png) + +## Use DxDataGrid to list the users + +You can take a look at [demo](https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/ColumnCustomization/Angular/Light/) provided by **DevExtreme** team or apply the following steps. + +Now, our application is ready to use `dx-data-grid` in `dev-extreme.component.ts` + +Firstly, we need to import `DxDataGridModule` in our module as follows. + +```typescript +// ... + +import { DxDataGridModule } from 'devextreme-angular'; + +@NgModule({ + // ... + imports: [ + // ... + DxDataGridModule + ], +}) +export class DevExtremeModule {} +``` + +At this point `dx-data-grid` is avaliable within our module and we can use it in our template. + +Change `dev-extreme.component.html` to the following + +```html + + + +``` + +It should display a table on the screen + +![A screenshot displaying dev extreme data grid with lots of columns](devextreme-first.png) + +Since, we did not specify any columns, `dx-data-grid` displayed every column avaliable. Let's pick some columns to make it more readable. + +Change `dev-extreme.component.html` to the following: + +```html + + + + + + + + + +``` + +which will display following table on the screen + +![A screenshot displaying dev extreme data grid with these columns: username, name, surname, email and phone number](devextreme-second.png) + +We can also utilize `abpLocalization` pipe to translate the headers of the table. To use `abpLocalization` pipe in our templates, we need to import `CoreModule` from `@abp/ng.core` into our module. + +```typescript +import { CoreModule } from '@abp/ng.core'; + +@NgModule({ + // ... + imports: [ + // ... + CoreModule + ], +}) +export class DevExtremeModule {} +``` + +And change the template to the following: + +```html + + + + + + + + + +``` + +The headers should change when a new language is selected; + +![A gif showing the headers of the table getting translated into the chosen language](devextreme-final.gif) + +## Conclusion + +In this article, we have seen how to integrate `DevExtreme` angular components into a project generated by `ABP CLI`. + +You can download source code of [the demo here](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Angular). diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/dev-extreme-page.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/dev-extreme-page.png new file mode 100644 index 0000000000..bd1b54eb3e Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/dev-extreme-page.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-final.gif b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-final.gif new file mode 100644 index 0000000000..5a39c443f0 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-final.gif differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-first.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-first.png new file mode 100644 index 0000000000..1019f8421d Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-first.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-second.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-second.png new file mode 100644 index 0000000000..24d147d57c Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/devextreme-second.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/localhost-running.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/localhost-running.png new file mode 100644 index 0000000000..f1c9d0707e Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/localhost-running.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/login-screen.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/login-screen.png new file mode 100644 index 0000000000..76aeec2599 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/login-screen.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/project-setup.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/project-setup.png new file mode 100644 index 0000000000..b613d45378 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/project-setup.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/users-on-dev-extreme.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/users-on-dev-extreme.png new file mode 100644 index 0000000000..4ff9289a68 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/users-on-dev-extreme.png differ diff --git a/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/users.png b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/users.png new file mode 100644 index 0000000000..87d86a0300 Binary files /dev/null and b/docs/en/Community-Articles/2020-09-28-Using-DevExtremeAngularComponents-With-The-ABP-Framework/users.png differ diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/How-To-Add-Custom-Property-To-The-User-Entity.md b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/How-To-Add-Custom-Property-To-The-User-Entity.md new file mode 100644 index 0000000000..3e8a3e9da2 --- /dev/null +++ b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/How-To-Add-Custom-Property-To-The-User-Entity.md @@ -0,0 +1,160 @@ +# How to Add Custom Properties to the User Entity + +## Introduction + +In this step-by-step article, I will explain how you can customize the user entity class, which is available in every web application you create using the ABP framework, according to your needs. When you read this article, you will learn how to override the services of built-in modules, extend the entities, extend data transfer objects and customize the user interface in the applications you develop using the ABP framework. + +You can see the screenshots below which we will reach at the end of the article. + +![custom-identity-user-list](./custom-identity-user-list.png) + +![new-user](./new-user.png) + +## Preparing the Project + +### Startup template and the initial run + +Abp Framework offers startup templates to get into the work faster. We can create a new startup template using Abp CLI: + +`abp new CustomizeUserDemo` + +> In this article, I will go through the MVC application, but it will work also in the [Angular](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No) application. + +After the download is finished, we can run **CustomizeUserDemo.DbMigrator** project to create the database migrations and seed the initial data (admin user, role, etc). Then we can run `CustomizeUserDemo.Web` to see that our application is working. + +> Default admin username is **admin** and password is **1q2w3E\*** + +![initial-project](./initial-project.png) + +In this article, we will go through a scenario together and find the solutions to our questions through this scenario. However, since the scenario is not a real-life scenario, it may be strange, please don't get too about this issue :) + +## Step-1 + +Add two new properties to the `AppUser` in the Users folder of the **CustomizeUserDemo.Domain** project as follows: + +```csharp +public string Title { get; protected set; } + +public int Reputation { get; protected set; } +``` + +## Step-2 + +Create the Users folder in the **CustomizeUserDemo.Domain.Shared** project, create the class `UserConsts` inside the folder and update the class you created as below: + +```csharp +public static class UserConsts +{ + public const string TitlePropertyName = "Title"; + + public const string ReputationPropertyName = "Reputation"; + + public const int MaxTitleLength = 64; + + public const double MaxReputationValue = 1_000; + + public const double MinReputationValue = 1; +} +``` + +## Step-3 + +Update the `CustomizeUserDemoEfCoreEntityExtensionMappings` class in the **CustomizeUserDemo.EntityFramework** project in the EntityFrameworkCore folder as below: + +```csharp +public static class CustomizeUserDemoEfCoreEntityExtensionMappings +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + CustomizeUserDemoGlobalFeatureConfigurator.Configure(); + CustomizeUserDemoModuleExtensionConfigurator.Configure(); + + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance + .MapEfCoreProperty( + nameof(AppUser.Title), + (entityBuilder, propertyBuilder) => + { + propertyBuilder.IsRequired(); + propertyBuilder.HasMaxLength(UserConsts.MaxTitleLength); + } + ).MapEfCoreProperty( + nameof(AppUser.Reputation), + (entityBuilder, propertyBuilder) => + { + propertyBuilder.HasDefaultValue(UserConsts.MinReputationValue); + } + ); + }); + } +} +``` + +This class can be used to map these extra properties to table fields in the database. Please read [this](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities) article to improve your understanding of what we are doing. + +So far, we have added our extra features to the `User` entity and matched these features with the `ef core`. + +Now we need to add migration to see what has changed in our database. This for, open the Package Manager Console (PMC) under the menu Tools > NuGet Package Manager. + +![nuget-package-manager](./nuget-package-manager.png) + +Select the **CustomizeUserDemo.EntityFramework.DbMigrations** as the **default project** and execute the following command: + +```bash +Add-Migration "Updated-User-Entity" +``` + +![added-new-migration](./added-new-migration.png) + +This will create a new migration class inside the `Migrations` folder of the **CustomizeUserDemo.EntityFrameworkCore.DbMigrations** project. + +> If you are using another IDE than the Visual Studio, you can use `dotnet-ef` tool as [documented here](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration). + +Finally, run the **CustomizeUserDemo.DbMigrator** project to update the database. + +When we updated the database, you can see that the `Title` and `Reputation` columns are added to the `Users` table. + +![user-table](./user-table.png) + +## Step-4 +Open the `CustomizeUserDemoModuleExtensionConfigurator` in the **CustomizeUserDemo.Domain.Shared** project, and change the contents of the `ConfigureExtraProperties` method as shown below: +```csharp +private static void ConfigureExtraProperties() +{ + ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity => + { + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( + UserConsts.TitlePropertyName, + options => + { + options.Attributes.Add(new RequiredAttribute()); + options.Attributes.Add( + new StringLengthAttribute(UserConsts.MaxTitleLength) + ); + } + ); + user.AddOrUpdateProperty( + UserConsts.ReputationPropertyName, + options => + { + options.DefaultValue = UserConsts.MinReputationValue; + options.Attributes.Add( + new RangeAttribute(UserConsts.MinReputationValue, UserConsts.MaxReputationValue) + ); + } + ); + }); + }); +} +``` + +That's it. Now let's run the application and look at the Identity user page. You can also try to edit and recreate a record if you want, it will work even though we haven't done anything extra. Here is the magic code behind ABP framework. + +If there is a situation you want to add, you can click the contribute button or make a comment. Also, if you like the article, don't forget to share it :) + +Happy coding :) diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/added-new-migration.png b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/added-new-migration.png new file mode 100755 index 0000000000..d459fdeed2 Binary files /dev/null and b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/added-new-migration.png differ diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/custom-identity-user-list.png b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/custom-identity-user-list.png new file mode 100644 index 0000000000..896ed947aa Binary files /dev/null and b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/custom-identity-user-list.png differ diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/initial-project.png b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/initial-project.png new file mode 100755 index 0000000000..6e08833853 Binary files /dev/null and b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/initial-project.png differ diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/new-user.png b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/new-user.png new file mode 100644 index 0000000000..d3a5c66198 Binary files /dev/null and b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/new-user.png differ diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/nuget-package-manager.png b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/nuget-package-manager.png new file mode 100755 index 0000000000..680bc9d2e7 Binary files /dev/null and b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/nuget-package-manager.png differ diff --git a/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/user-table.png b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/user-table.png new file mode 100755 index 0000000000..5bb71b8325 Binary files /dev/null and b/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/user-table.png differ diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index d0d8c8a8a1..b8706eea61 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -162,6 +162,41 @@ This example class inherits from the `IdentityUserManager` [domain service](Doma Check the [localization system](Localization.md) to learn how to localize the error messages. +### Example: Overriding a Controller + +````csharp +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.Account; +using Volo.Abp.DependencyInjection; + +namespace MyProject.Controllers +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(AccountController))] + public class MyAccountController : AccountController + { + public MyAccountController(IAccountAppService accountAppService) + : base(accountAppService) + { + + } + + public override async Task SendPasswordResetCodeAsync( + SendPasswordResetCodeDto input) + { + Logger.LogInformation("Your custom logic..."); + + await base.SendPasswordResetCodeAsync(input); + } + } +} +```` + +This example replaces the `AccountController` (An API Controller defined in the [Account Module](Modules/Account.md)) and overrides the `SendPasswordResetCodeAsync` method. + +**`[ExposeServices(typeof(AccountController))]` is essential** here since it registers this controller for the `AccountController` in the dependency injection system. `[Dependency(ReplaceServices = true)]` is also recommended to clear the old registration (even the ASP.NET Core DI system selects the last registered one). + ### Overriding Other Classes Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index 62021452bd..626719b2fc 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -102,9 +102,58 @@ namespace Acme.BookStore ```` * [Inject](Dependency-Injection.md) the `IDataFilter` service to your class. -* Use the `Disable` method within a `using` statement to create a code block where the `ISoftDelete` filter is disabled inside it (Always use it inside a `using` block to guarantee that the filter is reset to its previous state). +* Use the `Disable` method within a `using` statement to create a code block where the `ISoftDelete` filter is disabled inside it. -`IDataFilter.Enable` method can be used to enable a filter. `Enable` and `Disable` methods can be used in a nested way to define inner scopes. +In addition to the `Disable()` method; + +* `IDataFilter.Enable()` method can be used to enable a filter. `Enable` and `Disable` methods can be used in a **nested** way to define inner scopes. + +* `IDataFilter.IsEnabled()` can be used to check whether a filter is currently enabled or not. + +> Always use the `Disable` and `Enable` methods it inside a `using` block to guarantee that the filter is reset to its previous state. + +### The Generic IDataFilter Service + +`IDataFilter` service has a generic version, `IDataFilter` that injects a more restricted and explicit data filter based on the filter type. + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class MyBookService : ITransientDependency + { + private readonly IDataFilter _softDeleteFilter; + private readonly IRepository _bookRepository; + + public MyBookService( + IDataFilter softDeleteFilter, + IRepository bookRepository) + { + _softDeleteFilter = softDeleteFilter; + _bookRepository = bookRepository; + } + + public async Task> GetAllBooksIncludingDeletedAsync() + { + //Temporary disable the ISoftDelete filter + using (_softDeleteFilter.Disable()) + { + return await _bookRepository.GetListAsync(); + } + } + } +} +```` + +* This usage determines the filter type while injecting the `IDataFilter` service. +* In this case you can use the `Disable()` and `Enable()` methods without specifying the filter type. ## AbpDataFilterOptions diff --git a/docs/en/Distributed-Event-Bus-Kafka-Integration.md b/docs/en/Distributed-Event-Bus-Kafka-Integration.md index e4796adf55..57a90c4b83 100644 --- a/docs/en/Distributed-Event-Bus-Kafka-Integration.md +++ b/docs/en/Distributed-Event-Bus-Kafka-Integration.md @@ -89,7 +89,7 @@ Defining multiple connections is allowed. In this case, you can specify the conn } ```` -This allows you to use multiple RabbitMQ server in your application, but select one of them for the event bus. +This allows you to use multiple Kafka cluster in your application, but select one of them for the event bus. You can use any of the [ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html) properties as the connection properties. @@ -164,4 +164,4 @@ Configure(options => }); ```` -Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. \ No newline at end of file +Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. diff --git a/docs/en/Distributed-Event-Bus-Rebus-Integration.md b/docs/en/Distributed-Event-Bus-Rebus-Integration.md new file mode 100644 index 0000000000..7499c1d837 --- /dev/null +++ b/docs/en/Distributed-Event-Bus-Rebus-Integration.md @@ -0,0 +1,65 @@ +# Distributed Event Bus Rebus Integration + +> This document explains **how to configure the [Rebus](http://mookid.dk/category/rebus/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system + +## Installation + +Use the ABP CLI to add [Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) NuGet package to your project: + +* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. +* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Rebus` package. +* Run `abp add-package Volo.Abp.EventBus.Rebus` command. + +If you want to do it manually, install the [Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusRebusModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. + +## Configuration + +You can configure using the standard [configuration system](Configuration.md), like using the [options](Options.md) classes. + +### The Options Classes + +`AbpRebusEventBusOptions` classe can be used to configure the event bus options for the Rebus. + +You can configure this options inside the `PreConfigureServices` of your [module](Module-Development-Basics.md). + +**Example: Minimize configuration** + +```csharp +PreConfigure(options => +{ + options.InputQueueName = "eventbus"; +}); +``` + +Rebus has many options, you can use the `Configurer` property of `AbpRebusEventBusOptions` class to configure. + +Default events are **stored in memory**. See the [rebus document](https://github.com/rebus-org/Rebus/wiki/Transport) for more details. + +**Example: Configure the store** + +````csharp +PreConfigure(options => +{ + options.InputQueueName = "eventbus"; + options.Configurer = rebusConfigurer => + { + rebusConfigurer.Transport(t => t.UseMsmq("eventbus")); + rebusConfigurer.Subscriptions(s => s.UseJsonFile(@"subscriptions.json")); + }; +}); +```` + +You can use the `Publish` properpty of `AbpRebusEventBusOptions` class to change the publishing method + +**Example: Configure the event publishing** + +````csharp +PreConfigure(options => +{ + options.InputQueueName = "eventbus"; + options.Publish = async (bus, type, data) => + { + await bus.Publish(data); + }; +}); +```` diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index b1444d8eba..02216c7ac8 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -8,7 +8,8 @@ Distributed event bus system provides an **abstraction** that can be implemented * `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider. * `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it. -* `KafkaDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it. +* `KafkaDistributedEventBus` implements the distributed event bus with the [Kafka](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it. +* `RebusDistributedEventBus` implements the distributed event bus with the [Rebus](http://mookid.dk/category/rebus/). See the [Rebus integration document](Distributed-Event-Bus-Rebus-Integration.md) to learn how to configure it. Using a local event bus as default has a few important advantages. The most important one is that: It allows you to write your code compatible to distributed architecture. You can write a monolithic application now that can be split into microservices later. It is a good practice to communicate between bounded contexts (or between application modules) via distributed events instead of local events. @@ -44,7 +45,7 @@ namespace AbpDemo public virtual async Task ChangeStockCountAsync(Guid productId, int newCount) { await _distributedEventBus.PublishAsync( - new StockCountChangedEvent + new StockCountChangedEto { ProductId = productId, NewCount = newCount @@ -300,4 +301,4 @@ namespace AbpDemo } ```` -This example uses the `AutoMap` attribute of the AutoMapper to configure the mapping. You could create a profile class instead. Please refer to the AutoMapper document for more options. \ No newline at end of file +This example uses the `AutoMap` attribute of the AutoMapper to configure the mapping. You could create a profile class instead. Please refer to the AutoMapper document for more options. diff --git a/docs/en/Entity-Framework-Core-SQLite.md b/docs/en/Entity-Framework-Core-SQLite.md index 62a8ef815d..2a13bf530c 100644 --- a/docs/en/Entity-Framework-Core-SQLite.md +++ b/docs/en/Entity-Framework-Core-SQLite.md @@ -23,6 +23,16 @@ Find `UseSqlServer()` calls in your solution, replace with `UseSqlite()`. Check SQLite connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/sqlite/ ) for details of SQLite connection string options. +An example connection string is + +``` +{ + "ConnectionStrings": { + "Default": "Filename=./MySQLiteDBFile.sqlite" + } +} +``` + You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure. ## Re-Generate the Migrations @@ -38,4 +48,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/Getting-Started-React-Native.md b/docs/en/Getting-Started-React-Native.md index ec27d09463..a06257fec9 100644 --- a/docs/en/Getting-Started-React-Native.md +++ b/docs/en/Getting-Started-React-Native.md @@ -1,10 +1,57 @@ # Getting Started with the React Native +````json +//[doc-params] +{ + "Tiered": ["No", "Yes"] +} +```` + ABP platform provide basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated to your ABP based backends**. -When you **create a new application** as described in the [getting started document](Getting-Started.md), you have to add `-m react-native` option to include `react-native` project in your solution. +![React Native gif](./images/react-native-introduction.gif) + +## How to Prepare Development Environment + +Please follow the steps below to prepare your development environment for React Native. + +1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js v12 or v14 installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. +2. **[Optional] Install Yarn:** You may install Yarn v1 (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v6 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. +3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to React Native projects. +4. **Install an Emulator:** React Native applications need an Android emulator or an iOS simulator to run on your OS. See the [Android Studio Emulator](https://docs.expo.io/workflow/android-simulator/) or [iOS Simulator](https://docs.expo.io/workflow/ios-simulator/) on expo.io documentation to learn how to set up an emulator. + + +## How to Start a New React Native Project + +You have multiple options to initiate a new React Native project that works with ABP: + +### 1. Using ABP CLI + +ABP CLI is probably the most convenient and flexible way to initiate an ABP solution with a React Native application. Simply [install the ABP CLI](../../CLI.md) and run the following command in your terminal: + +```shell +abp new MyCompanyName.MyProjectName -csf -u -m react-native +``` + +> To see further options in the CLI, please visit the [CLI manual](../../CLI.md). + +This command will prepare a solution with an **Angular** or an **MVC** (depends on your choice), a **.NET Core**, and a **React Native** project in it. + +### 2. Direct Download + +You may [download a solution scaffold directly on ABP.io](https://abp.io/get-started) if you are more comfortable with GUI or simply want to try ABP without installing the CLI. -## Configure Your Local IP Address +Please do the following: + +1. Click on the "DIRECT DOWNLOAD" tab. +2. Fill out the short form about your project. +3. Click on the "Create now" button. + +...and a customized download will start in a few seconds. + +## How to Configure & Run the Backend + +> React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application on your **local IP address**. @@ -25,19 +72,14 @@ A React Native application running on an Android emulator or a physical phone ** {{ end }} -## Run the Server Application - Run the backend application as described in the [getting started document](Getting-Started.md). -> React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. -Go to the `react-native` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work): +## How to Configure & Run the React Native Application -```bash -yarn -``` - -* Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: +1. Make sure the [database migration is complete](./Getting-Started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](./Getting-Started?UI=NG&DB=EF&Tiered=No#run-the-application). +2. Open `react-native` folder and run `yarn` or `npm install` if you have not already. +3. Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: ![react native environment local IP](images/rn-environment-local-ip.png) @@ -51,22 +93,21 @@ yarn {{ end }} -Once all node modules are loaded, execute `yarn start` (or `npm start`) command: - -```bash -yarn start -``` +4. Run `yarn start` or `npm start`. Wait Expo CLI to start. Expo CLI opens the management interface on the `http://localhost:19002/` address. -Wait Expo CLI to start. Expo CLI opens the management interface on the `http://localhost:19002/` address. +> The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. ![expo-interface](images/rn-expo-interface.png) In the above management interface, you can start the application with an Android emulator, an iOS simulator or a physical phone by the scan the QR code with the [Expo Client](https://expo.io/tools#client). -> See the [Android Studio Emulator](https://docs.expo.io/workflow/android-simulator/), [iOS Simulator](https://docs.expo.io/workflow/ios-simulator/) documents on expo.io. - ![React Native login screen on iPhone 11](images/rn-login-iphone.png) Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can continue to develop your application based on this startup template. + + +## See Also + +* [React Native project structure](./Startup-Templates/Application#react-native) diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md index 55fcafe132..1e473b266f 100644 --- a/docs/en/Getting-Started.md +++ b/docs/en/Getting-Started.md @@ -3,13 +3,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC", "Blazor", "NG"], "DB": ["EF", "Mongo"], "Tiered": ["Yes", "No"] } ```` -This tutorial explains how to create a new {{if UI == "MVC"}} ASP.NET Core MVC (Razor Pages) web {{else if UI == "NG"}} Angular {{end}} application using the startup template. +This tutorial explains how to create a new web application using the [application startup template](Startup-Templates/Application.md). + +> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. ## Setup Your Development Environment @@ -20,11 +22,11 @@ First things first! Let's setup your development environment before creating the The following tools should be installed on your development machine: -* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). [1](#f-editor) +* [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). [1](#f-editor) * [.NET Core 3.1+](https://www.microsoft.com/net/download/dotnet-core/) -* [Node v12 or v14](https://nodejs.org/en/) -* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [2](#f-yarn) or npm v6+ (installed with Node) +* [Node v12 or v14](https://nodejs.org/) +* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [2](#f-yarn) or npm v6+ (already installed with Node) {{ if Tiered == "Yes" }} * [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)). @@ -37,7 +39,7 @@ The following tools should be installed on your development machine: ### Install the ABP CLI -[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based applications. +[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. > ABP CLI is a free & open source tool for the ABP framework. @@ -55,105 +57,60 @@ dotnet tool update -g Volo.Abp.Cli ## Create a New Project -> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. - -### Using the ABP CLI to Create a New Project - Use the `new` command of the ABP CLI to create a new project: ````shell -abp new Acme.BookStore{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}} +abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC"}} --tiered{{else}} --separate-identity-server{{end}}{{end}} ```` -{{ if UI == "NG" }} +> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. -* `-u` argument specifies the UI framework, `angular` in this case. +> Alternatively, you can select the "Direct Download" tab from the [ABP Framework web site](https://abp.io/get-started) to create a new solution. {{ if Tiered == "Yes" }} -* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint on the server. - -{{ end }} +{{ if UI == "MVC" }} -{{ end }} +* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. -{{ if DB == "Mongo" }} +{{ else }} -* `-d` argument specifies the database provider, `mongodb` in this case. +* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint on the server. {{ end }} -{{ if Tiered == "Yes" && UI != "NG" }} - -* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. - {{ end }} -> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. - -#### ABP CLI Commands & Options +### ABP CLI Commands & Options [ABP CLI document](./CLI.md) covers all of the available commands and options for the ABP CLI. This document uses the [application startup template](Startup-Templates/Application.md) to create a new web application. See the [ABP Startup Templates](Startup-Templates/Index.md) document for other templates. -> Alternatively, you can select the "Direct Download" tab from the [ABP Framework web site](https://abp.io/get-started) to create a new solution. - -## The Solution Structure - -{{ if UI == "MVC" }} - -After creating your project, you will have the following solution folders & files: - -![](images/solution-files-mvc.png) - -You will see the following solution structure when you open the `.sln` file in the Visual Studio: - -{{if DB == "Mongo"}} - -![vs-default-app-solution-structure](images/vs-app-solution-structure-mongodb.png) - -{{else}} - -![vs-default-app-solution-structure](images/vs-app-solution-structure{{if Tiered == "Yes"}}-tiered{{end}}.png) - -{{end}} - -{{ else if UI == "NG" }} -There are three folders in the created solution: +### The Solution Structure -![](images/solution-files-non-mvc.png) +The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. -* `angular` folder contains the Angular UI application. -* `aspnet-core` folder contains the backend solution. - -Open the `.sln` (Visual Studio solution) file under the `aspnet-core` folder: - -![vs-angular-app-backend-solution-structure](images/vs-spa-app-backend-structure{{if DB == "Mongo"}}-mongodb{{end}}.png) - -{{ end }} - -> ###### About the projects in your solution -> -> Your solution may have slightly different structure based on your **UI**, **database** and other preferences. +{{ if DB == "Mongo" }} -The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and also contains unit & integration test projects. +#### MongoDB Transactions -{{ if DB == "EF" }} +The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable the it in the *YourProjectMongoDbModule* class: -Integration tests projects are properly configured to work with **EF Core** & **SQLite in-memory** database. + ```csharp + Configure(options => + { + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; + }); + ``` -{{ else if DB == "Mongo" }} - -Integration tests projects are properly configured to work with in-memory **MongoDB** database created per test (used [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library). +> Or you can delete this code since this is already the default behavior. {{ end }} -> See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. - ## Create the Database ### Connection String -Check the **connection string** in the `appsettings.json` file under the {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}`.Web` project{{end}}{{else if UI == "NG" }}`.HttpApi.Host` project{{end}}: +Check the **connection string** in the `appsettings.json` file under the {{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}{{if UI=="MVC"}}`.Web` project{{else}}`.HttpApi.Host` project{{end}}{{end}} {{ if DB == "EF" }} @@ -163,7 +120,7 @@ Check the **connection string** in the `appsettings.json` file under the {{if UI } ```` -The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](https://docs.abp.io/en/abp/latest/Entity-Framework-Core) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). +The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). ### Apply the Migrations @@ -171,7 +128,7 @@ The solution uses the [Entity Framework Core Code First Migrations](https://docs #### Apply Migrations Using the DbMigrator -The solution comes with a `.DbMigrator` console application which applies migrations and also seed the initial data. It is useful on development as well as on production environment. +The solution comes with a `.DbMigrator` console application which applies migrations and also **seeds the initial data**. It is useful on **development** as well as on **production** environment. > `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. @@ -183,7 +140,7 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project** ![db-migrator-output](images/db-migrator-output.png) -> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. +> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. #### Using EF Core Update-Database Command @@ -223,7 +180,7 @@ The solution is configured to use **MongoDB** in your local computer, so you nee ### Seed Initial Data -The solution comes with a `.DbMigrator` console application which seeds the initial data. It is useful on development as well as on production environment. +The solution comes with a `.DbMigrator` console application which **seeds the initial data**. It is useful on **development** as well as on **production** environment. > `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. @@ -235,7 +192,7 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project** ![db-migrator-output](images/db-migrator-output.png) -> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. +> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. {{ end }} @@ -245,27 +202,27 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project** {{ if Tiered == "Yes" }} -Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. +1. Ensure that the `.IdentityServer` project is the startup project. Run this application that will open a **login** page in your browser. > Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. -You can login, but you cannot enter to the main application here. This is just the authentication server. +You can login, but you cannot enter to the main application here. This is **just the authentication server**. -Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. +2. Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. ![swagger-ui](images/swagger-ui.png) -This is the API application that is used by the web application. +This is the HTTP API that is used by the web application. -Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser +3. Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser ![mvc-tiered-app-home](images/bookstore-home.png) -Click to the **login** button which will redirect you to the `Identity Server` to login to the application: +Click to the **login** button which will redirect you to the *authentication server* to login to the application: ![bookstore-login](images/bookstore-login.png) -{{ else }} +{{ else # Tiered != "Yes" }} Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: @@ -273,11 +230,11 @@ Ensure that the `.Web` project is the startup project. Run the application which ![bookstore-login](images/bookstore-login.png) -{{ end }} +{{ end # Tiered }} -{{ else if UI != "MVC" }} +{{ else # UI != "MVC" }} -#### Running the HTTP API Host (server-side) +### Running the HTTP API Host (Server Side) {{ if Tiered == "Yes" }} @@ -287,15 +244,15 @@ Ensure that the `.IdentityServer` project is the startup project. Run the applic You can login, but you cannot enter to the main application here. This is just the authentication server. -{{ end }} - Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: -{{ if Tiered == "No" }} +{{ else # Tiered == "No" }} + +Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: > Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. -{{ end }} +{{ end # Tiered }} ![swagger-ui](images/swagger-ui.png) @@ -305,12 +262,25 @@ You can see the application APIs and test them here. Get [more info](https://swa > > Most of the HTTP APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. -{{ end }} +{{ end # UI }} -{{ if UI == "NG" }} -#### Running the Angular application (client-side) +{{ if UI == "Blazor" }} + +### Running the Blazor Application (Client Side) + +Ensure that the `.Blazor` project is the startup project and run the application. + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. -Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases) +Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: + +![bookstore-login](images/bookstore-login.png) + +{{ else if UI == "NG" }} + +### Running the Angular Application (Client Side) + +Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work) ```bash yarn @@ -322,11 +292,7 @@ Once all node modules are loaded, execute `yarn start` (or `npm start`) command: yarn start ``` -This will take care of compiling your `TypeScript` code, and automatically reloading your browser. -After it finishes, `Angular Live Development Server` will be listening on localhost:4200, -open your web browser and navigate to [localhost:4200](http://localhost:4200/) - - +It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. ![bookstore-login](images/bookstore-login.png) @@ -338,7 +304,7 @@ Enter **admin** as the username and **1q2w3E*** as the password to login to the If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. -See the "[Getting Started with the React Native](Getting-Started-React-Native.md)" document to learn how to configure and run the React Native application. +See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. ## Next diff --git a/docs/en/JavaScript/Dynamic-JavaScript-API-Clients.md b/docs/en/JavaScript/Dynamic-JavaScript-API-Clients.md deleted file mode 100644 index 539d3ac15a..0000000000 --- a/docs/en/JavaScript/Dynamic-JavaScript-API-Clients.md +++ /dev/null @@ -1,3 +0,0 @@ -## Dynamic JavaScript API Clients - -TODO \ No newline at end of file diff --git a/docs/en/Localization.md b/docs/en/Localization.md index 954fe8620f..a33dff1779 100644 --- a/docs/en/Localization.md +++ b/docs/en/Localization.md @@ -2,7 +2,7 @@ ABP's localization system is seamlessly integrated to the `Microsoft.Extensions.Localization` package and compatible with the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization). It adds some useful features and enhancements to make it easier to use in real life application scenarios. -## Volo.Abp.Localization Package +## Installation > This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. @@ -100,8 +100,6 @@ Configure(options => > The [application startup template](Startup-Templates/Application.md) sets `DefaultResourceType` to the localization resource of the application. -See the *Client Side* section below for a use case. - ### Short Localization Resource Name Localization resources are also available in the client (JavaScript) side. So, setting a short name for the localization resource makes it easy to use localization texts. Example: @@ -156,13 +154,13 @@ services.Configure(options => * If an extension file defines the same localized string, it overrides the string. -## Getting Localized Texts +## Getting the Localized Texts -### Server Side +Getting the localized text is pretty standard. -Getting the localized text on the server side is pretty standard. +### Simplest Usage In A Class -#### Simplest Usage In A Class +Just inject the `IStringLocalizer` service and use it like shown below: ````C# public class MyService @@ -183,9 +181,13 @@ public class MyService ##### Format Arguments -Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]` +Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]`. + +> Refer to the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) for details about using the localization. -#### Simplest Usage In A Razor View/Page +### Using In A Razor View/Page + +Use `IHtmlLocalizer` in razor views/pages; ````c# @inject IHtmlLocalizer Localizer @@ -193,53 +195,59 @@ Format arguments can be passed after the localization key. If your message is `H

@Localizer["HelloWorld"]

```` -Refer to the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) for details about using localization on the server side. - -### Client Side - -ABP provides JavaScript services to use the same localized texts in the client side. - -#### getResource - -`abp.localization.getResource` function is used to get a localization resource: - -````js -var testResource = abp.localization.getResource('Test'); -```` - -Then you can localize a string based on this resource: +### Special Base Classes -````js -var str = testResource('HelloWorld'); -```` +Some ABP Framework base classes provide a `L` property to use the localizer even easier. -#### localize +**Example: Localize a text in an application service method** -`abp.localization.localize` function is a shortcut where you can both specify the text name and the resource name: +```csharp +using System.Threading.Tasks; +using MyProject.Localization; +using Volo.Abp.Application.Services; -````js -var str = abp.localization.localize('HelloWorld', 'Test'); -```` +namespace MyProject +{ + public class TestAppService : ApplicationService + { + public TestAppService() + { + LocalizationResource = typeof(MyProjectResource); + } -`HelloWorld` is the text to localize, where `Test` is the localization resource name here. + public async Task DoIt() + { + var str = L["HelloWorld"]; + } + } +} +``` -If you don't specify the localization resource name, it uses the default localization resource defined on the `AbpLocalizationOptions` (see the *Default Resource* section above). Example: +When you set the `LocalizationResource` in the constructor, the `ApplicationService` class uses that resource type when you use the `L` property, just like in the `DoIt()` method. -````js -var str = abp.localization.localize('HelloWorld'); //uses the default resource -```` +Setting `LocalizationResource` in every application service can be tedious. You can create an abstract base application service class, set it there and derive your application services from that base class. This is already implemented when you create a new project with the [startup templates](Startup-Templates/Application.md). So, you can simply inherit from the base class directly use the `L` property: -##### Format Arguments +```csharp +using System.Threading.Tasks; -If your localized string contains arguments, like `Hello {0}, welcome!`, you can pass arguments to the localization methods. Examples: +namespace MyProject +{ + public class TestAppService : MyProjectAppService + { + public async Task DoIt() + { + var str = L["HelloWorld"]; + } + } +} +``` -````js -var str1 = abp.localization.getResource('Test')('HelloWelcomeMessage', 'John'); -var str2 = abp.localization.localize('HelloWorld', 'Test', 'John'); -```` +The `L` property is also available for some other base classes like `AbpController` and `AbpPageModel`. -Both of the samples above produce the output `Hello John, welcome!`. +## The Client Side -## See Also +See the following documents to learn how to reuse the same localization texts in the JavaScript side; -* [Localization in Angular UI](UI/Angular/Localization.md) \ No newline at end of file +* [Localization for the MVC / Razor Pages UI](UI/AspNetCore/JavaScript-API/Localization.md) +* [Localization for the Blazor UI](UI/Blazor/Localization.md) +* [Localization for the Angular UI](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-4_0.md b/docs/en/Migration-Guides/Abp-4_0.md new file mode 100644 index 0000000000..0d193d476b --- /dev/null +++ b/docs/en/Migration-Guides/Abp-4_0.md @@ -0,0 +1,117 @@ +# ABP Framework 3.3 to 4.0 Migration Guide + +## Auto API Controller Route Changes + +The route calculation for the [Auto API Controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) is changing with the ABP Framework version 4.0 ([#5325](https://github.com/abpframework/abp/issues/5325)). Previously, **camelCase** route paths were being used. Beginning from the version 4.0, it uses **kebab-case** route paths where it is possible. + +**A typical auto API before v4.0** + +![route-before-4](images/route-before-4.png) + +**camelCase route parts become kebab-case with 4.0** + +![route-4](images/route-4.png) + +If it is hard to change it for your application, you can continue to use the version 3.x route strategy, by following one of the approaches; + +* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: + +````csharp +options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.UseV3UrlStyle = true; + }); +```` + +This approach effects only the controllers for the `BookStoreApplicationModule`. + +* Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: + +```csharp +Configure(options => +{ + options.UseV3UrlStyle = true; +}); +``` + +Setting it globally effects all the modules in a modular application. + +## Identity Server Changes + +ABP Framework upgrades the [IdentityServer4](https://www.nuget.org/packages/IdentityServer4) library from 3.x to 4.x with the ABP Framework version 4.0. IdentityServer 4.x has a lot of changes, some of them are **breaking changes in the data structure**. + +### Entity Changes + +Entity changed don't directly affect your application, however it is good to know. + +#### ApiScope + +As the **most important breaking change**, Identity Server 4.x places the `ApiScope` as an independent aggregate root. Previously it was a part of the to `ApiResource` aggregate. This requires manual operation. See the *Database Changes* section. + +Also, added `Enabled(string)` and `Description(bool,true)` properties. + +#### ApiResource + +* Added `AllowedAccessTokenSigningAlgorithms (string)` and `ShowInDiscoveryDocument(bool, default: true)` properties + +#### Client + +* Added `RequireRequestObject (bool)` and `AllowedIdentityTokenSigningAlgorithms (string)` properties. +* Changed default value of `RequireConsent` from `true` to `false`. +* Changed default value of `RequirePkce` from `false` to `true`. + +#### DeviceFlowCodes + +* Added `SessionId (string)` and `Description (string)` properties. + +#### PersistedGrant + +* Added `SessionId (string)` and `Description(string)` and `ConsumedTime (DateTime?)` properties + +### Database Changes + +> Attention: **Please backup your database** before the migration! + +**If you are upgrading from 3.x, then there are some change should be done in your database.** + +#### Database Schema Migration + +If you are using **Entity Framework Core**, you need to add a new database migration, using the `Add-Migration` command, and apply changes to the database. Please **review the migration** script and read the sections below to understand if it affects your existing data. Otherwise, you may **loose some of your configuration**, which may not be easy to remember and re-configure. + +#### Seed Code + +If you haven't customize the `IdentityServerDataSeedContributor` and haven't customized the initial data inside the `IdentityServer*` tables; + +1. Update `IdentityServerDataSeedContributor` class by comparing to [the latest code](https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/IdentityServer/IdentityServerDataSeedContributor.cs). You probably only need to add the `CreateApiScopesAsync` method and the code related to it. +2. Then you can simply clear all the **table data** in these tables then execute the `DbMigrator` application again to fill it with the new configuration. + +#### Migrating the Configuration Data + +If you've customize your IdentityServer configuration in the database or in the seed data, you should understand the changes and upgrade your code/data accordingly. Especially, the following changes will affect your application: + +- `IdentityServerApiScopes` table's `Enabled` field is dropped and re-created. So, you need to manually enable the api scopes again. +- `IdentityServerApiResourceScopes` table is dropped and recreated. So, you need to backup and move your current data to the new table. +- `IdentityServerIdentityResourceClaims` table is dropped and recreated. So, you need to backup and move your current data to the new table. + +You may need to perform additional steps based on how much you made custom configurations. + +### Other IdentityServer Changes + +IdentityServer has removed the [public origin option](https://github.com/IdentityServer/IdentityServer4/pull/4335). It was resolving http/https conversion problems, but they decided to leave this to the developer. This is especially needed if you use a reverse proxy where your external protocol is HTTPS but internal protocol is HTTP. + +One simple solution is to add such a middleware into your ASP.NET Core pipeline, at the beginning. + +``` +app.Use((httpContext, next) => +{ + httpContext.Request.Scheme = "https"; + return next(); +}); +``` + +> This sample is obtained from the [ASP.NET Core documentation](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer#scenarios-and-use-cases). You can use it if you always use HTTPS in all environments. + +### Related Resources +* https://leastprivilege.com/2020/06/19/announcing-identityserver4-v4-0/ +* https://github.com/IdentityServer/IdentityServer4/issues/4592 diff --git a/docs/en/Migration-Guides/BlazorUI-3_3.md b/docs/en/Migration-Guides/BlazorUI-3_3.md new file mode 100644 index 0000000000..bb3d07f615 --- /dev/null +++ b/docs/en/Migration-Guides/BlazorUI-3_3.md @@ -0,0 +1,14 @@ +# Migration Guide for the Blazor UI from the v3.2 to the v3.3 + +## Startup Template Changes + +* Remove `Volo.Abp.Account.Blazor` NuGet package from your `.Blazor.csproj` and add `Volo.Abp.TenantManagement.Blazor` NuGet package. +* Remove the ``typeof(AbpAccountBlazorModule)`` from the dependency list of *YourProjectBlazorModule* class and add the `typeof(AbpTenantManagementBlazorModule)`. +* Add `@using Volo.Abp.BlazoriseUI` and `@using Volo.Abp.BlazoriseUI.Components` into the `_Imports.razor` file. +* Remove the `div` with `id="blazor-error-ui"` (with its contents) from the `wwwroot/index.html ` file, since the ABP Framework now shows error messages as a better message box. +* Update the`AddOidcAuthentication` options in your *YourProjectBlazorModule* class as described in the issue [#5913](https://github.com/abpframework/abp/issues/5913). + +## BlazoriseCrudPageBase to AbpCrudPageBase + +Renamed `BlazoriseCrudPageBase` to `AbpCrudPageBase`. Just update the usages. It also has some changes, you may need to update method calls/usages manually. + diff --git a/docs/en/Migration-Guides/images/route-4.png b/docs/en/Migration-Guides/images/route-4.png new file mode 100644 index 0000000000..0d57a57f3e Binary files /dev/null and b/docs/en/Migration-Guides/images/route-4.png differ diff --git a/docs/en/Migration-Guides/images/route-before-4.png b/docs/en/Migration-Guides/images/route-before-4.png new file mode 100644 index 0000000000..48d87c60c0 Binary files /dev/null and b/docs/en/Migration-Guides/images/route-before-4.png differ diff --git a/docs/en/Module-Development-Basics.md b/docs/en/Module-Development-Basics.md index e6f80233e4..4b76465a3e 100644 --- a/docs/en/Module-Development-Basics.md +++ b/docs/en/Module-Development-Basics.md @@ -57,7 +57,7 @@ public class BlogModule : AbpModule } ```` -See Configuration (TODO: link) document for more about the configuration system. +See the [Configuration](Configuration.md) document for more about the configuration system. #### Pre & Post Configure Services @@ -139,4 +139,4 @@ A depended module may depend on another module, but you only need to define your There are **two types of modules.** They don't have any structural difference but categorized by functionality and purpose: - **Framework modules**: 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**: 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. See [pre-built application modules](Modules/Index.md). \ No newline at end of file +- **Application 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. See [pre-built application modules](Modules/Index.md). diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md index ac86b13564..b38f59c03b 100644 --- a/docs/en/Modules/Identity.md +++ b/docs/en/Modules/Identity.md @@ -2,7 +2,7 @@ Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library. -See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... +**See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...** ## Identity Security Log diff --git a/docs/en/MongoDB.md b/docs/en/MongoDB.md index e7d8084f41..475eb94f1b 100644 --- a/docs/en/MongoDB.md +++ b/docs/en/MongoDB.md @@ -275,6 +275,19 @@ public class BookService > Important: You must reference to the `Volo.Abp.MongoDB` package from the project you want to access to the MongoDB API. This breaks encapsulation, but this is what you want in that case. +### Transactions + +MongoDB supports multi-document transactions starting from the version 4.0 and the ABP Framework supports it. However, the [startup template](Startup-templates/Index.md) **disables** transactions by default. If your MongoDB **server** supports transactions, you can enable the it in the *YourProjectMongoDbModule* class: + +```csharp +Configure(options => +{ + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; +}); +``` + +> Or you can delete this code since this is already the default behavior. + ### Advanced Topics #### Set Default Repository Classes diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 5138c9de04..8cdda55af2 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -1,53 +1,67 @@ -## Multi-Tenancy +# Multi-Tenancy -ABP Multi-tenancy module provides base functionality to create multi tenant applications. +Multi-Tenancy is a widely used architecture to create **SaaS applications** where the hardware and software **resources are shared by the customers** (tenants). ABP Framework provides all the base functionalities to create **multi tenant applications**. -Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that: +Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) the multi-tenancy as like that: > Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants. -### Volo.Abp.MultiTenancy Package +## Terminology: Host vs Tenant -Volo.Abp.MultiTenancy package defines fundamental interfaces to make your code "multi-tenancy ready". So, install it to your project using the package manager console (PMC): +There are two main side of a typical SaaS / Multi-tenant application: -```` -Install-Package Volo.Abp.MultiTenancy -```` +* A **Tenant** is a customer of the SaaS application that pays money to use the service. +* **Host** is the company that owns the SaaS application and manages the system. -> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. +The Host and the Tenant terms will be used for that purpose in the rest of the document. -Then you can add **AbpMultiTenancyModule** dependency to your module: +## Configuration -````C# -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; +### AbpMultiTenancyOptions: Enable/Disable Multi-Tenancy + +`AbpMultiTenancyOptions` is the main options class to **enable/disable the multi-tenancy** for your application. + +**Example: Enable multi-tenancy** -namespace MyCompany.MyProject +```csharp +Configure(options => { - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule - { - //... - } -} -```` + options.IsEnabled = true; +}); +``` + +> Multi-Tenancy is disabled in the ABP Framework by default. However, it is **enabled by default** when you create a new solution using the [startup template](Startup-Templates/Application.md). `MultiTenancyConsts` class in the solution has a constant to control it in a single place. + +### Database Architecture + +ABP Framework supports all the following approaches to store the tenant data in the database; + +* **Single Database**: All tenants are stored in a single database. +* **Database per Tenant**: Every tenant has a separate, dedicated database to store the data related to that tenant. +* **Hybrid**: Some tenants share a single databases while some tenants may have their own databases. -> With the "Multi-tenancy ready" concept, we intent to develop our code to be compatible with multi-tenancy approach. Then it can be used in a multi-tenant application or not, depending on the requirements of the final application. +[Tenant management module](Modules/Tenant-Management.md) (which comes pre-installed with the startup projects) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches. -#### Define Entities +## Usage -You can implement **IMultiTenant** interface for your entities to make them multi-tenancy ready. Example: +Multi-tenancy system is designed to **work seamlessly** and make your application code **multi-tenancy unaware** as much as possible. -````C# +### IMultiTenant + +You should implement the `IMultiTenant` interface for your [entities](Entities.md) to make them **multi-tenancy ready**. + +**Example: A multi-tenant *Product* entity** + +````csharp using System; using Volo.Abp.Domain.Entities; using Volo.Abp.MultiTenancy; -namespace MyCompany.MyProject +namespace MultiTenancyDemo.Products { - public class Product : AggregateRoot, IMultiTenant + public class Product : AggregateRoot, IMultiTenant { - public Guid? TenantId { get; set; } //IMultiTenant defines TenantId property + public Guid? TenantId { get; set; } //Defined by the IMultiTenant interface public string Name { get; set; } @@ -56,319 +70,341 @@ namespace MyCompany.MyProject } ```` -IMultiTenant requires to define a **TenantId** property in the implementing entity (See [entity documentation](Entities.md) for more about entities). +* `IMultiTenant` interface just defines a `TenantId` property. + +When you implement this interface, ABP Framework **automatically** [filters](Data-Filtering.md) entities for the current tenant when you query from database. So, you don't need to manually add `TenantId` condition while performing queries. A tenant can not access to data of another tenant by default. -#### Obtain Current Tenant's Id +#### Why the TenantId Property is Nullable? -Your code may require to get current tenant's id (regardless of how it's retrieved actually). You can [inject](Dependency-Injection.md) and use **ICurrentTenant** interface for such cases. Example: +`IMultiTenant.TenantId` is **nullable**. When it is null that means the entity is owned by the **Host** side and not owned by a tenant. It is useful when you create a functionality in your system that is both used by the tenant and the host sides. -````C# -using Volo.Abp.DependencyInjection; +For example, `IdentityUser` is an entity defined by the [Identity Module](Modules/Identity.md). The host and all the tenants have their own users. So, for the host side, users will have a `null` `TenantId` while tenant users will have their related `TenantId`. + +> **Tip**: If your entity is tenant-specific and has no meaning in the host side, you can force to not set `null` for the `TenantId` in the constructor of your entity. + +#### When to set the TenantId? + +ABP Framework doesn't set the `TenantId` for you (because of the cross tenant operations, ABP can not know the proper `TenantId` in some cases). So, you need to set it yourself **when you create a new multi-tenant entity**. + +##### Best Practice + +We suggest to set the `TenantId` in the constructor and never allow to change it again. So, the `Product` class can be re-written as below: + +````csharp +using System; +using Volo.Abp.Domain.Entities; using Volo.Abp.MultiTenancy; -namespace MyCompany.MyProject +namespace MultiTenancyDemo.Products { - public class MyService : ITransientDependency + public class Product : AggregateRoot, IMultiTenant { - private readonly ICurrentTenant _currentTenant; + //Private setter prevents changing it later + public Guid? TenantId { get; private set; } - public MyService(ICurrentTenant currentTenant) + public string Name { get; set; } + + public float Price { get; set; } + + protected Product() { - _currentTenant = currentTenant; + //This parameterless constructor is needed for ORMs } - - public void DoIt() + + public Product(string name, float price, Guid? tenantId) { - var tenantId = _currentTenant.Id; - //use tenantId in your code... + Name = name; + Price = price; + TenantId = tenantId; //Set in the constructor } } } ```` -#### Change Current Tenant - -TODO: ... - -#### Determining Current Tenant - -The first thing for a multi-tenant application is to determine the current tenant on the runtime. Volo.Abp.MultiTenancy package only provides abstractions (named as tenant resolver) for determining the current tenant, however it does not have any implementation out of the box. - -**Volo.Abp.AspNetCore.MultiTenancy** package has implementation to determine the current tenant from current web request (from subdomain, header, cookie, route... etc.). See Volo.Abp.AspNetCore.MultiTenancy Package section later in this document. +> You can see the [entities document](Entities.md) for a more about entities and aggregate roots. -##### Custom Tenant Resolvers +You typically use the `ICurrentTenant` to set the `TenantId` while creating a new `Product`. -You can add your custom tenant resolver to **AbpTenantResolveOptions** in your module's ConfigureServices method as like below: +**Example: Creating a new product in a [Domain Service](Domain-Services.md)** -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; -namespace MyCompany.MyProject +namespace MultiTenancyDemo.Products { - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule + public class ProductManager : DomainService { - public override void ConfigureServices(ServiceConfigurationContext context) + private readonly IRepository _productRepository; + + public ProductManager(IRepository productRepository) { - Configure(options => - { - options.TenantResolvers.Add(new MyCustomTenantResolveContributor()); - }); + _productRepository = productRepository; + } - //... + public async Task CreateAsync(string name, float price) + { + var product = new Product(name, price, CurrentTenant.Id); + return await _productRepository.InsertAsync(product); } } } ```` -`MyCustomTenantResolveContributor` must implement **ITenantResolveContributor** as shown below: +* `DomainService` base class (and some common base classes in the ABP Framework) provides the `CurrentTenant`, so you directly use it. Otherwise, you need to [inject](Dependency-Injection.md) the `ICurrentTenant` service. -````C# -using Volo.Abp.MultiTenancy; +### ICurrentTenant -namespace MyCompany.MyProject -{ - public class MyCustomTenantResolveContributor : ITenantResolveContributor - { - public void Resolve(ITenantResolveContext context) - { - context.TenantIdOrName = ... //find tenant id or tenant name from somewhere... - } - } -} -```` +`ICurrentTenant` is the main service to interact with the multi-tenancy infrastructure. + +`ApplicationService`, `DomainService`, `AbpController` and some other base classes already has pre-injected `CurrentTenant` properties. For other type of classes, you can inject the `ICurrentTenant` into your service. -A tenant resolver can set **TenantIdOrName** if it can determine it. If not, just leave it as is to allow next resolver to determine it. +#### Tenant Properties -#### Tenant Store +`ICurrentTenant` defines the following properties; -Volo.Abp.MultiTenancy package defines **ITenantStore** to abstract data source from the framework. You can implement ITenantStore to work with any data source (like a relational database) that stores information of your tenants. +* `Id` (`Guid`): Id of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined from the request. +* `Name` (`string`): Name of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined from the request. +* `IsAvailable` (`bool`): Returns `true` if the `Id` is not `null`. -... +#### Change the Current Tenant -##### Configuration Data Store +ABP Framework automatically filters the resources (database, cache...) based on the `ICurrentTenant.Id`. However, in some cases you may want to perform an operation on behalf of a specific tenant, generally when you are in the host context. -There is a built in (and default) tenant store, named ConfigurationTenantStore, that can be used to store tenants using standard [configuration system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) (with [Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration) package). Thus, you can define tenants as hard coded or get from your appsettings.json file. +`ICurrentTenant.Change` method changes the current tenant for a limited scope, so you can safely perform operations for the tenant. -###### Example: Define tenants as hard-coded +**Example: Get product count of a specific tenant** -````C# +````csharp using System; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Data; -using Volo.Abp.Modularity; -using Volo.Abp.MultiTenancy; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; -namespace MyCompany.MyProject +namespace MultiTenancyDemo.Products { - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule + public class ProductManager : DomainService { - public override void ConfigureServices(ServiceConfigurationContext context) + private readonly IRepository _productRepository; + + public ProductManager(IRepository productRepository) { - Configure(options => + _productRepository = productRepository; + } + + public async Task GetProductCountAsync(Guid? tenantId) + { + using (CurrentTenant.Change(tenantId)) { - options.Tenants = new[] - { - new TenantConfiguration( - Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id - "tenant1" //Name - ), - new TenantConfiguration( - Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id - "tenant2" //Name - ) - { - //tenant2 has a seperated database - ConnectionStrings = - { - {ConnectionStrings.DefaultConnectionStringName, "..."} - } - } - }; - }); + return await _productRepository.GetCountAsync(); + } } } } ```` -###### Example: Define tenants in appsettings.json +* `Change` method can be used in a **nested way**. It restores the `CurrentTenant.Id` to the previous value after the `using` statement. +* When you use `CurrentTenant.Id` inside the `Change` scope, you get the `tenantId` provided to the `Change` method. So, the repository also get this `tenantId` and can filter the database query accordingly. +* Use `CurrentTenant.Change(null)` to change scope to the host context. + +> Always use the `Change` method with a `using` statement like done in this example. -First create your configuration from your appsettings.json file as you always do. +### Data Filtering: Disable the Multi-Tenancy Filter -````C# -using System.IO; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Modularity; +As mentioned before, ABP Framework handles data isolation between tenants using the [Data Filtering](Data-Filtering.md) system. In some cases, you may want to disable it and perform a query on all the data, without filtering for the current tenant. + +**Example: Get count of products in the database, including all the products of all the tenants.** + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; using Volo.Abp.MultiTenancy; -namespace MyCompany.MyProject +namespace MultiTenancyDemo.Products { - [DependsOn(typeof(AbpMultiTenancyModule))] - public class MyModule : AbpModule + public class ProductManager : DomainService { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = BuildConfiguration(); + private readonly IRepository _productRepository; + private readonly IDataFilter _dataFilter; - Configure(configuration); + public ProductManager( + IRepository productRepository, + IDataFilter dataFilter) + { + _productRepository = productRepository; + _dataFilter = dataFilter; } - private static IConfigurationRoot BuildConfiguration() + public async Task GetProductCountAsync() { - return new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .Build(); + using (_dataFilter.Disable()) + { + return await _productRepository.GetCountAsync(); + } } } } + ```` -Then add a "**Tenants**" section to your appsettings.json: +See the [Data Filtering document](Data-Filtering.md) for more. -````json -"Tenants": [ - { - "Id": "446a5211-3d72-4339-9adc-845151f8ada0", - "Name": "tenant1" - }, - { - "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d", - "Name": "tenant2", - "ConnectionStrings": { - "Default": "...write tenant2's db connection string here..." - } - } - ] -```` +> Note that this approach won't work if your tenants have **separate databases** since there is no built-in way to query from multiple database in a single database query. You should handle it yourself if you need it. -##### Volo.Abp... Package (TODO) +## Infrastructure -TODO: This package implements ITenantStore using a real database... +### Determining the Current Tenant -#### Tenant Information +The first thing for a multi-tenant application is to determine the current tenant on the runtime. -ITenantStore works with **TenantConfiguration** class that has several properties for a tenant: +ABP Framework provides an extensible **Tenant Resolving** system for that purpose. Tenant Resolving system then used in the **Multi-Tenancy Middleware** to determine the current tenant for the current HTTP request. -* **Id**: Unique Id of the tenant. -* **Name**: Unique name of the tenant. -* **ConnectionStrings**: If this tenant has dedicated database(s) to store it's data, then connection strings can provide database connection strings (it may have a default connection string and connection strings per modules - TODO: Add link to Abp.Data package document). +#### Tenant Resolvers -A multi-tenant application may require additional tenant properties, but these are the minimal requirements for the framework to work with multiple tenants. +##### Default Tenant Resolvers -#### Change Tenant By Code +The following resolvers are provided and configured by default; -TODO... +* `CurrentUserTenantResolveContributor`: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for the security**. +* `QueryStringTenantResolveContributor`: Tries to find current tenant id from query string parameters. The parameter name is `__tenant` by default. +* `FormTenantResolveContributor`:Tries to find current tenant id from form parameters. The parameter name is `__tenant` by default. +* `RouteTenantResolveContributor`: Tries to find current tenant id from route (URL path). The variable name is `__tenant` by default. If you defined a route with this variable, then it can determine the current tenant from the route. +* `HeaderTenantResolveContributor`: Tries to find current tenant id from HTTP headers. The header name is `__tenant` by default. +* `CookieTenantResolveContributor`: Tries to find current tenant id from cookie values. The cookie name is `__tenant` by default. -### Volo.Abp.AspNetCore.MultiTenancy Package +###### Problems with the NGINX -Volo.Abp.AspNetCore.MultiTenancy package integrate multi-tenancy to ASP.NET Core applications. To install it to your project, run the following command on PMC: +You may have problems with the `__tenant` in the HTTP Headers if you're using the [nginx](https://www.nginx.com/) as the reverse proxy server. Because it doesn't allow to use underscore and some other special characters in the HTTP headers and you may need to manually configure it. See the following documents please: +http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers +http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers -```` -Install-Package Volo.Abp.AspNetCore.MultiTenancy -```` +###### AbpAspNetCoreMultiTenancyOptions -Then you can add **AbpAspNetCoreMultiTenancyModule** dependency to your module: +`__tenant` parameter name can be changed using `AbpAspNetCoreMultiTenancyOptions`. -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.MultiTenancy; +**Example:** -namespace MyCompany.MyProject +````csharp +services.Configure(options => { - [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))] - public class MyModule : AbpModule - { - //... - } -} + options.TenantKey = "MyTenantKey"; +}); ```` -#### Multi-Tenancy Middleware +> However, we don't suggest to change this value since some clients may assume the the `__tenant` as the parameter name and they might need to manually configure then. -Volo.Abp.AspNetCore.MultiTenancy package includes the multi-tenancy middleware... +##### Domain/Subdomain Tenant Resolver -````C# -app.UseMultiTenancy(); -```` +In a real application, most of times you will want to determine current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure the `AbpTenantResolveOptions` to add the domain tenant resolver. -TODO:... +**Example: Add a subdomain resolver** -#### Determining Current Tenant From Web Request - -Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box: - -* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for security**. -* **QueryStringTenantResolveContributor**: Tries to find current tenant id from query string parameter. Parameter name is "__tenant" by default. -* **RouteTenantResolveContributor**: Tries to find current tenant id from route (URL path). Variable name is "__tenant" by default. So, if you defined a route with this variable, then it can determine the current tenant from the route. -* **HeaderTenantResolveContributor**: Tries to find current tenant id from HTTP header. Header name is "__tenant" by default. -* **CookieTenantResolveContributor**: Tries to find current tenant id from cookie values. Cookie name is "__tenant" by default. +````csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.mydomain.com"); +}); +```` -> If you use nginx as a reverse proxy server, please note that if `TenantKey` contains an underscore or other special characters, there may be a problem, please refer to: -http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers -http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers +* `{0}` is the placeholder to determine current tenant's unique name. +* Add this code to the `ConfigureServices` method of your [module](Module-Development-Basics.md). +* This should be done in the *Web/API Layer* since the URL is a web related stuff. +##### Custom Tenant Resolvers -"__tenant" parameter name can be changed using AbpAspNetCoreMultiTenancyOptions. Example: +You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below: -````C# -services.Configure(options => +````csharp +Configure(options => { - options.TenantKey = "MyTenantKey"; + options.TenantResolvers.Add(new MyCustomTenantResolveContributor()); }); ```` -##### Domain Tenant Resolver - -In a real application, most of times you will want to determine current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure AbpTenantResolveOptions to add a domain tenant resolver. - -###### Example: Add a subdomain resolver +`MyCustomTenantResolveContributor` must inherit from the `TenantResolveContributorBase` (or implement the `ITenantResolveContributor`) as shown below: -````C# -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AspNetCore.MultiTenancy; -using Volo.Abp.Modularity; +````csharp +using System.Threading.Tasks; using Volo.Abp.MultiTenancy; -namespace MyCompany.MyProject +namespace MultiTenancyDemo.Web { - [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))] - public class MyModule : AbpModule + public class MyCustomTenantResolveContributor : TenantResolveContributorBase { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - //Subdomain format: {0}.mydomain.com - //Adding as the second highest priority resolver after 'CurrentUserTenantResolveContributor' to - //ensure the user cannot impersonate a different tenant. - options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com")); - }); + public override string Name => "Custom"; - //... + public override Task ResolveAsync(ITenantResolveContext context) + { + //TODO... } } } ```` -{0} is the placeholder to determine current tenant's unique name. +* A tenant resolver should set `context.TenantIdOrName` if it can determine it. If not, just leave it as is to allow the next resolver to determine it. +* `context.ServiceProvider` can be used if you need to additional services to resolve from the [dependency injection](Dependency-Injection.md) system. + +#### Multi-Tenancy Middleware -Instead of ``options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));`` you can use this shortcut: +Multi-Tenancy middleware is an ASP.NET Core request pipeline [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware) that determines the current tenant from the HTTP request and sets the `ICurrentTenant` properties. -````C# -options.AddDomainTenantResolver("{0}.mydomain.com"); +Multi-Tenancy middleware is typically placed just under the [authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication) middleware (`app.UseAuthentication()`): + +````csharp +app.UseMultiTenancy(); ```` -###### Example: Add a domain resolver +> This middleware is already configured in the startup templates, so you normally don't need to manually add it. + +### Tenant Store + +`ITenantStore` is used to get the tenant configuration from a data source. -````C# -options.AddDomainTenantResolver("{0}.com"); +#### Tenant Management Module + +The [tenant management module](Modules/Tenant-Management) is **included in the startup templates** and implements the `ITenantStore` interface to get the tenants and their configuration from a database. It also provides the necessary functionality and UI to manage the tenants and their connection strings. + +#### Configuration Data Store + +**If you don't want to use the tenant management module**, the `DefaultTenantStore` is used as the `ITenantStore` implementation. It gets the tenant configurations from the [configuration system](Configuration.md) (`IConfiguration`). You can either configure the `AbpDefaultTenantStoreOptions` [options](Options.md) or set it in your `appsettings.json` file: + +**Example: Define tenants in appsettings.json** + +````json +"Tenants": [ + { + "Id": "446a5211-3d72-4339-9adc-845151f8ada0", + "Name": "tenant1" + }, + { + "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d", + "Name": "tenant2", + "ConnectionStrings": { + "Default": "...tenant2's db connection string here..." + } + } + ] ```` +> It is recommended to **use the Tenant Management module**, which is already pre-configured when you create a new application with the ABP startup templates. + +### Other Multi-Tenancy Infrastructure + +ABP Framework was designed to respect to the multi-tenancy in every aspect and most of the times everything will work as expected. + +BLOB Storing, Caching, Data Filtering, Data Seeding, Authorization and all the other services are designed to properly work in a multi-tenant system. + +## The Tenant Management Module + +ABP Framework provides all the the infrastructure to create a multi-tenant application, but doesn't make any assumption about how you manage (create, delete...) your tenants. + +The [Tenant Management module](Modules/Tenant-Management.md) provides a basic UI to manage your tenants and set their connection strings. It is pre-configured for the [application startup template](Startup-Templates/Application.md). + ## See Also * [Features](Features.md) \ No newline at end of file diff --git a/docs/en/Redis-Cache.md b/docs/en/Redis-Cache.md new file mode 100644 index 0000000000..3e11584bce --- /dev/null +++ b/docs/en/Redis-Cache.md @@ -0,0 +1,45 @@ +# Redis Cache + +ABP Framework [Caching System](Caching.md) extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). So, **any provider** supported by the standard ASP.NET Core distributed cache can be usable in your application and can be configured just like **documented by Microsoft**. + +However, ABP provides an **integration package** for Redis Cache: [Volo.Abp.Caching.StackExchangeRedis](https://www.nuget.org/packages/Volo.Abp.Caching.StackExchangeRedis). There are two reasons for using this package, instead of the standard [Microsoft.Extensions.Caching.StackExchangeRedis](https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis/) package. + +1. It implements `SetManyAsync` and `GetManyAsync` methods. These are not standard methods of the Microsoft Caching library, but added by the ABP Framework [Caching](Caching.md) system. They **significiantly increases the performance** when you need to set/get multiple cache items with a single method call. +2. It **simplifies** the Redis cache **configuration** (will be explained below). + +> Volo.Abp.Caching.StackExchangeRedis is already uses the Microsoft.Extensions.Caching.StackExchangeRedis package, but extends and improves it. + +## Installation + +> This package is already installed in the application startup template if it is using Redis. + +Open a command line in the folder of your `.csproj` file and type the following ABP CLI command: + +````bash +abp add-package Volo.Abp.Caching.StackExchangeRedis +```` + +## Configuration + +Volo.Abp.Caching.StackExchangeRedis package automatically gets the redis [configuration](Configuration.md) from the `IConfiguration`. So, for example, you can set your configuration inside the `appsettings.json`: + +````js +"Redis": { + "IsEnabled": "true", + "Configuration": "127.0.0.1" +} +```` +The setting `IsEnabled` is optional and will be considered `true` if it is not set. + +Alternatively you can configure the standard [RedisCacheOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.stackexchangeredis.rediscacheoptions) [options](Options.md) class in the `ConfigureServices` method of your [module](Module-Development-Basics.md): + +````csharp +Configure(options => +{ + //... +}); +```` + +## See Also + +* [Caching](Caching.md) \ No newline at end of file diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index a222b9d803..0fc80984e7 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -142,7 +142,7 @@ var people = _personRepository You normally want to use `.ToListAsync()`, `.CountAsync()`... instead, to be able to write a **truly async code**. -However, you see that you can't use these async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because; +However, you see that you can't use all the async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because; * These async methods **are not standard LINQ methods** and they are defined in the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) NuGet package. * The standard template **doesn't have a reference** to the EF Core package from the domain and application layers, to be independent from the database provider. @@ -151,9 +151,9 @@ Based on your requirements and development model, you have the following options > Using async methods is strongly suggested! Don't use sync LINQ methods while executing database queries to be able to develop a scalable application. -### Option-1: Reference to the EF Core +### Option-1: Reference to the Database Provider Package -The easiest solution is to directly add the EF Core package from the project you want to use these async methods. +**The easiest solution** is to directly add the EF Core package from the project you want to use these async methods. > Add the [Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore) NuGet package to your project, which indirectly reference to the EF Core package. This ensures that you use the correct version of the EF Core compatible to the rest of your application. @@ -183,14 +183,34 @@ var people = ((IMongoQueryable)_personRepository .ToListAsync(); ```` -### Option-2: Custom Repository Methods +### Option-2: Use the IRepository Async Extension Methods -You can always create custom repository methods and use the database provider specific APIs, like async extension methods here. See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories. +ABP Framework provides async extension methods for the repositories, just similar to async LINQ extension methods. -This method is suggested; +**Example: Use `CountAsync` and `FirstOrDefaultAsync` methods on the repositories** -* If you want to **completely isolate** your domain & application layers from the database provider. -* If you develop a **reusable [application module](Modules/Index.md)** and don't want to force to a specific database provider, which should be done as a [best practice](Best-Practices/Index.md). +````csharp +var countAll = await _personRepository + .CountAsync(); + +var count = await _personRepository + .CountAsync(x => x.Name.StartsWith("A")); + +var book1984 = await _bookRepository + .FirstOrDefaultAsync(x => x.Name == "John"); +```` + +The standard LINQ extension methods are supported: *AllAsync, AnyAsync, AverageAsync, ContainsAsync, CountAsync, FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, LongCountAsync, MaxAsync, MinAsync, SingleAsync, SingleOrDefaultAsync, SumAsync, ToArrayAsync, ToListAsync*. + +This approach still **has a limitation**. You need to call the extension method directly on the repository object. For example, the below usage is **not supported**: + +```csharp +var count = await _bookRepository.Where(x => x.Name.Contains("A")).CountAsync(); +``` + +This is because the object returned from the `Where` method is not a repository object, it is a standard `IQueryable` interface. See the other options for such cases. + +This method is suggested **wherever possible**. ### Option-3: IAsyncQueryableExecuter @@ -245,6 +265,17 @@ ABP Framework executes the query asynchronously using the actual database provid This method is suggested; +* If you want to develop your application code **without depending** on the database provider. * If you are building a **reusable library** that doesn't have a database provider integration package, but needs to execute an `IQueryable` object in some case. -For example, ABP Framework uses the `IAsyncQueryableExecuter` in the `CrudAppService` base class (see the [application services](Application-Services.md) document). \ No newline at end of file +For example, ABP Framework uses the `IAsyncQueryableExecuter` in the `CrudAppService` base class (see the [application services](Application-Services.md) document). + +### Option-4: Custom Repository Methods + +You can always create custom repository methods and use the database provider specific APIs, like async extension methods here. See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories. + +This method is suggested; + +* If you want to **completely isolate** your domain & application layers from the database provider. +* If you develop a **reusable [application module](Modules/Index.md)** and don't want to force to a specific database provider, which should be done as a [best practice](Best-Practices/Index.md). + diff --git a/docs/en/Road-Map.md b/docs/en/Road-Map.md index 91fe94427e..8670ba080b 100644 --- a/docs/en/Road-Map.md +++ b/docs/en/Road-Map.md @@ -4,22 +4,18 @@ You can always check the milestone planning and the prioritized backlog issues o While we will **continue to add other exciting features**, we will work on the following major items in the **middle term**: -* **Blazor UI** for the framework and all the pre-built modules. +* **Blazor UI** for the framework and all the pre-built modules (in progress). * **.NET 5.0**! As Microsoft has announced that the .NET 5.0 will be released in November 2020, we will prepare for this change before and move to the .NET 5.0 just after Microsoft releases it. We hope a smooth transition. Beside this middle term goals, there are many features in the [backlog](https://github.com/abpframework/abp/milestone/2). Here, a list of some major items in the backlog; -* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a **gRPC integration** infrastructure (while it is [already possible](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) to create or consume gRPC endpoints for your application, we plan to create endpoints for the [standard application modules](https://docs.abp.io/en/abp/latest/Modules/Index)) +* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure (while it is [already possible](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) to create or consume gRPC endpoints for your application, we plan to create endpoints for the [standard application modules](https://docs.abp.io/en/abp/latest/Modules/Index)) * [#236](https://github.com/abpframework/abp/issues/236) Resource based authorization system * [#1754](https://github.com/abpframework/abp/issues/1754) / Multi-lingual entities -* [#347](https://github.com/abpframework/abp/issues/347) / Support MongoDB ACID transactions * [#633](https://github.com/abpframework/abp/issues/633) / Realtime notification system * [#57](https://github.com/abpframework/abp/issues/57) / Built-in CQRS infrastructure -* [#4222](https://github.com/abpframework/abp/issues/4222) / Kafka integration for the Distributed Event Bus * [#336](https://github.com/abpframework/abp/issues/336) / Health Check abstraction * [#2532](https://github.com/abpframework/abp/issues/2532), [#2564](https://github.com/abpframework/abp/issues/2465) / CosmosDB integration with EF Core and MongoDB API -* [#1168](https://github.com/abpframework/abp/issues/1168) / Official Vue UI startup template -* [#1638](https://github.com/abpframework/abp/issues/1638) Official React startup template * [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook system * [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB Integration for multitenancy * [#2296](https://github.com/abpframework/abp/issues/2296) / Feature toggling infrastructure diff --git a/docs/en/Settings.md b/docs/en/Settings.md index 32761ff201..567cb02c4c 100644 --- a/docs/en/Settings.md +++ b/docs/en/Settings.md @@ -2,7 +2,7 @@ [Configuration system](Configuration.md) is a good way to configure the application on startup. In addition to the configurations, ABP provides another way to set and get some application settings. -A setting is a name-value pair stored in a dynamic data source, generally in a database. Setting system is extensible and there are pre-built provides for a user, a tenant, global and default. +A setting is a name-value pair stored in a dynamic data source, generally in a database. Setting system is extensible and there are pre-built providers for a user, a tenant, global and default. ## Defining Settings @@ -67,7 +67,7 @@ public class MySettingDefinitionProvider : SettingDefinitionProvider > Using constants for the setting names is a good practice and ABP packages do it. `Abp.Mailing.Smtp.Host` setting name is a constant defined by the `EmailSettingNames` class (in the `Volo.Abp.Emailing` namespace). -## Reading Setting Values +## Reading the Setting Values ### ISettingProvider @@ -108,24 +108,15 @@ public class MyService } ```` -> `ISettingProvider` is a very common service and some base classes (like `IApplicationService`) already property-inject it. You can directly use the `SettingProvider` in such cases. +> `ISettingProvider` is a very common service and some base classes (like `IApplicationService`) already property-inject it. You can directly use the `SettingProvider` property in such cases. ### Reading Setting Values on the Client Side -If a setting is allowed to be visible on the client side, current value of the setting can also be read from the JavaScript code. Examples: +If a setting is allowed to be visible on the client side, current value of the setting can also be read from the client code. See the following documents to understand how to get the setting values in different UI types; -````js -//Gets a value as string. -var language = abp.setting.get('Abp.Localization.DefaultLanguage'); - -//Gets an integer value. -var requiredLength = abp.setting.getInt('Abp.Identity.Password.RequiredLength'); - -//Gets a boolean value. -var requireDigit = abp.setting.getBoolean('Abp.Identity.Password.RequireDigit'); -```` - -In addition, use `abp.setting.values` to get a dictionary of all the setting values. +* [MVC / Razor Pages](UI/AspNetCore/JavaScript-API/Settings.md) +* [Angular](UI/Angular/Settings.md) +* [Blazor](UI/Blazor/Settings.md) ## Setting Value Providers diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md index f6ba81b550..29c37e30b2 100644 --- a/docs/en/SignalR-Integration.md +++ b/docs/en/SignalR-Integration.md @@ -1,6 +1,6 @@ # SignalR Integration -> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages those simplify the integration and usage. +> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides SignalR integration packages those simplify the integration and usage. ## Installation diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 4e9bd63371..8cccf32382 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -33,6 +33,7 @@ abp new Acme.BookStore -t app This template provides multiple UI frameworks: * `mvc`: ASP.NET Core MVC UI with Razor Pages (default) +* `blazor`: Blazor UI * `angular`: Angular UI Use `-u` or `--ui` option to specify the UI framework: diff --git a/docs/en/Startup-Templates/Index.md b/docs/en/Startup-Templates/Index.md index 0c788a6a9c..b73779ecb8 100644 --- a/docs/en/Startup-Templates/Index.md +++ b/docs/en/Startup-Templates/Index.md @@ -5,3 +5,4 @@ While you can start with an empty project and add needed packages manually, star * [**app**](Application.md): Application template. * [**module**](Module.md): Module/service template. * [**console**](Console.md): Console template. +* [**WPF**](WPF.md): WPF template. diff --git a/docs/en/Startup-Templates/WPF.md b/docs/en/Startup-Templates/WPF.md new file mode 100644 index 0000000000..7eb6a5c73e --- /dev/null +++ b/docs/en/Startup-Templates/WPF.md @@ -0,0 +1,27 @@ +# WPF Application Startup Template + +This template is used to create a minimalist WPF application project. + +## How to Start With? + +First, install the [ABP CLI](../CLI.md) if you haven't installed before: + +````bash +dotnet tool install -g Volo.Abp.Cli +```` + +Then use the `abp new` command in an empty folder to create a new solution: + +````bash +abp new Acme.MyWpfApp -t wpf +```` + +`Acme.MyWpfApp` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. + +## Solution Structure + +After you use the above command to create a solution, you will have a solution like shown below: + +![basic-wpf-application-solution](../images/basic-wpf-application-solution.png) + +* `HelloWorldService` is a sample service that implements the `ITransientDependency` interface to register this service to the [dependency injection](../Dependency-Injection.md) system. \ No newline at end of file diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index 98263f64f7..3348679420 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the database provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) {{if UI == "MVC" && DB == "EF"}} @@ -217,7 +201,7 @@ Before updating the database, read the section below to learn how to seed some i > It's good to have some initial data in the database before running the application. This section introduces the [Data Seeding](../Data-Seeding.md) system of the ABP framework. You can skip this section if you don't want to create seed data, but it is suggested to follow it to learn this useful ABP Framework feature. -Create a class deriving from the `IDataSeedContributor` in the `*.Domain` project and copy the following code: +Create a class deriving from the `IDataSeedContributor` in the `*.Domain` project by copying the following code: ```csharp using System; @@ -278,18 +262,9 @@ Run the `Acme.BookStore.DbMigrator` application to update the database: ![bookstore-dbmigrator-on-solution](images/bookstore-dbmigrator-on-solution.png) -{{if DB == "EF"}} `.DbMigrator` is a console application that can be run to **migrate the database schema** and **seed the data** on **development** and **production** environments. -{{end}} - -{{if DB == "Mongo"}} - -While MongoDB **doesn't require** a database schema migration, it is still good to run this application since it **seeds the initial data** on the database. This application can be used on **development** and **production** environments. - -{{end}} - ## Create the Application Service The application layer is separated into two projects: @@ -426,7 +401,7 @@ namespace Acme.BookStore.Books ### BookAppService -It is time to implement the `IBookAppService` interface. Create a new class, named `BookAppService` in the `Books` namespace (folder) of the Acme.BookStore.Application project: +It is time to implement the `IBookAppService` interface. Create a new class, named `BookAppService` in the `Books` namespace (folder) of the `Acme.BookStore.Application` project: ````csharp using System; @@ -466,7 +441,7 @@ ABP can [**automagically**](../API/Auto-API-Controllers.md) configures your appl ### Swagger UI -The startup template is configured to run the [Swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application by pressing `CTRL+F5` and navigate to `https://localhost:/swagger/` on your browser. (Replace `` with your own port number.) +The startup template is configured to run the [Swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application ({{if UI=="MVC"}}`Acme.BookStore.Web`{{else}}`Acme.BookStore.HttpApi.Host`{{end}}) by pressing `CTRL+F5` and navigate to `https://localhost:/swagger/` on your browser. Replace `` with your own port number. You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md index 05ed6e343e..53f19d93dd 100644 --- a/docs/en/Tutorials/Part-10.md +++ b/docs/en/Tutorials/Part-10.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,16 +28,17 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) ## Introduction We have created `Book` and `Author` functionalities for the book store application. However, currently there is no relation between these entities. -In this tutorial, we will establish a **1 to N** relation between the `Book` and the `Author`. +In this tutorial, we will establish a **1 to N** relation between the `Author` and the `Book` entities. ## Add Relation to The Book Entity @@ -66,21 +50,21 @@ public Guid AuthorId { get; set; } {{if DB=="EF"}} -> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their entities (just like we will done below) which makes your application code simpler. +> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (just like we will done below) which makes your application code simpler. {{end}} ## Database & Data Migration -Added a new, required `AuthorId` property to the `Book` entity. But, what about the existing books on the database? They currently don't have `AuthorId`s and this will be a problem when we try to run the application. +Added a new, required `AuthorId` property to the `Book` entity. But, **what about the existing books** on the database? They currently don't have `AuthorId`s and this will be a problem when we try to run the application. -This is a typical migration problem and the decision depends on your case; +This is a **typical migration problem** and the decision depends on your case; * If you haven't published your application to the production yet, you can just delete existing books in the database, or you can even delete the entire database in your development environment. -* You can do it programmatically on data migration or seed phase. +* You can update the existing data programmatically on data migration or seed phase. * You can manually handle it on the database. -We prefer to **delete the database** {{if DB=="EF"}}(run the `Drop-Database` in the *Package Manager Console*){{end}} since this is just an example project and data loss is not important. Since this topic is not related to the ABP Framework, we don't go deeper for all the scenarios. +We prefer to **delete the database** {{if DB=="EF"}}(you can run the `Drop-Database` in the *Package Manager Console*){{end}} since this is just an example project and data loss is not important. Since this topic is not related to the ABP Framework, we don't go deeper for all the scenarios. {{if DB=="EF"}} @@ -108,6 +92,8 @@ Run the following command in the Package Manager Console (of the Visual Studio) Add-Migration "Added_AuthorId_To_Book" ```` +> Ensure that the `Acme.BookStore.EntityFrameworkCore.DbMigrations` is the Default project and the `Acme.BookStore.DbMigrator` is the startup project, as always. + This should create a new migration class with the following code in its `Up` method: ````csharp @@ -222,6 +208,8 @@ namespace Acme.BookStore The only change is that we set the `AuthorId` properties of the `Book` entities. +> Delete existing books or delete the database before executing the `DbMigrator`. See the *Database & Data Migration* section above for more info. + {{if DB=="EF"}} You can now run the `.DbMigrator` console application to **migrate** the **database schema** and **seed** the initial data. @@ -299,7 +287,7 @@ namespace Acme.BookStore.Books } ```` -This will be used in a new method will be added to the `IBookAppService`. +This will be used in a new method that will be added to the `IBookAppService`. ### IBookAppService @@ -1068,4 +1056,112 @@ Open the `/src/app/book/book.component.html` and add the following form group ju That's all. Just run the application and try to create or edit an author. +{{end}} + +{{if UI == "Blazor"}} + +### The Book List + +It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the `Acme.BookStore.Blazor` project and add the following `DataGridColumn` definition just after the `Name` (book name) column: + +````xml + +```` + +When you run the application, you can see the *Author* column on the table: + +![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors.png) + +### Create Book Modal + +Add the following field to the `@code` section of the `Books.razor` file: + +````csharp +IReadOnlyList authorList = Array.Empty(); +```` + +And fill it in the `OnInitializedAsync` method, by adding the following code to the end of the method: + +````csharp +authorList = (await AppService.GetAuthorLookupAsync()).Items; +```` + +The final `@code` block should be the following: + +````csharp +@code +{ + bool canCreateBook; + bool canEditBook; + bool canDeleteBook; + + //ADDED A NEW FIELD + IReadOnlyList authorList = Array.Empty(); + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + canCreateBook = await + AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create); + canEditBook = await + AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Edit); + canDeleteBook = await + AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Delete); + + //GET AUTHORS + authorList = (await AppService.GetAuthorLookupAsync()).Items; + } +} +```` + +Finally, add the following `Field` definition into the `ModalBody` of the *Create* modal, as the first item, before the `Name` field: + +````xml + + @L["Author"] + + +```` + +This requires to add a new localization key to the `en.json` file: + +````js +"PickAnAuthor": "Pick an author" +```` + +You can run the application to see the *Author Selection* while creating a new book: + +![book-create-modal-with-author](images/book-create-modal-with-author.png) + +### Edit Book Modal + +Add the following `Field` definition into the `ModalBody` of the *Edit* modal, as the first item, before the `Name` field: + +````xml + + @L["Author"] + + +```` + +That's all. We are reusing the `authorList` defined for the *Create* modal. + {{end}} \ No newline at end of file diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index bcec504886..f4fcf61e25 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) {{if UI == "MVC" && DB == "EF"}} @@ -115,7 +99,7 @@ We will use these dynamic proxy functions in the next sections to communicate to ## Localization -Before starting to the UI development, we first want to prepare the localization texts (you normally do when needed while developing your application). +Before starting to the UI development, we first want to prepare the localization texts (you normally do this when needed while developing your application). Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: @@ -341,9 +325,7 @@ You can run the application! The final UI of this part is shown below: This is a fully working, server side paged, sorted and localized table of books. -{{end}} - -{{if UI == "NG"}} +{{else if UI == "NG"}} ## Install NPM packages @@ -461,7 +443,7 @@ function configureRoutes(routes: RoutesService) { * `order` is the order of the menu item. * `layout` is the layout of the BooksModule's routes (there are three types of pre-defined layouts: `eLayoutType.application`, `eLayoutType.account` or `eLayoutType.empty`). -For more information, see the [RoutesService document](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice). +For more information, see the [RoutesService document](../UI/Angular/Modifying-the-Menu.md#via-routesservice). ### Service Proxy Generation @@ -551,7 +533,137 @@ Now you can see the final result on your browser: ![Book list final result](./images/bookstore-book-list.png) -{{end}} +{{else if UI == "Blazor"}} + +## Create a Books Page + +It's time to create something visible and usable! Right click to the `Pages` folder under the `Acme.BookStore.Blazor` project and add a new **razor component**, named `Books.razor`: + +![blazor-add-books-component](images/blazor-add-books-component.png) + +Replace the contents of this component as shown below: + +````html +@page "/books" + +

Books

+ +@code { + +} +```` + +### Add Books Page to the Main Menu + +Open the `BookStoreMenuContributor` class in the `Blazor` project add the following code to the end of the `ConfigureMainMenuAsync` method: + +````csharp +context.Menu.AddItem( + new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ).AddItem( + new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" + ) + ) +); +```` + +Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see the new menu item has been added to the main menu: + +![blazor-menu-bookstore](images/blazor-menu-bookstore.png) + +When you click to the Books menu item under the Book Store parent, you are being redirected to the new empty Books Page. + +### Book List + +We will use the [Blazorise library](https://blazorise.com/) as the UI component kit. It is a very powerful library that supports major HTML/CSS frameworks, including the Bootstrap. + +ABP Framework provides a generic base class, `AbpCrudPageBase<...>`, to create CRUD style pages. This base class is compatible to the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpCrudPageBase` to automate the code behind for the standard CRUD stuff. + +Open the `Books.razor` and replace the content as the following: + +````xml +@page "/books" +@using Volo.Abp.Application.Dtos +@using Volo.Abp.BlazoriseUI +@using Acme.BookStore.Books +@using Acme.BookStore.Localization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer L +@inherits AbpCrudPageBase + + + +

@L["Books"]

+
+ + + + + + + @L[$"Enum:BookType:{(int)context.Type}"] + + + + + @context.PublishDate.ToShortDateString() + + + + + + + @context.CreationTime.ToLongDateString() + + + + + +
+```` + +> If you see some syntax errors, you can ignore them if your application properly built and run. Visual Studio still has some bugs with Blazor. + +* Inherited from the `AbpCrudPageBase` which implements all the CRUD details for us. +* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base blass. +* Injected `IStringLocalizer` (as `L` object) and used for localization. + +While the code above pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better. + +#### About the AbpCrudPageBase + +We will continue to benefit from the `AbpCrudPageBase` for the books page. You could just inject the `IBookAppService` and perform all the server side calls yourself (thanks to the [Dynamic C# HTTP API Client Proxy](../API/Dynamic-CSharp-API-Clients.md) system of the ABP Framework). We will do it manually for the authors page to demonstrate how to call server side HTTP APIs in your Blazor applications. + +## Run the Final Application + +You can run the application! The final UI of this part is shown below: + +![blazor-bookstore-book-list](images/blazor-bookstore-book-list.png) + +This is a fully working, server side paged, sorted and localized table of books. + +{{end # UI }} ## The Next Part diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index be3dd75d64..dae7e4bfdb 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/docs/en/Tutorials/Part-3.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) {{if UI == "MVC" && DB == "EF"}} @@ -1175,6 +1159,394 @@ Clicking the "Delete" action calls the `delete` method which then shows a confir {{end}} +{{if UI == "Blazor"}} + +## Creating a New Book + +In this section, you will learn how to create a new modal dialog form to create a new book. Since we've inherited from the `AbpCrudPageBase`, we only need to develop the view part. + +### Add "New Button" Button + +Open the `Books.razor` and replace the `` section with the following code: + +````xml + + + +

@L["Books"]

+
+ + + + + +
+
+```` + +This will change the card header by adding a "New book" button to the right side: + +![blazor-add-book-button](images/blazor-add-book-button.png) + +Now, we can add a modal that will be opened when we click to the button. + +### Book Creation Modal + +Open the `Books.razor` and add the following code to the end of the page: + +````xml + + + + + @L["NewBook"] + + + + + @L["Name"] + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + + +```` + +* `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` method are defined by the base class. See the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components. + +That's all. Run the application and try to add a new book: + +![blazor-new-book-modal](images/blazor-new-book-modal.png) + +## Updating a Book + +Editing a books is similar to the creating a new book. + +### Actions Dropdown + +Open the `Books.razor` and add the following `DataGridColumn` section inside the `DataGridColumns` as the first item: + +````xml + + + + + @L["Actions"] + + + + @L["Edit"] + + + + + +```` + +* `OpenEditModalAsync` is defined in the base class which takes the `Id` of the entity (book) to edit. + +This adds an "Actions" dropdown to all the books inside the `DataGrid` with an `Edit` action: + +![blazor-edit-book-action](images/blazor-edit-book-action.png) + +### Edit Modal + +We can now define a modal to edit the book. Add the following code to the end of the `Books.razor` page: + +````xml + + + + + @EditingEntity.Name + + + + + @L["Name"] + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + + +```` + +### AutoMapper Configuration + +The base `AbpCrudPageBase` uses the [object to object mapping](../Object-To-Object-Mapping.md) system to convert an incoming `BookDto` object to a `CreateUpdateBookDto` object. So, we need to define the mapping. + +Open the `BookStoreBlazorAutoMapperProfile` inside the `Acme.BookStore.Blazor` project and change the content as the following: + +````csharp +using Acme.BookStore.Books; +using AutoMapper; + +namespace Acme.BookStore.Blazor +{ + public class BookStoreBlazorAutoMapperProfile : Profile + { + public BookStoreBlazorAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +* We've just added the `CreateMap();` line to define the mapping. + +### Test the Editing Modal + +You can now run the application and try to edit a book. + +![blazor-edit-book-modal](images/blazor-edit-book-modal.png) + +## Deleting a Book + +Open the `Books.razor` page and add the following `DropdownItem` under the "Edit" action inside the "Actions" `DropdownMenu`: + +````xml + + @L["Delete"] + +```` + +* `DeleteEntityAsync` is defined in the base class. + +Run the application and try to delete a book. + +## Full CRUD UI Code + +Here the complete code to create the book management CRUD page, that has been developed in the last two parts: + +````xml +@page "/books" +@using Volo.Abp.Application.Dtos +@using Volo.Abp.BlazoriseUI +@using Acme.BookStore.Books +@using Acme.BookStore.Localization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer L +@inherits AbpCrudPageBase + + + + + +

@L["Books"]

+
+ + + + + +
+
+ + + + + + + + @L["Actions"] + + + + @L["Edit"] + + + @L["Delete"] + + + + + + + + + @L[$"Enum:BookType:{(int)context.Type}"] + + + + + @context.PublishDate.ToShortDateString() + + + + + + + @context.CreationTime.ToLongDateString() + + + + + +
+ + + + + + @L["NewBook"] + + + + + @L["Name"] + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + + + + + + + + @EditingEntity.Name + + + + + @L["Name"] + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + + +```` + +{{end}} + ## The Next Part See the [next part](Part-4.md) of this tutorial. diff --git a/docs/en/Tutorials/Part-4.md b/docs/en/Tutorials/Part-4.md index 2868df8ba6..d9c000be5c 100644 --- a/docs/en/Tutorials/Part-4.md +++ b/docs/en/Tutorials/Part-4.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) {{if UI == "MVC" && DB == "EF"}} @@ -62,7 +46,9 @@ This part is also recorded as a video tutorial and ** The test projects are configured to use **SQLite in-memory** as the database. A separate database instance is created and seeded (with the data seed system) to prepare a fresh database for every test. +> The test projects are configured to use **SQLite in-memory** as the database. A separate database instance is created and seeded (with the [data seed system](../Data-Seeding.md)) to prepare a fresh database for every test. {{else if DB=="Mongo"}} -> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)** library is used to mock the MongoDB database. A separate database instance is created and seeded (with the data seed system) to prepare a fresh database for every test. +> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)** library is used to mock the MongoDB database. A separate database instance is created and seeded (with the [data seed system](../Data-Seeding.md)) to prepare a fresh database for every test. {{end}} @@ -89,9 +75,12 @@ If you had created a data seed contributor as described in the [first part](Part Add a new test class, named `BookAppService_Tests` in the `Books` namespace (folder) of the `Acme.BookStore.Application.Tests` project: ````csharp +using System; +using System.Linq; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; using Xunit; namespace Acme.BookStore.Books diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md index 5b2dfc0431..a293ec586e 100644 --- a/docs/en/Tutorials/Part-5.md +++ b/docs/en/Tutorials/Part-5.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) {{if UI == "MVC" && DB == "EF"}} @@ -145,6 +129,8 @@ Go to the *Administration -> Identity -> Roles* page, select *Permissions* actio Grant the permissions you want and save the modal. +> **Tip**: New permissions are automatically granted to the admin role if you run the `Acme.BookStore.DbMigrator` application. + ## Authorization Now, you can use the permissions to authorize the book management. @@ -186,6 +172,8 @@ namespace Acme.BookStore.Books Added code to the constructor. Base `CrudAppService` automatically uses these permissions on the CRUD operations. This makes the **application service** secure, but also makes the **HTTP API** secure since this service is automatically used as an HTTP API as explained before (see [auto API controllers](../API/Auto-API-Controllers.md)). +> You will see the declarative authorization, using the `[Authorize(...)]` attribute, later while developing the author management functionality. + {{if UI == "MVC"}} ### Razor Page @@ -327,6 +315,64 @@ if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) } ```` +You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return values. The final `BookStoreMenuContributor` class should be the following: + +````csharp +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Acme.BookStore.Localization; +using Acme.BookStore.MultiTenancy; +using Acme.BookStore.Permissions; +using Volo.Abp.TenantManagement.Web.Navigation; +using Volo.Abp.UI.Navigation; + +namespace Acme.BookStore.Web.Menus +{ + public class BookStoreMenuContributor : IMenuContributor + { + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + if (!MultiTenancyConsts.IsEnabled) + { + var administration = context.Menu.GetAdministration(); + administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName); + } + + var l = context.GetLocalizer(); + + context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/")); + + var bookStoreMenu = new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ); + + context.Menu.AddItem(bookStoreMenu); + + //CHECK the PERMISSION + if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) + { + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/Books" + )); + } + } + } +} +```` + {{else if UI == "NG"}} ### Angular Guard Configuration @@ -410,6 +456,189 @@ Open the `/src/app/book/book.component.html` file and replace the edit and delet * Added `abpPermission="BookStore.Books.Edit"` that hides the edit action if the current user has no editing permission. * Added `abpPermission="BookStore.Books.Delete"` that hides the delete action if the current user has no delete permission. +{{else if UI == "Blazor"}} + +### Authorize the Razor Component + +Open the `/Pages/Books.razor` file in the `Acme.BookStore.Blazor` project and add an `Authorize` attribute just after the `@page` directive and the following namespace imports (`@using` lines), as shown below: + +````html +@page "/books" +@attribute [Authorize(BookStorePermissions.Books.Default)] +@using Acme.BookStore.Permissions +@using Microsoft.AspNetCore.Authorization +... +```` + +Adding this attribute prevents to enter this page if the current hasn't logged in or hasn't granted for the given permission. In case of attempt, the user is redirected to the login page. + +### Show/Hide the Actions + +The book management page has a *New Book* button and *Edit* and *Delete* actions for each book. We should hide these buttons/actions if the current user has not granted for the related permissions. + +#### Get the Permissions On Initialization + +Add the following code block to the end of the `Books.razor` file: + +````csharp +@code +{ + bool canCreateBook; + bool canEditBook; + bool canDeleteBook; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + canCreateBook =await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create); + canEditBook = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Edit); + canDeleteBook = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Delete); + } +} +```` + +We will use these `bool` fields to check the permissions. `AuthorizationService` comes from the base class as an injected property. + +> **Blazor Tip**: While adding the C# code into a `@code` block is fine for small code parts, it is suggested to use the code behind approach to develop a more maintainable code base when the code block becomes longer. We will use this approach for the authors part. + +#### Hide the New Book Button + +Wrap the *New Book* button by an `if` block as shown below: + +````xml +@if (canCreateBook) +{ + +} +```` + +#### Hide the Edit/Delete Actions + +As similar to the *New Book* button, we can use `if` blocks to conditionally show/hide the *Edit* and *Delete* actions: + +````xml +@if (canEditBook) +{ + + @L["Edit"] + +} +@if (canDeleteBook) +{ + + @L["Delete"] + +} +```` + +#### About the Permission Caching + +You can run and test the permissions. Remove a book related permission from the admin role to see the related button/action disappears from the UI. + +**ABP Framework caches the permissions** of the current user in the client side. So, when you change a permission for yourself, you need to manually **refresh the page** to take the effect. If you don't refresh and try to use the prohibited action you get an HTTP 403 (forbidden) response from the server. + +> Changing a permission for a role or user immediately available on the server side. So, this cache system doesn't cause any security problem. + +### Menu Item + +Even we have secured all the layers of the book management page, it is still visible on the main menu of the application. We should hide the menu item if the current user has no permission. + +Open the `BookStoreMenuContributor` class in the `Acme.BookStore.Blazor` project, find the code block below: + +````csharp +context.Menu.AddItem( + new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ).AddItem( + new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" + ) + ) +); +```` + +And replace this code block with the following: + +````csharp +var bookStoreMenu = new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" +); + +context.Menu.AddItem(bookStoreMenu); + +//CHECK the PERMISSION +if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) +{ + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" + )); +} +```` + +You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return values. The final `BookStoreMenuContributor` class should be the following: + +````csharp +using System.Threading.Tasks; +using Acme.BookStore.Localization; +using Acme.BookStore.Permissions; +using Volo.Abp.UI.Navigation; + +namespace Acme.BookStore.Blazor +{ + public class BookStoreMenuContributor : IMenuContributor + { + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if(context.Menu.DisplayName != StandardMenus.Main) + { + return; + } + + var l = context.GetLocalizer(); + + context.Menu.Items.Insert( + 0, + new ApplicationMenuItem( + "BookStore.Home", + l["Menu:Home"], + "/", + icon: "fas fa-home" + ) + ); + + var bookStoreMenu = new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ); + + context.Menu.AddItem(bookStoreMenu); + + if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) + { + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" + )); + } + } + } +} +```` + {{end}} ## The Next Part diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md index b1b38f18dc..69c6574520 100644 --- a/docs/en/Tutorials/Part-6.md +++ b/docs/en/Tutorials/Part-6.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) ## Introduction @@ -242,16 +226,7 @@ This is a unique string represents the error code thrown by your application and "BookStore:00001": "There is already an author with the same name: {name}" ```` -Then open the `BookStoreDomainSharedModule` and add the following code block inside the `ConfigureServices` method: - -````csharp -Configure(options => -{ - options.MapCodeNamespace("BookStore", typeof(BookStoreResource)); -}); -```` - -Whenever you throw an `AuthorAlreadyExistsException`, the end use will see a nice error message on the UI. +Whenever you throw an `AuthorAlreadyExistsException`, the end use will see a this message on the UI. ## IAuthorRepository diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md index 81fd07f1bf..5cae52a4b1 100644 --- a/docs/en/Tutorials/Part-7.md +++ b/docs/en/Tutorials/Part-7.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) ## Introduction @@ -86,10 +70,14 @@ This is just like done for the `Book` entity before, so no need to explain again ## Create a new Database Migration -Open the **Package Manager Console** on Visual Studio and ensure that the **Default project** is `Acme.BookStore.EntityFrameworkCore.DbMigrations` in the Package Manager Console, as shown on the picture below. Also, set the `Acme.BookStore.Web` as the startup project (right click it on the solution explorer and click to "Set as Startup Project"). +Open the **Package Manager Console** on Visual Studio and ensure that the **Default project** is `Acme.BookStore.EntityFrameworkCore.DbMigrations` in the Package Manager Console, as shown on the picture below. Also, set the `Acme.BookStore.Web` (or `Acme.BookStore.HttpApi.Host`, depending on your solution) as the **startup project** (right click it on the solution explorer and click to "Set as Startup Project"). Run the following command to create a new database migration: +````bash +Add-Migration "Added_Authors" +```` + ![bookstore-add-migration-authors](images/bookstore-add-migration-authors.png) This will create a new migration class. Then run the `Update-Database` command to create the table on the database. diff --git a/docs/en/Tutorials/Part-8.md b/docs/en/Tutorials/Part-8.md index fc10a799f2..b7c805c17e 100644 --- a/docs/en/Tutorials/Part-8.md +++ b/docs/en/Tutorials/Part-8.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,9 +28,10 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) ## Introduction @@ -318,7 +302,7 @@ public async Task UpdateAsync(Guid id, UpdateAuthorDto input) {{if DB == "EF"}} -> **EF Core tip**: Entity Framework Core has a **change tracking** system and **automatically saves** any change to an entity at the end of the unit of work (You can simply think that the ABP Framework automatically calls `SaveChanges` at the end of the method). So, it will work as expected even if you don't call the `_authorRepository.UpdateAsync(...)` in the end of the method. If you don't consider to change the EF Core later, you can just remove this line. +> **EF Core Tip**: Entity Framework Core has a **change tracking** system and **automatically saves** any change to an entity at the end of the unit of work (You can simply think that the ABP Framework automatically calls `SaveChanges` at the end of the method). So, it will work as expected even if you don't call the `_authorRepository.UpdateAsync(...)` in the end of the method. If you don't consider to change the EF Core later, you can just remove this line. {{end}} diff --git a/docs/en/Tutorials/Part-9.md b/docs/en/Tutorials/Part-9.md index 0d78d584b8..374efff2b4 100644 --- a/docs/en/Tutorials/Part-9.md +++ b/docs/en/Tutorials/Part-9.md @@ -2,32 +2,15 @@ ````json //[doc-params] { - "UI": ["MVC","NG"], + "UI": ["MVC","Blazor","NG"], "DB": ["EF","Mongo"] } ```` -{{ -if UI == "MVC" - UI_Text="mvc" -else if UI == "NG" - UI_Text="angular" -else - UI_Text="?" -end -if DB == "EF" - DB_Text="Entity Framework Core" -else if DB == "Mongo" - DB_Text="MongoDB" -else - DB_Text="?" -end -}} - ## About This Tutorial In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -45,18 +28,19 @@ This tutorial is organized as the following parts; ### Download the Source Code -This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: +This tutorial has multiple versions based on your **UI** and **Database** preferences. We've prepared a few combinations of the source code to be downloaded: * [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) ## Introduction -This part explains how to create a CRUD page for the `Author` entity introduced in previous parts. +This part explains how to create a CRUD page for the `Author` entity introduced in the previous parts. {{if UI == "MVC"}} -## The Book List Page +## The Authors List Page Create a new razor page, `Index.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. @@ -509,7 +493,7 @@ That's all! You can run the application and try to edit an author. {{else if UI == "NG"}} -## The Author List Page, Create & Delete Authors +## The Author Management Page Run the following command line to create a new module, named `AuthorModule` in the root folder of the angular application: @@ -848,6 +832,367 @@ That's all! This is a fully working CRUD page, you can create, edit and delete a {{end}} +{{if UI == "Blazor"}} + +## The Author Management Page + +### Authors Razor Component + +Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStore.Blazor` project with the following content: + +````xml +@page "/authors" +@using Acme.BookStore.Authors +@using Acme.BookStore.Localization +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Extensions.Localization +@using Volo.Abp.ObjectMapping +@inject IAuthorAppService AuthorAppService +@inject IStringLocalizer L +@inject IAuthorizationService AuthorizationService +@inject IUiMessageService UiMessageService +@inject IObjectMapper ObjectMapper + + + + +

@L["Authors"]

+
+ + + @if (CanCreateAuthor) + { + + } + + +
+
+ + + + + + + + @L["Actions"] + + + @if (CanEditAuthor) + { + + @L["Edit"] + + } + @if (CanDeleteAuthor) + { + + @L["Delete"] + + } + + + + + + + + @context.BirthDate.ToShortDateString() + + + + + +
+ + + + + + @L["NewAuthor"] + + + + + @L["Name"] + + + + @L["BirthDate"] + + + + @L["ShortBio"] + + + + + + + + + + + + + + + @EditingAuthor.Name + + + + + @L["Name"] + + + + @L["BirthDate"] + + + + @L["ShortBio"] + + + + + + + + + +```` + +* This code is similar to the `Books.razor`, except it doesn't inherit from the `BlazorisePageBase`, but uses its own implementation. +* Injects the `IAuthorAppService` to consume the server side HTTP APIs from the UI. We can directly inject application service interfaces and use just like regular method calls by the help of [Dynamic C# HTTP API Client Proxy System](../API/Dynamic-CSharp-API-Clients.md), which performs REST API calls for us. See the `Authors` class below to see the usage. +* Injects the `IAuthorizationService` to check [permissions](../Authorization.md). +* Injects the `IObjectMapper` for [object to object mapping](../Object-To-Object-Mapping.md). + +Create a new code behind file, `Authors.razor.cs`, under the `Pages` folder, with the following content: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Permissions; +using Blazorise; +using Blazorise.DataGrid; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Blazor.Pages +{ + public partial class Authors + { + private IReadOnlyList AuthorList { get; set; } + + private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; + private int CurrentPage { get; set; } + private string CurrentSorting { get; set; } + private int TotalCount { get; set; } + + private bool CanCreateAuthor { get; set; } + private bool CanEditAuthor { get; set; } + private bool CanDeleteAuthor { get; set; } + + private CreateAuthorDto NewAuthor { get; set; } + + private Guid EditingAuthorId { get; set; } + private UpdateAuthorDto EditingAuthor { get; set; } + + private Modal CreateAuthorModal { get; set; } + private Modal EditAuthorModal { get; set; } + + public Authors() + { + NewAuthor = new CreateAuthorDto(); + EditingAuthor = new UpdateAuthorDto(); + } + + protected override async Task OnInitializedAsync() + { + await SetPermissionsAsync(); + await GetAuthorsAsync(); + } + + private async Task SetPermissionsAsync() + { + CanCreateAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Create); + + CanEditAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Edit); + + CanDeleteAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Delete); + } + + private async Task GetAuthorsAsync() + { + var result = await AuthorAppService.GetListAsync( + new GetAuthorListDto + { + MaxResultCount = PageSize, + SkipCount = CurrentPage * PageSize, + Sorting = CurrentSorting + } + ); + + AuthorList = result.Items; + TotalCount = (int)result.TotalCount; + } + + private async Task OnDataGridReadAsync(DataGridReadDataEventArgs e) + { + CurrentSorting = e.Columns + .Where(c => c.Direction != SortDirection.None) + .Select(c => c.Field + (c.Direction == SortDirection.Descending ? " DESC" : "")) + .JoinAsString(","); + CurrentPage = e.Page - 1; + + await GetAuthorsAsync(); + + StateHasChanged(); + } + + private void OpenCreateAuthorModal() + { + NewAuthor = new CreateAuthorDto(); + CreateAuthorModal.Show(); + } + + private void CloseCreateAuthorModal() + { + CreateAuthorModal.Hide(); + } + + private void OpenEditAuthorModal(AuthorDto author) + { + EditingAuthorId = author.Id; + EditingAuthor = ObjectMapper.Map(author); + EditAuthorModal.Show(); + } + + private async Task DeleteAuthorAsync(AuthorDto author) + { + var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; + if (!await UiMessageService.ConfirmAsync(confirmMessage)) + { + return; + } + + await AuthorAppService.DeleteAsync(author.Id); + await GetAuthorsAsync(); + } + + private void CloseEditAuthorModal() + { + EditAuthorModal.Hide(); + } + + private async Task CreateAuthorAsync() + { + await AuthorAppService.CreateAsync(NewAuthor); + await GetAuthorsAsync(); + CreateAuthorModal.Hide(); + } + + private async Task UpdateAuthorAsync() + { + await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); + await GetAuthorsAsync(); + EditAuthorModal.Hide(); + } + } +} +```` + +This class typically defines the properties and methods used by the `Authors.razor` page. + +### Object Mapping + +`Authors` class uses the `IObjectMapper` in the `OpenEditAuthorModal` method. So, we need to define this mapping. + +Open the `BookStoreBlazorAutoMapperProfile.cs` in the `Acme.BookStore.Blazor` project and add the following mapping code in the constructor: + +````csharp +CreateMap(); +```` + +You will need to declare a `using Acme.BookStore.Authors;` statement to the beginning of the file. + +### Add to the Main Menu + +Open the `BookStoreMenuContributor.cs` in the `Acme.BookStore.Blazor` project and add the following code to the end of the `ConfigureMainMenuAsync` method: + +````csharp +if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) +{ + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Authors", + l["Menu:Authors"], + url: "/authors" + )); +} +```` + +### Localizations + +We should complete the localizations we've used above. Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project and add the following entries: + +````json +"Menu:Authors": "Authors", +"Authors": "Authors", +"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", +"BirthDate": "Birth date", +"NewAuthor": "New author" +```` + +### Run the Application + +Run and login to the application. **If you don't see the Authors menu item under the Book Store menu, that means you don't have the permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: + +![bookstore-author-permissions](images/bookstore-author-permissions.png) + +As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: + +![bookstore-authors-page](images/bookstore-authors-blazor-ui.png) + +That's all! This is a fully working CRUD page, you can create, edit and delete the authors. + +> **Tip**: If you run the `.DbMigrator` console application after defining a new permission, it automatically grants these new permissions to the admin role and you don't need to manually grant the permissions yourself. + +{{end}} + ## The Next Part See the [next part](Part-10.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/images/blazor-add-book-button.png b/docs/en/Tutorials/images/blazor-add-book-button.png new file mode 100644 index 0000000000..58a607d3b1 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-add-book-button.png differ diff --git a/docs/en/Tutorials/images/blazor-add-books-component.png b/docs/en/Tutorials/images/blazor-add-books-component.png new file mode 100644 index 0000000000..ec0412d31e Binary files /dev/null and b/docs/en/Tutorials/images/blazor-add-books-component.png differ diff --git a/docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors.png b/docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors.png new file mode 100644 index 0000000000..75c5306c3c Binary files /dev/null and b/docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors.png differ diff --git a/docs/en/Tutorials/images/blazor-bookstore-book-list.png b/docs/en/Tutorials/images/blazor-bookstore-book-list.png new file mode 100644 index 0000000000..91450f47c2 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-bookstore-book-list.png differ diff --git a/docs/en/Tutorials/images/blazor-edit-book-action.png b/docs/en/Tutorials/images/blazor-edit-book-action.png new file mode 100644 index 0000000000..f1179dae58 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-edit-book-action.png differ diff --git a/docs/en/Tutorials/images/blazor-edit-book-modal.png b/docs/en/Tutorials/images/blazor-edit-book-modal.png new file mode 100644 index 0000000000..e63e796d34 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-edit-book-modal.png differ diff --git a/docs/en/Tutorials/images/blazor-menu-bookstore.png b/docs/en/Tutorials/images/blazor-menu-bookstore.png new file mode 100644 index 0000000000..f9f334aa1d Binary files /dev/null and b/docs/en/Tutorials/images/blazor-menu-bookstore.png differ diff --git a/docs/en/Tutorials/images/blazor-new-book-modal.png b/docs/en/Tutorials/images/blazor-new-book-modal.png new file mode 100644 index 0000000000..dd284a0ffd Binary files /dev/null and b/docs/en/Tutorials/images/blazor-new-book-modal.png differ diff --git a/docs/en/Tutorials/images/book-create-modal-with-author.png b/docs/en/Tutorials/images/book-create-modal-with-author.png new file mode 100644 index 0000000000..5e662553aa Binary files /dev/null and b/docs/en/Tutorials/images/book-create-modal-with-author.png differ diff --git a/docs/en/Tutorials/images/bookstore-authors-blazor-ui.png b/docs/en/Tutorials/images/bookstore-authors-blazor-ui.png new file mode 100644 index 0000000000..5b9480957d Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-authors-blazor-ui.png differ diff --git a/docs/en/UI/Angular/Config-State.md b/docs/en/UI/Angular/Config-State.md index bda3250b32..b1a882d43a 100644 --- a/docs/en/UI/Angular/Config-State.md +++ b/docs/en/UI/Angular/Config-State.md @@ -93,51 +93,6 @@ const searchUrl = this.config.getApiUrl("search"); This method returns the `url` of a specific API based on the key given as its only parameter. If there is no key, `'default'` is used. -### How to Get All Settings From the Store - -You can use the `getSettings` method of `ConfigStateService` to get all of the settings object from the configuration state. Here is how you get all settings: - -```js -// this.config is instance of ConfigStateService - -const settings = this.config.getSettings(); -``` - -In addition, the method lets you search settings by **passing a keyword** to it. - -```js -const localizationSettings = this.config.getSettings("Localization"); -/* -{ - 'Abp.Localization.DefaultLanguage': 'en' -} -*/ -``` - -Beware though, **settings search is case sensitive**. - -### How to Get a Specific Setting From the Store - -You can use the `getSetting` method of `ConfigStateService` to get a specific setting from the configuration state. Here is an example: - -```js -// this.config is instance of ConfigStateService - -const defaultLang = this.config.getSetting("Abp.Localization.DefaultLanguage"); -// 'en' -``` - -### How to Get a Specific Feature From the Store - -You can use the `getFeature` method of `ConfigStateService` to get a specific feature from the configuration state. Here is an example: - -```js -// this.config is instance of ConfigStateService - -const isChatEnabled = this.config.getFeature("Chat.Enable"); -// 'en' -``` - ### How to Get a Specific Permission From the Store You can use the `getGrantedPolicy` method of `ConfigStateService` to get a specific permission from the configuration state. For that, you should pass a policy key as parameter to the method. @@ -231,6 +186,11 @@ Note that **you do not have to call this method at application initiation**, bec Please refer to `Config.Environment` type for all the properties you can pass to `dispatchSetEnvironment` as parameter. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13). +## See Also + +- [Settings](./Settings.md) +- [Features](./Features.md) + ## What's Next? -- [Modifying the Menu](./Modifying-the-Menu.md) \ No newline at end of file +- [HTTP Requests](./Http-Requests) diff --git a/docs/en/UI/Angular/Content-Projection-Service.md b/docs/en/UI/Angular/Content-Projection-Service.md index db7c52be81..7e03776835 100644 --- a/docs/en/UI/Angular/Content-Projection-Service.md +++ b/docs/en/UI/Angular/Content-Projection-Service.md @@ -1,4 +1,4 @@ -# Content Projection +# Projecting Angular Content You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. @@ -75,4 +75,4 @@ projectContent | TemplateRef>( ## What's Next? -- [TrackByService](./Track-By-Service.md) +- [Confirmation Popup](./Confirmation-Service.md) diff --git a/docs/en/UI/Angular/Custom-Setting-Page.md b/docs/en/UI/Angular/Custom-Setting-Page.md index e4ee6d45c2..9a17f6ad7b 100644 --- a/docs/en/UI/Angular/Custom-Setting-Page.md +++ b/docs/en/UI/Angular/Custom-Setting-Page.md @@ -35,7 +35,3 @@ export class AppComponent { Navigate to `/setting-management` route to see the changes: ![Custom Settings Tab](./images/custom-settings.png) - -## What's Next? - -- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md) diff --git a/docs/en/UI/Angular/Dom-Insertion-Service.md b/docs/en/UI/Angular/Dom-Insertion-Service.md index 88f30136b3..d56e4f8b0e 100644 --- a/docs/en/UI/Angular/Dom-Insertion-Service.md +++ b/docs/en/UI/Angular/Dom-Insertion-Service.md @@ -1,4 +1,4 @@ -# Dom Insertion (of Scripts and Styles) +# Inserting Scripts & Styles to DOM You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. @@ -137,4 +137,4 @@ The `has` method returns a boolean value that indicates the given content has al ## What's Next? -- [ContentProjectionService](./Content-Projection-Service.md) +- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index 402e110f94..1b78a11cc6 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -4,7 +4,7 @@ Every application needs some **environment** variables. In Angular world, this i Current `Environment` configuration holds sub config classes as follows: -```typescript +```js export interface Environment { apis: Apis; application: Application; @@ -16,7 +16,7 @@ export interface Environment { ## Apis -```typescript +```js export interface Apis { [key: string]: ApiConfig; default: ApiConfig; @@ -54,9 +54,9 @@ Everything else will be sent to `"https://localhost:8080"` * `rootNamespace` **(new)** : Root namespace of the related API. e.g. Acme.BookStore -## Application +## Application -```typescript +```js export interface Application { name: string; baseUrl?: string; @@ -69,7 +69,7 @@ Everything else will be sent to `"https://localhost:8080"` * `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver) -## AuthConfig +## AuthConfig For authentication, we use angular-oauth2-oidc. Please check their [docs](https://github.com/manfredsteyer/angular-oauth2-oidc) out @@ -80,7 +80,7 @@ Abp Framework supports this out of box. To integrate an existing config json into the `environment`, you need to set `remoteEnv` -```typescript +```js export type customMergeFn = ( localEnv: Partial, remoteEnv: any, @@ -105,5 +105,4 @@ export interface RemoteEnv { ## What's Next? -* [Service Proxies](./Service-Proxies.md) - +- [About Feature Libraries](./Feature-Libraries.md) diff --git a/docs/en/UI/Angular/Feature-Libraries.md b/docs/en/UI/Angular/Feature-Libraries.md new file mode 100644 index 0000000000..937bae7245 --- /dev/null +++ b/docs/en/UI/Angular/Feature-Libraries.md @@ -0,0 +1,98 @@ +# About Feature Libraries + +ABP has an ever-growing number of feature modules and [introducing a new one](../../Module-Development-Basics.md) is always possible. When the UI is Angular, these features have modular Angular libraries accompanying them. + +## Feature Library Content + +Each library has at least two modules: + +1. The main module contains all components, services, types, enums, etc. to deliver the required UI when the feature is loaded. From here on, we will refer to these modules as **"feature module"**. +2. There is also a **"config module"** per library which helps us configure applications to run these modules or make them accessible. + +## How to Add a Feature Library to Your Project + + + +The manual setup of a feature library has three steps: + +### 1. Install the Library + +Feature libraries are usually published as an npm package. If a library you want to use does not exist in your project, you may install it via the following command: + +```shell +yarn add @my-company-name/my-project-name +``` + +...or... + +```shell +npm install @my-company-name/my-project-name +``` + +The `my-company-name` and `my-project-name` parts are going to change according to the package you want to use. For example, if we want to install the ABP Identity module, the package installation will be as seen below: + +```shell +yarn add @abp/ng.identity +``` + +> Identity is used just as an example. If you have initiated your project with ABP CLI or ABP Suite, the identity library will already be installed and configured in your project. + +### 2. Import the Config Module + +As of ABP v3.0, every lazy-loaded module has a config module available via a secondary entry point on the same package. Importing them in your root module looks like this: + +```js +import { IdentityConfigModule } from "@abp/ng.identity/config"; + +@NgModule({ + imports: [ + // other imports + IdentityConfigModule.forRoot(), + ], + // providers, declarations, and bootstrap +}) +export class AppModule {} +``` + +We need the config modules for actions required before feature modules are loaded (lazily). For example, the above import configures the menu to display links to identity pages. + +Furthermore, depending on the library, the `.forRoot` static method may receive some options that configure how the feature works. + +### 3. Import the Feature Module + +Finally, the feature module should be [loaded lazily via Angular router](https://angular.io/guide/lazy-loading-ngmodules). If you open the `/src/app/app-routing.module.ts` file, you should see `IdentityModule` is loaded exactly as follows: + +```js +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +const routes: Routes = [ + // other routes + { + path: "identity", + loadChildren: () => + import("@abp/ng.identity").then((m) => m.IdentityModule.forLazy()), + }, + // other routes +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} +``` + +When you load the identity feature like this, the "Users" page, for example, will have a route path of `/identity/users`. [1](#f-modify-route) + +Depending on the library, the `.forLazy` static method may also receive some options that configure how the feature works. + +--- + +1 _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./Modifying-the-Menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ [↩](#a-modify-route) + +--- + +## What's Next? + +- [Service Proxies](./Service-Proxies.md) diff --git a/docs/en/UI/Angular/Features.md b/docs/en/UI/Angular/Features.md index f749030a26..930fc2e527 100644 --- a/docs/en/UI/Angular/Features.md +++ b/docs/en/UI/Angular/Features.md @@ -1,3 +1,37 @@ -# Angular UI: Features +# Features -> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system. \ No newline at end of file +You can get the value of a feature on the client-side using the [config state service](./Config-State.md) if it is allowed by the feature definition on the server-side. + +> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system. + +## Before Use + +To use the `ConfigStateService`, you must inject it in your class as a dependency. You do not have to provide the service explicitly, because it is already **provided in root**. + +```js +import { ConfigStateService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private config: ConfigStateService) {} +} +``` + +## How to Get a Specific Feature + +You can use the `getFeature` method of `ConfigStateService` to get a specific feature from the configuration state. Here is an example: + +```js +// this.config is instance of ConfigStateService + +const defaultLang = this.config.getFeature("Identity.TwoFactor"); +// 'Optional' +``` + +You can then check the value of the feature to perform your logic. Please note that **feature keys are case-sensitive**. + +## What's Next? + +- [Permission Management](./Permission-Management.md) diff --git a/docs/en/UI/Angular/Lazy-Load-Service.md b/docs/en/UI/Angular/Lazy-Load-Service.md index b4eddf7b25..344b446107 100644 --- a/docs/en/UI/Angular/Lazy-Load-Service.md +++ b/docs/en/UI/Angular/Lazy-Load-Service.md @@ -1,4 +1,4 @@ -# How to Lazy Load Scripts and Styles +# Lazy Loading Scripts & Styles You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way. @@ -210,4 +210,4 @@ load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Obser ## What's Next? -- [DomInsertionService](./Dom-Insertion-Service.md) +- [Projecting Angular Content](./Content-Projection-Service.md) diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md index f65f0db4cd..d534558363 100644 --- a/docs/en/UI/Angular/List-Service.md +++ b/docs/en/UI/Angular/List-Service.md @@ -1,4 +1,4 @@ -# Querying Lists Easily with ListService +# Working with Lists `ListService` is a utility service to provide an easy pagination, sorting, and search implementation. @@ -177,3 +177,8 @@ As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for ini ``` **Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable. + + +## What's Next? + +- [Easy *ngFor trackBy](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md index eec4db0cb4..1efdbca359 100644 --- a/docs/en/UI/Angular/Localization.md +++ b/docs/en/UI/Angular/Localization.md @@ -219,11 +219,22 @@ If you see an error like this, you should pass the `cultureNameLocaleFileMap` pr See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales). +## Adding new culture +```js +//app.module.ts + +import { storeLocaleData } from '@abp/ng.core'; +import( +/* webpackChunkName: "_locale-your-locale-js"*/ +/* webpackMode: "eager" */ +'@angular/common/locales/your-locale.js' +).then(m => storeLocaleData(m.default, 'your-locale')); +``` ## See Also * [Localization in ASP.NET Core](../../Localization.md) ## What's Next? -* [Permission Management](./Permission-Management.md) +* [Settings](./Settings.md) diff --git a/docs/en/UI/Angular/Migration-Guide-v3.md b/docs/en/UI/Angular/Migration-Guide-v3.md index 810d830ca3..6a7b86efcb 100644 --- a/docs/en/UI/Angular/Migration-Guide-v3.md +++ b/docs/en/UI/Angular/Migration-Guide-v3.md @@ -10,7 +10,7 @@ The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dr Open a terminal at your root folder and run the following command: -```sh +```shell yarn ng update @angular/cli @angular/core --force ``` @@ -26,7 +26,7 @@ On the other hand, it would be better if you check which packages to update firs When Angular reports the packages above, your command would look like this: -```sh +```shell yarn ng update @angular/cli @angular/core ng-zorro-antd --force ``` @@ -473,5 +473,5 @@ Some interfaces have long been marked as deprecated and now they are removed. ## What's Next? -* [Environment](./Environment.md) +* [Quick Start](./Quick-Start.md) diff --git a/docs/en/UI/Angular/Multi-Tenancy.md b/docs/en/UI/Angular/Multi-Tenancy.md index fb9fb54f76..c3e54c5f83 100644 --- a/docs/en/UI/Angular/Multi-Tenancy.md +++ b/docs/en/UI/Angular/Multi-Tenancy.md @@ -2,8 +2,8 @@ ABP Angular UI supports the multi-tenancy. The following features related to multi-tenancy are available in the startup templates. - ![Tenants Page](./images/tenants-page.png) +

Tenants page

On the page above, you can; @@ -13,13 +13,12 @@ On the page above, you can; - Edit an existing tenant. - Delete a tenant. - ![Tenant Switching Component](./images/tenant-switching-box.png) +

Tenant Switching Component

You can switch between existing tenants by using the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page). Angular UI sends the selected tenant id to the backend as `__tenant` header on each request. - ## Domain Tenant Resolver Angular UI can get the tenant name from the app running URL. You can determine the current tenant by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). To do this, you need to set the `application.baseUrl` property in the environment: @@ -32,10 +31,10 @@ Subdomain resolver: export const environment = { //... application: { - baseUrl: 'https://{0}.mydomain.com/' + baseUrl: "https://{0}.mydomain.com/", }, //... -} +}; ``` **{0}** is the placeholder to determine current tenant's unique name. @@ -44,7 +43,6 @@ After the configuration above, if your app runs on the `mytenant1.mydomain.com`, > **Important Note:** If you define the `baseUrl` with the placeholder (**{0}**), the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page) will be hidden. - Domain resolver: ```js @@ -53,10 +51,10 @@ Domain resolver: export const environment = { //... application: { - baseUrl: 'https://{0}.com/' + baseUrl: "https://{0}.com/", }, //... -} +}; ``` After the configuration above, if your app runs on the `mytenant.com`, the app will get the tenant name as **mytenant**. @@ -71,29 +69,28 @@ The **{0}** placeholder can be put to the API URLs in the environment to determi export const environment = { //... application: { - baseUrl: 'https://{0}.mydomain.com/', + baseUrl: "https://{0}.mydomain.com/", //... }, oAuthConfig: { - issuer: 'https://{0}.ids.mydomain.com', + issuer: "https://{0}.ids.mydomain.com", //... }, apis: { default: { - url: 'https://{0}.api.mydomain.com', + url: "https://{0}.api.mydomain.com", }, AbpIdentity: { - url: 'https://{0}.identity.mydomain.com', + url: "https://{0}.identity.mydomain.com", }, }, -} +}; ``` > **Important Note:** The `application.baseUrl` and the `{0}` placeholder in the value of the `baseUrl` property are required to be able to get tenant from running URL. Other placeholders in API URLs are optional. After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get tenant name as **mytenant1** and replace the environment object in `ConfigState` on app initialization as follows: - ```js // environment object in ConfigState @@ -128,8 +125,8 @@ The app sends the `__tenant` header that contains the current tenant id on each ## See Also -* [Multi Tenancy in ABP](../../Multi-Tenancy.md) +- [Multi Tenancy in ABP](../../Multi-Tenancy.md) ## What's Next? -- [Confirmation Popup](./Confirmation-Service.md) +- [Managing RxJS Subscriptions](./Subscription-Service.md) diff --git a/docs/en/UI/Angular/PWA-Configuration.md b/docs/en/UI/Angular/PWA-Configuration.md new file mode 100644 index 0000000000..a6aee244ba --- /dev/null +++ b/docs/en/UI/Angular/PWA-Configuration.md @@ -0,0 +1,346 @@ +# PWA Configuration + +[Progressive Web Apps](https://web.dev/progressive-web-apps/) are web applications which, although not as integrated to the OS as a native app, can take advantage of native features. They can be discovered via search engines, installed on devices with a single tap or click, and shared via a regular link. They also can work offline and get updates when new content is available. + +Converting your Angular application to a PWA is easy. + +## 1. Install Angular PWA + +Run the following command in the root folder of your Angular application: + +```shell +yarn ng add @angular/pwa +``` + +...or... + +```shell +npm run ng add @angular/pwa +``` + +This will install the `@angular/service-worker` package and make your default app a PWA. Alternatively, you may add `project` parameter to target a specific app in your workspace: + +```shell +yarn ng add @angular/pwa --project MyProjectName +``` + +Here is the output of the command: + +Angular PWA updates and creates files + +So, Angular CLI updates some files and add a few others: + +- **ngsw-config.json** is where the [service worker configuration](https://angular.io/guide/service-worker-config) is placed. Not all PWAs have this file. It is specific to Angular. +- **manifest.webmanifest** is a [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) and provides information about your app in JSON format. +- **icons** are placeholder icons that are referred to in your web app manifest. We will replace these in a minute. +- **angular.json** has following modifications: + - `assets` include _manifest.webmanifest_. + - `serviceWorker` is `true` in production build. + - `ngswConfigPath` refers to _ngsw-config.json_. +- **package.json** has _@angular/service-worker_ as a new dependency. +- **app.module.ts** imports `ServiceWorkerModule` and registers a service worker filename. +- **index.html** has following modifications: + - A `` element that refers to _manifest.webmanifest_. + - A `` tag that sets a theme color. + +## 2. Update the Web App Manifest + +### 2.1. Set the Name of Your App + +The `name` and the `short_name` properties in the generated manifest are derived from your project name. Let's change them. + +Open the _manifest.webmanifest_ file and update `name` and `short_name` props: + +```json +{ + /* rest of the manifest meta data */ + "short_name": "My Project", + "name": "My Project: My Catch-Phrase" +} +``` + +The short name must be really short because it will be displayed on anywhere with limited space, like the launcher and the home screen. + +### 2.2. Add a Description + +The `@angular/pwa` schematic we just added does not insert a description to your manifest file, but, according to [web app manifest standards](https://www.w3.org/TR/appmanifest/#description-member), you should. + +So, open the _manifest.webmanifest_ file and place the description as seen below: + +```json +{ + /* rest of the manifest meta data */ + "description": "My short project description giving a slightly better idea about my app" +} +``` + +As a bonus, providing a description [along with other criteria](https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-edgehtml/microsoft-store#criteria-for-automatic-submission) helps Bing web crawler to index your app and automatically submit your app to Microsoft Store in `.appx` format. + +### 2.3. Set App Colors + +Angular generates the manifest file with a default `theme_color` and `background_color`. Change these according to your brand identity: + +Open the _manifest.webmanifest_ file and update `theme_color` and `background_color` properties: + +```json +{ + /* rest of the manifest meta data */ + "theme_color": "#000000", + "background_color": "#ffffff" +} +``` + +Then open _index.html_ and change the theme color meta tag as below: + +```html + +``` + +### 2.4. Replace App Icons & Add Splash Screens + +We need to update the icons and add some splash screens. This normally is time-consuming, but we will use the marvelous [pwa-asset-generator](https://github.com/onderceylan/pwa-asset-generator#readme) library. + +First, open the _manifest.webmanifest_ file and remove all elements in the `icons` property: + +```json +{ + /* rest of the manifest meta data */ + "icons": [] +} +``` + +Then, run the following command in your terminal (changing the path of course): + +```shell +npx pwa-asset-generator /path/to/your/logo.png ./src/assets/pwa -i ./src/index.html -m ./src/manifest.webmanifest +``` + +Open the _manifest.webmanifest_ file again. You will see this: + +```json +{ + /* rest of the manifest meta data */ + "icons": [ + { + "src": "../manifest-icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "../manifest-icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable any" + } + ] +} +``` + +In addition to updated icons, the library will generate splash screens. However, Apple requires all splash screens to be added in your _index.html_ and displays a blank screen at startup otherwise. So, the following tags will be inserted into the _index.html_ file: + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## 3. Configure Service Worker + +### 3.1 Modify Asset Groups + +Angular has defined some static files to be cached by the service worker, but they are not 100% accurate. Let's change it. + +Open _ngsw-config.json_ file and replace its content with this: + +```json +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": [ + "/favicon.ico", + "/index.html", + "/manifest.webmanifest", + "/*.css", + "/common-es2015.*.js", + "/main-es2015.*.js", + "/polyfills-es2015.*.js", + "/runtime-es2015.*.js", + "/vendor-es2015.*.js" + ] + } + }, + { + "name": "modules", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/*-es2015.*.js", + "!/common-es2015.*.js", + "!/main-es2015.*.js", + "!/polyfills-es2015.*.js", + "!/runtime-es2015.*.js", + "!/vendor-es2015.*.js" + ] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/**", + "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" + ] + } + } + ] +} +``` + +In case you want to cache other static files, please refer to the [service worker configuration document](https://angular.io/guide/service-worker-config#assetgroups) on Angular.io. + +### 3.2 Set Data Groups + +This part is unique to your project. We recommend being very careful about which endpoints to cache. Please refer to [service worker configuration document](https://angular.io/guide/service-worker-config#datagroups) on Angular.io for details. + +## What's Next? + +- [Config State](./Config-State.md) diff --git a/docs/en/UI/Angular/Quick-Start.md b/docs/en/UI/Angular/Quick-Start.md new file mode 100644 index 0000000000..a58671156c --- /dev/null +++ b/docs/en/UI/Angular/Quick-Start.md @@ -0,0 +1,212 @@ +# ABP Angular Quick Start + +## How to Prepare Development Environment + +Please follow the steps below to prepare your development environment for Angular. + +1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js v12 or v14 installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. +2. **[Optional] Install Yarn:** You may install Yarn v1 (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v6 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. +3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to Angular projects. ABP project templates even contain plugin recommendations for VS Code users, which VS Code will ask you to install when you open the Angular project folder. Here is a list of recommended extensions: + - [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=angular.ng-template) + - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + - [TSLint](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin) + - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=visualstudioexptteam.vscodeintellicode) + - [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) + - [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) + - [Angular 10 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout](https://marketplace.visualstudio.com/items?itemName=Mikael.Angular-BeastCode) + - [JavaScript (ES6) code snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets) + - [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) + - [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory) + - [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) + +## How to Start a New Angular Project + +You have multiple options to initiate a new Angular project that works with ABP: + +### 1. Using ABP CLI + +ABP CLI is probably the most convenient and flexible way to initiate an ABP solution with an Angular frontend. Simply [install the ABP CLI](../../CLI.md) and run the following command in your terminal: + +```shell +abp new MyCompanyName.MyProjectName -csf -u angular +``` + +> To see further options in the CLI, please visit the [CLI manual](../../CLI.md). + +This command will prepare a solution with an Angular and a .NET Core project in it. Please visit [Getting Started section](../../Getting-Started.md?UI=NG&DB=EF&Tiered=No#abp-cli-commands-options) for further instructions on how to set up the backend of your solution. + +To continue reading without checking other methods, visit [Angular project structure section](#angular-project-structure). + +### 2. Direct Download + +You may [download a solution scaffold directly on ABP.io](https://abp.io/get-started) if you are more comfortable with GUI or simply want to try ABP without installing the CLI. + +Please do the following: + +1. Click on the "DIRECT DOWNLOAD" tab. +2. Fill out the short form about your project. +3. Click on the "Create now" button. + +...and a customized download will start in a few seconds. + +To avoid IDE warnings, run `yarn` or `npm install` in the Angular root folder before you continue to the next section. It is not imperative but recommended. + +## Angular Project Structure + +After creating a solution, open its "angular" directory in your IDE. This is how the contents of the root folder looks like: + +Angular project root folder structure + +Here is what these folders and files are for: + +- **.vscode** has extension recommendations in it. +- **e2e** is a separate app for possible end-to-end tests. +- **src** is where the source files for your application are placed. We will have a closer look in a minute. +- **.browserlistrc** helps [configuring browser compatibility of your Angular app](https://angular.io/guide/build#configuring-browser-compatibility). +- **.editorconfig** helps you have a shared coding style for separate editors and IDEs. Check [EditorConfig.org](https://editorconfig.org/) for details. +- **.gitignore** defined which files and folders should not be tracked by git. Check [git documentation](https://git-scm.com/docs/gitignore) for details. +- **.prettierrc** includes simple coding style choices for [Prettier](https://prettier.io/), an auto-formatter for TypeScript, HTML, CSS, and more. If you install recommended extensions to VS Code, you will never have to format your code anymore. +- **angular.json** is where Angular workspace is defined. It holds project configurations and workspace preferences. Please refer to [Angular workspace configuration](https://angular.io/guide/workspace-config) for details. +- **karma.conf.js** holds [Karma test runner](https://karma-runner.github.io/) configurations. +- **package.json** is where your [package dependencies](https://angular.io/guide/npm-packages) are listed. It also includes some useful scripts for developing, testing, and building your application. +- **README.md** includes some of Angular CLI command examples. You either have to install Angular CLI globally or run these commands starting with `yarn` or `npx` to make them work. +- **start.ps1** is a simple PowerShell script to install dependencies and start a [development server via Angular CLI](https://angular.io/cli/serve), but you probably will not need that after reading this document. +- **tsconfig.json** and all other [tsconfig files](https://angular.io/guide/typescript-configuration) in general, include some TypeScript and Angular compile options. +- **yarn.lock** enables installing consistent package versions across different devices so that working application build will not break because of a package update. Please read [Yarn documentation](https://classic.yarnpkg.com/en/docs/yarn-lock/) if you are interested in more information on the topic. If you have decided to use npm, please remove this file and keep the [package-lock.json](https://docs.npmjs.com/files/package-lock.json) instead. + +Now let us take a look at the contents of the source folder. + +Angular project source folder structure + +- **app** is the main directory you put your application files in. Any module, component, directive, service, pipe, guard, interceptor, etc. should be placed here. You are free to choose any folder structure, but [organizing Angular applications based on modules](https://angular.io/guide/module-types) is generally a fine practice. +- **home** is a predefined module and acts as a welcome page. It also demonstrates how a feature-based folder structure may look like. More complex features will probably have sub-features, thus inner folders. You may change the home folder however you like. +- **shared** is spared for reusable code that works for several modules. Some, including yours truly, may disagree with using a single module for all shared code, so consider adding standalone sub-modules inside this folder instead of adding everything into **shared.module.ts**. +- **app-routing.module.ts** is where your top-level routes are defined. Angular is capable of [lazy loading feature modules](https://angular.io/guide/lazy-loading-ngmodules), so not all routes will be here. You may think of Angular routing as a tree and this file is the top of the tree. +- **app.component.ts** is essentially the top component that holds the dynamic application layout. +- **app.module.ts** is the [root module](https://angular.io/guide/bootstrapping) that includes information about how parts of your application are related and what to run at the initiation of your application. +- **route.provider.ts** is used for [modifying the menu](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu). +- **assets** is for static files. A file (e.g. an image) placed in this folder will be available as is when the application is served. +- **environments** includes one file per environment configuration. There are two configurations by default, but you may always introduce another one. These files are directly referred to in _angular.json_ and help you have different builds and application variables. Please refer to [configuring Angular application environments](https://angular.io/guide/build#configuring-application-environments) for details. +- **index.html** is the HTML page served to visitors and will contain everything required to run your application. Servers should be configured to redirect every request to this page so that the Angular router can take over. Do not worry about how to add JavaScript and CSS files to it, because Angular CLI will do it automatically. +- **main.ts** bootstraps and configures Angular application to run in the browser. It is production-ready, so forget about it. +- **polyfill.ts** is where you can add polyfills if you want to [support legacy browsers](https://angular.io/guide/browser-support). +- **style.scss** is the default entry point for application styles. You can change this or add new entry points in _angular.json_. +- **test.ts** helps the unit test runner discover and bootstrap spec files. + +Phew! So many files, right? Yet, **most of them are typically not subject to change** or, even when they are so, the CLI tooling will do the job for you. The main focus should be on the app folder and its content. + +Next, we will take a look at the commands used to prepare, build, and serve our application. + +## How to Run the Angular Application + +Now that you know about the files and folders, we can get the application up and running. + +1. Make sure the [database migration is complete](../../Getting-Started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](../../Getting-Started?UI=NG&DB=EF&Tiered=No#run-the-application). +2. Run `yarn` or `npm install` if you have not already. +3. Run `yarn start` or `npm start`. The first compilation may take a while. This will start a [live development server](#angular-live-development-server) and launch your default browser in the end. +4. Visit the browser page that opens after the compilation [1](#f-certificate-error). + +New ABP Angular project home page + +You may modify the behavior of the **start script** (in the package.json file) by changing the parameters passed to the `ng serve` command. For instance, if you do not want a browser window to open next time you run the script, remove `--open` from the end of it. Please check [ng serve documentation](https://angular.io/cli/serve) for all available options. + +### Angular Live Development Server + +The development server of Angular is based on [Webpack DevServer](https://webpack.js.org/configuration/dev-server/). It tracks changes to source files and syncs the browser window after an incremental re-compilation every time [2](#f-dev-server) you make one. Your experience will be like this: + +Angular Live Development Server compiles again on template change and removes a button from the page displayed by the browser. + +Please keep in mind that you should not use this server in production. To provide the fastest experience, the compiler skips some heavy optimizations and the development server is simply not built for multiple clients. The next section will describe what to do. + +--- + +Error caused by browser blocking access to backend + +1 _If you see the error above when you run the Angular app, your browser might be blocking access to the API because of the self-signed certificate. Visit that address and allow access to it (once). When you see the Swagger interface, you are good to go._ [↩](#a-certificate-error) + +2 _Sometimes, depending on the file changed, Webpack may miss the change and cannot reflect it in the browser. For example, tsconfig files are not being tracked. In such a case, please restart the development server._ [↩](#a-dev-server) + +--- + +## How to Build the Angular Application + +An Angular application can have multiple [build targets](https://angular.io/guide/glossary#target), i.e. **configurations in angular.json** which define how [Architect](https://angular.io/guide/glossary#architect) will build applications and libraries. Usually, each build configuration has a separate environment variable file. Currently, the project has two: One for development and one for production. + +```js +// this is what environment variables look like +// can be found at /src/environments/environment.ts + +import { Config } from '@abp/ng.core'; + +const baseUrl = 'http://localhost:4200'; + +export const environment = { + production: false, + application: { + baseUrl, + name: 'MyProjectName', + logoUrl: '', + }, + oAuthConfig: { + issuer: 'https://localhost:44381', + redirectUri: baseUrl, + clientId: 'MyProjectName_App', + responseType: 'code', + scope: 'offline_access MyProjectName', + }, + apis: { + default: { + url: 'https://localhost:44381', + rootNamespace: 'MyCompanyName.MyProjectName', + }, + }, +} as Config.Environment; +``` + +When you run the development server, variables defined in _environment.ts_ take effect. Similarly, in production mode, the default environment is replaced by _environment.prod.ts_ and completely different variables become effective. You may even [create a new build configuration](https://angular.io/guide/workspace-config#build-configs) and set [file replacements](https://angular.io/guide/build#configure-target-specific-file-replacements) to use a completely new environment. For now, we will start a production build: + +1. Open your terminal and navigate to the root Angular folder. +2. Run `yarn` or `npm install` if you have not installed dependencies already. +3. Run `yarn build:prod` or `npm run build:prod`. + +Angular compiler optimizing the build using Terser + +Depending on project size, the compilation may take a few minutes. When it is finished, the compiled output will be placed inside the _/dist_ folder. Voila! You have deployment-ready build artifacts. + +> The amount of optimization performed on the source is the main difference between a regular build and a production one. Production builds have a much smaller size and are more performant. + +## How to Deploy the Angular Application + +Angular web applications run on the browser and require no server except for a [static web server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_web_server) to deliver files to the client. To see that it works, please make sure the backend application is up and then run the following command in your terminal: + +```shell +# please replace MyProjectName with your project name + +npx servor dist/MyProjectName index.html 4200 --browse +``` + +This command will download and start a simple static server, a browser window at `http://localhost:4200` will open, and the compiled output of your project will be served. + +Of course, you need your application to run on an optimized web server and become available to everyone. This is quite straight-forward: + +1. Create a new static web server instance. You can use a service like [Azure App Service](https://azure.microsoft.com/tr-tr/services/app-service/web/), [Firebase](https://firebase.google.com/docs/hosting), [Netlify](https://www.netlify.com/), [Vercel](https://vercel.com/), or even [GitHub Pages](https://angular.io/guide/deployment#deploy-to-github-pages). Another option is maintaining own web server with [NGINX](https://www.nginx.com/), [IIS](https://www.iis.net/), [Apache HTTP Server](https://httpd.apache.org/), or equivalent. +2. Copy the files from `dist/MyProjectName` [1](#f-dist-folder-name) to a publicly served destination on the server via CLI of the service provider, SSH, or FTP (whichever is available). This step would be defined as a job if you have a CI/CD flow. +3. [Configure the server](https://angular.io/guide/deployment#server-configuration) to redirect all requests to the _index.html_ file. Some services do that automatically. Others require you [to add a file to the bundle via assets](https://angular.io/guide/workspace-config#assets-configuration) which describes the server how to do the redirections. Occasionally, you may need to do manual configuration. + +In addition, you can [deploy your application to certain targets using the Angular CLI](https://angular.io/guide/deployment#automatic-deployment-with-the-cli). Here are some deploy targets: + +- [Azure](https://github.com/Azure/ng-deploy-azure#readme) +- [Firebase](https://github.com/angular/angularfire#readme) +- [Netlify](https://github.com/ngx-builders/netlify-builder#readme) +- [Vercel](https://github.com/vercel/ng-deploy-vercel#readme) +- [GitHub Pages](https://github.com/angular-schule/angular-cli-ghpages/#readme) + +--- + +1 _The compiled output will be placed under `/dist` in a folder by the project name._ [↩](#a-dist-folder-name) + +--- + +## What's Next? + +- [Environment Variables](./Environment.md) diff --git a/docs/en/UI/Angular/Service-Proxies.md b/docs/en/UI/Angular/Service-Proxies.md index 7dfb2c052c..92c4b01871 100644 --- a/docs/en/UI/Angular/Service-Proxies.md +++ b/docs/en/UI/Angular/Service-Proxies.md @@ -1,16 +1,15 @@ ## Service Proxies -It is common to call a REST endpoint in the server from our Angular applications. In this case, we generally create **services** (those have methods for each service method on the server side) and **model objects** (matches to [DTOs](../../Data-Transfer-Objects) in the server side). +Calling a REST endpoint from Angular applications is common. We usually create **services** matching server-side controllers and **interfaces** matching [DTOs](../../Data-Transfer-Objects) to interact with the server. This often results in manually transforming C# code into TypeScript equivalents and that is unfortunate, if not intolerable. -In addition to manually creating such server-interacting services, we could use tools like [NSWAG](https://github.com/RicoSuter/NSwag) to generate service proxies for us. But NSWAG has the following problems we've experienced: +To avoid manual effort, we might use a tool like [NSWAG](https://github.com/RicoSuter/NSwag) that generates service proxies. However, NSWAG has some disadvantages: -- It generates a **big, single** .ts file which has some problems; - - It get **too large** when your application grows. - - It doesn't fit into the **[modular](../../Module-Development-Basics) approach** of the ABP framework. -- It creates a bit **ugly code**. We want to have a clean code (just like if we write manually). -- It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies. +- It generates **a single .ts file** which gets **too large** as your application grows. Also, this single file does not fit the **[modular](../../Module-Development-Basics) approach** of ABP. +- To be honest, the generated code is a bit **ugly**. We would like to produce code that looks as if someone wrote it. +- Since swagger.json **does not reflect the exact method signature** of backend services, NSWAG cannot reflect them on the client-side as well. + +ABP introduces an endpoint that exposes server-side method contracts. When the `generate-proxy` command is run, ABP CLI makes an HTTP request to this endpoint and generates better-aligned client proxies in TypeScript. It organizes folders according to namespaces, adds barrel exports, and reflects method signatures in Angular services. -ABP CLI changes that via the `generate-proxy` command. It automatically generates the client proxies in TypeScript. by creating folders which separated by module names in the `src/app` folder. Run the following command in the **root folder** of the angular application: ```bash @@ -19,13 +18,13 @@ abp generate-proxy The command without any parameters creates proxies only for your own application's services and places them in your default Angular application. There are several parameters you may use to modify this behavior. See the [CLI documentation](../../CLI) for details. -The generated files will be placed in a folder called `proxy` at the root of target project. +The generated files will be placed in a folder called `proxy` at the root of the target project. ![generated-files-via-generate-proxy](./images/generated-files-via-generate-proxy.png) Each folder will have models, enums, and services defined at related namespace accompanied by a barrel export, i.e. an `index.ts` file for easier imports. -> The cammand is able to find application/library roots by reading `angular.json` file. Make sure you have either defined your target project as the `defaultProject` or pass the `--target` parameter to the command. This also means that you may have a monorepo workspace. +> The command can find application/library roots by reading the `angular.json` file. Make sure you have either defined your target project as the `defaultProject` or pass the `--target` parameter to the command. This also means that you may have a monorepo workspace. ### Angular Project Configuration @@ -74,9 +73,9 @@ export const environment: Config.Environment = { The `generate-proxy` command generates one service per back-end controller and a method (property with a function value actually) for each action in the controller. These methods call backend APIs via [RestService](./Http-Requests#restservice). -A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config) +A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's `RemoteServiceName`. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config) -The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting them into constructor as shown below: +The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting them into the constructor as shown below: ```js import { BookService } from '@proxy/books'; @@ -114,7 +113,7 @@ export class BookComponent implements OnInit { ### Enums -Enums have always been difficult to populate in the frontend. The `generate-proxy` command genarates enums in a separate file and exports a ready-to-use options constant from the same file. So you can import them as follows: +Enums have always been difficult to populate in the frontend. The `generate-proxy` command generates enums in a separate file and exports a ready-to-use "options constant" from the same file. So you can import them as follows: ```js import { bookGenreOptions } from "@proxy/books"; @@ -132,7 +131,7 @@ export class BookComponent implements OnInit { ``` @@ -141,4 +140,4 @@ export class BookComponent implements OnInit { ## What's Next? -- [HTTP Requests](./Http-Requests) +- [PWA Configuration](./PWA-Configuration.md) diff --git a/docs/en/UI/Angular/Settings.md b/docs/en/UI/Angular/Settings.md new file mode 100644 index 0000000000..f3a5775838 --- /dev/null +++ b/docs/en/UI/Angular/Settings.md @@ -0,0 +1,59 @@ +# Settings + +You can get settings on the client-side using the [config state service](./Config-State.md) if they are allowed by their setting definition on the server-side. + +> This document only explains how settings work in the Angular UI projects. See the [settings document](../../../Settings.md) to understand the ABP setting system. + +## Before Use + +To use the `ConfigStateService`, you must inject it in your class as a dependency. You do not have to provide the service explicitly, because it is already **provided in root**. + +```js +import { ConfigStateService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private config: ConfigStateService) {} +} +``` + +## How to Get a Specific Setting + +You can use the `getSetting` method of `ConfigStateService` to get a specific setting from the configuration state. Here is an example: + +```js +// this.config is instance of ConfigStateService + +const defaultLang = this.config.getSetting("Abp.Localization.DefaultLanguage"); +// 'en' +``` + +### How to Get All Settings From the Store + +You can use the `getSettings` method of `ConfigStateService` to obtain all settings as an object where the object properties are setting names and property values are setting values. + +```js +// this.config is instance of ConfigStateService + +const settings = this.config.getSettings(); +// all settings as a key value pair +``` + +Additionally, the method lets you search settings by **passing a keyword** to it. + +```js +const localizationSettings = this.config.getSettings("Localization"); +/* +{ + 'Abp.Localization.DefaultLanguage': 'en' +} +*/ +``` + +Beware though, **settings search is case-sensitive**. + +## What's Next? + +- [Features](./Features.md) diff --git a/docs/en/UI/Angular/Subscription-Service.md b/docs/en/UI/Angular/Subscription-Service.md index 5d920df96e..dee77cbf1e 100644 --- a/docs/en/UI/Angular/Subscription-Service.md +++ b/docs/en/UI/Angular/Subscription-Service.md @@ -1,4 +1,4 @@ -# Easy Unsubscription for Your Observables +# Managing RxJS Subscriptions `SubscriptionService` is a utility service to provide an easy unsubscription from RxJS observables in Angular components and directives. Please see [why you should unsubscribe from observables on instance destruction](https://angular.io/guide/lifecycle-hooks#cleaning-up-on-instance-destruction). @@ -200,4 +200,4 @@ class DemoComponent implements OnInit { ## What's Next? -- [ListService](./List-Service.md) +- [Working with Lists](./List-Service.md) diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md index faa5632ab8..4d94b8cf6d 100644 --- a/docs/en/UI/Angular/Toaster-Service.md +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -67,7 +67,7 @@ With the options above, the toast overlay looks like this: ![toast](./images/toast.png) -### How to Remove a Toast Overlay +### How to Remove a Toast Overlay The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast: @@ -85,7 +85,103 @@ The all open toasts can be removed manually via the `clear` method: ```js this.toaster.clear(); ``` +## Replacing ToasterService with 3rd party toaster libraries +If you want the ABP Framework to utilize 3rd party libraries for the toasters instead of the built-in one, you can provide a service that implements `Toaster.Service` interface, and provide it as follows (ngx-toastr library used in example): + +> You can use *LocalizationService* for toaster messages translations. +```js +// your-custom-toaster.service.ts +import { Injectable } from '@angular/core'; +import { Config, LocalizationService } from '@abp/ng.core'; +import { Toaster } from '@abp/ng.theme.shared'; +import { ToastrService } from 'ngx-toastr'; + +@Injectable() +export class CustomToasterService implements Toaster.Service { + constructor(private toastr: ToastrService, private localizationService: LocalizationService) {} + + error( + message: Config.LocalizationParam, + title?: Config.LocalizationParam, + options?: Partial, + ) { + return this.show(message, title, 'error', options); + } + + clear(): void { + this.toastr.clear(); + } + + info( + message: Config.LocalizationParam, + title: Config.LocalizationParam | undefined, + options: Partial | undefined, + ): Toaster.ToasterId { + return this.show(message, title, 'info', options); + } + + remove(id: number): void { + this.toastr.remove(id); + } + + show( + message: Config.LocalizationParam, + title: Config.LocalizationParam, + severity: Toaster.Severity, + options: Partial, + ): Toaster.ToasterId { + const translatedMessage = this.localizationService.instant(message); + const translatedTitle = this.localizationService.instant(title); + const toasterOptions = { + positionClass: 'toast-bottom-right', + tapToDismiss: options.tapToDismiss, + ...(options.sticky && { + extendedTimeOut: 0, + timeOut: 0, + }), + }; + const activeToast = this.toastr.show( + translatedMessage, + translatedTitle, + toasterOptions, + `toast-${severity}`, + ); + return activeToast.toastId; + } + + success( + message: Config.LocalizationParam, + title: Config.LocalizationParam | undefined, + options: Partial | undefined, + ): Toaster.ToasterId { + return this.show(message, title, 'success', options); + } + + warn( + message: Config.LocalizationParam, + title: Config.LocalizationParam | undefined, + options: Partial | undefined, + ): Toaster.ToasterId { + return this.show(message, title, 'warning', options); + } +} +``` +```js +// app.module.ts + +import { ToasterService } from '@abp/ng.theme.shared'; + +@NgModule({ + providers: [ + // ... + { + provide: ToasterService, + useClass: CustomToasterService, + }, + ] +}) +``` ## API ### success @@ -156,4 +252,4 @@ Removes all open toasts. ## What's Next? -- [Config State](./Config-State.md) +- [Modifying the Menu](./Modifying-the-Menu.md) diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md index 050706accd..7883be4754 100644 --- a/docs/en/UI/Angular/Track-By-Service.md +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -1,4 +1,4 @@ -# Easy TrackByFunction Implementation +# Easy *ngFor trackBy `TrackByService` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose. @@ -116,4 +116,4 @@ class DemoComponent { ## What's Next? -- [SubscriptionService](./Subscription-Service.md) +- [Inserting Scripts & Styles to DOM](./Dom-Insertion-Service.md) diff --git a/docs/en/UI/Angular/images/pwa-configuration-ng-add.png b/docs/en/UI/Angular/images/pwa-configuration-ng-add.png new file mode 100644 index 0000000000..f96ac3bef1 Binary files /dev/null and b/docs/en/UI/Angular/images/pwa-configuration-ng-add.png differ diff --git a/docs/en/UI/Angular/images/quick-start---angular-live-development-server.gif b/docs/en/UI/Angular/images/quick-start---angular-live-development-server.gif new file mode 100644 index 0000000000..3c290b43a0 Binary files /dev/null and b/docs/en/UI/Angular/images/quick-start---angular-live-development-server.gif differ diff --git a/docs/en/UI/Angular/images/quick-start---new-project-home-page.png b/docs/en/UI/Angular/images/quick-start---new-project-home-page.png new file mode 100644 index 0000000000..6d11e08aa2 Binary files /dev/null and b/docs/en/UI/Angular/images/quick-start---new-project-home-page.png differ diff --git a/docs/en/UI/Angular/images/quick-start---production-build-optimization.png b/docs/en/UI/Angular/images/quick-start---production-build-optimization.png new file mode 100644 index 0000000000..54e7d10d51 Binary files /dev/null and b/docs/en/UI/Angular/images/quick-start---production-build-optimization.png differ diff --git a/docs/en/UI/Angular/images/quick-start---root-folder-structure.png b/docs/en/UI/Angular/images/quick-start---root-folder-structure.png new file mode 100644 index 0000000000..3912c755d5 Binary files /dev/null and b/docs/en/UI/Angular/images/quick-start---root-folder-structure.png differ diff --git a/docs/en/UI/Angular/images/quick-start---self-signed-certificate-error.png b/docs/en/UI/Angular/images/quick-start---self-signed-certificate-error.png new file mode 100644 index 0000000000..5876bf7592 Binary files /dev/null and b/docs/en/UI/Angular/images/quick-start---self-signed-certificate-error.png differ diff --git a/docs/en/UI/Angular/images/quick-start---source-folder-structure.png b/docs/en/UI/Angular/images/quick-start---source-folder-structure.png new file mode 100644 index 0000000000..6bcb2edb74 Binary files /dev/null and b/docs/en/UI/Angular/images/quick-start---source-folder-structure.png differ diff --git a/docs/en/UI/AspNetCore/Basic-Theme.md b/docs/en/UI/AspNetCore/Basic-Theme.md new file mode 100644 index 0000000000..3435e94a8b --- /dev/null +++ b/docs/en/UI/AspNetCore/Basic-Theme.md @@ -0,0 +1,3 @@ +# ASP.NET Core MVC / Razor Pages: The Basic Theme + +This document is in progress. See the [Theming](Theming.md) document. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Breadcrumbs.md b/docs/en/UI/AspNetCore/Breadcrumbs.md new file mode 100644 index 0000000000..cc3bcd95be --- /dev/null +++ b/docs/en/UI/AspNetCore/Breadcrumbs.md @@ -0,0 +1,3 @@ +# Toolbars + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md index 63c5cec225..77dee7a383 100644 --- a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md +++ b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md @@ -94,7 +94,8 @@ An example mapping configuration is shown below: mappings: { "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", - "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" + "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/", + "@node_modules/bootstrap-v4-rtl/dist/**/*": "@libs/bootstrap-v4-rtl/dist/" } ```` diff --git a/docs/en/UI/AspNetCore/Customization-User-Interface.md b/docs/en/UI/AspNetCore/Customization-User-Interface.md index f1f0174c89..c4e196fbb9 100644 --- a/docs/en/UI/AspNetCore/Customization-User-Interface.md +++ b/docs/en/UI/AspNetCore/Customization-User-Interface.md @@ -376,7 +376,7 @@ Assume that you need to add the Google Analytics script to the layout (that will ![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) -**NotificationViewComponent.cs** +**GoogleAnalyticsViewComponent.cs** ````csharp public class GoogleAnalyticsViewComponent : AbpViewComponent @@ -441,7 +441,7 @@ Layout system allows themes to define standard, named layouts and allows any pag * "**Account**": This layout is used by login, register and other similar pages. It is used for the pages under the `/Pages/Account` folder by default. * "**Empty**": Empty and minimal layout. -These names are defined in the `StandardLayouts` class as constants. You can definitely create your own layouts, but these are standard layout names and implemented by all the themes out of the box. +These names are defined in the `StandardLayouts` class as constants. You can definitely create your own layouts, but these are the standard layout names and implemented by all the themes out of the box. #### Layout Location diff --git a/docs/en/UI/AspNetCore/Forms-Validation.md b/docs/en/UI/AspNetCore/Forms-Validation.md new file mode 100644 index 0000000000..48d7937013 --- /dev/null +++ b/docs/en/UI/AspNetCore/Forms-Validation.md @@ -0,0 +1,206 @@ +# ASP.NET Core MVC / Razor Pages: Forms & Validation + +ABP Framework provides infrastructure and conventions to make easier to create forms, localize display names for the form elements and handle server & client side validation; + +* [abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper automates **creating a complete form** from a C# model class: Creates the input elements, handles localization and client side validation. +* [ABP Form tag helpers](Tag-Helpers/Form-elements.md) (`abp-input`, `abp-select`, `abp-radio`...) render **a single form element** with handling localization and client side validation. +* ABP Framework automatically **localizes the display name** of a form element without needing to add a `[DisplayName]` attribute. +* **Validation errors** are automatically localized based on the user culture. + +> This document is for the **client side validation** and it doesn't cover the server side validation. Check the [validation document](../../Validation.md) for server side validation infrastructure. + +## The Classic Way + +In a typical Bootstrap based ASP.NET Core MVC / Razor Pages UI, you [need to write](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#client-side-validation) such a boilerplate code to create a simple form element: + +````html +
+ + + +
+```` + +You can continue to use this approach if you need or prefer it. However, ABP Form tag helpers can produce the same output with a minimal code. + +## ABP Dynamic Forms + +[abp-dynamic-form](Tag-Helpers/Dynamic-Forms.md) tag helper completely automates the form creation. Take this model class as an example: + +```csharp +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace MyProject.Web.Pages +{ + public class MovieViewModel + { + [Required] + [StringLength(256)] + public string Name { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime ReleaseDate { get; set; } + + [Required] + [TextArea] + [StringLength(1000)] + public string Description { get; set; } + + public Genre Genre { get; set; } + + public float? Price { get; set; } + + public bool PreOrder { get; set; } + } +} +``` + +It uses the data annotation attributes to define validation rules and UI styles for the properties. `Genre`, is an `enum` in this example: + +````csharp +namespace MyProject.Web.Pages +{ + public enum Genre + { + Classic, + Action, + Fiction, + Fantasy, + Animation + } +} +```` + +In order to create the form in a razor page, create a property in your `PageModel` class: + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace MyProject.Web.Pages +{ + public class CreateMovieModel : PageModel + { + [BindProperty] + public MovieViewModel Movie { get; set; } + + public void OnGet() + { + Movie = new MovieViewModel(); + } + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + //TODO: Save the Movie + } + } + } +} +``` + +Then you can render the form in the `.cshtml` file: + +```html +@page +@model MyProject.Web.Pages.CreateMovieModel + +

Create a new Movie

+ + +``` + +The result is shown below: + +![abp-dynamic-form-result](../../images/abp-dynamic-form-result.png) + +See the *Localization & Validation* section below to localize the field display names and see how the validation works. + +> See [its own document](Tag-Helpers/Dynamic-Forms.md) for all options of the `abp-dynamic-form` tag helper. + +## ABP Form Tag Helpers + +`abp-dynamic-form` covers most of the scenarios and allows you to control and customize the form using the attributes. + +However, if you want to **render the form body yourself** (for example, you may want to fully control the **form layout**), you can directly use the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md). The same auto-generated form above can be created using the ABP Form Tag Helpers as shown below: + +```html +@page +@model MyProject.Web.Pages.CreateMovieModel + +

Create a new Movie

+ +
+ + + + + + + Save + +``` + +> See the [ABP Form Tag Helpers](Tag-Helpers/Form-elements.md) document for details of these tag helpers and their options. + +## Validation & Localization + +Both of the Dynamic Form and the Form Tag Helpers **automatically validate** the input based on the data annotation attributes and shows validation error messages on the user interface. Error messages are **automatically localized** based on the current culture. + +**Example: User leaves empty a required string property** + +![abp-form-input-validation-error](../../images/abp-form-input-validation-error.png) + +The error message below is shown if the language is French: + +![abp-form-input-validation-error](../../images/abp-form-input-validation-error-french.png) + +Validation errors are already [translated](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization) a lot of languages. You can [contribute](../../Contribution/Index.md) to the translation for your own language or override the texts for your own application by following the [localization](../../Localization.md) documentation. + +## Display Name Localization + +ABP Framework uses the property name as the field name on the user interface. You typically want to [localize](../../Localization.md) this name based on the current culture. + +ABP Framework can conventionally localize the fields on the UI when you add the localization keys to the localization JSON files. + +Example: French localization for the *Name* property (add into the `fr.json` in the application): + +````js +"Name": "Nom" +```` + +Then the UI will use the given name for French language: + +![abp-form-input-validation-error](../../images/abp-form-input-validation-error-french-name.png) + +### Using the `DisplayName:` Prefix + +Directly using the property name as the localization key may be a problem if you need to use the property name for other purpose, which a different translation value. In this case, use the `DisplayName:` prefix for the localization key: + +````js +"DisplayName:Name": "Nom" +```` + +ABP prefers to use the `DisplayName:Name` key over the `Name` key if it does exists. + +### Using a Custom Localization Key + +If you need, you can use the `[DisplayName]` attribute to specify the localization key for a specific property: + +````csharp +[DisplayName("MyNameKey")] +public string Name { get; set; } +```` + +In this case, you can add an entry to the localization file using the key `MyNameKey`. + +> If you use the `[DisplayName]` but not add a corresponding entity to the localization file, then ABP Framework shows the given key as the field name, `MyNameKey` for this case. So, it provides a way to specify a hard coded display name even if you don't need to use the localization system. + +## See Also + +* [Server Side Validation](../../Validation.md) diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Ajax.md b/docs/en/UI/AspNetCore/JavaScript-API/Ajax.md new file mode 100644 index 0000000000..dca3105b5c --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Ajax.md @@ -0,0 +1,150 @@ +# ASP.NET Core MVC / Razor Pages UI JavaScript AJAX API + +`abp.ajax` API provides a convenient way of performing AJAX calls to the server. It internally uses JQuery's `$.ajax`, but automates some common tasks for you; + +* Automatically **handles & localize the errors** and informs the user (using the [abp.message](Message.md)). So you typically don't care about errors. +* Automatically adds **anti forgery** token to the HTTP header to satisfy CSRF protection validation on the server side. +* Automatically sets **default options** and allows to configure the defaults in a single place. +* Can **block** a UI part (or the full page) during the AJAX operation. +* Allows to fully customize any AJAX call, by using the standard `$.ajax` **options**. + +> While `abp.ajax` makes the AJAX call pretty easier, you typically will use the [Dynamic JavaScript Client Proxy](Dynamic-JavaScript-Client-Proxies.md) system to perform calls to your server side HTTP APIs. `abp.ajax` can be used when you need to perform low level AJAX operations. + +## Basic Usage + +`abp.ajax` accepts an options object that is accepted by the standard [$.ajax](https://api.jquery.com/jquery.ajax/#jQuery-ajax-settings). All the standard options are valid. It returns a [promise](https://api.jquery.com/category/deferred-object/) as the return value. + +**Example: Get the list of users** + +````js +abp.ajax({ + type: 'GET', + url: '/api/identity/users' +}).then(function(result){ + console.log(result); +}); +```` + +This command logs the list of users to the console, if you've **logged in** to the application and have [permission](../../../Authorization.md) for the user management page of the [Identity Module](../../../Modules/Identity.md). + +## Error Handling + +The example AJAX call above shows an **error message** if you haven't login to the application or you don't have the necessary permissions to perform this request: + +![ajax-error](../../../images/ajax-error.png) + +All kinds of errors are automatically handled by `abp.ajax`, unless you want to disable it. + +### Standard Error Response + +`abp.ajax` is compatible with the [exception handling system](../../../Exception-Handling.md) of the ABP Framework and it properly handles the standard error format returned from the server. A typical error message is a JSON as like below: + +````json +{ + "error": { + "code": "App:010042", + "message": "This topic is locked and can not add a new message", + "details": "A more detailed info about the error..." + } +} +```` + +The error message is directly shown to the user, using the `message` and `details` properties. + +### Non-Standard Error Response & HTTP Status Codes + +It also handles errors even if the standard error format was not sent by the server. This can be case if you bypass the ABP exception handling system and manually build the HTTP response on the server. In that case, **HTTP status codes** are considered. + +The following HTTP Status Codes are pre-defined; + +* **401**: Shows an error message like "*You should be authenticated (sign in) in order to perform this operation*". When the users click the OK button, they are redirected to the home page of the application to make them login again. +* **403**: Shows an error message like "*You are not allowed to perform this operation*". +* **404**: Shows an error message like "*The resource requested could not found on the server*". +* **Others**: Shows a generic error message like "*An error has occurred. Error detail not sent by server*". + +All these messages are localized based on the current user's language. + +### Manually Handling the Errors + +Since `abp.ajax` returns a promise, you can always chain a `.cactch(...)` call to register a callback that is executed if the AJAX request fails. + +**Example: Show an alert if the AJAX request fails** + +````js +abp.ajax({ + type: 'GET', + url: '/api/identity/users' +}).then(function(result){ + console.log(result); +}).catch(function(){ + alert("request failed :("); +}); +```` + +While your callback is fired, ABP still handles the error itself. If you want to disable automatic error handling, pass `abpHandleError: false` the the `abp.ajax` options. + +**Example: Disable the auto error handling** + +````js +abp.ajax({ + type: 'GET', + url: '/api/identity/users', + abpHandleError: false //DISABLE AUTO ERROR HANDLING +}).then(function(result){ + console.log(result); +}).catch(function(){ + alert("request failed :("); +}); +```` + +If you set `abpHandleError: false` and don't catch the error yourself, then the error will be hidden and the request silently fails. `abp.ajax` still logs the error to the browser console (see the *Configuration* section to override it). + +## Configuration + +`abp.ajax` has a **global configuration** that you can customize based on your requirements. + +### Default AJAX Options + +`abp.ajax.defaultOpts` object is used to configure default options used while performing an AJAX call, unless you override them. Default value of this object is shown below: + +````js +{ + dataType: 'json', + type: 'POST', + contentType: 'application/json', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } +} +```` + +So, if you want to change the default request type, you can do it as shown below: + +````js +abp.ajax.defaultOpts.type = 'GET'; +```` + +Write this code before all of your JavaScript code. You typically want to place such a configuration into a separate JavaScript file and add it to the layout using the global [bundle](../Bundling-Minification.md). + +### Log/Show Errors + +The following functions can be overridden to customize the logging and showing the error messages: + +* `abp.ajax.logError` function logs errors using the [abp.log.error(...)](Logging.md) by default. +* `abp.ajax.showError` function shows the error message using the [abp.message.error(...)](Message.md) by default. +* `abp.ajax.handleErrorStatusCode` handles different HTTP status codes and shows different messages based on the code. +* `abp.ajax.handleAbpErrorResponse` handles the errors sent with the standard ABP error format. +* `abp.ajax.handleNonAbpErrorResponse` handles the non-standard error responses. +* `abp.ajax.handleUnAuthorizedRequest` handles responses with `401` status code and redirect users to the home page of the application. + +**Example: Override the `logError` function** + +````js +abp.ajax.logError = function(error) { + //... +} +```` + +### Other Options + +* `abp.ajax.ajaxSendHandler` function is used to intercept the AJAX requests and add antiforgery token to the HTTP header. Note that this works for all AJAX requests, even if you don't use the `abp.ajax`. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Auth.md b/docs/en/UI/AspNetCore/JavaScript-API/Auth.md new file mode 100644 index 0000000000..45af8c4b3e --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Auth.md @@ -0,0 +1,24 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Auth API + +Auth API allows you to check permissions (policies) for the current user in the client side. In this way, you can conditionally show/hide UI parts or perform your client side logic based on the current permissions. + +> This document only explains the JavaScript API. See the [authorization document](../../../Authorization.md) to understand the ABP authorization & permission system. + +## Basic Usage + +`abp.auth.isGranted(...)` function is used to check if a permission/policy has granted or not: + +````js +if (abp.auth.isGranted('DeleteUsers')) { + //TODO: Delete the user +} else { + alert("You don't have permission to delete a user!"); +} +```` + +## Other Fields & Functions + +* ` abp.auth.isAnyGranted(...)`: Gets one or more permission/policy names and returns `true` if at least one of them has granted. +* `abp.auth.areAllGranted(...)`: Gets one or more permission/policy names and returns `true` if all of them of them have granted. +* `abp.auth.policies`: This is an object where its keys are the permission/policy names. You can find all permission/policy names here. +* `abp.auth.grantedPolicies`: This is an object where its keys are the permission/policy names. You can find the granted permission/policy names here. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Block-Busy.md b/docs/en/UI/AspNetCore/JavaScript-API/Block-Busy.md new file mode 100644 index 0000000000..943a651432 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Block-Busy.md @@ -0,0 +1,56 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript UI Block/Busy API + +UI Block API disables (blocks) the page or a part of the page. + +## Basic Usage + +**Example: Block (disable) the complete page** + +````js +abp.ui.block(); +```` + +**Example: Block (disable) an HTML element** + +````js +abp.ui.block('#MyContainer'); +```` + +**Example: Enables the previously blocked element or page:** + +````js +abp.ui.unblock(); +```` + +## Options + +`abp.ui.block()` method can get an options object which may contain the following fields: + +* `elm`: An optional selector to find the element to be blocked (e.g. `#MyContainerId`). If not provided, the entire page is blocked. The selector can also be directly passed to the `block()` method as shown above. +* `busy`: Set to `true` to show a progress indicator on the blocked area. +* `promise`: A promise object with `always` or `finally` callbacks. This can be helpful if you want to automatically unblock the blocked area when a deferred operation completes. + +**Example: Block an element with busy indicator** + +````js +abp.ui.block({ + elm: '#MySection', + busy: true +}); +```` + +The resulting UI will look like below: + +![ui-busy](../../../images/ui-busy.png) + +## setBusy + +`abp.ui.setBusy(...)` and `abp.ui.clearBusy()` are shortcut functions if you want to use the block with `busy` option. + +**Example: Block with busy** + +````js +abp.ui.setBusy('#MySection'); +```` + +Then you can use `abp.ui.clearBusy();` to re-enable the busy area/page. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/CurrentUser.md b/docs/en/UI/AspNetCore/JavaScript-API/CurrentUser.md new file mode 100644 index 0000000000..038bb9dff1 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/CurrentUser.md @@ -0,0 +1,49 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript CurrentUser API + +`abp.currentUser` is an object that contains information about the current user of the application. + +> This document only explains the JavaScript API. See the [CurrentUser document](../../../CurrentUser.md) to get information about the current user in the server side. + +## Authenticated User + +If the user was authenticated, this object will be something like below: + +````js +{ + isAuthenticated: true, + id: "34f1f4a7-13cc-4b91-84d1-b91c87afa95f", + tenantId: null, + userName: "john", + name: "John", + surName: "Nash", + email: "john.nash@abp.io", + emailVerified: true, + phoneNumber: null, + phoneNumberVerified: false, + roles: ["moderator","supporter"] +} +```` + +So, `abp.currentUser.userName` returns `john` in this case. + +## Anonymous User + +If the user was not authenticated, this object will be something like below: + +````js +{ + isAuthenticated: false, + id: null, + tenantId: null, + userName: null, + name: null, + surName: null, + email: null, + emailVerified: false, + phoneNumber: null, + phoneNumberVerified: false, + roles: [] +} +```` + +You can check `abp.currentUser.isAuthenticated` to understand if the use was authenticated or not. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/DOM.md b/docs/en/UI/AspNetCore/JavaScript-API/DOM.md new file mode 100644 index 0000000000..cdd4cef9db --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/DOM.md @@ -0,0 +1,117 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript DOM API + +`abp.dom` (Document Object Model) provides events that you can subscribe to get notified when elements dynamically added to and removed from the page (DOM). + +It is especially helpful if you want to initialize the new loaded elements. This is generally needed when you dynamically add elements to DOM (for example, get some HTML elements via AJAX) after page initialization. + +> ABP uses the [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to observe the changes made on the DOM. + +## Node Events + +### onNodeAdded + +This event is triggered when an element is added to the DOM. Example: + +````js +abp.dom.onNodeAdded(function(args){ + console.log(args.$el); +}); +```` + +`args` object has the following fields; + +* `$el`: The JQuery selection to get the new element inserted to the DOM. + +### onNodeRemoved + +This event is triggered when an element is removed from the DOM. Example: + +````js +abp.dom.onNodeRemoved(function(args){ + console.log(args.$el); +}); +```` + +`args` object has the following fields; + +* `$el`: The JQuery selection to get the element removed from the DOM. + +## Pre-Build Initializers + +ABP Framework is using the DOM events to initialize some kind of HTML elements when they are added to the DOM after than the page was already initialized. + +> Note that the same initializers also work if these elements were already included in the initial DOM. So, whether they are initially or lazy loaded, they work as expected. + +### Form Initializer + +The Form initializer (defined as `abp.dom.initializers.initializeForms`) initializes the lazy loaded forms; + +* Automatically enabled the `unobtrusive` validation on the form. +* Can automatically show a confirmation message when you submit the form. To enable this feature, just add `data-confirm` attribute with a message (like `data-confirm="Are you sure?"`) to the `form` element. +* If the `form` element has `data-ajaxForm="true"` attribute, then automatically calls the `.abpAjaxForm()` on the `form` element, to make the form posted via AJAX. + +See the [Forms & Validation](../Forms-Validation.md) document for more. + +### Script Initializer + +Script initializer (`abp.dom.initializers.initializeScript`) can execute a JavaScript code for a DOM element. + +**Example: Lazy load a component and execute some code when the element has loaded** + +Assume that you've a container to load the element inside: + +````html +
+```` + +And this is the component that will be loaded via AJAX from the server and inserted into the container: + +````html +
+

Sample message

+
+```` + +`data-script-class="MyCustomClass"` indicates the JavaScript class that will be used to perform some logic on this element: + +`MyCustomClass` is a global object defined as shown below: + +````js +MyCustomClass = function(){ + + function initDom($el){ + $el.css('color', 'red'); + } + + return { + initDom: initDom + } +}; +```` + +`initDom` is the function that is called by the ABP Framework. The `$el` argument is the loaded HTML element as a JQuery selection. + +Finally, you can load the component inside the container after an AJAX call: + +````js +$(function () { + setTimeout(function(){ + $.get('/get-my-element').then(function(response){ + $('#LazyComponent').html(response); + }); + }, 2000); +}); +```` + +Script Initialization system is especially helpful if you don't know how and when the component will be loaded into the DOM. This can be possible if you've developed a reusable UI component in a library and you want the application developer shouldn't care how to initialize the component in different use cases. + +> Script initialization doesn't work if the component was loaded in the initial DOM. In this case, you are responsible to initialize it. + +### Other Initializers + +The following Bootstrap components and libraries are automatically initialized when they are added to the DOM: + +* Tooltip +* Popover +* Timeage + diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Dynamic-JavaScript-Client-Proxies.md b/docs/en/UI/AspNetCore/JavaScript-API/Dynamic-JavaScript-Client-Proxies.md new file mode 100644 index 0000000000..5270852b60 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Dynamic-JavaScript-Client-Proxies.md @@ -0,0 +1,3 @@ +# ASP.NET Core MVC / Razor Pages UI: Dynamic JavaScript Client Proxies + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Events.md b/docs/en/UI/AspNetCore/JavaScript-API/Events.md new file mode 100644 index 0000000000..3425355f44 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Events.md @@ -0,0 +1,88 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Events API + +`abp.event` object is a simple service that is used to publish and subscribe to global events **in the browser**. + +> This API is not related to server side local or distributed events. It works in the browser boundaries to make the UI components (code parts) communicate in a loosely coupled way. + +## Basic Usage + +### Publishing Events + +Use `abp.event.trigger` to publish events. + +**Example: Publish a *Basket Updated* event** + +````js +abp.event.trigger('basketUpdated'); +```` + +This will trigger all the subscribed callbacks. + +### Subscribing to the Events + +Use `abp.event.on` to subscribe to events. + +**Example: Consume the *Basket Updated* event** + +````js +abp.event.on('basketUpdated', function() { + console.log('Handled the basketUpdated event...'); +}); +```` + +You start to get events after you subscribe to the event. + +### Unsubscribing from the Events + +If you need to unsubscribe from a pre-subscribed event, you can use the `abp.event.off(eventName, callback)` function. In this case, you have the callback as a separate function declaration. + +**Example: Subscribe & Unsubscribe** + +````js +function onBasketUpdated() { + console.log('Handled the basketUpdated event...'); +} + +//Subscribe +abp.event.on('basketUpdated', onBasketUpdated); + +//Unsubscribe +abp.event.off('basketUpdated', onBasketUpdated); +```` + +You don't get events after you unsubscribe from the event. + +## Event Arguments + +You can pass arguments (of any count) to the `trigger` method and get them in the subscription callback. + +**Example: Add the basket as the event argument** + +````js +//Subscribe to the event +abp.event.on('basketUpdated', function(basket) { + console.log('The new basket object: '); + console.log(basket); +}); + +//Trigger the event +abp.event.trigger('basketUpdated', { + items: [ + { + "productId": "123", + "count": 2 + }, + { + "productId": "832", + "count": 1 + } + ] +}); +```` + +### Multiple Arguments + +If you want to pass multiple arguments, you can pass like `abp.event.on('basketUpdated', arg0, arg1, agr2)`. Then you can add the same argument list to the callback function on the subscriber side. + +> **Tip:** Alternatively, you can send a single object that has a separate field for each argument. This makes easier to extend/change the event arguments in the future without breaking the subscribers. + diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Features.md b/docs/en/UI/AspNetCore/JavaScript-API/Features.md new file mode 100644 index 0000000000..8d58347f49 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Features.md @@ -0,0 +1,29 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Features API + +`abp.features` API allows you to check features or get the values of the features on the client side. You can read the current value of a feature in the client side only if it is allowed by the feature definition (on the server side). + +> This document only explains the JavaScript API. See the [Features](../../../Features.md) document to understand the ABP Features system. + +## Basic Usage + +````js +//Gets a value as string. +var value = abp.features.get('ExportingToExcel'); + +//Check the feature is enabled +var enabled = abp.features.isEnabled('ExportingToExcel.Enabled'); +```` + +## All Values + +`abp.features.values` can be used to access to the all feature values. + +An example value of this object is shown below: + +````js +{ + Identity.TwoFactor: "Optional", + ExportingToExcel.Enabled: "true", + ... +} +```` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Index.md b/docs/en/UI/AspNetCore/JavaScript-API/Index.md index 9f584eacfb..3473140392 100644 --- a/docs/en/UI/AspNetCore/JavaScript-API/Index.md +++ b/docs/en/UI/AspNetCore/JavaScript-API/Index.md @@ -1,23 +1,19 @@ # JavaScript API -ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. +ABP provides a set of JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform common application requirements easily in the client side and integrate to the server side. ## APIs -* abp.ajax -* abp.auth -* abp.currentUser -* abp.dom -* abp.event -* abp.features -* abp.localization -* abp.log -* abp.ModalManager -* abp.notify -* abp.security -* abp.setting -* abp.ui -* abp.utils -* abp.ResourceLoader -* abp.WidgetManager -* Other APIs \ No newline at end of file +* [AJAX](Ajax.md) +* [Auth](Auth.md) +* [CurrentUser](CurrentUser.md) +* [DOM](DOM.md) +* [Events](Events.md) +* [Features](Features.md) +* [Localization](Localization.md) +* [Logging](Logging.md) +* [ResourceLoader](ResourceLoader.md) +* [Settings](Settings.md) +* [UI Block/Busy](Block-Busy.md) +* [UI Message](Message.md) +* [UI Notification](Notify.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Localization.md b/docs/en/UI/AspNetCore/JavaScript-API/Localization.md new file mode 100644 index 0000000000..0897b99268 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Localization.md @@ -0,0 +1,143 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Localization API + +Localization API allows you to reuse the server side localization resources in the client side. + +> This document only explains the JavaScript API. See the [localization document](../../../Localization.md) to understand the ABP localization system. + +## Basic Usage + +`abp.localization.getResource(...)` function is used to get a localization resource: + +````js +var testResource = abp.localization.getResource('Test'); +```` + +Then you can localize a string based on this resource: + +````js +var str = testResource('HelloWorld'); +```` + +`abp.localization.localize(...)` function is a shortcut where you can both specify the text name and the resource name: + +````js +var str = abp.localization.localize('HelloWorld', 'Test'); +```` + +`HelloWorld` is the text to localize, where `Test` is the localization resource name here. + +### Fallback Logic + +If given texts was not localized, localization method returns the given key as the localization result. + +### Default Localization Resource + +If you don't specify the localization resource name, it uses the **default localization resource** defined on the `AbpLocalizationOptions` (see the [localization document](../../../Localization.md)). + +**Example: Using the default localization resource** + +````js +var str = abp.localization.localize('HelloWorld'); //uses the default resource +```` + +### Format Arguments + +If your localized string contains arguments, like `Hello {0}, welcome!`, you can pass arguments to the localization methods. Examples: + +````js +var testSource = abp.localization.getResource('Test'); +var str1 = testSource('HelloWelcomeMessage', 'John'); +var str2 = abp.localization.localize('HelloWelcomeMessage', 'Test', 'John'); +```` + +Assuming the `HelloWelcomeMessage` is localized as `Hello {0}, welcome!`, both of the samples above produce the output `Hello John, welcome!`. + +## Other Properties & Methods + +### abp.localization.values + +`abp.localization.values` property stores all the localization resources, keys and their values. + +### abp.localization.isLocalized + +Returns a boolean indicating that if the given text was localized or not. + +**Example** + +````js +abp.localization.isLocalized('ProductName', 'MyResource'); +```` + +Returns `true` if the `ProductName` text was localized for the `MyResource` resource. Otherwise, returns `false`. You can leave the resource name empty to use the default localization resource. + +### abp.localization.defaultResourceName + +`abp.localization.defaultResourceName` can be set to change the default localization resource. You normally don't set this since the ABP Framework automatically sets is based on the server side configuration. + +### abp.localization.currentCulture + +`abp.localization.currentCulture` returns an object to get information about the **currently selected language**. + +An example value of this object is shown below: + +````js +{ + "displayName": "English", + "englishName": "English", + "threeLetterIsoLanguageName": "eng", + "twoLetterIsoLanguageName": "en", + "isRightToLeft": false, + "cultureName": "en", + "name": "en", + "nativeName": "English", + "dateTimeFormat": { + "calendarAlgorithmType": "SolarCalendar", + "dateTimeFormatLong": "dddd, MMMM d, yyyy", + "shortDatePattern": "M/d/yyyy", + "fullDateTimePattern": "dddd, MMMM d, yyyy h:mm:ss tt", + "dateSeparator": "/", + "shortTimePattern": "h:mm tt", + "longTimePattern": "h:mm:ss tt" + } +} +```` + +### abp.localization.languages + +Used to get list of all **available languages** in the application. An example value of this object is shown below: + +````js +[ + { + "cultureName": "en", + "uiCultureName": "en", + "displayName": "English", + "flagIcon": null + }, + { + "cultureName": "fr", + "uiCultureName": "fr", + "displayName": "Français", + "flagIcon": null + }, + { + "cultureName": "pt-BR", + "uiCultureName": "pt-BR", + "displayName": "Português", + "flagIcon": null + }, + { + "cultureName": "tr", + "uiCultureName": "tr", + "displayName": "Türkçe", + "flagIcon": null + }, + { + "cultureName": "zh-Hans", + "uiCultureName": "zh-Hans", + "displayName": "简体中文", + "flagIcon": null + } +] +```` + diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Logging.md b/docs/en/UI/AspNetCore/JavaScript-API/Logging.md new file mode 100644 index 0000000000..a46203b019 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Logging.md @@ -0,0 +1,50 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Logging API + +`abp.log` API is used to write simple logs in the client side. + +> The logs are written to console, using the `console.log`, by default. + +> This document is for simple client side logging. See the [Logging](../../../Logging.md) document for server side logging system. + +## Basic Usage + +Use one of the `abp.log.xxx(...)` methods based on the severity of your log message. + +````js +abp.log.debug("Some debug log here..."); //Logging a simple debug message +abp.log.info({ name: "john", age: 42 }); //Logging an object as an information log +abp.log.warn("A warning message"); //Logging a warning message +abp.log.error('An error happens...'); //Error message +abp.log.fatal('Network connection has gone away!'); //Fatal error +```` + +## Log Levels + +There are 5 levels for a log message: + +* DEBUG = 1 +* INFO = 2 +* WARN = 3 +* ERROR = 4 +* FATAL = 5 + +These are defined in the `abp.log.levels` object (like `abp.log.levels.WARN`). + +### Changing the Current Log Level + +You can control the log level as shown below: + +````js +abp.log.level = abp.log.levels.WARN; +```` + +Default log level is `DEBUG`. + +### Logging with Specifying the Level + +Instead of calling `abp.log.info(...)` function, you can use the `abp.log.log` by specifying the log level as a parameter: + +````js +abp.log.log("log message...", abp.log.levels.INFO); +```` + diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Message.md b/docs/en/UI/AspNetCore/JavaScript-API/Message.md new file mode 100644 index 0000000000..68c74914c6 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Message.md @@ -0,0 +1,128 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Message API + +Message API is used to show nice looking messages to the user as a blocking dialog. Message API is an abstraction provided by the ABP Framework and implemented using the [SweetAlert2](https://sweetalert2.github.io/) library by default. + +## Quick Example + +Use `abp.message.success(...)` function to show a success message: + +````js +abp.message.success('Your changes have been successfully saved!', 'Congratulations'); +```` + +It will show a dialog on the UI: + +![js-message-success](../../../images/js-message-success.png) + +## Informative Messages + +There are four types of informative message functions: + +* `abp.message.info(...)` +* `abp.message.success(...)` +* `abp.message.warn(...)` +* `abp.message.error(...)` + +All these methods get two parameters: + +* `message`: The message (`string`) to be shown. +* `title`: An optional (`string`) title. + +**Example: Show an error message** + +````js +abp.message.error('Your credit card number is not valid!'); +```` + +![js-message-error](../../../images/js-message-error.png) + +## Confirmation Message + +`abp.message.confirm(...)` function can be used to get a confirmation from the user. + +**Example** + +Use the following code to get a confirmation result from the user: + +````js +abp.message.confirm('Are you sure to delete the "admin" role?') +.then(function(confirmed){ + if(confirmed){ + console.log('TODO: deleting the role...'); + } +}); +```` + +The resulting UI will be like shown below: + +![js-message-confirm](../../../images/js-message-confirm.png) + +If user has clicked the `Yes` button, the `confirmed` argument in the `then` callback function will be `true`. + +> "*Are you sure?*" is the default title (localized based on the current language) and you can override it. + +### The Return Value + +The return value of the `abp.message.confirm(...)` function is a promise, so you can chain a `then` callback as shown above. + +### Parameters + +`abp.message.confirm(...)` function has the following parameters: + +* `message`: A message (string) to show to the user. +* `titleOrCallback` (optional): A title or a callback function. If you supply a string, it is shown as the title. If you supply a callback function (that gets a `bool` parameter) then it's called with the result. +* `callback` (optional): If you've passes a title to the second parameter, you can pass your callback function as the 3rd parameter. + +Passing a callback function is an alternative to the `then` callback shown above. + +**Example: Providing all the parameters and getting result with the callback function** + +````js +abp.message.confirm( + 'Are you sure to delete the "admin" role?', + 'Be careful!', + function(confirmed){ + if(confirmed){ + console.log('TODO: deleting the role...'); + } + }); +```` + +## SweetAlert Configuration + +The Message API is implemented using the [SweetAlert2](https://sweetalert2.github.io/) library by default. If you want to change its configuration, you can set the options in the `abp.libs.sweetAlert.config` object. The default configuration object is shown below: + +````js +{ + 'default': { + }, + info: { + icon: 'info' + }, + success: { + icon: 'success' + }, + warn: { + icon: 'warning' + }, + error: { + icon: 'error' + }, + confirm: { + icon: 'warning', + title: 'Are you sure?', + buttons: ['Cancel', 'Yes'] + } +} +```` + +> "Are you sure?", "Cancel" and "Yes" texts are automatically localized based on the current language. + +So, if you want to set the `warn` icon, you can set it like: + +````js +abp.libs.sweetAlert.config.warn.icon = 'error'; +```` + +See the [SweetAlert document](https://sweetalert2.github.io/) for all the configuration options. + diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Notify.md b/docs/en/UI/AspNetCore/JavaScript-API/Notify.md new file mode 100644 index 0000000000..371aa8d4b9 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Notify.md @@ -0,0 +1,45 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Notify API + +Notify API is used to show toast style, auto disappearing UI notifications to the end user. It is implemented by the [Toastr](https://github.com/CodeSeven/toastr) library by default. + +## Quick Example + +Use `abp.notify.success(...)` function to show a success message: + +````js +abp.notify.success( + 'The product "Acme Atom Re-Arranger" has been successfully deleted.', + 'Deleted the Product' +); +```` + +A notification message is shown at the bottom right of the page: + +![js-message-success](../../../images/js-notify-success.png) + +## Notification Types + +There are four types of pre-defined notifications; + +* `abp.notify.success(...)` +* `abp.notify.info(...)` +* `abp.notify.warn(...)` +* `abp.notify.error(...)` + +All of the methods above gets the following parameters; + +* `message`: A message (`string`) to show to the user. +* `title`: An optional title (`string`). +* `options`: Additional options to be passed to the underlying library, to the Toastr by default. + +## Toastr Configuration + +The notification API is implemented by the [Toastr](https://github.com/CodeSeven/toastr) library by default. You can see its own configuration options. + +**Example: Show toast messages on the top right of the page** + +````js +toastr.options.positionClass = 'toast-top-right'; +```` + +> ABP sets this option to `toast-bottom-right` by default. You can override it just as shown above. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/JavaScript-API/ResourceLoader.md b/docs/en/UI/AspNetCore/JavaScript-API/ResourceLoader.md new file mode 100644 index 0000000000..10b8c36f59 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/ResourceLoader.md @@ -0,0 +1,40 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Resource Loader API + +`abp.ResourceLoader` is a service that can load a JavaScript or CSS file on demand. It guarantees to load the file only once even if you request multiple times. + +## Loading Script Files + +`abp.ResourceLoader.loadScript(...)` function **loads** a JavaScript file from the server and **executes** it. + +**Example: Load a JavaScript file** + +````js +abp.ResourceLoader.loadScript('/Pages/my-script.js'); +```` + +### Parameters + +`loadScript` function can get three parameters; + +* `url` (required, `string`): The URL of the script file to be loaded. +* `loadCallback` (optional, `function`): A callback function that is called once the script is loaded & executed. In this callback you can safely use the code in the script file. This callback is called even if the file was loaded before. +* `failCallback` (optional, `function`): A callback function that is called if loading the script fails. + +**Example: Provide the `loadCallback` argument** + +````js +abp.ResourceLoader.loadScript('/Pages/my-script.js', function() { + console.log('successfully loaded :)'); +}); +```` + +## Loading Style Files + +`abp.ResourceLoader.loadStyle(...)` function adds a `link` element to the `head` of the document for the given URL, so the CSS file is automatically loaded by the browser. + +**Example: Load a CSS file** + +````js +abp.ResourceLoader.loadStyle('/Pages/my-styles.css'); +```` + diff --git a/docs/en/UI/AspNetCore/JavaScript-API/Settings.md b/docs/en/UI/AspNetCore/JavaScript-API/Settings.md new file mode 100644 index 0000000000..cf96cecdd0 --- /dev/null +++ b/docs/en/UI/AspNetCore/JavaScript-API/Settings.md @@ -0,0 +1,33 @@ +# ASP.NET Core MVC / Razor Pages UI: JavaScript Setting API + +Localization API allows you to get the values of the settings on the client side. You can read the current value of a setting in the client side only if it is allowed by the setting definition (on the server side). + +> This document only explains the JavaScript API. See the [settings document](../../../Settings.md) to understand the ABP setting system. + +## Basic Usage + +````js +//Gets a value as string. +var language = abp.setting.get('Abp.Localization.DefaultLanguage'); + +//Gets an integer value. +var requiredLength = abp.setting.getInt('Abp.Identity.Password.RequiredLength'); + +//Gets a boolean value. +var requireDigit = abp.setting.getBoolean('Abp.Identity.Password.RequireDigit'); +```` + +## All Values + +`abp.setting.values` can be used to obtain all the setting values as an object where the object properties are setting names and property values are the setting values. + +An example value of this object is shown below: + +````js +{ + Abp.Localization.DefaultLanguage: "en", + Abp.Timing.TimeZone: "UTC", + ... +} +```` + diff --git a/docs/en/UI/AspNetCore/Layout-Hooks.md b/docs/en/UI/AspNetCore/Layout-Hooks.md index 49b334c6c0..7039749142 100644 --- a/docs/en/UI/AspNetCore/Layout-Hooks.md +++ b/docs/en/UI/AspNetCore/Layout-Hooks.md @@ -1,3 +1,105 @@ -# Layout Hooks +# ASP.NET Core MVC / Razor Pages Layout Hooks -TODO \ No newline at end of file +ABP Framework theming system places the page layout into the [theme](Theming.md) NuGet packages. That means the final application doesn't include a `Layout.cshtml`, so you can't directly change the layout code to customize it. + +You copy the theme code into your solution. In this case you are completely free to customize it. However, then you won't be able to get automatic updates of the theme (by upgrading the theme NuGet package). + +ABP Framework provides different ways of [customizing the UI](Customization-User-Interface.md). + +The **Layout Hook System** allows you to **add code** at some specific parts of the layout. All layouts of all themes should implement these hooks. Finally, you can add a **view component** into a hook point. + +## Example: Add Google Analytics Script + +Assume that you need to add the Google Analytics script to the layout (that will be available for all the pages). First, **create a view component** in your project: + +![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) + +**NotificationViewComponent.cs** + +````csharp +public class GoogleAnalyticsViewComponent : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); + } +} +```` + +**Default.cshtml** + +````html + +```` + +Change `UA-xxxxxx-1` with your own code. + +You can then add this component to any of the hook points in the `ConfigureServices` of your module: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, //The hook name + typeof(GoogleAnalyticsViewComponent) //The component to add + ); +}); +```` + +Now, the GA code will be inserted in the `head` of the page as the last item. + +### Specifying the Layout + +The configuration above adds the `GoogleAnalyticsViewComponent` to all layouts. You may want to only add to a specific layout: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, + typeof(GoogleAnalyticsViewComponent), + layout: StandardLayouts.Application //Set the layout to add + ); +}); +```` + +See the *Layouts* section below to learn more about the layout system. + +## Layout Hook Points + +There are some pre-defined layout hook points. The `LayoutHooks.Head.Last` used above was one of them. The standard hook points are; + +* `LayoutHooks.Head.First`: Used to add a component as the first item in the HTML head tag. +* `LayoutHooks.Head.Last`: Used to add a component as the last item in the HTML head tag. +* `LayoutHooks.Body.First`: Used to add a component as the first item in the HTML body tag. +* `LayoutHooks.Body.Last`: Used to add a component as the last item in the HTML body tag. +* `LayoutHooks.PageContent.First`: Used to add a component just before the page content (the `@RenderBody()` in the layout). +* `LayoutHooks.PageContent.Last`: Used to add a component just after the page content (the `@RenderBody()` in the layout). + +> You (or the modules you are using) can add **multiple items to the same hook point**. All of them will be added to the layout by the order they were added. + +## Layouts + +Layout system allows themes to define standard, named layouts and allows any page to select a proper layout for its purpose. There are three pre-defined layouts: + +* "**Application**": The main (and the default) layout for an application. It typically contains header, menu (sidebar), footer, toolbar... etc. +* "**Account**": This layout is used by login, register and other similar pages. It is used for the pages under the `/Pages/Account` folder by default. +* "**Empty**": Empty and minimal layout. + +These names are defined in the `StandardLayouts` class as constants. You can definitely create your own layouts, but these are the standard layout names and implemented by all the themes out of the box. + +### Layout Location + +You can find the layout files [here](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts) for the basic theme. You can take them as references to build your own layouts or you can override them if necessary. + +## See Also + +* [Customizing the User Interface](Customization-User-Interface.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Modals.md b/docs/en/UI/AspNetCore/Modals.md new file mode 100644 index 0000000000..9de9542307 --- /dev/null +++ b/docs/en/UI/AspNetCore/Modals.md @@ -0,0 +1,483 @@ +# ASP.NET Core MVC / Razor Pages UI: Modals + +While you can continue to use the standard [Bootstrap way](https://getbootstrap.com/docs/4.5/components/modal/) to create, open and manage modals in your applications, ABP Framework provides a **flexible** way to manage modals by **automating common tasks** for you. + +**Example: A modal dialog to create a new role entity** + +![modal-manager-example-modal](../../images/modal-manager-example-modal.png) + +ABP Framework provides the following benefits for such a modal with a form inside it; + +* **Lazy loads** the modal HTML into the page and **removes** it from the DOM once its closed. This makes easy to consume a reusable modal dialog. Also, every time you open the modal, it will be a fresh new modal, so you don't have to deal with resetting the modal content. +* **Auto-focuses** the first input of the form once the modal has been opened. +* Automatically determines the **form** inside a modal and posts the form via **AJAX** instead of normal page post. +* Automatically checks if the form inside the modal **has changed, but not saved**. It warns the user in this case. +* Automatically **disables the modal buttons** (save & cancel) until the AJAX operation completes. +* Makes it easy to register a **JavaScript object that is initialized** once the modal has loaded. + +So, it makes you write less code when you deal with the modals, especially the modals with a form inside. + +## Basic Usage + +### Creating a Modal as a Razor Page + +To demonstrate the usage, we are creating a simple Razor Page, named `ProductInfoModal.cshtml`, under the `/Pages/Products` folder: + +![modal-page-on-rider](../../images/modal-page-on-rider.png) + +**ProductInfoModal.cshtml Content:** + +````html +@page +@model MyProject.Web.Pages.Products.ProductInfoModalModel +@{ + Layout = null; +} + + + +

@Model.ProductName

+
+ +
+

+ @Model.ProductDescription +

+

+ Reference: https://acme.com/catalog/ +

+
+ +
+```` + +* This page sets the `Layout` to `null` since we will show this as a modal. So, no need to wrap with a layout. +* It uses [abp-modal tag helper](Tag-Helpers/Modals.md) to simplify creating the modal HTML code. You can use the standard Bootstrap modal code if you prefer it. + +**ProductInfoModalModel.cshtml.cs Content:** + +```csharp +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace MyProject.Web.Pages.Products +{ + public class ProductInfoModalModel : AbpPageModel + { + public string ProductName { get; set; } + + public string ProductDescription { get; set; } + + public string ProductImageUrl { get; set; } + + public void OnGet() + { + ProductName = "Acme Indestructo Steel Ball"; + ProductDescription = "The ACME Indestructo Steel Ball is completely indestructible, there is nothing that can destroy it!"; + ProductImageUrl = "https://acme.com/catalog/acmeindestructo.jpg"; + } + } +} +``` + +You can surely get the product info from a database or API. We are setting the properties hard-coded for the sake of simplicity, + +### Defining the Modal Manager + +Once you have a modal, you can open it in any page using some simple **JavaScript** code. + +First, create an `abp.ModalManager` object by setting the `viewUrl`, in the JavaScript file of the page that will use the modal: + +````js +var productInfoModal = new abp.ModalManager({ + viewUrl: '/Products/ProductInfoModal' +}); +```` + +> If you only need to specify the `viewUrl`, you can directly pass it to the `ModalManager` constructor, as a shortcut. Example: `new abp.ModalManager('/Products/ProductInfoModal');` + +### Opening the Modal + +Then open the modal whenever you need: + +````js +productInfoModal.open(); +```` + +You typically want to open the modal when something happens; For example, when the user clicks a button: + +````js +$('#OpenProductInfoModal').click(function(){ + productInfoModal.open(); +}); +```` + +The resulting modal will be like that: + +![modal-example-product-info](../../images/modal-example-product-info.png) + +#### Opening the Modal with Arguments + +When you call the `open()` method, `ModalManager` loads the modal HTML by requesting it from the `viewUrl`. You can pass some **query string parameters** to this URL when you open the modal. + +**Example: Pass the product id while opening the modal** + +````js +productInfoModal.open({ + productId: 42 +}); +```` + +You can add a `productId` parameter to the get method: + +````csharp +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace MyProject.Web.Pages.Products +{ + public class ProductInfoModalModel : AbpPageModel + { + //... + + public async Task OnGetAsync(int productId) //Add productId parameter + { + //TODO: Get the product with database with the given productId + //... + } + } +} +```` + +In this way, you can use the `productId` to query the product from a data source. + +## Modals with Forms + +`abp.ModalManager` handles various common tasks (described in the introduction) when you want to use a form inside the modal. + +### Example Modal with a Form + +This section shows an example form to create a new product. + +#### Creating the Razor Page + +For this example, creating a new Razor Page, named `ProductCreateModal.cshtml`, under the `/Pages/Products` folder: + +![product-create-modal-page-on-rider](../../images/product-create-modal-page-on-rider.png) + +**ProductCreateModal.cshtml Content:** + +````html +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model MyProject.Web.Pages.Products.ProductCreateModalModel +@{ + Layout = null; +} +
+ + + + + + + + + +
+```` + +* The `abp-modal` has been wrapped by the `form`. This is needed to place the `Save` and the `Cancel` buttons into the form. In this way, the `Save` button acts as the `submit` button for the `form`. +* Used the [abp-input tag helpers](Tag-Helpers/Form-Elements.md) to simplify to create the form elements. Otherwise, you need to write more HTML. + +**ProductCreateModal.cshtml.cs Content:** + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace MyProject.Web.Pages.Products +{ + public class ProductCreateModalModel : AbpPageModel + { + [BindProperty] + public PoductCreationDto Product { get; set; } + + public async Task OnGetAsync() + { + //TODO: Get logic, if available + } + + public async Task OnPostAsync() + { + //TODO: Save the Product... + + return NoContent(); + } + } +} +``` + +* This is a simple `PageModal` class. The `[BindProperty]` make the form binding to the model when you post (submit) the form; The standard ASP.NET Core system. +* `OnPostAsync` returns `NoContent` (this method is defined by the base `AbpPageModel` class). Because we don't need to a return value in the client side, after the form post operation. + +**PoductCreationDto:** + +`ProductCreateModalModel` uses a `PoductCreationDto` class defined as shown below: + +````csharp +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace MyProject.Web.Pages.Products +{ + public class PoductCreationDto + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + [TextArea(Rows = 4)] + [StringLength(2000)] + public string Description { get; set; } + + [DataType(DataType.Date)] + public DateTime ReleaseDate { get; set; } + } +} +```` + +* `abp-input` Tag Helper can understand the data annotation attributes and uses them to shape and validate the form elements. See the [abp-input tag helpers](Tag-Helpers/Form-Elements.md) document to learn more. + +#### Defining the Modal Manager + +Again, create an `abp.ModalManager` object by setting the `viewUrl`, in the JavaScript file of the page that will use the modal: + +````js +var productCreateModal = new abp.ModalManager({ + viewUrl: '/Products/ProductCreateModal' +}); +```` + +#### Opening the Modal + +Then open the modal whenever you need: + +````js +productCreateModal.open(); +```` + +You typically want to open the modal when something happens; For example, when the user clicks a button: + +````js +$('#OpenProductCreateModal').click(function(){ + productCreateModal.open(); +}); +```` + +So, the complete code will be something like that (assuming you have a `button` with `id` is `OpenProductCreateModal` on the view side): + +```js +$(function () { + + var productCreateModal = new abp.ModalManager({ + viewUrl: '/Products/ProductCreateModal' + }); + + $('#OpenProductCreateModal').click(function () { + productCreateModal.open(); + }); + +}); +``` + +The resulting modal will be like that: + +![modal-example-product-create](../../images/modal-example-product-create.png) + +#### Saving the Modal + +When you click to the `Save` button, the form is posted to the server. If the server returns a **success response**, then the `onResult` event is triggered with some arguments including the server response and the modal is automatically closed. + +An example callback that logs the arguments passed to the `onResult` method: + +````js +productCreateModal.onResult(function(){ + console.log(arguments); +}); +```` + +If the server returns a failed response, it shows the error message returned from the server and keeps the modal open. + +> See the *Modal Manager Reference* section below for other modal events. + +#### Canceling the Modal + +If you click to the Cancel button with some changes made but not saved, you get such a warning message: + +![modal-manager-cancel-warning](../../images/modal-manager-cancel-warning.png) + +If you don't want such a check & message, you can add `data-check-form-on-close="false"` attribute to your `form` element. Example: + +````html +
+```` + +### Form Validation + +`ModalManager` automatically triggers the form validation when you click to the `Save` button or hit the `Enter` key on the form: + +![modal-manager-validation](../../images/modal-manager-validation.png) + +See the [Forms & Validation document](Forms-Validation.md) to learn more about the validation. + +## Modals with Script Files + +You may need to perform some logic for your modal. To do that, create a JavaScript file like below: + +````js +abp.modals.ProductInfo = function () { + + function initModal(modalManager, args) { + var $modal = modalManager.getModal(); + var $form = modalManager.getForm(); + + $modal.find('h3').css('color', 'red'); + + console.log('initialized the modal...'); + }; + + return { + initModal: initModal + }; +}; +```` + +* This code simply adds a `ProductInfo` class into the `abp.modals` namespace. The `ProductInfo` class exposes a single public function: `initModal`. +* `initModal` method is called by the `ModalManager` once the modal HTML is inserted to DOM and ready for the initialization logic. +* `modalManager` parameter is the `ModalManager` object related to this modal instance. So, you can use any function on it in your code. See the *ModalManager Reference* section. + +Then include this file to the page that you use the modal: + +````html + + +```` + +* We've use the `abp-script` Tag Helper here. See the [Bundling & Minification](Bundling-Minification.md) document if you want to understand it. You can use the standard `script` tag. It doesn't matter for this case. + +Finally, set the `modalClass` option while creating the `ModalManager` instance: + +````js +var productInfoModal = new abp.ModalManager({ + viewUrl: '/Products/ProductInfoModal', + modalClass: 'ProductInfo' //Matches to the abp.modals.ProductInfo +}); +```` + +### Lazy Loading the Script File + +Instead of adding the `ProductInfoModal.js` to the page you use the modal, you can configure it to lazy load the script file when the first time the modal is opened. + +Example: + +````js +var productInfoModal = new abp.ModalManager({ + viewUrl: '/Products/ProductInfoModal', + scriptUrl: '/Pages/Products/ProductInfoModal.js', //Lazy Load URL + modalClass: 'ProductInfo' +}); +```` + +* `scriptUrl` is used to set the URL to load the script file of the modal. +* In this case, you no longer need to include the `ProductInfoModal.js` to the page. It will be loaded on demand. + +#### Tip: Bundling & Minification + +While lazy loading seems cool at the beginning, it requires an additional call to the server when you first open the modal. + +Instead, you can use the [Bundling & Minification](Bundling-Minification.md) system to create a bundle (that is a single and minified file on production) for all the used script files for a page: + +````html + + + + +```` + +This is efficient if the script file is not large and frequently opened while users use the page. + +Alternatively, you can define the `abp.modals.ProductInfo` class in the page's main JavaScript file if the modal is only and always used in the same page. In this case, you don't need to another external script file at all. + +## ModalManager Reference + +### Options + +Options can be passed when you create a new `ModalManager` object: + +````js +var productInfoModal = new abp.ModalManager({ + viewUrl: '/Products/ProductInfoModal', + //...other options +}); +```` + +Here, the list of all available options; + +* `viewUrl` (required, `string`): The URL to lazy load the HTML of the modal. +* `scriptUrl` (optional, `string`): A URL to lazy load a JavaScript file. It is loaded only once, when the modal first opened. +* `modalClass` (optional, `string`): A JavaScript class defined in the `abp.modals` namespace that can be used to execute code related to the modal. + +### Functions + +When you create a new `ModalManager` object, you can use its functions to perform operations on the modal. Example: + +````js +var myModal = new abp.ModalManager({ + //...options +}); + +//Open the modal +myModal.open(); + +//Close the modal +myModal.close(); +```` + +Here, the list of all available functions of the `ModalManager` object; + +* `open([args])`: Opens the modal dialog. It can get an `args` object that is converted to query string while getting the `viewUrl` from the server. For example, if `args` is `{ productId: 42 }`, then the `ModalManager` passes `?productId=42` to the end of the `viewUrl` while loading the view from the server. +* `reopen()`: Opens the modal with the latest provided `args` for the `open()` method. So, it is a shortcut if you want to re-open the modal with the same `args`. +* `close()`: Closes the modal. The modal HTML is automatically removed from DOM once it has been closed. +* `getModalId()`: Gets the `id` attribute of the container that contains the view returned from the server. This is a unique id per modal and it doesn't change after you create the `ModalManager`. +* `getModal()`: Returns the modal wrapper DOM element (the HTML element with the `modal` CSS class) as a JQuery selection, so you can perform any JQuery method on it. +* `getForm()`: Returns the `form` HTML element as a JQuery selection, so you can perform any JQuery method on it. It returns `null` if the modal has no form inside it. +* `getArgs()` Gets the latest arguments object provided while opening the modal. +* `getOptions()`: Gets the options object passed to the `ModalManager` constructor. +* `setResult(...)`: Triggers the `onResult` event with the provided arguments. You can pass zero or more arguments those are directly passed to the `onResult` event. This function is generally called by the modal script to notify the page that uses the modal. + +### Events + +When you create a new `ModalManager` object, you can use its functions register to events of the modal. Examples: + +````js +var myModal = new abp.ModalManager({ + //...options +}); + +myModal.onOpen(function () { + console.log('opened the modal...'); +}); + +myModal.onClose(function () { + console.log('closed the modal...'); +}); +```` + +Here, the list of all available functions to register to events of the `ModalManager` object; + +* `onOpen(callback)`: Registers a callback function to get notified once the modal is opened. It is triggered when the modal is completely visible on the UI. +* `onClose(callback)`: Registers a callback function to get notified once the modal is closed. It is triggered when the modal is completely invisible on the UI. +* `onResult(callback)`: Registers a callback function that is triggered when the ``setResult(...)` method is called. All the parameters sent to the `setResult` method is passed to the callback. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Navigation-Menu.md b/docs/en/UI/AspNetCore/Navigation-Menu.md index 4e0164ec72..ddbfbbd857 100644 --- a/docs/en/UI/AspNetCore/Navigation-Menu.md +++ b/docs/en/UI/AspNetCore/Navigation-Menu.md @@ -1,3 +1,205 @@ -# Navigation Menu +# ASP.NET Core MVC / Razor Pages UI: Navigation Menu + +Every application has a main menu to allow users to navigate to pages/screens of the application. Some applications may contain more than one menu in different sections of the UI. + +ABP Framework is a [modular](../../Module-Development-Basics.md) application development framework. **Every module may need to add items to the menu**. + +So, ABP Framework **provides a menu infrastructure** where; + +* The application or the modules can add items to a menu, without knowing how the menu is rendered. +* The [theme](Theming.md) properly renders the menu. + +## Adding Menu Items + +In order to add menu items (or manipulate the existing items) you need to create a class implementing the `IMenuContributor` interface. + +**Example: Add a *CRM* menu item with *Customers* and *Orders* sub menu items** + +```csharp +using System.Threading.Tasks; +using MyProject.Localization; +using Volo.Abp.UI.Navigation; + +namespace MyProject.Web.Menus +{ + public class MyProjectMenuContributor : IMenuContributor + { + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + var l = context.GetLocalizer(); + + context.Menu.AddItem( + new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"]) + .AddItem(new ApplicationMenuItem( + name: "MyProject.Crm.Customers", + displayName: l["Menu:Customers"], + url: "/crm/customers") + ).AddItem(new ApplicationMenuItem( + name: "MyProject.Crm.Orders", + displayName: l["Menu:Orders"], + url: "/crm/orders") + ) + ); + } + } +} +``` + +* This example adds items only to the main menu (`StandardMenus.Main`: see the *Standard Menus* section below). +* It gets a `IStringLocalizer` from `context` to [localize](../../Localization.md) the display names of the menu items. +* Adds the Customers and Orders as children of the CRM menu. + +Once you create a menu contributor, you need to add it to the `AbpNavigationOptions` in the `ConfigureServices` method of your module: + +````csharp +Configure(options => +{ + options.MenuContributors.Add(new MyProjectMenuContributor()); +}); +```` + +When you run the application, you will see the menu items added to the main menu: + +![nav-main-menu](../../images/nav-main-menu.png) + +> The menu is rendered by the current UI theme. So, the look of the main menu can be completely different based on your theme. + +Here, a few notes on the menu contributors; + +* ABP Framework calls the `ConfigureMenuAsync` method **whenever need to render** the menu. +* Every menu item can have **children**. So, you can add menu items with **unlimited depth** (however, your UI theme may not support unlimited depth). +* Only leaf menu items have `url`s normally. When you click to a parent menu, its sub menu is opened or closed, you don't navigate the `url` of a parent menu item. +* If a menu item has no children and has no `url` defined, then it is not rendered on the UI. This simplifies to authorize the menu items: You only authorize the child items (see the next section). If none of the children are authorized, then the parent automatically disappears. + +### Menu Item Properties + +There are more options of a menu item (the constructor of the `ApplicationMenuItem` class). Here, the list of all available options; + +* `name` (`string`, required): The unique name of the menu item. +* `displayName` (`string`, required): Display name/text of the menu item. You can [localize](../../Localization.md) this as shown before. +* `url` (`string`): The URL of the menu item. +* `icon` (`string`): An icon name. Free [Font Awesome](https://fontawesome.com/) icon classes are supported out of the box. Example: `fa fa-book`. You can use any CSS font icon class as long as you include the necessary CSS files to your application. +* `order` (`int`): The order of the menu item. Default value is `1000`. Items are sorted by the adding order unless you specify an order value. +* `customData` (`object`): A custom object that you can associate to the menu item and use it while rendering the menu item. +* `target` (`string`): Target of the menu item. Can be `null` (default), "_blank", "_*self*", "_parent", "_*top*" or a frame name for web applications. +* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute. +* `cssClass` (`string`): Additional string classes for the menu item. + +### Authorization + +As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. + +One use case is the [authorization](Authorization.md). You typically want to add menu items by checking a permission. + +**Example: Check if the current user has a permission** + +````csharp +if (await context.IsGrantedAsync("MyPermissionName")) +{ + //...add menu items +} +```` + +> You can use `context.AuthorizationService` to directly access to the `IAuthorizationService`. + +### Resolving Dependencies + +`context.ServiceProvider` can be used to resolve any service dependency. + +**Example: Get a service** + +````csharp +var myService = context.ServiceProvider.GetRequiredService(); +//...use the service +```` + +> You don't need to care about releasing/disposing services. ABP Framework handles it. + +### 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): + +![nav-main-menu-administration](../../images/nav-main-menu-administration.png) + +If you want to add menu items under the *Administration* menu item, you can use the `context.Menu.GetAdministration()` extension method: + +````csharp +context.Menu.GetAdministration().AddItem(...) +```` + +### Manipulating the Existing Menu Items + +ABP Framework executes the menu contributors by the [module dependency order](../../Module-Development-Basics.md). So, you can manipulate the menu items that your application or module (directly or indirectly) depends on. + +**Example: Set an icon for the `Users` menu item added by the [Identity Module](../../Modules/Identity.md)** + +````csharp +var userMenu = context.Menu.FindMenuItem(IdentityMenuNames.Users); +userMenu.Icon = "fa fa-users"; +```` + +> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors. + +## Standard Menus + +A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus: + +* `Main`: The main menu of the application. Contains links to the page of the application. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.Main`. +* `User`: User profile menu. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.User`. + +The `Main` menu already covered above. The `User` menu is available when a user has logged in: + +![user-menu](../../images/user-menu.png) + +You can add items to the `User` menu by checking the `context.Menu.Name` as shown below: + +```csharp +if (context.Menu.Name == StandardMenus.User) +{ + //...add items +} +``` + +## IMenuManager + +`IMenuManager` is generally used by the UI [theme](Theming.md) to render the menu items on the UI. So, **you generally don't need to directly use** the `IMenuManager`. + +**Example: Getting the `Main` menu items** + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.UI.Navigation; + +namespace MyProject.Web.Pages +{ + public class IndexModel : PageModel + { + private readonly IMenuManager _menuManager; + + public IndexModel(IMenuManager menuManager) + { + _menuManager = menuManager; + } + + public async Task OnGetAsync() + { + var mainMenu = await _menuManager.GetAsync(StandardMenus.Main); + + foreach (var menuItem in mainMenu.Items) + { + //... + } + } + } +} +``` -TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Page-Alerts.md b/docs/en/UI/AspNetCore/Page-Alerts.md new file mode 100644 index 0000000000..cbf6eb493d --- /dev/null +++ b/docs/en/UI/AspNetCore/Page-Alerts.md @@ -0,0 +1,84 @@ +# ASP.NET Core MVC / Razor Pages: Page Alerts + +It is common to show error, warning or information alerts to inform the user. An example *Service Interruption* alert is shown below: + +![page-alert-example](../../images/page-alert-example.png) + +## Basic Usage + +If you directly or indirectly inherit from `AbpPageModel`, you can use the `Alerts` property to add alerts to be rendered after the request completes. + +**Example: Show a Warning alert** + +```csharp +namespace MyProject.Web.Pages +{ + public class IndexModel : MyProjectPageModel //or inherit from AbpPageModel + { + public void OnGet() + { + Alerts.Warning( + text: "We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!", + title: "Service Interruption" + ); + } + } +} +``` + +This usage renders an alert that was shown above. If you need to localize the messages, you can always use the standard [localization](../../Localization.md) system. + +### Exceptions / Invalid Model States + +It is typical to show alerts when you manually handle exceptions (with try/catch statements) or want to handle `!ModelState.IsValid` case and warn the user. For example, the Account Module shows a warning if user enters an incorrect username or password: + +![page-alert-account-layout](../../images/page-alert-account-layout.png) + +> Note that you generally don't need to manually handle exceptions since ABP Framework provides an automatic [exception handling](../../Exception-Handling.md) system. + +### Alert Types + +`Warning` is used to show a warning alert. Other common methods are `Info`, `Danger` and `Success`. + +Beside the standard methods, you can use the `Alerts.Add` method by passing an `AlertType` `enum` with one of these values: `Default`, `Primary`, `Secondary`, `Success`, `Danger`, `Warning`, `Info`, `Light`, `Dark`. + +### Dismissible + +All alert methods gets an optional `dismissible` parameter. Default value is `true` which makes the alert box dismissible. Set it to `false` to create a sticky alert box. + +## IAlertManager + +If you need to add alert messages from another part of your code, you can inject the `IAlertManager` service and use its `Alerts` list. + +**Example: Inject the `IAlertManager`** + +```csharp +using Volo.Abp.AspNetCore.Mvc.UI.Alerts; +using Volo.Abp.DependencyInjection; + +namespace MyProject.Web.Pages +{ + public class MyService : ITransientDependency + { + private readonly IAlertManager _alertManager; + + public MyService(IAlertManager alertManager) + { + _alertManager = alertManager; + } + + public void Test() + { + _alertManager.Alerts.Add(AlertType.Danger, "Test message!"); + } + } +} +``` + +## Notes + +### AJAX Requests + +Page Alert system was designed to be used in a regular full page request. It is not for AJAX/partial requests. The alerts are rendered in the page layout, so a full page refresh is needed. + +For AJAX requests, it is more proper to throw exceptions (e.g. `UserFriendlyException`). See the [exception handling](../../Exception-Handling.md) document. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Badges.md b/docs/en/UI/AspNetCore/Tag-Helpers/Badges.md index 66727cc112..c627273062 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Badges.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Badges.md @@ -2,11 +2,11 @@ ## Introduction -`abp-badge` and `abp-badge-pill` are abp tags for badges. +`abp-badge` and `abp-badge-pill` are ABP Tag Helper attributes for `a` and `span` html tags. Basic usage: -````csharp +````html Primary Info Danger @@ -22,8 +22,7 @@ See the [badges demo page](https://bootstrap-taghelpers.abp.io/Components/Badges * Indicates the type of the badge. Should be one of the following values: - * `_` (default value) - * `Default` (default value) + * `Default` * `Primary` * `Secondary` * `Success` @@ -35,6 +34,7 @@ See the [badges demo page](https://bootstrap-taghelpers.abp.io/Components/Badges Example: -````csharp +````html Danger ```` + diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Blockquote.md b/docs/en/UI/AspNetCore/Tag-Helpers/Blockquote.md new file mode 100644 index 0000000000..a63187aeb3 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Blockquote.md @@ -0,0 +1,18 @@ +# Blockquote + +`abp-blockquote` is the main container for blockquote items. + +Basic usages: + +````html + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.

+
+ + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.

+
Someone famous in Source Title
+
+```` + +It adds `blockquote` class to main container, also adds `blockquote-footer` class to inner `footer` element and `mb-0` class to inner `p` element. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Borders.md b/docs/en/UI/AspNetCore/Tag-Helpers/Borders.md index 1dc070e8ad..e1d98ac7d8 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Borders.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Borders.md @@ -2,11 +2,11 @@ ## Introduction -`abp-border` is a main element for border styling. +`abp-border` is ABP Tag Helper attribute for border styling. Basic usage: -````csharp +````html @@ -124,3 +124,5 @@ A value indicates type, position and the color of the border. Should be one of t * `Bottom_Light_0` * `Bottom_Dark_0` * `Bottom_White_0` + +(Values with `_0` at the end is for [Subtractive](https://getbootstrap.com/docs/4.0/utilities/borders/#subtractive) usages) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md b/docs/en/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md index 59332cf8de..b7082696c5 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Breadcrumbs.md @@ -6,7 +6,7 @@ Basic usage: -````csharp +````html diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Button-groups.md b/docs/en/UI/AspNetCore/Tag-Helpers/Button-groups.md index 93865e0d74..bfd558f89e 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Button-groups.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Button-groups.md @@ -6,7 +6,7 @@ Basic usage: -````csharp +````html Left Middle diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Carousel.md b/docs/en/UI/AspNetCore/Tag-Helpers/Carousel.md index 6088977068..3b91087838 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Carousel.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Carousel.md @@ -6,7 +6,7 @@ Basic usage: -````csharp +````html diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md b/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md index 38fab4d44b..dfbe8ea248 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md @@ -6,7 +6,7 @@ Basic usage: -````xml +````html Link with href diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md index 3992c7d8a1..0c374a9cf8 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,7 +1,5 @@ # Dynamic Forms -`Warning:` Before getting into this document, be sure that you have clearly understood [abp form elements](Form-elements.md) document. - ## Introduction `abp-dynamic-form` creates a bootstrap form for a given c# model. @@ -272,4 +270,8 @@ You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You ca ````csharp [Display(Name = "Name")] public string Name { get; set; } -```` \ No newline at end of file +```` + +## See Also + +* [Form Elements](Form-elements.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Figure.md b/docs/en/UI/AspNetCore/Tag-Helpers/Figure.md new file mode 100644 index 0000000000..e3b0e0d0a4 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Figure.md @@ -0,0 +1,14 @@ +# Figures + +`abp-figure` is the main container for bootstrap figure items. + +Basic usage: + +````html + + + A caption for the above image. + +```` + +It adds `figure` class to main container, also adds `figure-img` class to inner `abp-image` element and `figure-caption` class to inner `abp-figcaption` element. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md index 229460de92..54c37af688 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -13,19 +13,26 @@ ABP Framework also adds some **useful features** to the standard bootstrap compo Here, the list of components those are wrapped by the ABP Framework: * [Alerts](Alerts.md) +* [Badges](Badges.md)) +* [Blockquote](Blockquote.md) +* [Borders](Borders.md) +* [Breadcrumb](Breadcrumb.md) * [Buttons](Buttons.md) * [Cards](Cards.md) +* [Carousel](Carousel.md) * [Collapse](Collapse.md) * [Dropdowns](Dropdowns.md) +* [Figures](Figure.md) * [Grids](Grids.md) * [List Groups](List-Groups.md) * [Modals](Modals.md) +* [Navigation](Navs.md) * [Paginator](Paginator.md) * [Popovers](Popovers.md) * [Progress Bars](Progress-Bars.md) +* [Tables](Tables.md) * [Tabs](Tabs.md) * [Tooltips](Tooltips.md) -* ... > Until all the tag helpers are documented, you can visit https://bootstrap-taghelpers.abp.io/ to see them with live samples. diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md index 9cf1258373..c811dee0bc 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md @@ -1,5 +1,7 @@ # Modals +> This document explains the details of the `abp-modal` Tag Helper, which simplifies to build the HTML markup for a modal dialog. Read [that documentation](../Modals.md) to learn how to work with modals. + ## Introduction `abp-modal` is a main element to create a modal. @@ -18,8 +20,6 @@ Basic usage: ```` - - ## Demo See the [modals demo page](https://bootstrap-taghelpers.abp.io/Components/Modals) to see it in action. diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Navs.md b/docs/en/UI/AspNetCore/Tag-Helpers/Navs.md index c7503fa51d..9c8347b831 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Navs.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Navs.md @@ -6,7 +6,7 @@ Basic usage: -````csharp +````html Active @@ -78,7 +78,7 @@ See the [navs demo page](https://bootstrap-taghelpers.abp.io/Components/Navs) to Example: -````csharp +````html Navbar diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md index 9969bcf3cc..c30e68eed9 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -12,7 +12,7 @@ Basic usage: Model: -````xml +````csharp using Microsoft.AspNetCore.Mvc.RazorPages; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Tables.md b/docs/en/UI/AspNetCore/Tag-Helpers/Tables.md index 50e6c3baf9..db30db6a09 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Tables.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Tables.md @@ -6,7 +6,7 @@ Basic usage: -````csharp +````html diff --git a/docs/en/UI/AspNetCore/Theming.md b/docs/en/UI/AspNetCore/Theming.md index e41545635c..594a882821 100644 --- a/docs/en/UI/AspNetCore/Theming.md +++ b/docs/en/UI/AspNetCore/Theming.md @@ -1,3 +1,180 @@ -# ASP.NET Core MVC / Razor Pages Theming +# ASP.NET Core MVC / Razor Pages: UI Theming -TODO \ No newline at end of file +## Introduction + +ABP Framework provides a complete **UI Theming** system with the following goals: + +* Reusable [application modules](../../Modules/Index.md) are developed **theme-independent**, so they can work with any UI theme. +* UI theme is **decided by the final application**. +* The theme is distributed via NuGet/NPM packages, so it is **easily upgradable**. +* The final application can **customize** the selected theme. + +In order to accomplish these goals, ABP Framework; + +* Determines a set of **base libraries** used and adapted by all the themes. So, module and application developers can depend on and use these libraries without depending on a particular theme. +* Provides a system that consists of [navigation menus](Navigation-Menu.md), [toolbars](Toolbars.md), [layout hooks](Layout-Hooks.md)... that is implemented by all the themes. So, the modules and the application to contribute to the layout to compose a consistent application UI. + +### Current Themes + +Currently, two themes are **officially provided**: + +* The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. +* The [Lepton Theme](https://commercial.abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP Commercial](https://commercial.abp.io/) license. + +There are also some community-driven themes for the ABP Framework (you can search on the web). + +## Overall + +### The Base Libraries + +All the themes must depend on the [@abp/aspnetcore.mvc.ui.theme.shared](https://www.npmjs.com/package/@abp/aspnetcore.mvc.ui.theme.shared) NPM package, so they are indirectly depending on the following libraries: + +* [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. +* [JQuery](https://jquery.com/) for DOM manipulation. +* [DataTables.Net](https://datatables.net/) for data grids. +* [JQuery Validation](https://jqueryvalidation.org/) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [SweetAlert](https://sweetalert.js.org/) to show fancy alert message and confirmation dialogs. +* [Toastr](https://github.com/CodeSeven/toastr) to show toast notifications. +* [Lodesh](https://lodash.com/) as a utility library. +* [Luxon](https://moment.github.io/luxon/) for date/time operations. +* [JQuery Form](https://github.com/jquery-form/form) for AJAX forms. +* [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers. +* [Select2](https://select2.org/) for better select/combo boxes. +* [Timeago](http://timeago.yarp.com/) to show automatically updating fuzzy timestamps. +* [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars. + +These libraries are selected as the base libraries and available to the applications and modules. + +#### Abstractions / Wrappers + +There are some abstractions in the ABP Framework to make your code independent from some of these libraries too. Examples; + +* [Tag Helpers](Tag-Helpers/Index.md) makes it easy to generate the Bootstrap UIs. +* JavaScript [Message](JavaScript-API/Message.md) and [Notification](JavaScript-API/Notify.md) APIs provides abstractions to use the Sweetalert and Toastr. +* [Forms & Validation](Forms-Validation.md) system automatically handles the validation, so you mostly don't directly type any validation code. + +### The Standard Layouts + +The main responsibility of a theme is to provide the layouts. There are **three pre-defined layouts must be implemented by all the themes**: + +* **Application**: The default layout which is used by the main application pages. +* **Account**: Mostly used by the [account module](../../Modules/Account.md) for login, register, forgot password... pages. +* **Empty**: The Minimal layout that has no layout components at all. + +Layout names are constants defined in the `Volo.Abp.AspNetCore.Mvc.UI.Theming.StandardLayouts` class. + +#### The Application Layout + +This is the default layout which is used by the main application pages. The following image shows the user management page in the [Basic Theme](Basic-Theme.md) application layout: + +![basic-theme-application-layout](../../images/basic-theme-application-layout.png) + +And the same page is shown below with the [Lepton Theme](https://commercial.abp.io/themes) application layout: + +![lepton-theme-application-layout](../../images/lepton-theme-application-layout.png) + +As you can see, the page is the same, but the look is completely different in the themes above. + +The application layout typically includes the following parts; + +* A [main menu](Navigation-Menu.md) +* Main [Toolbar](Toolbars.md) with the following components; + * User menu + * Language switch dropdown +* [Page alerts](Page-Alerts.md) +* The page content (aka `RenderBody()`) +* [Layout hooks](Layout-Hooks.md) + +Some themes may provide more parts like breadcrumbs, page header & toolbar... etc. See the *Layout Parts* section. + +#### The Account Layout + +The Account layout is typically used by the [account module](../../Modules/Account.md) for login, register, forgot password... pages. + +![basic-theme-account-layout](../../images/basic-theme-account-layout.png) + +This layout typically provides the following parts; + +* Language switch dropdown +* Tenant switch area (if the application is [multi-tenant](../../Multi-Tenancy.md) and the current is resolved by the cookie) +* [Page alerts](Page-Alerts.md) +* The page content (aka `RenderBody()`) +* [Layout hooks](Layout-Hooks.md) + +The [Basic Theme](Basic-Theme.md) renders the top navigation bar for this layout too (as shown above) + +Here, the account layout of the Lepton Theme: + +![lepton-theme-account-layout](../../images/lepton-theme-account-layout.png) + +The [Lepton Theme](https://commercial.abp.io/themes) shows the application logo and footer in this layout. + +> You can override theme layouts completely or partially in an application to [customize](Customization-User-Interface.md) it. + +#### The Empty Layout + +The empty layout provides an empty page, however it typically includes the following parts; + +* [Page alerts](Page-Alerts.md) +* The page content (aka `RenderBody()`) +* [Layout hooks](Layout-Hooks.md) + +## Implementing a Theme + +### 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.Mvc.UI.Theme.Basic) 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 ITheme Interface + +`ITheme` interface is used by the ABP Framework to select the layout for the current page. A theme must implement this interface to provide the requested layout path. + +This is the `ITheme` implementation of the [Basic Theme](Basic-Theme.md). + +````csharp +using Volo.Abp.AspNetCore.Mvc.UI.Theming; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic +{ + [ThemeName(Name)] + public class BasicTheme : ITheme, ITransientDependency + { + public const string Name = "Basic"; + + public virtual string GetLayout(string name, bool fallbackToDefault = true) + { + switch (name) + { + case StandardLayouts.Application: + return "~/Themes/Basic/Layouts/Application.cshtml"; + case StandardLayouts.Account: + return "~/Themes/Basic/Layouts/Account.cshtml"; + case StandardLayouts.Empty: + return "~/Themes/Basic/Layouts/Empty.cshtml"; + default: + return fallbackToDefault + ? "~/Themes/Basic/Layouts/Application.cshtml" + : null; + } + } + } +} +```` + +* `[ThemeName]` attribute is required and a theme must have a unique name, `Basic` in this sample. +* `GetLayout` method should return a path if the requested layout (`name`) is provided by the theme. *The Standard Layouts* should be implemented if the theme is aimed to be used by a standard application. It may implement additional layouts. + +Once the theme implements the `ITheme` interface, it should add the theme to the `AbpThemingOptions` in the `ConfigureServices` method of the [module](../../Module-Development-Basics.md). + +````csharp +Configure(options => +{ + options.Themes.Add(); +}); +```` + +#### The IThemeSelector Service + +ABP Framework allows to use multiple themes together. This is why `options.Themes` is a list. `IThemeSelector` service selects the theme on the runtime. The application developer can set the `AbpThemingOptions.DefaultThemeName` to set the theme to be used, or replace the `IThemeSelector` service implementation (the default implementation is `DefaultThemeSelector`) to completely control the theme selection on runtime. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Widgets.md b/docs/en/UI/AspNetCore/Widgets.md index ec57c522d1..83cf314361 100644 --- a/docs/en/UI/AspNetCore/Widgets.md +++ b/docs/en/UI/AspNetCore/Widgets.md @@ -305,6 +305,27 @@ public class CountersWidgetController : AbpController > A widget supposed to be refreshed in two ways: In the first way, when you use a `RefreshUrl`, it re-rendered on the server and replaced by the HTML returned from server. In the second way the widget gets data (generally a JSON object) from server and refreshes itself in the client side (see the refresh method in the Widget JavaScript API section). +## AutoInitialize + +`WidgetAttribute` has an `AutoInitialize` property (`bool`) that can be set to automatically initialize a widget on page ready & whenever the widget is added to the DOM. The default value is `false`. + +If a widget is configured to be auto initialized, then a `WidgetManager` (see below) automatically created and initialized for instances of this widget. This is useful when the widget instances are not grouped and separately works (they don't require to init or refresh together). + +Setting the `AutoInitialize` to `true` is equivalent to write such a code yourself: + +````js +$('.abp-widget-wrapper[data-widget-name="MySimpleWidget"]') + .each(function () { + var widgetManager = new abp.WidgetManager({ + wrapper: $(this), + }); + + widgetManager.init($(this)); + }); +```` + +> `AutoInitialize` also supports widgets loaded/refreshed via AJAX (added to the DOM later) and/or used in a nested way (a widget inside another widget). If you don't need to group multiple widgets and control with a single `WidgetManager`, `AutoInitialize` is the recommended approach. + ## JavaScript API A widget may need to be rendered and refreshed in the client side. In such cases, you can use ABP's `WidgetManager` and define APIs for your widgets. diff --git a/docs/en/UI/Blazor/Localization.md b/docs/en/UI/Blazor/Localization.md new file mode 100644 index 0000000000..e6cf761ce3 --- /dev/null +++ b/docs/en/UI/Blazor/Localization.md @@ -0,0 +1,3 @@ +# Blazor UI: Localization + +Blazor applications can reuse the same `IStringLocalizer` service that is explained in the [localization document](../../Localization.md). All the localization resources and texts available in the server side are usable in the Blazor application. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Overall.md b/docs/en/UI/Blazor/Overall.md new file mode 100644 index 0000000000..74c469e3c4 --- /dev/null +++ b/docs/en/UI/Blazor/Overall.md @@ -0,0 +1,6 @@ +# Blazor UI for the ABP Framework + +The detailed documentation for the Blazor UI is in progress. However, you can follow the documents below to start with the Blazor UI today. + +* [Get started](https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor) with the Blazor UI for the ABP Framework. +* [Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor) with the Blazor UI. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Services/Notification.md b/docs/en/UI/Blazor/Services/Notification.md new file mode 100644 index 0000000000..0e5ababa0e --- /dev/null +++ b/docs/en/UI/Blazor/Services/Notification.md @@ -0,0 +1,3 @@ +# Blazor UI Notification + +`UiNotificationService` is used to show toastr style notifications on the user interface. The documentation is in progress... \ No newline at end of file diff --git a/docs/en/UI/Blazor/Settings.md b/docs/en/UI/Blazor/Settings.md new file mode 100644 index 0000000000..276462c411 --- /dev/null +++ b/docs/en/UI/Blazor/Settings.md @@ -0,0 +1,3 @@ +# Blazor UI: Settings + +Blazor applications can reuse the same `ISettingProvider` service that is explained in the [settings document](../../Settings.md). \ No newline at end of file diff --git a/docs/en/Validation.md b/docs/en/Validation.md index 15c3f13412..c4821adf65 100644 --- a/docs/en/Validation.md +++ b/docs/en/Validation.md @@ -112,6 +112,29 @@ namespace Acme.BookStore > ABP framework uses the [dynamic proxying / interception](Dynamic-Proxying-Interceptors.md) system to perform the validation. In order to make it working, your method should be **virtual** or your service should be injected and used over an **interface** (like `IMyService`). +#### Enabling/Disabling Validation + +You can use the `[DisableValidation]` to disable it for methods, classs and properties. + +````csharp +[DisableValidation] +public Void MyMethod() +{ +} + +[DisableValidation] +public class InputClass +{ + public string MyProperty { get; set; } +} + +public class InputClass +{ + [DisableValidation] + public string MyProperty { get; set; } +} +```` + ### AbpValidationException Once ABP determines a validation error, it throws an exception of type `AbpValidationException`. Your application code can throw `AbpValidationException`, but most of the times it is not needed. @@ -155,4 +178,4 @@ public class MyObjectValidationContributor ## FluentValidation Integration -Volo.Abp.FluentValidation package integrates the FluentValidation library to the validation system (by implementing the `IObjectValidationContributor`). See the [FluentValidation Integration document](FluentValidation.md) for more. \ No newline at end of file +Volo.Abp.FluentValidation package integrates the FluentValidation library to the validation system (by implementing the `IObjectValidationContributor`). See the [FluentValidation Integration document](FluentValidation.md) for more. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 0273030308..1f4e77541f 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -11,6 +11,10 @@ "text": "Console Application", "path": "Startup-Templates/Console.md" }, + { + "text": "WPF Application", + "path": "Startup-Templates/WPF.md" + }, { "text": "Empty Web Project", "path": "Getting-Started-AspNetCore-Application.md" @@ -99,11 +103,15 @@ "path": "CLI.md" }, { - "text": "Authentication", + "text": "Authentication & Security", "items": [ { "text": "Social/External Logins", "path": "Authentication/Social-External-Logins.md" + }, + { + "text": "CSRF/XSRF & Anti Forgery", + "path": "CSRF-Anti-Forgery.md" } ] }, @@ -156,7 +164,13 @@ }, { "text": "Caching", - "path": "Caching.md" + "path": "Caching.md", + "items": [ + { + "text": "Redis Cache", + "path": "Redis-Cache.md" + } + ] }, { "text": "Logging", @@ -206,6 +220,10 @@ { "text": "Kafka Integration", "path": "Distributed-Event-Bus-Kafka-Integration.md" + }, + { + "text": "Rebus Integration", + "path": "Distributed-Event-Bus-Rebus-Integration.md" } ] } @@ -386,8 +404,24 @@ "text": "User Interface", "items": [ { - "text": "ASP.NET Core", + "text": "ASP.NET Core MVC / Razor Pages", "items": [ + { + "text": "Navigation / Menus", + "path": "UI/AspNetCore/Navigation-Menu.md" + }, + { + "text": "Forms & Validation", + "path": "UI/AspNetCore/Forms-Validation.md" + }, + { + "text": "Modals", + "path": "UI/AspNetCore/Modals.md" + }, + { + "text": "Page Alerts", + "path": "UI/AspNetCore/Page-Alerts.md" + }, { "text": "Client Side Package Management", "path": "UI/AspNetCore/Client-Side-Package-Management.md" @@ -414,10 +448,75 @@ "text": "Widgets", "path": "UI/AspNetCore/Widgets.md" }, + { + "text": "Layout Hooks", + "path": "UI/AspNetCore/Layout-Hooks.md" + }, { "text": "Theming", "path": "UI/AspNetCore/Theming.md" }, + { + "text": "JavaScript API", + "items": [ + { + "text": "Overall", + "path": "UI/AspNetCore/JavaScript-API/Index.md" + }, + { + "text": "Localization", + "path": "UI/AspNetCore/JavaScript-API/Localization.md" + }, + { + "text": "Auth", + "path": "UI/AspNetCore/JavaScript-API/Auth.md" + }, + { + "text": "Current User", + "path": "UI/AspNetCore/JavaScript-API/CurrentUser.md" + }, + { + "text": "Settings", + "path": "UI/AspNetCore/JavaScript-API/Settings.md" + }, + { + "text": "Features", + "path": "UI/AspNetCore/JavaScript-API/Features.md" + }, + { + "text": "AJAX", + "path": "UI/AspNetCore/JavaScript-API/Ajax.md" + }, + { + "text": "Message", + "path": "UI/AspNetCore/JavaScript-API/Message.md" + }, + { + "text": "Notify", + "path": "UI/AspNetCore/JavaScript-API/Notify.md" + }, + { + "text": "Block/Busy", + "path": "UI/AspNetCore/JavaScript-API/Block-Busy.md" + }, + { + "text": "Events", + "path": "UI/AspNetCore/JavaScript-API/Events.md" + }, + { + "text": "DOM", + "path": "UI/AspNetCore/JavaScript-API/DOM.md" + }, + { + "text": "Logging", + "path": "UI/AspNetCore/JavaScript-API/Logging.md" + }, + { + "text": "Resource Loader", + "path": "UI/AspNetCore/JavaScript-API/ResourceLoader.md" + } + ] + }, { "text": "Customize/Extend the UI", "path": "UI/AspNetCore/Customization-User-Interface.md" @@ -425,83 +524,141 @@ ] }, { - "text": "Angular", + "text": "Blazor", "items": [ { - "text": "Migration Guide v2.x to v3", - "path": "UI/Angular/Migration-Guide-v3.md" - }, - { - "text": "Environment", - "path": "UI/Angular/Environment.md" - }, - { - "text": "Service Proxies", - "path": "UI/Angular/Service-Proxies.md" - }, - { - "text": "HTTP Requests", - "path": "UI/Angular/HTTP-Requests.md" - }, - { - "text": "Localization", - "path": "UI/Angular/Localization.md" - }, - { - "text": "Permission Management", - "path": "UI/Angular/Permission-Management.md" - }, - { - "text": "Multi Tenancy", - "path": "UI/Angular/Multi-Tenancy.md" - }, - { - "text": "Confirmation Popup", - "path": "UI/Angular/Confirmation-Service.md" - }, - { - "text": "Toast Overlay", - "path": "UI/Angular/Toaster-Service.md" - }, - { - "text": "Config State", - "path": "UI/Angular/Config-State.md" + "text": "Overall", + "path": "UI/Blazor/Overall.md" }, { - "text": "Modifying the Menu", - "path": "UI/Angular/Modifying-the-Menu.md" - }, - { - "text": "Component Replacement", - "path": "UI/Angular/Component-Replacement.md" - }, - { - "text": "Custom Setting Page", - "path": "UI/Angular/Custom-Setting-Page.md" - }, + "text": "Services", + "items": [ + { + "text": "Overall", + "path": "UI/Blazor/Services/Notification.md" + } + ] + } + ] + }, + { + "text": "Angular", + "items": [ { - "text": "Lazy Loading Scripts & Styles", - "path": "UI/Angular/Lazy-Load-Service.md" + "text": "Migration Guide v2.x to v3", + "path": "UI/Angular/Migration-Guide-v3.md" }, { - "text": "DomInsertionService", - "path": "UI/Angular/Dom-Insertion-Service.md" + "text": "Quick Start", + "path": "UI/Angular/Quick-Start.md" }, { - "text": "ContentProjectionService", - "path": "UI/Angular/Content-Projection-Service.md" + "text": "Development", + "items": [ + { + "text": "Environment Variables", + "path": "UI/Angular/Environment.md" + }, + { + "text": "About Feature Libraries", + "path": "UI/Angular/Feature-Libraries.md" + }, + { + "text": "Service Proxies", + "path": "UI/Angular/Service-Proxies.md" + }, + { + "text": "PWA Configuration", + "path": "UI/Angular/PWA-Configuration.md" + } + ] }, { - "text": "TrackByService", - "path": "UI/Angular/Track-By-Service.md" + "text": "Core Functionality", + "items": [ + { + "text": "Config State", + "path": "UI/Angular/Config-State.md" + }, + { + "text": "HTTP Requests", + "path": "UI/Angular/HTTP-Requests.md" + }, + { + "text": "Localization", + "path": "UI/Angular/Localization.md" + }, + { + "text": "Settings", + "path": "UI/Angular/Settings.md" + }, + { + "text": "Features", + "path": "UI/Angular/Features.md" + }, + { + "text": "Permission Management", + "path": "UI/Angular/Permission-Management.md" + }, + { + "text": "Multi Tenancy", + "path": "UI/Angular/Multi-Tenancy.md" + } + ] }, { - "text": "SubscriptionService", - "path": "UI/Angular/Subscription-Service.md" + "text": "Utilities", + "items": [ + { + "text": "Managing RxJS Subscriptions", + "path": "UI/Angular/Subscription-Service.md" + }, + { + "text": "Working with Lists", + "path": "UI/Angular/List-Service.md" + }, + { + "text": "Easy *ngFor trackBy", + "path": "UI/Angular/Track-By-Service.md" + }, + { + "text": "Inserting Scripts & Styles to DOM", + "path": "UI/Angular/Dom-Insertion-Service.md" + }, + { + "text": "Lazy Loading Scripts & Styles", + "path": "UI/Angular/Lazy-Load-Service.md" + }, + { + "text": "Projecting Angular Content", + "path": "UI/Angular/Content-Projection-Service.md" + }, + { + "text": "Confirmation Popup", + "path": "UI/Angular/Confirmation-Service.md" + }, + { + "text": "Toast Overlay", + "path": "UI/Angular/Toaster-Service.md" + } + ] }, { - "text": "ListService", - "path": "UI/Angular/List-Service.md" + "text": "Customization", + "items": [ + { + "text": "Modifying the Menu", + "path": "UI/Angular/Modifying-the-Menu.md" + }, + { + "text": "Component Replacement", + "path": "UI/Angular/Component-Replacement.md" + }, + { + "text": "Custom Setting Page", + "path": "UI/Angular/Custom-Setting-Page.md" + } + ] } ] }, @@ -652,6 +809,10 @@ { "text": "Console", "path": "Startup-Templates/Console.md" + }, + { + "text": "WPF", + "path": "Startup-Templates/WPF.md" } ] }, diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json index 23d079f9bb..f28a3b49ef 100644 --- a/docs/en/docs-params.json +++ b/docs/en/docs-params.json @@ -5,6 +5,7 @@ "displayName": "UI", "values": { "MVC": "MVC / Razor Pages", + "Blazor": "Blazor", "NG": "Angular" } }, diff --git a/docs/en/images/abp-dynamic-form-result.png b/docs/en/images/abp-dynamic-form-result.png new file mode 100644 index 0000000000..f9ecf14bd0 Binary files /dev/null and b/docs/en/images/abp-dynamic-form-result.png differ diff --git a/docs/en/images/abp-form-input-validation-error-french-name.png b/docs/en/images/abp-form-input-validation-error-french-name.png new file mode 100644 index 0000000000..5112cd2597 Binary files /dev/null and b/docs/en/images/abp-form-input-validation-error-french-name.png differ diff --git a/docs/en/images/abp-form-input-validation-error-french.png b/docs/en/images/abp-form-input-validation-error-french.png new file mode 100644 index 0000000000..cb8316d04b Binary files /dev/null and b/docs/en/images/abp-form-input-validation-error-french.png differ diff --git a/docs/en/images/abp-form-input-validation-error.png b/docs/en/images/abp-form-input-validation-error.png new file mode 100644 index 0000000000..2c60adc343 Binary files /dev/null and b/docs/en/images/abp-form-input-validation-error.png differ diff --git a/docs/en/images/ajax-error.png b/docs/en/images/ajax-error.png new file mode 100644 index 0000000000..2b9bc2bb37 Binary files /dev/null and b/docs/en/images/ajax-error.png differ diff --git a/docs/en/images/basic-theme-account-layout.png b/docs/en/images/basic-theme-account-layout.png new file mode 100644 index 0000000000..bccd0e20cc Binary files /dev/null and b/docs/en/images/basic-theme-account-layout.png differ diff --git a/docs/en/images/basic-theme-application-layout.png b/docs/en/images/basic-theme-application-layout.png new file mode 100644 index 0000000000..353e294056 Binary files /dev/null and b/docs/en/images/basic-theme-application-layout.png differ diff --git a/docs/en/images/basic-wpf-application-solution.png b/docs/en/images/basic-wpf-application-solution.png new file mode 100644 index 0000000000..5943acd93f Binary files /dev/null and b/docs/en/images/basic-wpf-application-solution.png differ diff --git a/docs/en/images/js-message-confirm.png b/docs/en/images/js-message-confirm.png new file mode 100644 index 0000000000..876ae8d98e Binary files /dev/null and b/docs/en/images/js-message-confirm.png differ diff --git a/docs/en/images/js-message-error.png b/docs/en/images/js-message-error.png new file mode 100644 index 0000000000..fd8abb1d7e Binary files /dev/null and b/docs/en/images/js-message-error.png differ diff --git a/docs/en/images/js-message-success.png b/docs/en/images/js-message-success.png new file mode 100644 index 0000000000..bbf418a37c Binary files /dev/null and b/docs/en/images/js-message-success.png differ diff --git a/docs/en/images/js-notify-success.png b/docs/en/images/js-notify-success.png new file mode 100644 index 0000000000..04489bc7ce Binary files /dev/null and b/docs/en/images/js-notify-success.png differ diff --git a/docs/en/images/lepton-theme-account-layout.png b/docs/en/images/lepton-theme-account-layout.png new file mode 100644 index 0000000000..e627ba3eae Binary files /dev/null and b/docs/en/images/lepton-theme-account-layout.png differ diff --git a/docs/en/images/lepton-theme-application-layout.png b/docs/en/images/lepton-theme-application-layout.png new file mode 100644 index 0000000000..3d89929abc Binary files /dev/null and b/docs/en/images/lepton-theme-application-layout.png differ diff --git a/docs/en/images/modal-example-product-create.png b/docs/en/images/modal-example-product-create.png new file mode 100644 index 0000000000..89ea0226ef Binary files /dev/null and b/docs/en/images/modal-example-product-create.png differ diff --git a/docs/en/images/modal-example-product-info.png b/docs/en/images/modal-example-product-info.png new file mode 100644 index 0000000000..5d037c1125 Binary files /dev/null and b/docs/en/images/modal-example-product-info.png differ diff --git a/docs/en/images/modal-manager-cancel-warning.png b/docs/en/images/modal-manager-cancel-warning.png new file mode 100644 index 0000000000..fc00902e0f Binary files /dev/null and b/docs/en/images/modal-manager-cancel-warning.png differ diff --git a/docs/en/images/modal-manager-example-modal.png b/docs/en/images/modal-manager-example-modal.png new file mode 100644 index 0000000000..867c6d410e Binary files /dev/null and b/docs/en/images/modal-manager-example-modal.png differ diff --git a/docs/en/images/modal-manager-validation.png b/docs/en/images/modal-manager-validation.png new file mode 100644 index 0000000000..516a178e66 Binary files /dev/null and b/docs/en/images/modal-manager-validation.png differ diff --git a/docs/en/images/modal-page-on-rider.png b/docs/en/images/modal-page-on-rider.png new file mode 100644 index 0000000000..522885489d Binary files /dev/null and b/docs/en/images/modal-page-on-rider.png differ diff --git a/docs/en/images/nav-main-menu-administration.png b/docs/en/images/nav-main-menu-administration.png new file mode 100644 index 0000000000..21ad977264 Binary files /dev/null and b/docs/en/images/nav-main-menu-administration.png differ diff --git a/docs/en/images/nav-main-menu.png b/docs/en/images/nav-main-menu.png new file mode 100644 index 0000000000..2784c035c2 Binary files /dev/null and b/docs/en/images/nav-main-menu.png differ diff --git a/docs/en/images/page-alert-account-layout.png b/docs/en/images/page-alert-account-layout.png new file mode 100644 index 0000000000..2442cb5d14 Binary files /dev/null and b/docs/en/images/page-alert-account-layout.png differ diff --git a/docs/en/images/page-alert-example.png b/docs/en/images/page-alert-example.png new file mode 100644 index 0000000000..266671757a Binary files /dev/null and b/docs/en/images/page-alert-example.png differ diff --git a/docs/en/images/product-create-modal-page-on-rider.png b/docs/en/images/product-create-modal-page-on-rider.png new file mode 100644 index 0000000000..01cf93eaf0 Binary files /dev/null and b/docs/en/images/product-create-modal-page-on-rider.png differ diff --git a/docs/en/images/react-native-introduction.gif b/docs/en/images/react-native-introduction.gif new file mode 100644 index 0000000000..15963556aa Binary files /dev/null and b/docs/en/images/react-native-introduction.gif differ diff --git a/docs/en/images/ui-busy.png b/docs/en/images/ui-busy.png new file mode 100644 index 0000000000..63a1d7d74a Binary files /dev/null and b/docs/en/images/ui-busy.png differ diff --git a/docs/en/images/user-menu.png b/docs/en/images/user-menu.png new file mode 100644 index 0000000000..af0671dbdc Binary files /dev/null and b/docs/en/images/user-menu.png differ diff --git a/docs/zh-Hans/Distributed-Event-Bus-Rebus-Integration.md b/docs/zh-Hans/Distributed-Event-Bus-Rebus-Integration.md new file mode 100644 index 0000000000..409b935ace --- /dev/null +++ b/docs/zh-Hans/Distributed-Event-Bus-Rebus-Integration.md @@ -0,0 +1,63 @@ +# 分布式事件总线Rebus集成 + +> 本文解释了**如何配置[Rebus](http://mookid.dk/category/rebus/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统. + +## 安装 + +使用ABP CLI添加[Volo.Abp.EventBus.Rebus[Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus)NuGet包到你的项目: + +* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装. +* 在你想要安装 `Volo.Abp.EventBus.Rebus` 包的 `.csproj` 文件目录打开命令行(终端). +* 运行 `abp add-package Volo.Abp.EventBus.Rebus` 命令. + +如果你想要手动安装,安装[Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusRebusModule))]` 到你的项目[模块](Module-Development-Basics.md)类. + +## 配置 + +可以使用配置使用标准的[配置系统](Configuration.md),如[选项](Options.md)类. + +`AbpRebusEventBusOptions` 类用于配置事件总线选项. + +你可以在你的[模块](Module-Development-Basics.md)的 `PreConfigureServices` 方法配置选项. + +**示例: 最小化配置** + +```csharp +PreConfigure(options => +{ + options.InputQueueName = "eventbus"; +}); +``` + +Rebus 有很多选项,你可以使用 `AbpRebusEventBusOptions` 的 `Configurer` 属性来配置. + +默认事件**存储在内存中**. 参阅[rebus文档](https://github.com/rebus-org/Rebus/wiki/Transport)了解更多信息. + +**示例: 配置存储** + +````csharp +PreConfigure(options => +{ + options.InputQueueName = "eventbus"; + options.Configurer = rebusConfigurer => + { + rebusConfigurer.Transport(t => t.UseMsmq("eventbus")); + rebusConfigurer.Subscriptions(s => s.UseJsonFile(@"subscriptions.json")); + }; +}); +```` + +你可以使用 `AbpRebusEventBusOptions` 的 `Publish` 属性来更改发布方法. + +**示例: 配置事件发布** + +````csharp +PreConfigure(options => +{ + options.InputQueueName = "eventbus"; + options.Publish = async (bus, type, data) => + { + await bus.Publish(data); + }; +}); +```` diff --git a/docs/zh-Hans/Distributed-Event-Bus.md b/docs/zh-Hans/Distributed-Event-Bus.md index 68b7466f8a..4b4203911b 100644 --- a/docs/zh-Hans/Distributed-Event-Bus.md +++ b/docs/zh-Hans/Distributed-Event-Bus.md @@ -9,6 +9,7 @@ * `LocalDistributedEventBus` 是默认实现,实现作为进程内工作的分布式事件总线. 是的!如果没有配置真正的分布式提供程序,**默认实现的工作方式与[本地事件总线](Local-Event-Bus.md)一样**. * `RabbitMqDistributedEventBus` 通过[RabbitMQ](https://www.rabbitmq.com/)实现分布式事件总线. 请参阅[RabbitMQ集成文档](Distributed-Event-Bus-RabbitMQ-Integration.md)了解如何配置它. * `KafkaDistributedEventBus` 通过[Kafka](https://kafka.apache.org/)实现分布式事件总线. 请参阅[Kafka集成文档](Distributed-Event-Bus-Kafka-Integration.md)了解如何配置它. +* `RebusDistributedEventBus` 通过[Rebus](http://mookid.dk/category/rebus/)实现分布式事件总线. 请参阅[Rebus集成文档](Distributed-Event-Bus-Rebus-Integration.md)了解如何配置它. 使用本地事件总线作为默认具有一些重要的优点. 最重要的是:它允许你编写与分布式体系结构兼容的代码. 您现在可以编写一个整体应用程序,以后可以拆分成微服务. 最好通过分布式事件而不是本地事件在边界上下文之间(或在应用程序模块之间)进行通信. diff --git a/docs/zh-Hans/Getting-Started.md b/docs/zh-Hans/Getting-Started.md index bf46816d32..c6b3bd70df 100644 --- a/docs/zh-Hans/Getting-Started.md +++ b/docs/zh-Hans/Getting-Started.md @@ -153,6 +153,19 @@ abp new Acme.BookStore{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} - > 请参阅[应用程序模板文档](Startup-Templates/Application.md)详细了解解决方案结构. +{{ if DB == "Mongo" }} + +> [启动模板](Startup-templates/Index.md)默认在 `.MongoDB` 项目中**禁用**了工作单元事务. 如果你的MongoDB服务器支持事务,你可以手动启用工作单元的事务: + + ```csharp + Configure(options => + { + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled; + }); + ``` + +{{ end }} + ## 创建数据库 ### 连接字符串 diff --git a/docs/zh-Hans/MongoDB.md b/docs/zh-Hans/MongoDB.md index 64df7d180b..c90b9b5740 100644 --- a/docs/zh-Hans/MongoDB.md +++ b/docs/zh-Hans/MongoDB.md @@ -233,6 +233,19 @@ public class BookService > 重要:如果你想访问MongoDB API,你需要在你的项目中引用`Volo.Abp.MongoDB`.这会破坏封装,但在这种情况下,这就是你想要的. +#### 事务 + +MongoDB在4.0版本开始支持事务, ABP在3.2版本加入了对MongoDb事务的支持. 如果你升级到3.2版本,需要将[MongoDbSchemaMigrator](https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.MongoDB/MongoDb/MongoDbMyProjectNameDbSchemaMigrator.cs)添加到你的 `.MongoDB` 项目中. + +[启动模板](Startup-templates/Index.md)默认在 `.MongoDB` 项目中**禁用**了工作单元事务. 如果你的MongoDB服务器支持事务,你可以手动启用工作单元的事务: + +```csharp +Configure(options => +{ + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled; +}); +``` + #### 高级主题 ##### 设置默认的仓储类 diff --git a/docs/zh-Hans/Multi-Tenancy.md b/docs/zh-Hans/Multi-Tenancy.md index 3e65a97cef..8e92d10673 100644 --- a/docs/zh-Hans/Multi-Tenancy.md +++ b/docs/zh-Hans/Multi-Tenancy.md @@ -127,13 +127,14 @@ namespace MyCompany.MyProject `MyCustomTenantResolveContributor`必须像下面这样实现**ITenantResolveContributor**接口: ````C# +using System.Threading.Tasks; using Volo.Abp.MultiTenancy; namespace MyCompany.MyProject { public class MyCustomTenantResolveContributor : ITenantResolveContributor { - public void Resolve(ITenantResolveContext context) + public override Task ResolveAsync(ITenantResolveContext context) { context.TenantIdOrName = ... //从其他地方获取租户id或租户名字... } @@ -305,6 +306,7 @@ Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前W * **CurrentUserTenantResolveContributor**: 如果当前用户已登录,从当前用户的声明中获取租户Id. **出于安全考虑,应该始终将其做为第一个Contributor**. * **QueryStringTenantResolveContributor**: 尝试从query string参数中获取当前租户,默认参数名为"__tenant". +* **FormTenantResolveContributor**: 尝试从form参数中获取当前租户,默认参数名为"__tenant". * **RouteTenantResolveContributor**:尝试从当前路由中获取(URL路径),默认是变量名是"__tenant".所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户. * **HeaderTenantResolveContributor**: 尝试从HTTP header中获取当前租户,默认的header名称是"__tenant". * **CookieTenantResolveContributor**: 尝试从当前cookie中获取当前租户.默认的Cookie名称是"__tenant". diff --git a/docs/zh-Hans/Startup-Templates/Index.md b/docs/zh-Hans/Startup-Templates/Index.md index fbc91c7549..268f51e98b 100644 --- a/docs/zh-Hans/Startup-Templates/Index.md +++ b/docs/zh-Hans/Startup-Templates/Index.md @@ -6,4 +6,5 @@ * [**app**](Application.md): 应用程序模板. * [**module**](Module.md): 模块/服务模板. -* [**console**](Console.md): 控制台模板. \ No newline at end of file +* [**console**](Console.md): 控制台模板. +* [**WPF**](WPF.md): WPF模板. diff --git a/docs/zh-Hans/Startup-Templates/Module.md b/docs/zh-Hans/Startup-Templates/Module.md index 03f87a5c0c..aaab99a477 100644 --- a/docs/zh-Hans/Startup-Templates/Module.md +++ b/docs/zh-Hans/Startup-Templates/Module.md @@ -133,7 +133,7 @@ abp new Acme.IssueManagement -t module --no-ui ##### 如何运行? -将其设置成启动项, 使用包管理控制台运行 `Update-Database` 命令迁移数据库,然后运行应用程序. 默认用户名: `admin` 密码: `1q2w3E*`. +将`host/YourProjectName.Web.Unified`设置成启动项, 使用包管理控制台运行 `Update-Database` 命令迁移数据库,然后运行应用程序. 默认用户名: `admin` 密码: `1q2w3E*`. #### 分离部署&数据库 场景 diff --git a/docs/zh-Hans/Startup-Templates/WPF.md b/docs/zh-Hans/Startup-Templates/WPF.md new file mode 100644 index 0000000000..31a76eaa60 --- /dev/null +++ b/docs/zh-Hans/Startup-Templates/WPF.md @@ -0,0 +1,27 @@ +# WPF应用程序启动模板 + +此模板用于创建一个最小的依赖关系的ABP WPF应用程序项目. + +## 如何开始? + +首先,如果你没有安装[ABP CLI](../CLI.md),请先安装它: + +````bash +dotnet tool install -g Volo.Abp.Cli +```` + +在一个空文件夹使用 `abp new` 命令创建新解决方案: + +````bash +abp new Acme.MyWpfApp -t wpf +```` + +`Acme.MyWpfApp` 是解决方案的名称, 如*YourCompany.YourProduct*. 你可以使用单级或多级名称. + +## 解决方案结构 + +使用以上命令创建解决方案后,你会得到如下所示的解决方案: + +![basic-wpf-application-solution](../images/basic-wpf-application-solution.png) + +* `HelloWorldService` 是一个实现了 `ITransientDependency` 接口的示例服务. 它会自动注册到[依赖注入](../Dependency-Injection.md)系统. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index 101835559b..99a7e3d9fb 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -270,18 +270,8 @@ namespace Acme.BookStore ![bookstore-dbmigrator-on-solution](images/bookstore-dbmigrator-on-solution.png) -{{if DB == "EF"}} - `.DbMigrator` 是一个控制台使用程序,可以在**开发**和**生产**环境**迁移数据库架构**和**初始化种子数据**. -{{end}} - -{{if DB == "Mongo"}} - -尽管MongoDB**不需要**数据库架构迁移,但仍建议运行该应用程序,因为它可以在数据库上播种初始数据. 可以在**开发**和**生产**环境中使用. - -{{end}} - ## 创建应用程序 应用程序层由两个分离的项目组成: diff --git a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md index 7daf6c5d4b..c94c29d27f 100644 --- a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md +++ b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md @@ -70,7 +70,7 @@ namespace Acme.BookStore.Web.Pages.Identity.Users 在这种情况下; -1. 像上面描述过的那术重写C#页面模型类,但不需要替换已存在的页面模型类. +1. 像上面描述过的那样重写C#页面模型类,但不需要替换已存在的页面模型类. 2. 像上面描述过的那样重写Razor页面,并且更改@model指向新的页面模型 #### 示例 diff --git a/docs/zh-Hans/Virtual-File-System.md b/docs/zh-Hans/Virtual-File-System.md index e2e4c9e865..59c1fcf1f2 100644 --- a/docs/zh-Hans/Virtual-File-System.md +++ b/docs/zh-Hans/Virtual-File-System.md @@ -1,6 +1,6 @@ ## 虚拟文件系统 -虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们象物理文件一样使用. +虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们像物理文件一样使用. ## 安装 @@ -196,4 +196,4 @@ public class MyWebAppModule : AbpModule ### 物理文件 -物理文件总是覆盖虚拟文件. 这意味着如果你把一个文件放在 `/wwwroot/my-folder/my-file.css`,它将覆盖虚拟文件系统相同位置的文件.因此你需要知道在模块中定义的文件路径来覆盖它们. \ No newline at end of file +物理文件总是覆盖虚拟文件. 这意味着如果你把一个文件放在 `/wwwroot/my-folder/my-file.css`,它将覆盖虚拟文件系统相同位置的文件.因此你需要知道在模块中定义的文件路径来覆盖它们. diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 5df3376507..1f9f6ce9c8 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -12,7 +12,11 @@ "path": "Startup-Templates/Console.md" }, { - "text": "空Web应用程序t", + "text": "WPF应用程序", + "path": "Startup-Templates/WPF.md" + }, + { + "text": "空Web应用程序", "path": "Getting-Started-AspNetCore-Application.md" } ] @@ -175,6 +179,10 @@ { "text": "Kafka 集成", "path": "Distributed-Event-Bus-Kafka-Integration.md" + }, + { + "text": "Rebus 集成", + "path": "Distributed-Event-Bus-Rebus-Integration.md" } ] } @@ -623,6 +631,10 @@ { "text": "控制台", "path": "Startup-Templates/Console.md" + }, + { + "text": "WPF", + "path": "Startup-Templates/WPF.md" } ] }, diff --git a/docs/zh-Hans/images/basic-wpf-application-solution.png b/docs/zh-Hans/images/basic-wpf-application-solution.png new file mode 100644 index 0000000000..5943acd93f Binary files /dev/null and b/docs/zh-Hans/images/basic-wpf-application-solution.png differ diff --git a/framework/.editorconfig b/framework/.editorconfig new file mode 100644 index 0000000000..9f20b90112 --- /dev/null +++ b/framework/.editorconfig @@ -0,0 +1,131 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\Projects\Volosoft\abp\framework codebase based on best match to current usage at 2.10.2020. +# You can modify the rules from these initially generated values to suit your own policies +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] + + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - indentation options + +#indent switch case contents. +csharp_indent_case_contents = true +#indent switch labels +csharp_indent_switch_labels = true + +#Formatting - new line options + +#place catch statements on a new line +csharp_new_line_before_catch = true +#place else statements on a new line +csharp_new_line_before_else = true +#require members of object intializers to be on separate lines +csharp_new_line_before_members_in_object_initializers = true +#require braces to be on a new line for accessors, methods, lambdas, object_collection_array_initializers, control_blocks, types, and properties (also known as "Allman" style) +csharp_new_line_before_open_brace = accessors, methods, lambdas, object_collection_array_initializers, control_blocks, types, properties + +#Formatting - organize using options + +#sort System.* using directives alphabetically, and place them before other usings +dotnet_sort_system_directives_first = true + +#Formatting - spacing options + +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer block bodies for methods +csharp_style_expression_bodied_methods = false:suggestion +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:suggestion + +#Style - expression level options + +#prefer out variables to be declared inline in the argument list of a method call when possible +csharp_style_inlined_variable_declaration = true:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer default over default(T) +csharp_prefer_simple_default_expression = true:suggestion +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion +#prefer inferred tuple element names +dotnet_style_prefer_inferred_tuple_names = true:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,protected,private,virtual,async,static,override,readonly,abstract:suggestion + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +#Style - qualification options + +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 51484f7c04..257c039265 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -319,37 +319,45 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BlobStoring.Aliyun EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BlobStoring.Aliyun.Tests", "test\Volo.Abp.BlobStoring.Aliyun.Tests\Volo.Abp.BlobStoring.Aliyun.Tests.csproj", "{8E49687A-E69F-49F2-8DB0-428D0883A937}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Aws", "src\Volo.Abp.BlobStoring.Aws\Volo.Abp.BlobStoring.Aws.csproj", "{50968CDE-1029-4051-B2E5-B69D0ECF2A18}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BlobStoring.Aws", "src\Volo.Abp.BlobStoring.Aws\Volo.Abp.BlobStoring.Aws.csproj", "{50968CDE-1029-4051-B2E5-B69D0ECF2A18}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Aws.Tests", "test\Volo.Abp.BlobStoring.Aws.Tests\Volo.Abp.BlobStoring.Aws.Tests.csproj", "{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BlobStoring.Aws.Tests", "test\Volo.Abp.BlobStoring.Aws.Tests\Volo.Abp.BlobStoring.Aws.Tests.csproj", "{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Kafka", "src\Volo.Abp.Kafka\Volo.Abp.Kafka.csproj", "{2A864049-9CD5-4493-8CDB-C408474D43D4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Kafka", "src\Volo.Abp.Kafka\Volo.Abp.Kafka.csproj", "{2A864049-9CD5-4493-8CDB-C408474D43D4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EventBus.Kafka", "src\Volo.Abp.EventBus.Kafka\Volo.Abp.EventBus.Kafka.csproj", "{C1D891B0-AE83-42CB-987D-425A2787DE78}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.EventBus.Kafka", "src\Volo.Abp.EventBus.Kafka\Volo.Abp.EventBus.Kafka.csproj", "{C1D891B0-AE83-42CB-987D-425A2787DE78}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.GlobalFeatures", "src\Volo.Abp.GlobalFeatures\Volo.Abp.GlobalFeatures.csproj", "{04F44063-C952-403A-815F-EFB778BDA125}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.GlobalFeatures", "src\Volo.Abp.GlobalFeatures\Volo.Abp.GlobalFeatures.csproj", "{04F44063-C952-403A-815F-EFB778BDA125}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.GlobalFeatures.Tests", "test\Volo.Abp.GlobalFeatures.Tests\Volo.Abp.GlobalFeatures.Tests.csproj", "{231F1581-AA21-44C3-BF27-51EB3AD5355C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.GlobalFeatures.Tests", "test\Volo.Abp.GlobalFeatures.Tests\Volo.Abp.GlobalFeatures.Tests.csproj", "{231F1581-AA21-44C3-BF27-51EB3AD5355C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiLingualObject", "src\Volo.Abp.MultiLingualObject\Volo.Abp.MultiLingualObject.csproj", "{C9142DED-1F6C-4385-A37D-81E46B233306}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiLingualObject", "src\Volo.Abp.MultiLingualObject\Volo.Abp.MultiLingualObject.csproj", "{C9142DED-1F6C-4385-A37D-81E46B233306}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.MultiLingualObject.Tests", "test\Volo.Abp.MultiLingualObject.Tests\Volo.Abp.MultiLingualObject.Tests.csproj", "{A30D63B0-E952-4052-BAEE-38B8BF924093}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiLingualObject.Tests", "test\Volo.Abp.MultiLingualObject.Tests\Volo.Abp.MultiLingualObject.Tests.csproj", "{A30D63B0-E952-4052-BAEE-38B8BF924093}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Client.IdentityModel.WebAssembly", "src\Volo.Abp.Http.Client.IdentityModel.WebAssembly\Volo.Abp.Http.Client.IdentityModel.WebAssembly.csproj", "{3D35A1E0-A9A1-404F-9B55-5F1A7EB6D5B8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.IdentityModel.WebAssembly", "src\Volo.Abp.Http.Client.IdentityModel.WebAssembly\Volo.Abp.Http.Client.IdentityModel.WebAssembly.csproj", "{3D35A1E0-A9A1-404F-9B55-5F1A7EB6D5B8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.Client.Common", "src\Volo.Abp.AspNetCore.Mvc.Client.Common\Volo.Abp.AspNetCore.Mvc.Client.Common.csproj", "{8A22D962-016E-474A-8BB7-F831F0ABF3AC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.Client.Common", "src\Volo.Abp.AspNetCore.Mvc.Client.Common\Volo.Abp.AspNetCore.Mvc.Client.Common.csproj", "{8A22D962-016E-474A-8BB7-F831F0ABF3AC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly", "src\Volo.Abp.AspNetCore.Components.WebAssembly\Volo.Abp.AspNetCore.Components.WebAssembly.csproj", "{E1A62D10-F2FB-4040-BD60-11A3934058DF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Components.WebAssembly", "src\Volo.Abp.AspNetCore.Components.WebAssembly\Volo.Abp.AspNetCore.Components.WebAssembly.csproj", "{E1A62D10-F2FB-4040-BD60-11A3934058DF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlazoriseUI", "src\Volo.Abp.BlazoriseUI\Volo.Abp.BlazoriseUI.csproj", "{4EBDDB1B-D6C5-4FAE-B5A7-2171B18CDFA5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BlazoriseUI", "src\Volo.Abp.BlazoriseUI\Volo.Abp.BlazoriseUI.csproj", "{4EBDDB1B-D6C5-4FAE-B5A7-2171B18CDFA5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme", "src\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj", "{ABC27C10-C0FF-44CB-B4FF-A09C0B79F695}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme", "src\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj", "{ABC27C10-C0FF-44CB-B4FF-A09C0B79F695}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.Theming", "src\Volo.Abp.AspNetCore.Components.WebAssembly.Theming\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj", "{29CA7471-4E3E-4E75-8B33-001DDF682F01}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Components.WebAssembly.Theming", "src\Volo.Abp.AspNetCore.Components.WebAssembly.Theming\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj", "{29CA7471-4E3E-4E75-8B33-001DDF682F01}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Autofac.WebAssembly", "src\Volo.Abp.Autofac.WebAssembly\Volo.Abp.Autofac.WebAssembly.csproj", "{37F89B0B-1C6B-426F-A5EE-676D1956D9E9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Autofac.WebAssembly", "src\Volo.Abp.Autofac.WebAssembly\Volo.Abp.Autofac.WebAssembly.csproj", "{37F89B0B-1C6B-426F-A5EE-676D1956D9E9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Authentication.OpenIdConnect", "src\Volo.Abp.AspNetCore.Authentication.OpenIdConnect\Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj", "{DEFE3DB2-EA4F-4F90-87FC-B25D64427BC5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Authentication.OpenIdConnect", "src\Volo.Abp.AspNetCore.Authentication.OpenIdConnect\Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj", "{DEFE3DB2-EA4F-4F90-87FC-B25D64427BC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.EventBus.Rebus", "src\Volo.Abp.EventBus.Rebus\Volo.Abp.EventBus.Rebus.csproj", "{F689967F-1EF1-4D75-8BA4-2F2F3506B1F3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ExceptionHandling", "src\Volo.Abp.ExceptionHandling\Volo.Abp.ExceptionHandling.csproj", "{B9D1ADCB-D552-4626-A1F1-78FF72C1E822}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Components", "src\Volo.Abp.AspNetCore.Components\Volo.Abp.AspNetCore.Components.csproj", "{89840441-5A3A-4FD7-9CB4-E5B52FAEF72A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Swashbuckle", "src\Volo.Abp.Swashbuckle\Volo.Abp.Swashbuckle.csproj", "{DD9519E0-5A68-48DC-A051-7BF2AC922F3E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1037,6 +1045,22 @@ Global {DEFE3DB2-EA4F-4F90-87FC-B25D64427BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DEFE3DB2-EA4F-4F90-87FC-B25D64427BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DEFE3DB2-EA4F-4F90-87FC-B25D64427BC5}.Release|Any CPU.Build.0 = Release|Any CPU + {F689967F-1EF1-4D75-8BA4-2F2F3506B1F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F689967F-1EF1-4D75-8BA4-2F2F3506B1F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F689967F-1EF1-4D75-8BA4-2F2F3506B1F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F689967F-1EF1-4D75-8BA4-2F2F3506B1F3}.Release|Any CPU.Build.0 = Release|Any CPU + {B9D1ADCB-D552-4626-A1F1-78FF72C1E822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9D1ADCB-D552-4626-A1F1-78FF72C1E822}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9D1ADCB-D552-4626-A1F1-78FF72C1E822}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9D1ADCB-D552-4626-A1F1-78FF72C1E822}.Release|Any CPU.Build.0 = Release|Any CPU + {89840441-5A3A-4FD7-9CB4-E5B52FAEF72A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89840441-5A3A-4FD7-9CB4-E5B52FAEF72A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89840441-5A3A-4FD7-9CB4-E5B52FAEF72A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89840441-5A3A-4FD7-9CB4-E5B52FAEF72A}.Release|Any CPU.Build.0 = Release|Any CPU + {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Release|Any CPU.Build.0 = Release|Any CPU {C9142DED-1F6C-4385-A37D-81E46B233306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9142DED-1F6C-4385-A37D-81E46B233306}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9142DED-1F6C-4385-A37D-81E46B233306}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1220,6 +1244,10 @@ Global {29CA7471-4E3E-4E75-8B33-001DDF682F01} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {37F89B0B-1C6B-426F-A5EE-676D1956D9E9} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {DEFE3DB2-EA4F-4F90-87FC-B25D64427BC5} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {F689967F-1EF1-4D75-8BA4-2F2F3506B1F3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {B9D1ADCB-D552-4626-A1F1-78FF72C1E822} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {89840441-5A3A-4FD7-9CB4-E5B52FAEF72A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {DD9519E0-5A68-48DC-A051-7BF2AC922F3E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {C9142DED-1F6C-4385-A37D-81E46B233306} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {A30D63B0-E952-4052-BAEE-38B8BF924093} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj index 1d30366e67..97b9c6dc0a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 Volo.Abp.AspNetCore.Authentication.JwtBearer Volo.Abp.AspNetCore.Authentication.JwtBearer $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo.Abp.AspNetCore.Authentication.OAuth.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo.Abp.AspNetCore.Authentication.OAuth.csproj index ae032e5b9e..18e9a271dc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo.Abp.AspNetCore.Authentication.OAuth.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo.Abp.AspNetCore.Authentication.OAuth.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 Volo.Abp.AspNetCore.Authentication.OAuth Volo.Abp.AspNetCore.Authentication.OAuth $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj index 30f56491c5..bac9441f00 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj @@ -4,12 +4,12 @@ - netcoreapp3.1 + net5.0 - + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicTheme.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicTheme.cs index 74c81ed72c..0de4e5de7a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicTheme.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicTheme.cs @@ -1,5 +1,6 @@ using Volo.Abp.AspNetCore.Components.WebAssembly.Theming; using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Routing; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme @@ -15,6 +16,11 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme { options.AdditionalAssemblies.Add(typeof(AbpAspNetCoreComponentsWebAssemblyBasicThemeModule).Assembly); }); + + Configure(options => + { + options.Contributors.Add(new BasicThemeToolbarContributor()); + }); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs new file mode 100644 index 0000000000..8316a1480e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme +{ + public class BasicThemeToolbarContributor : IToolbarContributor + { + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); + context.Toolbar.Items.Add(new ToolbarItem(typeof(LoginDisplay))); + } + + return Task.CompletedTask; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenuItem.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor similarity index 67% rename from framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenuItem.razor rename to framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor index 8194896f61..574a684213 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenuItem.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor @@ -2,14 +2,15 @@ @{ var elementId = MenuItem.ElementId ?? "MenuItem_" + MenuItem.Name.Replace(".", "_"); var cssClass = string.IsNullOrEmpty(MenuItem.CssClass) ? string.Empty : MenuItem.CssClass; + var disabled = MenuItem.IsDisabled ? "disabled" : string.Empty; var url = string.IsNullOrEmpty(MenuItem.Url) ? "#" : MenuItem.Url; } @if (MenuItem.IsLeaf) { if (MenuItem.Url != null) { - } } @@ -26,7 +27,7 @@ else { - -} -@code { - [Parameter] - public ApplicationMenuItem MenuItem { get; set; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor.cs new file mode 100644 index 0000000000..d0ff42879c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic +{ + public partial class FirstLevelNavMenuItem : IDisposable + { + [Inject] private NavigationManager NavigationManager { get; set; } + + [Parameter] + public ApplicationMenuItem MenuItem { get; set; } + + public bool IsSubMenuOpen { get; set; } + + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void ToggleSubMenu() + { + IsSubMenuOpen = !IsSubMenuOpen; + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } + + private void OnLocationChanged(object sender, LocationChangedEventArgs e) + { + IsSubMenuOpen = false; + StateHasChanged(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor index 88a005d7bf..e4767cc031 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor @@ -1,13 +1,22 @@ @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Volo.Abp.Users +@using Volo.Abp.MultiTenancy @inject ICurrentUser CurrentUser +@inject ICurrentTenant CurrentTenant @inject NavigationManager Navigation @inject SignOutSessionStateManager SignOutManager - @CurrentUser.UserName + @if (CurrentTenant.Name != null) + { + @CurrentTenant.Name\@CurrentUser.UserName + } + else + { + @CurrentUser.UserName + } @if (Menu != null) @@ -17,7 +26,10 @@ @menuItem.DisplayName } } - + + @UiLocalizer["ManageYourAccount"] + + Logout @@ -32,7 +44,7 @@ { Navigation.NavigateTo(uri); } - + private async Task BeginSignOut() { await SignOutManager.SetSignOutState(); diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs index e0ad688513..17718dbcfb 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -1,18 +1,50 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Volo.Abp.Http.Client; using Volo.Abp.UI.Navigation; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic { - public partial class LoginDisplay + public partial class LoginDisplay : IDisposable { - [Inject] protected IMenuManager MenuManager { get; set; } - + [Inject] + protected IMenuManager MenuManager { get; set; } + + [Inject] + protected IStringLocalizer UiLocalizer { get; set; } + + [Inject] + protected IOptions RemoteServiceOptions { get; set; } + protected ApplicationMenu Menu { get; set; } + protected string ServerUrl { get; set; } + protected string ServerAccountUrl { get; set; } + protected override async Task OnInitializedAsync() { Menu = await MenuManager.GetAsync(StandardMenus.User); + + ServerUrl = RemoteServiceOptions.Value.RemoteServices.Default?.BaseUrl?.TrimEnd('/'); + ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; + + Navigation.LocationChanged += OnLocationChanged; + } + + protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) + { + ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; + StateHasChanged(); + } + + public void Dispose() + { + Navigation.LocationChanged -= OnLocationChanged; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor index af88212f0a..45496a7984 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor @@ -4,26 +4,22 @@
+ @Body + +
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor.cs new file mode 100644 index 0000000000..2464275c2c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic +{ + public partial class MainLayout : IDisposable + { + [Inject] private NavigationManager NavigationManager { get; set; } + + private bool IsCollapseShown { get; set; } + + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void ToggleCollapse() + { + IsCollapseShown = !IsCollapseShown; + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } + + private void OnLocationChanged(object sender, LocationChangedEventArgs e) + { + IsCollapseShown = false; + StateHasChanged(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor index e1ede1c0fd..b35ec8ba2e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor @@ -2,6 +2,6 @@ { foreach (var menuItem in Menu.Items) { - + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor.cs index e47603e163..1779d73d28 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavMenu.razor.cs @@ -6,22 +6,14 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic { public partial class NavMenu { - [Inject] protected IMenuManager MenuManager { get; set; } + [Inject] + protected IMenuManager MenuManager { get; set; } protected ApplicationMenu Menu { get; set; } - private bool collapseNavMenu = true; - - private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; - - protected override async Task OnInitializedAsync() + protected async override Task OnInitializedAsync() { Menu = await MenuManager.GetAsync(StandardMenus.Main); } - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavToolbar.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavToolbar.razor new file mode 100644 index 0000000000..5eea07ea8f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavToolbar.razor @@ -0,0 +1,8 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavToolbar.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavToolbar.razor.cs new file mode 100644 index 0000000000..6fddf263b3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/NavToolbar.razor.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic +{ + public partial class NavToolbar + { + [Inject] + private IToolbarManager ToolbarManager { get; set; } + + private List ToolbarItemRenders { get; set; } = new List(); + + protected override async Task OnInitializedAsync() + { + var toolbar = await ToolbarManager.GetAsync(StandardToolbars.Main); + + ToolbarItemRenders.Clear(); + + foreach (var item in toolbar.Items) + { + ToolbarItemRenders.Add(builder => + { + builder.OpenComponent(0, item.ComponentType); + builder.CloseComponent(); + }); + } + } + + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/SecondLevelNavMenuItem.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/SecondLevelNavMenuItem.razor new file mode 100644 index 0000000000..4e75e26b50 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/SecondLevelNavMenuItem.razor @@ -0,0 +1,45 @@ +@using Volo.Abp.UI.Navigation +@{ + var elementId = MenuItem.ElementId ?? "MenuItem_" + MenuItem.Name.Replace(".", "_"); + var cssClass = string.IsNullOrEmpty(MenuItem.CssClass) ? string.Empty : MenuItem.CssClass; + var disabled = MenuItem.IsDisabled ? "disabled" : string.Empty; + var url = string.IsNullOrEmpty(MenuItem.Url) ? "#" : MenuItem.Url; +} +@if (MenuItem.IsLeaf) +{ + if (MenuItem.Url != null) + { + + @if (MenuItem.Icon != null) + { + if (MenuItem.Icon.StartsWith("fa")) + { + + } + } + @MenuItem.DisplayName + + } +} +else +{ + +} +@code { +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/SecondLevelNavMenuItem.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/SecondLevelNavMenuItem.razor.cs new file mode 100644 index 0000000000..1749b471cc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/SecondLevelNavMenuItem.razor.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic +{ + public partial class SecondLevelNavMenuItem : IDisposable + { + [Inject] private NavigationManager NavigationManager { get; set; } + + [Parameter] + public ApplicationMenuItem MenuItem { get; set; } + + public bool IsSubMenuOpen { get; set; } + + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void ToggleSubMenu() + { + IsSubMenuOpen = !IsSubMenuOpen; + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } + + private void OnLocationChanged(object sender, LocationChangedEventArgs e) + { + IsSubMenuOpen = false; + StateHasChanged(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/_Imports.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/_Imports.razor index f1ebc76453..7d695e9344 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/_Imports.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/_Imports.razor @@ -8,3 +8,5 @@ @using Volo.Abp.AspNetCore.Components.WebAssembly @using Blazorise @using Blazorise.DataGrid +@using Volo.Abp.BlazoriseUI; +@using Volo.Abp.BlazoriseUI.Components; \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj index 783af0b202..3984b8c297 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj @@ -1,11 +1,10 @@ - + - netstandard2.1 - 3.0 + net5.0 Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/theme.css b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/libs/abp/css/theme.css similarity index 100% rename from framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/theme.css rename to framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/libs/abp/css/theme.css diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/theme.js b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/theme.js deleted file mode 100644 index 8a5b94c7c6..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/wwwroot/theme.js +++ /dev/null @@ -1,16 +0,0 @@ -$(function () { - $('.dropdown-menu a.dropdown-toggle').on('click', function (e) { - if (!$(this).next().hasClass('show')) { - $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); - } - - var $subMenu = $(this).next(".dropdown-menu"); - $subMenu.toggleClass('show'); - - $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function (e) { - $('.dropdown-submenu .show').removeClass("show"); - }); - - return false; - }); -}); \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/AbpToolbarOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/AbpToolbarOptions.cs new file mode 100644 index 0000000000..a2b006117f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/AbpToolbarOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public class AbpToolbarOptions + { + [NotNull] + public List Contributors { get; } + + public AbpToolbarOptions() + { + Contributors = new List(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarConfigurationContext.cs new file mode 100644 index 0000000000..825b72b3f4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarConfigurationContext.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Localization; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public interface IToolbarConfigurationContext : IServiceProviderAccessor + { + Toolbar Toolbar { get; } + + IAuthorizationService AuthorizationService { get; } + + IStringLocalizerFactory StringLocalizerFactory { get; } + + Task IsGrantedAsync(string policyName); + + [CanBeNull] + IStringLocalizer GetDefaultLocalizer(); + + [NotNull] + public IStringLocalizer GetLocalizer(); + + [NotNull] + public IStringLocalizer GetLocalizer(Type resourceType); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarContributor.cs new file mode 100644 index 0000000000..363741c22c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarContributor.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public interface IToolbarContributor + { + Task ConfigureToolbarAsync(IToolbarConfigurationContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarManager.cs new file mode 100644 index 0000000000..0f1a6b29d3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/IToolbarManager.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public interface IToolbarManager + { + Task GetAsync(string name); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/StandardToolbars.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/StandardToolbars.cs new file mode 100644 index 0000000000..dfdf773422 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/StandardToolbars.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public static class StandardToolbars + { + public const string Main = "Main"; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/Toolbar.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/Toolbar.cs new file mode 100644 index 0000000000..8ca6dfab24 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/Toolbar.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public class Toolbar + { + public string Name { get; } + + public List Items { get; } + + public Toolbar([NotNull] string name) + { + Name = Check.NotNull(name, nameof(name)); + Items = new List(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarConfigurationContext.cs new file mode 100644 index 0000000000..7174dbe431 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarConfigurationContext.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public class ToolbarConfigurationContext : IToolbarConfigurationContext + { + public IServiceProvider ServiceProvider { get; } + private readonly object _serviceProviderLock = new object(); + + private TRef LazyGetRequiredService(Type serviceType, ref TRef reference) + { + if (reference == null) + { + lock (_serviceProviderLock) + { + if (reference == null) + { + reference = (TRef)ServiceProvider.GetRequiredService(serviceType); + } + } + } + + return reference; + } + + public IAuthorizationService AuthorizationService => LazyGetRequiredService(typeof(IAuthorizationService), ref _authorizationService); + private IAuthorizationService _authorizationService; + + private IStringLocalizerFactory _stringLocalizerFactory; + public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(typeof(IStringLocalizerFactory),ref _stringLocalizerFactory); + + public Toolbar Toolbar { get; } + + public ToolbarConfigurationContext(Toolbar toolbar, IServiceProvider serviceProvider) + { + Toolbar = toolbar; + ServiceProvider = serviceProvider; + } + + public Task IsGrantedAsync(string policyName) + { + return AuthorizationService.IsGrantedAsync(policyName); + } + + [CanBeNull] + public IStringLocalizer GetDefaultLocalizer() + { + return StringLocalizerFactory.CreateDefaultOrNull(); + } + + [NotNull] + public IStringLocalizer GetLocalizer() + { + return StringLocalizerFactory.Create(); + } + + [NotNull] + public IStringLocalizer GetLocalizer(Type resourceType) + { + return StringLocalizerFactory.Create(resourceType); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarItem.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarItem.cs new file mode 100644 index 0000000000..8400a9c88f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarItem.cs @@ -0,0 +1,23 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public class ToolbarItem + { + public Type ComponentType + { + get => _componentType; + set => _componentType = Check.NotNull(value, nameof(value)); + } + private Type _componentType; + + public int Order { get; set; } + + public ToolbarItem([NotNull] Type componentType, int order = 0) + { + Order = order; + ComponentType = Check.NotNull(componentType, nameof(componentType)); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarManager.cs new file mode 100644 index 0000000000..311795ac4f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Toolbars/ToolbarManager.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars +{ + public class ToolbarManager : IToolbarManager, ITransientDependency + { + protected AbpToolbarOptions Options { get; } + protected IServiceProvider ServiceProvider { get; } + + public ToolbarManager( + IOptions options, + IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + Options = options.Value; + } + + public async Task GetAsync(string name) + { + var toolbar = new Toolbar(name); + + using (var scope = ServiceProvider.CreateScope()) + { + var context = new ToolbarConfigurationContext(toolbar, scope.ServiceProvider); + + foreach (var contributor in Options.Contributors) + { + await contributor.ConfigureToolbarAsync(context); + } + } + + return toolbar; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj index 86902f7038..58e0c279ae 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj @@ -4,8 +4,7 @@ - netstandard2.1 - 3.0 + net5.0 diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000000..7d2a868f57 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item{display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css.map b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css.map new file mode 100644 index 0000000000..30a397279a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","bootstrap.css","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_interactions.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v4.5.3 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n","// Do not forget to update getting-started/theming.md!\n:root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-`"); - break; - case AbpModalButtons.Close: - sb.AppendLine(""); - break; - case AbpModalButtons.Save: - sb.AppendLine(""); - break; - case AbpModalButtons.Save | AbpModalButtons.Cancel: - sb.AppendLine(""); - sb.AppendLine(""); - break; - case AbpModalButtons.Save | AbpModalButtons.Close: - sb.AppendLine(""); - sb.AppendLine(""); - break; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Localization; +using System.Text; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +{ + public class AbpModalFooterTagHelperService : AbpTagHelperService + { + private readonly IStringLocalizer _localizer; + + public AbpModalFooterTagHelperService(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "div"; + output.Attributes.AddClass("modal-footer"); + + if (TagHelper.Buttons != AbpModalButtons.None) + { + output.PostContent.SetHtmlContent(CreateContent()); + } + + ProcessButtonsAlignment(output); + } + + protected virtual string CreateContent() + { + var sb = new StringBuilder(); + + switch (TagHelper.Buttons) + { + case AbpModalButtons.Cancel: + sb.AppendLine(GetCancelButton()); + break; + case AbpModalButtons.Close: + sb.AppendLine(GetCloseButton()); + break; + case AbpModalButtons.Save: + sb.AppendLine(GetSaveButton()); + break; + case AbpModalButtons.Save | AbpModalButtons.Cancel: + sb.AppendLine(GetCancelButton()); + sb.AppendLine(GetSaveButton()); + break; + case AbpModalButtons.Save | AbpModalButtons.Close: + sb.AppendLine(GetCloseButton()); + sb.AppendLine(GetSaveButton()); + break; } - return sb.ToString(); - } - + return sb.ToString(); + } + + protected virtual string GetSaveButton() + { + var icon = new TagBuilder("i"); + icon.AddCssClass("fa"); + icon.AddCssClass("fa-check"); + + var span = new TagBuilder("span"); + span.InnerHtml.Append(_localizer["Save"]); + + var element = new TagBuilder("button"); + element.Attributes.Add("type", "submit"); + element.AddCssClass("btn"); + element.AddCssClass("btn-primary"); + element.Attributes.Add("data-busy-text", _localizer["SavingWithThreeDot"]); + element.InnerHtml.AppendHtml(icon); + element.InnerHtml.AppendHtml(" "); + element.InnerHtml.AppendHtml(span); + + return element.ToHtmlString(); + } + + protected virtual string GetCloseButton() + { + var element = new TagBuilder("button"); + element.Attributes.Add("type", "button"); + element.Attributes.Add("data-dismiss", "modal"); + element.AddCssClass("btn"); + element.AddCssClass("btn-secondary"); + element.InnerHtml.Append(_localizer["Close"]); + + return element.ToHtmlString(); + } + + protected virtual string GetCancelButton() + { + var element = new TagBuilder("button"); + element.Attributes.Add("type", "button"); + element.Attributes.Add("data-dismiss", "modal"); + element.AddCssClass("btn"); + element.AddCssClass("btn-secondary"); + element.InnerHtml.Append(_localizer["Cancel"]); + + return element.ToHtmlString(); + } + protected virtual void ProcessButtonsAlignment(TagHelperOutput output) { if (TagHelper.ButtonAlignment == ButtonsAlign.Default) @@ -62,6 +109,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal return; } output.Attributes.AddClass("justify-content-" + TagHelper.ButtonAlignment.ToString().ToLowerInvariant()); - } - } -} \ No newline at end of file + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs index 79cddd1f53..7473245e96 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs @@ -1,11 +1,20 @@ -using System.Text; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal { public class AbpModalHeaderTagHelperService : AbpTagHelperService { + protected IStringLocalizer L { get; } + + public AbpModalHeaderTagHelperService(IStringLocalizer localizer) + { + L = localizer; + } + public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "div"; @@ -16,22 +25,27 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal protected virtual string CreatePreContent() { - var sb = new StringBuilder(); - - sb.AppendLine("

" + TagHelper.Title + "
"); + var title = new TagBuilder("h5"); + title.AddCssClass("modal-title"); + title.InnerHtml.Append(TagHelper.Title); - return sb.ToString(); + return title.ToHtmlString(); } protected virtual string CreatePostContent() { - var sb = new StringBuilder(); - - sb.AppendLine(" "); + var span = new TagBuilder("span"); + span.Attributes.Add("aria-hidden", "true"); + span.InnerHtml.AppendHtml("×"); + + var button = new TagBuilder("button"); + button.AddCssClass("close"); + button.Attributes.Add("type", "button"); + button.Attributes.Add("data-dismiss", "modal"); + button.Attributes.Add("aria-label", L["Close"].Value); + button.InnerHtml.AppendHtml(span); - return sb.ToString(); + return button.ToHtmlString(); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelperService.cs index 3524b27d35..78d664b562 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelperService.cs @@ -1,30 +1,66 @@ -using System.Linq; -using System.Text; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; - +using System.Text; +using System.Threading.Tasks; + namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal { public class AbpModalTagHelperService : AbpTagHelperService { - public override void Process(TagHelperContext context, TagHelperOutput output) + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { output.TagName = null; - output.PreContent.SetHtmlContent(CreatePreContent(output)); - output.PostContent.SetHtmlContent(CreatePostContent()); - } - protected virtual string CreatePreContent(TagHelperOutput output) - { - var sb = new StringBuilder(); + var childContent = await output.GetChildContentAsync(); + + SetContent(context, output, childContent); + } - var attritubutes = output.Attributes.Select(a => " " + a.Name + "=\"" + a.Value + "\" ").ToList(); - var attritubutesAsJoin = string.Join(" ", attritubutes.ToArray()); + protected virtual void SetContent(TagHelperContext context, TagHelperOutput output, TagHelperContent childContent) + { + var modalContent = GetModalContentElement(context, output, childContent); + var modalDialog = GetModalDialogElement(context, output, modalContent); + var modal = GetModal(context, output, modalDialog); - sb.AppendLine("
"); - sb.AppendLine("
"); - sb.AppendLine("
"); + output.Content.SetHtmlContent(modal); + } + + protected virtual TagBuilder GetModalContentElement(TagHelperContext context, TagHelperOutput output, TagHelperContent childContent) + { + var element = new TagBuilder("div"); + element.AddCssClass(GetModalContentClasses()); + element.InnerHtml.SetHtmlContent(childContent); + return element; + } - return sb.ToString(); + protected virtual TagBuilder GetModalDialogElement(TagHelperContext context, TagHelperOutput output, IHtmlContent innerHtml) + { + var element = new TagBuilder("div"); + element.AddCssClass(GetModalDialogClasses()); + element.Attributes.Add("role", "document"); + element.InnerHtml.SetHtmlContent(innerHtml); + return element; + } + + protected virtual TagBuilder GetModal(TagHelperContext context, TagHelperOutput output, IHtmlContent innerHtml) + { + var element = new TagBuilder("div"); + element.AddCssClass(GetModalClasses()); + element.Attributes.Add("tabindex", "-1"); + element.Attributes.Add("role", "dialog"); + element.Attributes.Add("aria-hidden", "true"); + + foreach (var attr in output.Attributes) + { + element.Attributes.Add(attr.Name, attr.Value.ToString()); + } + + SetDataAttributes(element); + + element.InnerHtml.SetHtmlContent(innerHtml); + + return element; } protected virtual string GetModalClasses() @@ -63,17 +99,14 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal return "data-backdrop=\"static\" "; } return string.Empty; - } - - protected virtual string CreatePostContent() - { - var sb = new StringBuilder(); - - sb.AppendLine("
"); - sb.AppendLine("
"); - sb.AppendLine("
"); - - return sb.ToString(); + } + + protected virtual void SetDataAttributes(TagBuilder builder) + { + if (TagHelper.Static == true) + { + builder.Attributes.Add("data-backdrop", "static"); + } } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Nav/AbpNavbarToggleTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Nav/AbpNavbarToggleTagHelperService.cs index 1b53894ac2..df48452c40 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Nav/AbpNavbarToggleTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Nav/AbpNavbarToggleTagHelperService.cs @@ -1,11 +1,21 @@ -using System; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Localization; +using System; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Nav { public class AbpNavbarToggleTagHelperService : AbpTagHelperService { + protected IStringLocalizer L { get; } + + public AbpNavbarToggleTagHelperService(IStringLocalizer stringLocalizer) + { + L = stringLocalizer; + } + public override void Process(TagHelperContext context, TagHelperOutput output) { SetRandomNameIfNotProvided(); @@ -18,10 +28,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Nav protected virtual void SetToggleButton(TagHelperContext context, TagHelperOutput output) { - output.PreElement.SetHtmlContent( - ""); + var span = new TagBuilder("span"); + span.AddCssClass("navbar-toggler-icon"); + + var button = new TagBuilder("button"); + button.AddCssClass("navbar-toggler"); + button.Attributes.Add("type", "button"); + button.Attributes.Add("data-toggle", "collapse"); + button.Attributes.Add("data-target", "#" + TagHelper.Id); + button.Attributes.Add("aria-controls", TagHelper.Id); + button.Attributes.Add("aria-expanded", "false"); + button.Attributes.Add("aria-label", L["ToggleNavigation"].Value); + button.InnerHtml.AppendHtml(span); + + output.PreElement.SetHtmlContent(button); } protected virtual void SetRandomNameIfNotProvided() diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs index 9c37e77767..71f3bfe247 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs @@ -1,13 +1,13 @@ -using System; -using System.Linq; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Localization.Resources.AbpUi; +using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Localization; +using System; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; @@ -134,7 +134,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination SetHrefAttribute(currentPage, attributeList); - tagHelperOutput.Content.SetHtmlContent(localizer[localizationKey]); + tagHelperOutput.Content.SetContent(localizer[localizationKey]); var renderedHtml = tagHelperOutput.Render(_encoder); @@ -166,7 +166,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination var localizer = _stringLocalizerFactory.Create(typeof(AbpUiResource)); var pagerInfo = (TagHelper.ShowInfo ?? false) ? - "
" + localizer["PagerInfo{0}{1}{2}", TagHelper.Model.ShowingFrom, TagHelper.Model.ShowingTo, TagHelper.Model.TotalItemsCount] + "
\r\n" + "
" + _encoder.Encode(localizer["PagerInfo{0}{1}{2}", TagHelper.Model.ShowingFrom, TagHelper.Model.ShowingTo, TagHelper.Model.TotalItemsCount]) + "
\r\n" : ""; return diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs index 7ecb351290..5ada0dfbc5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs @@ -1,11 +1,20 @@ -using System; -using System.Linq; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; - +using System.Linq; +using System.Web; + namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover { public class AbpPopoverTagHelperService : AbpTagHelperService - { + { + protected IHtmlGenerator HtmlGenerator { get; } + + public AbpPopoverTagHelperService(IHtmlGenerator htmlGenerator) + { + HtmlGenerator = htmlGenerator; + } + public override void Process(TagHelperContext context, TagHelperOutput output) { if (!TagHelper.Disabled ?? true) @@ -21,38 +30,51 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover SetDisabled(context, output); } } + protected virtual void SetDisabled(TagHelperContext context, TagHelperOutput output) - { - var triggerAsHtml = TagHelper.Dismissible ?? false ? "data-trigger=\"focus\" " : ""; + { + var triggerValue = TagHelper.Dismissible ?? false ? "focus" : ""; if (TagHelper.Hoverable ?? false) { - if (triggerAsHtml.Contains("focus")) + if (triggerValue.Contains("focus")) { - triggerAsHtml = triggerAsHtml.Replace("focus", "focus hover"); + triggerValue = triggerValue.Replace("focus", "focus hover"); } else { - triggerAsHtml = "data-trigger=\"hover\" "; + triggerValue = "hover"; } - } - var dataPlacementAsHtml = "data-placement=\"" + GetDirectory().ToString().ToLowerInvariant() + "\" "; - + } + + var dataPlacement = GetDirectory().ToString().ToLowerInvariant(); // data-placement="default" with data-trigger="focus" causes Cannot read property 'indexOf' of undefined at computeAutoPlacement(bootstrap.bundle.js?_v=637146714627330435:2185) error if (IsDismissibleOrHoverable() && GetDirectory() == PopoverDirectory.Default) { //dataPlacementAsHtml = string.Empty; //bootstrap default placement is right, abp's is top. - dataPlacementAsHtml = dataPlacementAsHtml.Replace("default", "top"); + dataPlacement = dataPlacement.Replace("default", "top"); } + var titleAttribute = output.Attributes.FirstOrDefault(at => at.Name == "title"); - var titleAsHtml = titleAttribute == null ? "" : "title=\"" + titleAttribute.Value + "\" "; - var preElementHtml = ""; - var postElementHtml = ""; - output.PreElement.SetHtmlContent(preElementHtml); - output.PostElement.SetHtmlContent(postElementHtml); + var span = new TagBuilder("span"); + span.AddCssClass("d-inline-block"); + span.Attributes.Add("tabindex", "0"); + span.Attributes.Add("data-toggle", "popover"); + span.Attributes.Add("data-content", GetDataContent()); + span.Attributes.Add("data-trigger", triggerValue); + span.Attributes.Add("data-placement", dataPlacement); + + if (titleAttribute != null) + { + span.Attributes.Add("title", HttpUtility.HtmlDecode(titleAttribute.Value.ToString())); + } + + output.PreElement.SetHtmlContent(span.RenderStartTag()); + output.PostElement.SetHtmlContent(span.RenderEndTag()); output.Attributes.Add("style", "pointer-events: none;"); } + protected virtual void SetDataTriggerIfDismissible(TagHelperContext context, TagHelperOutput output) { if (TagHelper.Dismissible ?? false) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabDropdownTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabDropdownTagHelperService.cs index fb724af9c8..8746e4a48b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabDropdownTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabDropdownTagHelperService.cs @@ -1,7 +1,8 @@ -using System; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab @@ -31,14 +32,26 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab var link = TagHelper.Name; var title = TagHelper.Title; - return "
  • " + - "" + - title + - "" + - "
    " + - AbpTabDropdownItemsActivePlaceholder + - "
    " + - "
  • "; + var anchor = new TagBuilder("a"); + anchor.AddCssClass("nav-link dropdown-toggle"); + anchor.Attributes.Add("id", id); + anchor.Attributes.Add("data-toggle", "dropdown"); + anchor.Attributes.Add("href", "#" + link); + anchor.Attributes.Add("role", "button"); + anchor.Attributes.Add("aria-haspopup", "true"); + anchor.Attributes.Add("aria-expanded", "false"); + anchor.InnerHtml.Append(title); + + var menu = new TagBuilder("div"); + menu.AddCssClass("dropdown-menu"); + menu.InnerHtml.Append(AbpTabDropdownItemsActivePlaceholder); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("nav-item dropdown"); + listItem.InnerHtml.AppendHtml(anchor); + listItem.InnerHtml.AppendHtml(menu); + + return listItem.ToHtmlString(); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabLinkTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabLinkTagHelperService.cs index 54a29bf691..1955127114 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabLinkTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabLinkTagHelperService.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; +using System.Threading.Tasks; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab @@ -10,7 +11,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { SetPlaceholderForNameIfNotProvided(); - + var tabHeader = GetTabHeaderItem(context, output); var tabHeaderItems = context.GetValue>(TabItems); @@ -30,12 +31,28 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab if (!string.IsNullOrWhiteSpace(TagHelper.ParentDropdownName)) { - return "" + title + ""; + var anchor = new TagBuilder("a"); + anchor.AddCssClass("dropdown-item"); + anchor.Attributes.Add("id", id); + anchor.Attributes.Add("href", href); + anchor.InnerHtml.Append(title); + + return anchor.ToHtmlString(); } + else + { + var anchor = new TagBuilder("a"); + anchor.AddCssClass("nav-link " + AbpTabItemActivePlaceholder); + anchor.Attributes.Add("id", id); + anchor.Attributes.Add("href", href); + anchor.InnerHtml.Append(title); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("nav-item"); + listItem.InnerHtml.AppendHtml(anchor); - return "
  • " + - title + - "
  • "; + return listItem.ToHtmlString(); + } } protected virtual void SetPlaceholderForNameIfNotProvided() @@ -46,4 +63,4 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs index 2de18d0592..9080e49d83 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabTagHelperService.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Extensions; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab @@ -34,16 +35,52 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab var attributes = GetTabHeaderAttributes(context, output); var classAttributesAsString = attributes.Where(a=>a.Name == "class").ToList().Select(a=>a.Value).JoinAsString(" "); - var otherAttributesAsString = attributes.Where(a => a.Name != "class").ToList().ToHtmlAttributesAsString(); + var otherAttributes = attributes.Where(a => a.Name != "class").ToList(); if (!string.IsNullOrWhiteSpace(TagHelper.ParentDropdownName)) { - return "" + title + ""; + var anchor = new TagBuilder("a"); + anchor.AddCssClass("dropdown-item " + classAttributesAsString); + anchor.Attributes.Add("id", id); + anchor.Attributes.Add("href", "#" + link); + anchor.Attributes.Add("data-toggle", "tab"); + anchor.Attributes.Add("role", "tab"); + anchor.Attributes.Add("aria-controls", control); + anchor.Attributes.Add("aria-selected", "false"); + + foreach (var attr in otherAttributes) + { + anchor.Attributes.Add(attr.Name, attr.Value.ToString()); + } + + anchor.InnerHtml.Append(title); + + return anchor.ToHtmlString(); + } + else + { + var anchor = new TagBuilder("a"); + anchor.AddCssClass("nav-link " + classAttributesAsString + " " + AbpTabItemActivePlaceholder); + anchor.Attributes.Add("id", id); + anchor.Attributes.Add("data-toggle", TabItemsDataTogglePlaceHolder); + anchor.Attributes.Add("href", "#" + link); + anchor.Attributes.Add("role", "tab"); + anchor.Attributes.Add("aria-controls", control); + anchor.Attributes.Add("aria-selected", AbpTabItemSelectedPlaceholder); + + foreach (var attr in otherAttributes) + { + anchor.Attributes.Add(attr.Name, attr.Value.ToString()); + } + + anchor.InnerHtml.Append(title); + + var listItem = new TagBuilder("li"); + listItem.AddCssClass("nav-item"); + listItem.InnerHtml.AppendHtml(anchor); + + return listItem.ToHtmlString(); } - - return "
  • " + - title + - "
  • "; } protected virtual string GetTabContentItem(TagHelperContext context, TagHelperOutput output, string content) @@ -53,11 +90,22 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab var attributes = GetTabContentAttributes(context, output); var classAttributesAsString = attributes.Where(a => a.Name == "class").ToList().Select(a => a.Name).JoinAsString(" "); - var otherAttributesAsString = attributes.Where(a => a.Name != "class").ToList().ToHtmlAttributesAsString(); + var otherAttributes = attributes.Where(a => a.Name != "class").ToList(); + + var wrapper = new TagBuilder("div"); + wrapper.AddCssClass("tab-pane fade " + classAttributesAsString + " " + AbpTabItemShowActivePlaceholder); + wrapper.Attributes.Add("id", id); + wrapper.Attributes.Add("role", "tabpanel"); + wrapper.Attributes.Add("aria-labelledby", headerId); + + foreach (var attr in otherAttributes) + { + wrapper.Attributes.Add(attr.Name, attr.Value.ToString()); + } + + wrapper.InnerHtml.AppendHtml(content); - return "
    " + - content + - "
    "; + return wrapper.ToHtmlString(); } protected virtual void SetPlaceholderForNameIfNotProvided() @@ -83,4 +131,4 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab .Select(a=> new TagHelperAttribute(a.Name.Substring(prefix.Length), a.Value)).ToList(); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs index a28e0c7057..92e50990a4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs @@ -1,9 +1,11 @@ -using System; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid; @@ -11,6 +13,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab { public class AbpTabsTagHelperService : AbpTagHelperService { + protected IHtmlGenerator HtmlGenerator { get; } + + public AbpTabsTagHelperService(IHtmlGenerator htmlGenerator) + { + HtmlGenerator = htmlGenerator; + } + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { SetRandomNameIfNotProvided(); @@ -35,7 +44,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab } } - + protected virtual string CombineHeadersAndContents(TagHelperContext context, TagHelperOutput output, string headers, string contents) { var combined = new StringBuilder(); @@ -61,31 +70,34 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab var navClass = TagHelper.TabStyle == TabStyle.Tab ? " nav-tabs" : " nav-pills"; var verticalClass = GetVerticalPillClassIfVertical(); - var surroundedHeaders = "
      " + Environment.NewLine + - headers + - "
    "; + var listElement = new TagBuilder("ul"); + listElement.AddCssClass("nav" + verticalClass + navClass); + listElement.Attributes.Add("id", id); + listElement.Attributes.Add("role", "tablist"); + listElement.InnerHtml.AppendHtml(headers); - return surroundedHeaders; + return listElement.ToHtmlString(); } protected virtual string SurroundContents(TagHelperContext context, TagHelperOutput output, string contents) { var id = TagHelper.Name + "Content"; - var surroundedContents = "
    " + Environment.NewLine + - contents + - "
    "; + var wrapper = new TagBuilder("div"); + wrapper.AddCssClass("tab-content"); + wrapper.Attributes.Add("id", id); + wrapper.InnerHtml.AppendHtml(contents); - return surroundedContents; + return wrapper.ToHtmlString(); } protected virtual string PlaceInsideColumn(string contents, int columnSize) { - var surroundedContents = "
    " + Environment.NewLine + - contents + - "
    "; + var wrapper = new TagBuilder("div"); + wrapper.AddCssClass("col-md-" + columnSize); + wrapper.InnerHtml.AppendHtml(contents); - return surroundedContents; + return wrapper.ToHtmlString(); } protected virtual void PlaceInsideRow(TagHelperOutput output) @@ -213,7 +225,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab protected virtual string SetTabItemNameIfNotProvided(string content, int index) { - return content.Replace(TabItemNamePlaceHolder, TagHelper.Name + "_" + index); + return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name) + "_" + index); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/TagBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/TagBuilderExtensions.cs new file mode 100644 index 0000000000..cab43b963b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/TagBuilderExtensions.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers +{ + internal static class TagBuilderExtensions + { + public static string ToHtmlString(this TagBuilder tagBuilder) + { + return tagBuilder.ToHtmlString(HtmlEncoder.Default); + } + + public static string ToHtmlString(this TagBuilder tagBuilder, HtmlEncoder htmlEncoder) + { + using (var writer = new StringWriter()) + { + tagBuilder.WriteTo(writer, htmlEncoder); + return writer.ToString(); + } + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tooltip/AbpTooltipTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tooltip/AbpTooltipTagHelperService.cs index f95a0097cd..acef4a0951 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tooltip/AbpTooltipTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tooltip/AbpTooltipTagHelperService.cs @@ -1,6 +1,6 @@ -using System; -using System.Linq; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Linq; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tooltip { @@ -24,12 +24,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tooltip var directory = GetDirectory() != TooltipDirectory.Default ? GetDirectory() : TooltipDirectory.Top; output.Attributes.Add("data-placement", directory.ToString().ToLowerInvariant()); - output.PreElement.SetHtmlContent( - "" + Environment.NewLine); + var span = new TagBuilder("span"); + span.AddCssClass("d-inline-block"); + span.Attributes.Add("tabindex", "0"); + span.Attributes.Add("data-toggle", "tooltip"); + span.Attributes.Add("data-placement", directory.ToString().ToLowerInvariant()); + span.Attributes.Add("title", GetTitle()); + + output.PreElement.SetHtmlContent(span.RenderStartTag()); - output.PostElement.SetHtmlContent(Environment.NewLine + ""); + output.PostElement.SetHtmlContent(span.RenderEndTag()); output.Attributes.Add("style", "pointer-events: none;"); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj index 2fb94ac218..de4be2fb8c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.Bootstrap Volo.Abp.AspNetCore.Mvc.UI.Bootstrap @@ -12,7 +12,7 @@ true Library - + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo.Abp.AspNetCore.Mvc.UI.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo.Abp.AspNetCore.Mvc.UI.Bundling.csproj index 84ea011233..ed2da41347 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo.Abp.AspNetCore.Mvc.UI.Bundling.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo.Abp.AspNetCore.Mvc.UI.Bundling.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.Bundling Volo.Abp.AspNetCore.Mvc.UI.Bundling diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.csproj index 437ae8c45d..54f4e62b59 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json index c02e7e0e46..d1a9f1f09e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json @@ -1,4 +1,4 @@ -{ +{ "culture": "en", "texts": { "GivenTenantIsNotAvailable": "Given tenant is not available: {0}", diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/es.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/es.json new file mode 100644 index 0000000000..d6fd84dcbe --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/es.json @@ -0,0 +1,12 @@ +{ + "culture": "es", + "texts": { + "GivenTenantIsNotAvailable": "El inquilino {0} no está disponible", + "Tenant": "Inquilino", + "Switch": "cambiar", + "Name": "Nombre", + "SwitchTenant": "Cambiar inquilino", + "SwitchTenantHint": "Deje en blanco el campo nombre para cambiar el lado del host.", + "NotSelected": "No seleccionado" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/fr.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/fr.json new file mode 100644 index 0000000000..dbd17957fc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/fr.json @@ -0,0 +1,12 @@ +{ + "culture": "fr", + "texts": { + "GivenTenantIsNotAvailable": "Le Tenant donné n'est pas disponible: {0}", + "Tenant": "Tenant", + "Switch": "Changer", + "Name": "Nom", + "SwitchTenantHint": "Laissez le champ de nom vide pour passer du côté hôte.", + "SwitchTenant": "Changer le Tenant", + "NotSelected": "Non séléctionné" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/hu.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/hu.json new file mode 100644 index 0000000000..057e9d2968 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/hu.json @@ -0,0 +1,12 @@ +{ + "culture": "hu", + "texts": { + "GivenTenantIsNotAvailable": "A megadott előfizető nem érhető el: {0}", + "Tenant": "Előfizető", + "Switch": "váltás", + "Name": "Neve", + "SwitchTenantHint": "Hagyja üresen az előfizető nevet hogy a host oldalra kapcsoljon.", + "SwitchTenant": "Előfizető váltás", + "NotSelected": "Nincs kiválasztva" + } + } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj index 6cb0a02153..2c3701953e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.Packages Volo.Abp.AspNetCore.Mvc.UI.Packages diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml index 491594d966..b57f6da4bb 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml @@ -1,3 +1,3 @@ -@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components +@using Volo.Abp.Ui.Branding @inject IBrandingProvider BrandingProvider -@BrandingProvider.AppName \ No newline at end of file +@BrandingProvider.AppName diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml index 5fcd5951cd..06b06c08dc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml @@ -1,19 +1,17 @@ @using Microsoft.Extensions.Localization @using Microsoft.Extensions.Options @using Volo.Abp.AspNetCore.MultiTenancy -@using Volo.Abp.AspNetCore.Mvc.AntiForgery @using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook @using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.MainNavbar @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.PageAlerts -@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components @using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles @using Volo.Abp.MultiTenancy @using Volo.Abp.Localization -@inject IAbpAntiForgeryManager AbpAntiForgeryManager +@using Volo.Abp.Ui.Branding @inject IBrandingProvider BrandingProvider @inject IOptions MultiTenancyOptions @inject ICurrentTenant CurrentTenant @@ -22,7 +20,6 @@ @{ Layout = null; - AbpAntiForgeryManager.SetCookie(); var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options var rtl = CultureHelper.IsRtl ? "rtl" : string.Empty; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml index 7b06538510..5514145e80 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml @@ -1,20 +1,17 @@ -@using Volo.Abp.AspNetCore.Mvc.AntiForgery -@using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook +@using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook @using Volo.Abp.AspNetCore.Mvc.UI.Layout @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.MainNavbar @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.PageAlerts -@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components @using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles @using Volo.Abp.Localization -@inject IAbpAntiForgeryManager AbpAntiForgeryManager +@using Volo.Abp.Ui.Branding @inject IBrandingProvider BrandingProvider @inject IPageLayout PageLayout @{ Layout = null; - AbpAntiForgeryManager.SetCookie(); var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options var pageTitle = ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title; //TODO: Discard to get from Title diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml index 9871bcbb25..70c7d5e1e8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml @@ -1,19 +1,16 @@ -@using Volo.Abp.AspNetCore.Mvc.AntiForgery -@using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook +@using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook @using Volo.Abp.AspNetCore.Mvc.UI.Layout @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.PageAlerts -@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components @using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetScripts @using Volo.Abp.AspNetCore.Mvc.UI.Widgets.Components.WidgetStyles @using Volo.Abp.Localization -@inject IAbpAntiForgeryManager AbpAntiForgeryManager +@using Volo.Abp.Ui.Branding @inject IBrandingProvider BrandingProvider @inject IPageLayout PageLayout @{ Layout = null; - AbpAntiForgeryManager.SetCookie(); var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options var pageTitle = ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title; //TODO: Discard to get from Title diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj index 206224f918..b421036ab0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml index 281f6c73ed..7733f51fdf 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml @@ -9,11 +9,11 @@ - - - - - + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj index 111a6a898a..3d20feea08 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo @@ -26,7 +26,7 @@
    - + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Components/DefaultBrandingProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Components/DefaultBrandingProvider.cs deleted file mode 100644 index a85c03364c..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Components/DefaultBrandingProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Volo.Abp.DependencyInjection; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components -{ - public class DefaultBrandingProvider : IBrandingProvider, ITransientDependency - { - public virtual string AppName => "MyApplication"; - - public virtual string LogoUrl => null; - - public virtual string LogoReverseUrl => null; - } -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Components/IBrandingProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Components/IBrandingProvider.cs deleted file mode 100644 index 1c20f7fc6e..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Components/IBrandingProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components -{ - public interface IBrandingProvider - { - string AppName { get; } - - /// - /// Logo on white background - /// - string LogoUrl { get; } - - /// - /// Logo on dark background - /// - string LogoReverseUrl { get; } - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs index 9682ec838e..0b05941f61 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Controllers/ErrorController.cs @@ -19,18 +19,21 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers private readonly IStringLocalizer _localizer; private readonly AbpErrorPageOptions _abpErrorPageOptions; private readonly IExceptionNotifier _exceptionNotifier; + private readonly AbpExceptionHandlingOptions _exceptionHandlingOptions; public ErrorController( IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, IHttpExceptionStatusCodeFinder httpExceptionStatusCodeFinder, IOptions abpErrorPageOptions, - IStringLocalizer localizer, - IExceptionNotifier exceptionNotifier) + IStringLocalizer localizer, + IExceptionNotifier exceptionNotifier, + IOptions exceptionHandlingOptions) { _errorInfoConverter = exceptionToErrorInfoConverter; _statusCodeFinder = httpExceptionStatusCodeFinder; _localizer = localizer; _exceptionNotifier = exceptionNotifier; + _exceptionHandlingOptions = exceptionHandlingOptions.Value; _abpErrorPageOptions = abpErrorPageOptions.Value; } @@ -44,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers await _exceptionNotifier.NotifyAsync(new ExceptionNotificationContext(exception)); - var errorInfo = _errorInfoConverter.Convert(exception); + var errorInfo = _errorInfoConverter.Convert(exception, _exceptionHandlingOptions.SendExceptionsDetailsToClients); if (httpStatusCode == 0) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj index 4b0fbc24cf..d26564d92b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 true Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js index cc4122a373..3e60d5f9c2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js @@ -1,7 +1,3 @@ -/** - * TODO: Document & prepare typescript definitions - * TODO: Refactor & test more - */ var abp = abp || {}; $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only for the form we are working on! Also this should be decided by the form itself! @@ -12,7 +8,7 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f abp.ModalManager = (function () { - var CallbackList = function () { //TODO: To a seperated file + var CallbackList = function () { var _callbacks = []; return { @@ -67,12 +63,11 @@ $.validator.defaults.ignore = ''; //TODO: Would be better if we can apply only f _$modal = _$modalContainer.find('.modal'); _$form = _$modalContainer.find('form'); if (_$form.length) { - //TODO: data-ajaxForm comparison seems wrong! - if (_$form.attr('data-ajaxForm') === undefined || _$form.attr('data-ajaxForm') === false) { + if (_$form.attr('data-ajaxForm') !== 'false') { _$form.abpAjaxForm(); } - if (_$form.attr('data-check-form-on-close') === undefined || _$form.attr('data-check-form-on-close') != 'false') { + if (_$form.attr('data-check-form-on-close') !== 'false') { _$form.needConfirmationOnUnsavedClose(_$modal); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js index 8d85f6f6d8..2bbd1aa7a3 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js @@ -15,19 +15,23 @@ return; } - var getVisibilityValue = function (visibilityField, record) { + var getVisibilityValue = function (visibilityField, record, tableInstance) { if (visibilityField === undefined) { return true; } if (abp.utils.isFunction(visibilityField)) { - return visibilityField(record); + return visibilityField(record, tableInstance); } else { return visibilityField; } }; - var _createDropdownItem = function (record, fieldItem) { + var htmlEncode = function (html) { + return $('
    ').text(html).html(); + } + + var _createDropdownItem = function (record, fieldItem, tableInstance) { var $li = $('
  • '); var $a = $(''); @@ -41,7 +45,7 @@ $a.append($("").addClass(fieldItem.iconClass + " mr-1")); } - $a.append(fieldItem.text); + $a.append(htmlEncode(fieldItem.text)); } if (fieldItem.action) { @@ -50,14 +54,14 @@ if (!$(this).closest('li').hasClass('disabled')) { if (fieldItem.confirmMessage) { - abp.message.confirm(fieldItem.confirmMessage({ record: record })) + abp.message.confirm(fieldItem.confirmMessage({ record: record, table: tableInstance })) .done(function (accepted) { if (accepted) { - fieldItem.action({ record: record }); + fieldItem.action({ record: record, table: tableInstance }); } }); } else { - fieldItem.action({ record: record }); + fieldItem.action({ record: record, table: tableInstance }); } } }); @@ -67,17 +71,60 @@ return $li; }; - var _createButtonDropdown = function (record, field) { + var _createButtonDropdown = function (record, field, tableInstance) { + if(field.items.length === 1) { + var firstItem = field.items[0]; + if (!getVisibilityValue(firstItem.visible, record, tableInstance)) { + return ""; + } + + var $button = $(''); + + if (firstItem.displayNameHtml) { + $button.html(firstItem.text); + } else { + if (firstItem.icon !== undefined && firstItem.icon) { + $button.append($("").addClass("fa fa-" + firstItem.icon + " mr-1")); + } else if (firstItem.iconClass) { + $button.append($("").addClass(firstItem.iconClass + " mr-1")); + } + $button.append(htmlEncode(firstItem.text)); + } + + if (firstItem.enabled && !firstItem.enabled({ record: record, table: tableInstance })) { + $button.addClass('disabled'); + } + + if (firstItem.action) { + $button.click(function (e) { + e.preventDefault(); + + if (!$(this).hasClass('disabled')) { + if (firstItem.confirmMessage) { + abp.message.confirm(firstItem.confirmMessage({ record: record, table: tableInstance })) + .done(function (accepted) { + if (accepted) { + firstItem.action({ record: record, table: tableInstance }); + } + }); + } else { + firstItem.action({ record: record, table: tableInstance }); + } + } + }); + } + + return $button; + } + var $container = $('
    ') .addClass('dropdown') .addClass('action-button'); var $dropdownButton = $(' + + } + else + { + + } + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/UiMessageAlert.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/UiMessageAlert.razor.cs new file mode 100644 index 0000000000..59e4aa3820 --- /dev/null +++ b/framework/src/Volo.Abp.BlazoriseUI/Components/UiMessageAlert.razor.cs @@ -0,0 +1,140 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Blazorise; +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components.WebAssembly; + +namespace Volo.Abp.BlazoriseUI.Components +{ + public partial class UiMessageAlert : ComponentBase, IDisposable + { + protected Modal ModalRef { get; set; } + + protected virtual bool IsConfirmation + => MessageType == UiMessageType.Confirmation; + + protected virtual bool CenterMessage + => Options?.CenterMessage ?? true; + + protected virtual bool ShowMessageIcon + => Options?.ShowMessageIcon ?? true; + + protected virtual object MessageIcon => Options?.MessageIcon ?? MessageType switch + { + UiMessageType.Info => IconName.Info, + UiMessageType.Success => IconName.Check, + UiMessageType.Warning => IconName.Exclamation, + UiMessageType.Error => IconName.Times, + UiMessageType.Confirmation => IconName.QuestionCircle, + _ => null, + }; + + protected virtual string MessageIconColor => MessageType switch + { + // gets the color in the order of importance: Blazorise > Bootstrap > fallback color + UiMessageType.Info => "var(--b-theme-info, var(--info, #17a2b8))", + UiMessageType.Success => "var(--b-theme-success, var(--success, #28a745))", + UiMessageType.Warning => "var(--b-theme-warning, var(--warning, #ffc107))", + UiMessageType.Error => "var(--b-theme-danger, var(--danger, #dc3545))", + UiMessageType.Confirmation => "var(--b-theme-secondary, var(--secondary, #6c757d))", + _ => null, + }; + + protected virtual string MessageIconStyle + { + get + { + var sb = new StringBuilder(); + + sb.Append($"color:{MessageIconColor}"); + + return sb.ToString(); + } + } + + protected virtual string OkButtonText + => Options?.OkButtonText ?? "OK"; + + protected virtual string ConfirmButtonText + => Options?.ConfirmButtonText ?? "Confirm"; + + protected virtual string CancelButtonText + => Options?.CancelButtonText ?? "Cancel"; + + [Parameter] public UiMessageType MessageType { get; set; } + + [Parameter] public string Title { get; set; } + + [Parameter] public string Message { get; set; } + + [Parameter] public TaskCompletionSource Callback { get; set; } + + [Parameter] public UiMessageOptions Options { get; set; } + + [Parameter] public EventCallback Okayed { get; set; } + + [Parameter] public EventCallback Confirmed { get; set; } + + [Parameter] public EventCallback Canceled { get; set; } + + [Inject] protected BlazoriseUiMessageService UiMessageService { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + + UiMessageService.MessageReceived += OnMessageReceived; + } + + private void OnMessageReceived(object sender, UiMessageEventArgs e) + { + MessageType = e.MessageType; + Message = e.Message; + Title = e.Title; + Options = e.Options; + Callback = e.Callback; + + ModalRef.Show(); + } + + public void Dispose() + { + if (UiMessageService != null) + { + UiMessageService.MessageReceived -= OnMessageReceived; + } + } + + protected Task OnOkClicked() + { + ModalRef.Hide(); + + return Okayed.InvokeAsync(null); + } + + protected Task OnConfirmClicked() + { + ModalRef.Hide(); + + if (IsConfirmation && Callback != null) + { + Callback.SetResult(true); + } + + return Confirmed.InvokeAsync(null); + } + + protected Task OnCancelClicked() + { + ModalRef.Hide(); + + if (IsConfirmation && Callback != null) + { + Callback.SetResult(false); + } + + return Canceled.InvokeAsync(null); + } + } +} diff --git a/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj b/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj index f7875a0f4a..e696a2f915 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj +++ b/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj @@ -1,15 +1,10 @@ - + - netstandard2.1 - $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; - false - false - false - + net5.0 @@ -17,8 +12,8 @@ - - + + diff --git a/framework/src/Volo.Abp.BlazoriseUI/Volo/Abp/BlazoriseUI/BlazoriseCrudPageBase.cs b/framework/src/Volo.Abp.BlazoriseUI/Volo/Abp/BlazoriseUI/BlazoriseCrudPageBase.cs deleted file mode 100644 index 5889737745..0000000000 --- a/framework/src/Volo.Abp.BlazoriseUI/Volo/Abp/BlazoriseUI/BlazoriseCrudPageBase.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Blazorise; -using Blazorise.DataGrid; -using Localization.Resources.AbpUi; -using Microsoft.AspNetCore.Components; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.AspNetCore.Components.WebAssembly; -using Volo.Abp.ObjectMapping; - -namespace Volo.Abp.BlazoriseUI -{ - public abstract class BlazoriseCrudPageBase - : BlazoriseCrudPageBase - where TAppService : ICrudAppService - where TEntityDto : IEntityDto, new() - { - - } - - public abstract class BlazoriseCrudPageBase - : BlazoriseCrudPageBase - where TAppService : ICrudAppService - where TEntityDto : IEntityDto, new() - where TGetListInput : new() - { - - } - - public abstract class BlazoriseCrudPageBase - : BlazoriseCrudPageBase - where TAppService : ICrudAppService - where TEntityDto : IEntityDto - where TCreateInput : new() - where TGetListInput : new() - { - - } - - public abstract class BlazoriseCrudPageBase - : BlazoriseCrudPageBase - where TAppService : ICrudAppService - where TEntityDto : IEntityDto - where TCreateInput : new() - where TUpdateInput : new() - where TGetListInput : new() - { - - } - - public abstract class BlazoriseCrudPageBase - : OwningComponentBase - where TAppService : ICrudAppService - where TGetOutputDto : IEntityDto - where TGetListOutputDto : IEntityDto - where TCreateInput : new() - where TUpdateInput : new() - where TGetListInput : new() - { - [Inject] protected TAppService AppService { get; set; } - [Inject] protected IUiMessageService UiMessageService { get; set; } - [Inject] protected IStringLocalizer UiLocalizer { get; set; } - - protected virtual int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; - - protected int CurrentPage; - protected string CurrentSorting; - protected int? TotalCount; - protected IReadOnlyList Entities = Array.Empty(); - protected TCreateInput NewEntity; - protected TKey EditingEntityId; - protected TUpdateInput EditingEntity; - protected Modal CreateModal; - protected Modal EditModal; - - protected Type ObjectMapperContext { get; set; } - - protected IObjectMapper ObjectMapper - { - get - { - if (_objectMapper != null) - { - return _objectMapper; - } - - if (ObjectMapperContext == null) - { - return LazyGetRequiredService(ref _objectMapper); - } - - return LazyGetRequiredService( - typeof(IObjectMapper<>).MakeGenericType(ObjectMapperContext), - ref _objectMapper - ); - } - } - - private IObjectMapper _objectMapper; - - protected TService LazyGetRequiredService(ref TService reference) - => LazyGetRequiredService(typeof(TService), ref reference); - - protected TRef LazyGetRequiredService(Type serviceType, ref TRef reference) - { - if (reference == null) - { - reference = (TRef) ScopedServices.GetRequiredService(serviceType); - } - - return reference; - } - - protected BlazoriseCrudPageBase() - { - NewEntity = new TCreateInput(); - EditingEntity = new TUpdateInput(); - } - - protected override async Task OnInitializedAsync() - { - await GetEntitiesAsync(); - } - - protected virtual async Task GetEntitiesAsync() - { - var input = await CreateGetListInputAsync(); - var result = await AppService.GetListAsync(input); - Entities = result.Items; - TotalCount = (int?) result.TotalCount; - } - - protected virtual Task CreateGetListInputAsync() - { - var input = new TGetListInput(); - - if (input is ISortedResultRequest sortedResultRequestInput) - { - sortedResultRequestInput.Sorting = CurrentSorting; - } - - if (input is IPagedResultRequest pagedResultRequestInput) - { - pagedResultRequestInput.SkipCount = CurrentPage * PageSize; - } - - if (input is ILimitedResultRequest limitedResultRequestInput) - { - limitedResultRequestInput.MaxResultCount = PageSize; - } - - return Task.FromResult(input); - } - - protected virtual async Task OnDataGridReadAsync(DataGridReadDataEventArgs e) - { - CurrentSorting = e.Columns - .Where(c => c.Direction != SortDirection.None) - .Select(c => c.Field + (c.Direction == SortDirection.Descending ? " DESC" : "")) - .JoinAsString(","); - CurrentPage = e.Page - 1; - - await GetEntitiesAsync(); - - StateHasChanged(); - } - - protected virtual Task OpenCreateModalAsync() - { - NewEntity = new TCreateInput(); - CreateModal.Show(); - return Task.CompletedTask; - } - - protected virtual Task CloseCreateModalAsync() - { - CreateModal.Hide(); - return Task.CompletedTask; - } - - protected virtual async Task OpenEditModalAsync(TKey id) - { - var entityDto = await AppService.GetAsync(id); - EditingEntityId = id; - EditingEntity = MapToEditingEntity(entityDto); - EditModal.Show(); - } - - protected virtual TUpdateInput MapToEditingEntity(TGetOutputDto entityDto) - { - return ObjectMapper.Map(entityDto); - } - - protected virtual Task CloseEditModalAsync() - { - EditModal.Hide(); - return Task.CompletedTask; - } - - protected virtual async Task CreateEntityAsync() - { - await AppService.CreateAsync(NewEntity); - await GetEntitiesAsync(); - CreateModal.Hide(); - } - - protected virtual async Task UpdateEntityAsync() - { - await AppService.UpdateAsync(EditingEntityId, EditingEntity); - await GetEntitiesAsync(); - EditModal.Hide(); - } - - protected virtual async Task DeleteEntityAsync(TGetListOutputDto entity) - { - if (!await UiMessageService.ConfirmAsync(GetDeleteConfirmationMessage(entity))) - { - return; - } - - await AppService.DeleteAsync(entity.Id); - await GetEntitiesAsync(); - } - - protected virtual string GetDeleteConfirmationMessage(TGetListOutputDto entity) - { - return UiLocalizer["ItemWillBeDeletedMessage"]; - } - } -} diff --git a/framework/src/Volo.Abp.BlazoriseUI/_Imports.razor b/framework/src/Volo.Abp.BlazoriseUI/_Imports.razor new file mode 100644 index 0000000000..37be71828c --- /dev/null +++ b/framework/src/Volo.Abp.BlazoriseUI/_Imports.razor @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Components.Web +@using Blazorise \ No newline at end of file diff --git a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo.Abp.BlobStoring.Aliyun.csproj b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo.Abp.BlobStoring.Aliyun.csproj index c9b29dc83f..f3f8249f50 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo.Abp.BlobStoring.Aliyun.csproj +++ b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo.Abp.BlobStoring.Aliyun.csproj @@ -1,4 +1,4 @@ - + diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo.Abp.BlobStoring.Aws.csproj b/framework/src/Volo.Abp.BlobStoring.Aws/Volo.Abp.BlobStoring.Aws.csproj index d30601d1e8..796da8aa26 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo.Abp.BlobStoring.Aws.csproj +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo.Abp.BlobStoring.Aws.csproj @@ -2,7 +2,7 @@ - + netstandard2.0 false diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs index 739818c0d9..fff851d716 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs @@ -54,7 +54,7 @@ namespace Volo.Abp.BlobStoring.Aws Check.NotNullOrWhiteSpace(configuration.AccessKeyId, nameof(configuration.AccessKeyId)); Check.NotNullOrWhiteSpace(configuration.SecretAccessKey, nameof(configuration.SecretAccessKey)); - return new AmazonS3Client(configuration.AccessKeyId, configuration.SecretAccessKey, configuration.Region); + return new AmazonS3Client(configuration.AccessKeyId, configuration.SecretAccessKey, region); } protected virtual AWSCredentials GetAwsCredentials( @@ -190,4 +190,4 @@ namespace Volo.Abp.BlobStoring.Aws return temporaryCredentialsCache; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.BlobStoring.Minio/Volo.Abp.BlobStoring.Minio.csproj b/framework/src/Volo.Abp.BlobStoring.Minio/Volo.Abp.BlobStoring.Minio.csproj index 1c080067ed..822189c677 100644 --- a/framework/src/Volo.Abp.BlobStoring.Minio/Volo.Abp.BlobStoring.Minio.csproj +++ b/framework/src/Volo.Abp.BlobStoring.Minio/Volo.Abp.BlobStoring.Minio.csproj @@ -1,7 +1,7 @@ - + - + netstandard2.0 Volo.Abp.BlobStoring.Minio diff --git a/framework/src/Volo.Abp.BlobStoring.Minio/Volo/Abp/BlobStoring/Minio/MinioBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring.Minio/Volo/Abp/BlobStoring/Minio/MinioBlobProvider.cs index 204b1a6a1a..1a3f14e97b 100644 --- a/framework/src/Volo.Abp.BlobStoring.Minio/Volo/Abp/BlobStoring/Minio/MinioBlobProvider.cs +++ b/framework/src/Volo.Abp.BlobStoring.Minio/Volo/Abp/BlobStoring/Minio/MinioBlobProvider.cs @@ -72,11 +72,11 @@ namespace Volo.Abp.BlobStoring.Minio } var memoryStream = new MemoryStream(); - await client.GetObjectAsync(containerName, blobName, async stream => + await client.GetObjectAsync(containerName, blobName, (stream) => { if (stream != null) { - await stream.CopyToAsync(memoryStream); + stream.CopyTo(memoryStream); } else { diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj index 95a15ddc8e..48304420e6 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs index b0f7d68ee4..0f71cbdd25 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs @@ -14,17 +14,21 @@ namespace Volo.Abp.Caching.StackExchangeRedis public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); - - context.Services.AddStackExchangeRedisCache(options => + + var redisEnabled = configuration["Redis:IsEnabled"]; + if (redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled)) { - var redisConfiguration = configuration["Redis:Configuration"]; - if (!redisConfiguration.IsNullOrEmpty()) + context.Services.AddStackExchangeRedisCache(options => { - options.Configuration = configuration["Redis:Configuration"]; - } - }); + var redisConfiguration = configuration["Redis:Configuration"]; + if (!redisConfiguration.IsNullOrEmpty()) + { + options.Configuration = redisConfiguration; + } + }); - context.Services.Replace(ServiceDescriptor.Singleton()); + context.Services.Replace(ServiceDescriptor.Singleton()); + } } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj index 19d5b456ec..757d95d2bc 100644 --- a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj +++ b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index b26feec136..ccc1529f2b 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Nito.AsyncEx; -using Volo.Abp.DependencyInjection; using Volo.Abp.ExceptionHandling; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; @@ -31,7 +30,7 @@ namespace Volo.Abp.Caching ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, IDistributedCacheKeyNormalizer keyNormalizer, - IHybridServiceScopeFactory serviceScopeFactory, + IServiceScopeFactory serviceScopeFactory, IUnitOfWorkManager unitOfWorkManager) : base( distributedCacheOption: distributedCacheOption, cache: cache, @@ -69,7 +68,7 @@ namespace Volo.Abp.Caching protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } - protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } protected IUnitOfWorkManager UnitOfWorkManager { get; } @@ -85,7 +84,7 @@ namespace Volo.Abp.Caching ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, IDistributedCacheKeyNormalizer keyNormalizer, - IHybridServiceScopeFactory serviceScopeFactory, + IServiceScopeFactory serviceScopeFactory, IUnitOfWorkManager unitOfWorkManager) { _distributedCacheOption = distributedCacheOption.Value; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj index 40eecfd06a..13f81e730b 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj +++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj @@ -14,7 +14,6 @@ - @@ -22,6 +21,7 @@ + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs index a79b52b4f2..29238a3e35 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs @@ -36,6 +36,7 @@ namespace Volo.Abp.Cli options.Commands["switch-to-stable"] = typeof(SwitchToStableCommand); options.Commands["switch-to-nightly"] = typeof(SwitchToNightlyCommand); options.Commands["translate"] = typeof(TranslateCommand); + options.Commands["build"] = typeof(BuildCommand); }); } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs new file mode 100644 index 0000000000..6df892b472 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Build +{ + public class DefaultBuildProjectListSorter : IBuildProjectListSorter, ITransientDependency + { + public List SortByDependencies( + List source, + IEqualityComparer comparer = null) + { + /* See: http://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp + * http://en.wikipedia.org/wiki/Topological_sorting + */ + + var sorted = new List(); + var visited = new Dictionary(comparer); + + foreach (var item in source) + { + SortByDependenciesVisit(source, item, sorted, visited); + } + + return sorted; + } + + private void SortByDependenciesVisit( + List source, + DotNetProjectInfo item, + List sorted, + Dictionary visited) + { + bool inProcess; + var alreadyVisited = visited.TryGetValue(item, out inProcess); + + if (alreadyVisited) + { + if (inProcess) + { + throw new ArgumentException("Cyclic dependency found! Item: " + item); + } + } + else + { + visited[item] = true; + + var dependencies = item.Dependencies; + if (dependencies != null) + { + foreach (var dependency in dependencies) + { + var dependencyItem = source.FirstOrDefault(e => e.CsProjPath == dependency.CsProjPath); + if (dependencyItem != null) + { + SortByDependenciesVisit(source, dependencyItem, sorted, visited); + } + } + } + + visited[item] = false; + sorted.Add(item); + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs new file mode 100644 index 0000000000..e57c6837c0 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LibGit2Sharp; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Build +{ + public class DefaultBuildStatusGenerator : IBuildStatusGenerator, ITransientDependency + { + private readonly IGitRepositoryHelper _gitRepositoryHelper; + + public DefaultBuildStatusGenerator(IGitRepositoryHelper gitRepositoryHelper) + { + _gitRepositoryHelper = gitRepositoryHelper; + } + + public GitRepositoryBuildStatus Generate( + DotNetProjectBuildConfig buildConfig, + List changedProjects, + List buildSucceededProjects) + { + var lastCommitId = _gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository); + var repoFriendlyName = _gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository); + + var status = new GitRepositoryBuildStatus( + buildConfig.GitRepository.Name, + repoFriendlyName + ); + + if (ShouldUpdateRepositoryCommitId(buildConfig, changedProjects, buildSucceededProjects)) + { + status.CommitId = lastCommitId; + } + + status.SucceedProjects = changedProjects.Where(p => + p.RepositoryName == buildConfig.GitRepository.Name && + buildSucceededProjects.Contains(p.CsProjPath) + ) + .Select(e => new DotNetProjectBuildStatus + { + CsProjPath = e.CsProjPath, + CommitId = lastCommitId + }).ToList(); + + foreach (var dependingRepository in buildConfig.GitRepository.DependingRepositories) + { + GenerateBuildStatusInternal( + buildConfig, + dependingRepository, + changedProjects, + buildSucceededProjects, + status + ); + } + + return status; + } + + private bool ShouldUpdateRepositoryCommitId( + DotNetProjectBuildConfig buildConfig, + List changedProjects, + List buildSucceededProjects) + { + if (!buildConfig.SlFilePath.IsNullOrEmpty()) + { + return false; + } + + if (changedProjects.Count == 0 || buildSucceededProjects.Count == 0) + { + return false; + } + + return changedProjects.Count == buildSucceededProjects.Count; + } + + private void GenerateBuildStatusInternal( + DotNetProjectBuildConfig buildConfig, + GitRepository gitRepository, + List changedProjects, + List buildSucceededProjects, + GitRepositoryBuildStatus status) + { + var lastCommitId = _gitRepositoryHelper.GetLastCommitId(gitRepository); + var repoFriendlyName = _gitRepositoryHelper.GetFriendlyName(gitRepository); + + var dependingRepositoryStatus = new GitRepositoryBuildStatus( + gitRepository.Name, + repoFriendlyName + ); + + if (ShouldUpdateRepositoryCommitId(buildConfig, changedProjects, buildSucceededProjects)) + { + dependingRepositoryStatus.CommitId = lastCommitId; + } + + dependingRepositoryStatus.SucceedProjects = changedProjects.Where(p => + p.RepositoryName == gitRepository.Name && + buildSucceededProjects.Contains(p.CsProjPath) + ) + .Select(e => new DotNetProjectBuildStatus() + { + CsProjPath = e.CsProjPath, + CommitId = lastCommitId + }).ToList(); + + foreach (var dependingRepository in gitRepository.DependingRepositories) + { + GenerateBuildStatusInternal( + buildConfig, + dependingRepository, + changedProjects, + buildSucceededProjects, + dependingRepositoryStatus + ); + } + + status.DependingRepositories.Add(dependingRepositoryStatus); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs new file mode 100644 index 0000000000..e866c425b5 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using LibGit2Sharp; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Build +{ + public class DefaultChangedProjectFinder : IChangedProjectFinder, ITransientDependency + { + private readonly IRepositoryBuildStatusStore _repositoryBuildStatusStore; + private readonly IGitRepositoryHelper _gitRepositoryHelper; + + private readonly IDotNetProjectDependencyFiller _dotNetProjectDependencyFiller; + private readonly IBuildProjectListSorter _buildProjectListSorter; + + private readonly List _changeDetectionFileExtensions = new List + { + ".cs", + ".csproj", + ".cshtml" + }; + + public DefaultChangedProjectFinder( + IRepositoryBuildStatusStore repositoryBuildStatusStore, + IGitRepositoryHelper gitRepositoryHelper, + IDotNetProjectDependencyFiller dotNetProjectDependencyFiller, + IBuildProjectListSorter buildProjectListSorter) + { + _repositoryBuildStatusStore = repositoryBuildStatusStore; + _gitRepositoryHelper = gitRepositoryHelper; + _dotNetProjectDependencyFiller = dotNetProjectDependencyFiller; + _buildProjectListSorter = buildProjectListSorter; + } + + public List Find(DotNetProjectBuildConfig buildConfig) + { + if (!buildConfig.SlFilePath.IsNullOrEmpty()) + { + return FindBySlnFile(buildConfig); + } + + return FindByRepository(buildConfig); + } + + /// + /// Returns list of projects in a repository and its depending repositories sorted by dependencies + /// + /// + /// + private List FindAllProjects(DotNetProjectBuildConfig buildConfig) + { + var projects = new List(); + + AddProjectsOfRepository(buildConfig.GitRepository, projects); + + _dotNetProjectDependencyFiller.Fill(projects); + + var allSortedProjectList = _buildProjectListSorter.SortByDependencies( + projects, + new DotNetProjectInfoEqualityComparer() + ); + + FilterIgnoredDirectories(allSortedProjectList, buildConfig.GitRepository); + + return allSortedProjectList; + } + + private void FilterIgnoredDirectories(List projects, GitRepository gitRepository) + { + foreach (var ignoredDirectory in gitRepository.IgnoredDirectories) + { + projects = projects.Where(e => + !e.CsProjPath.StartsWith(Path.Combine(gitRepository.RootPath, ignoredDirectory))) + .ToList(); + } + + foreach (var dependingRepository in gitRepository.DependingRepositories) + { + FilterIgnoredDirectories(projects, dependingRepository); + } + } + + private void AddProjectsOfRepository(GitRepository gitRepository, List projects) + { + var allCsProjFiles = Directory.GetFiles( + gitRepository.RootPath, + "*.csproj", + SearchOption.AllDirectories + ).ToList(); + + projects.AddRange( + allCsProjFiles.Select(csProjPath => new DotNetProjectInfo(gitRepository.Name, csProjPath, false)) + ); + + foreach (var dependingRepository in gitRepository.DependingRepositories) + { + AddProjectsOfRepository(dependingRepository, projects); + } + } + + private List FindByRepository(DotNetProjectBuildConfig buildConfig) + { + var gitRepositoryBuildStatus = _repositoryBuildStatusStore.Get( + buildConfig.BuildName, + buildConfig.GitRepository + ); + + var allSortedProjectList = FindAllProjects(buildConfig); + + MarkProjectsForBuild( + buildConfig.GitRepository, + gitRepositoryBuildStatus, + buildConfig.ForceBuild, + allSortedProjectList + ); + + return allSortedProjectList.Where(e => e.ShouldBuild).ToList(); + } + + private void MarkProjectsForBuild( + GitRepository repository, + GitRepositoryBuildStatus repositoryBuildStatus, + bool forceBuild, + List allProjectList) + { + if (forceBuild || repositoryBuildStatus == null || repositoryBuildStatus.CommitId.IsNullOrEmpty()) + { + // Mark all projects for build + allProjectList.ForEach(e => e.ShouldBuild = true); + } + else + { + MarkChangedProjectsForBuild( + repository, + repositoryBuildStatus, + allProjectList + ); + } + + if (!repository.DependingRepositories.Any()) + { + return; + } + + foreach (var dependingRepository in repository.DependingRepositories) + { + var dependingRepositoryBuildStatus = repositoryBuildStatus?.GetChild(dependingRepository.Name); + MarkProjectsForBuild( + dependingRepository, + dependingRepositoryBuildStatus, + forceBuild, + allProjectList + ); + } + } + + private void MarkChangedProjectsForBuild( + GitRepository repository, + GitRepositoryBuildStatus status, + List allProjectList) + { + using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git"))) + { + var firstCommit = status.CommitId.IsNullOrEmpty() + ? null + : repo.Lookup(status.CommitId); + + var repoDifferences = repo.Diff.Compare(firstCommit?.Tree, repo.Head.Tip.Tree); + + var fileExtensionPredicate = PredicateBuilder.New(true); + + foreach (var changeDetectionFileExtension in _changeDetectionFileExtensions) + { + fileExtensionPredicate = fileExtensionPredicate.Or( + e => e.Path.EndsWith(changeDetectionFileExtension) + ); + } + + var files = repoDifferences + .Where(fileExtensionPredicate) + .Where(e => e.Status != ChangeKind.Deleted) + .Select(e => e) + .ToList(); + + var affectedCsProjFiles = FindAffectedCsProjFiles(repository.RootPath, files); + var lastCommitId = _gitRepositoryHelper.GetLastCommitId(repository); + + foreach (var file in affectedCsProjFiles) + { + var csProjPath = Path.Combine(repository.RootPath, file); + if (status.SucceedProjects.Any(p => p.CsProjPath == csProjPath && p.CommitId == lastCommitId)) + { + continue; + } + + // Filter ignored directories + var isIgnored = repository.IgnoredDirectories.Any(ignoredDirectory => + csProjPath.StartsWith(Path.Combine(repository.RootPath, ignoredDirectory))); + if (isIgnored) + { + continue; + } + + allProjectList.MarkForBuild(repository.Name, csProjPath); + AddDependingProjectsToList(repository.Name, csProjPath, allProjectList); + } + } + } + + private List FindBySlnFile(DotNetProjectBuildConfig buildConfig) + { + var allProjectList = FindAllProjects(buildConfig); + + var slFine = new FileInfo(buildConfig.SlFilePath); + var csProjFiles = slFine.Directory.GetFiles( + "*.csproj", + SearchOption.AllDirectories + ).Select(e => e.FullName) + .ToList(); + + foreach (var csProjFile in csProjFiles) + { + MarkDependantProjectsForBuild(buildConfig.GitRepository, csProjFile, allProjectList); + } + + return _buildProjectListSorter.SortByDependencies( + allProjectList, + new DotNetProjectInfoEqualityComparer() + ).Where(e => e.ShouldBuild).ToList(); + } + + private void MarkDependantProjectsForBuild( + GitRepository gitRepository, + string csProjFilePath, + List allProjectList) + { + var repositoryName = gitRepository.FindRepositoryOf(csProjFilePath); + var project = new DotNetProjectInfo(repositoryName, csProjFilePath, true); + + if (allProjectList.IsMarkedForBuild(repositoryName, csProjFilePath)) + { + return; + } + + allProjectList.MarkForBuild(project); + AddProjectDependencies(gitRepository, project, allProjectList); + } + + private void AddProjectDependencies( + GitRepository gitRepository, + DotNetProjectInfo project, + List allProjectList) + { + var projectInfo = allProjectList.FirstOrDefault(e => e.CsProjPath == project.CsProjPath); + if (projectInfo == null) + { + return; + } + + var dependencies = projectInfo.Dependencies; + + foreach (var dependency in dependencies) + { + if (allProjectList.IsMarkedForBuild(dependency.RepositoryName, dependency.CsProjPath)) + { + continue; + } + + allProjectList.MarkForBuild(dependency.RepositoryName, dependency.CsProjPath); + + AddProjectDependencies(gitRepository, dependency, allProjectList); + } + } + + private void AddDependingProjectsToList( + string repositoryName, + string csProjPath, + List allProjectList) + { + var dependingProjects = allProjectList.Where( + e => e.Dependencies.Any(d => d.RepositoryName == repositoryName && d.CsProjPath == csProjPath) + ).Select(e => new DotNetProjectInfo(e.RepositoryName, e.CsProjPath, true)).ToList(); + + if (!dependingProjects.Any()) + { + return; + } + + foreach (var dependingProject in dependingProjects) + { + if (allProjectList.IsMarkedForBuild(dependingProject)) + { + continue; + } + + allProjectList.MarkForBuild(dependingProject); + AddDependingProjectsToList(dependingProject.RepositoryName, dependingProject.CsProjPath, + allProjectList); + } + } + + private List FindAffectedCsProjFiles(string repositoryPath, List files) + { + var affectedProjectFiles = new List(); + foreach (var file in files) + { + var filePath = Path.Combine(repositoryPath, file.Path); + if (filePath.EndsWith(".csproj")) + { + affectedProjectFiles.Add(filePath); + } + + if (!filePath.EndsWith(".cs")) + { + continue; + } + + var classFile = new FileInfo(filePath); + var csProjPath = FindBelongingProjectPathOfClass(classFile.Directory?.FullName); + if (csProjPath.IsNullOrEmpty() || affectedProjectFiles.Contains(csProjPath)) + { + continue; + } + + affectedProjectFiles.Add(csProjPath); + } + + return affectedProjectFiles; + } + + private string FindBelongingProjectPathOfClass(string directoryPath) + { + var files = Directory.GetFiles(directoryPath, "*.csproj", SearchOption.TopDirectoryOnly); + if (files.Length == 1) + { + return files.First(); + } + + var directoryInfo = new DirectoryInfo(directoryPath); + + if (directoryInfo.Parent == null) + { + return null; + } + + return FindBelongingProjectPathOfClass(directoryInfo.Parent.FullName); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs new file mode 100644 index 0000000000..65749fd45c --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.Cli.Utils; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Build +{ + public class DefaultDotNetProjectBuilder : IDotNetProjectBuilder, ITransientDependency + { + public List Build(List projects, string arguments) + { + var builtProjects = new ConcurrentBag(); + var totalProjectCountToBuild = projects.Count; + var buildingProjectIndex = 0; + + try + { + foreach (var project in projects) + { + if (builtProjects.Contains(project.CsProjPath)) + { + continue; + } + + buildingProjectIndex++; + + Console.WriteLine( + "Building....: " + " (" + buildingProjectIndex + "/" + + totalProjectCountToBuild + ")" + project.CsProjPath + ); + + BuildInternal(project, arguments, builtProjects); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + + return builtProjects.ToList(); + } + + private void BuildInternal(DotNetProjectInfo project, string arguments, ConcurrentBag builtProjects) + { + var buildArguments = arguments.TrimStart('"').TrimEnd('"'); + Console.WriteLine("Executing...: dotnet build " + project.CsProjPath + " " + buildArguments); + + var output = CmdHelper.RunCmdAndGetOutput( + "dotnet build " + project.CsProjPath + " " + buildArguments, + out int buildStatus + ); + + if (buildStatus == 0) + { + builtProjects.Add(project.CsProjPath); + WriteOutput(output, ConsoleColor.Green); + } + else + { + WriteOutput(output, ConsoleColor.Red); + Console.WriteLine("Build failed for :" + project.CsProjPath); + throw new Exception("Build failed!"); + } + } + + private void WriteOutput(string text, ConsoleColor color) + { + var currentConsoleColor = Console.ForegroundColor; + Console.ForegroundColor = color; + Console.WriteLine(text); + Console.ForegroundColor = currentConsoleColor; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs new file mode 100644 index 0000000000..9a9da0766a --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.Cli.Build +{ + public class DotNetProjectBuildConfig + { + public string BuildName { get; set; } + + public string SlFilePath { get; set; } + + public GitRepository GitRepository { get; set; } + + public bool ForceBuild { get; set; } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs new file mode 100644 index 0000000000..10092777c2 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Cli.Build +{ + public class DotNetProjectBuildStatus + { + public string CsProjPath { get; set; } + + public string CommitId { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs new file mode 100644 index 0000000000..b9c883a45d --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Build +{ + public class DotNetProjectDependencyFiller : IDotNetProjectDependencyFiller, ITransientDependency + { + public void Fill(List projects) + { + foreach (var project in projects) + { + FillProjectDependencies(project); + } + } + + private void FillProjectDependencies(DotNetProjectInfo project) + { + var projectNode = XElement.Load(project.CsProjPath); + var referenceNodes = projectNode.Descendants("ItemGroup").Descendants("ProjectReference"); + + foreach (var referenceNode in referenceNodes) + { + if (referenceNode.Attribute("Include") == null) + { + continue; + } + + var relativePath = referenceNode.Attribute("Include").Value; + var file = new FileInfo(project.CsProjPath); + var referenceProjectInfo = new FileInfo(Path.Combine(file.Directory.FullName, relativePath)); + + var referenceProject = new DotNetProjectInfo(project.RepositoryName, referenceProjectInfo.FullName, false); + project.Dependencies.Add(referenceProject); + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs new file mode 100644 index 0000000000..3a8fe825ca --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + [Serializable] + public class DotNetProjectInfo + { + public string RepositoryName { get; set; } + + public string CsProjPath { get; set; } + + public bool ShouldBuild { get; set; } + + public List Dependencies { get; set; } + + public DotNetProjectInfo(string repositoryName, string csProjPath, bool shouldBuild) + { + RepositoryName = repositoryName; + CsProjPath = csProjPath; + ShouldBuild = shouldBuild; + Dependencies = new List(); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs new file mode 100644 index 0000000000..c6f8e5dd53 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + public class DotNetProjectInfoEqualityComparer : EqualityComparer + { + public override bool Equals(DotNetProjectInfo x, DotNetProjectInfo y) + { + return (x == null && y == null) || (x != null && y != null && x.CsProjPath == y.CsProjPath); + } + + public override int GetHashCode(DotNetProjectInfo obj) + { + return obj == null ? 0 : obj.CsProjPath.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs new file mode 100644 index 0000000000..ace85e5257 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.Cli.Build +{ + public static class DotNetProjectInfoExtensions + { + public static void MarkForBuild(this List projects, string repositoryName, string csProjPath) + { + var project = projects.FirstOrDefault(e => + e.RepositoryName == repositoryName && e.CsProjPath == csProjPath + ); + + if (project == null) + { + return; + } + + project.ShouldBuild = true; + } + + public static void MarkForBuild(this List projects, DotNetProjectInfo project) + { + projects.MarkForBuild(project.RepositoryName, project.CsProjPath); + } + + public static bool IsMarkedForBuild(this List projects, string repositoryName, + string csProjPath) + { + var project = projects.FirstOrDefault(e => + e.RepositoryName == repositoryName && e.CsProjPath == csProjPath + ); + + if (project == null) + { + return false; + } + + return project.ShouldBuild; + } + + public static bool IsMarkedForBuild(this List projects, DotNetProjectInfo project) + { + return projects.IsMarkedForBuild(project.RepositoryName, project.CsProjPath); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs new file mode 100644 index 0000000000..5aed3d0941 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Linq; +using LibGit2Sharp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.Cli.Build +{ + public class FileSystemDotNetProjectBuildConfigReader : IDotNetProjectBuildConfigReader, ITransientDependency + { + private readonly IJsonSerializer _jsonSerializer; + private string _buildConfigName = "abp-build-config.json"; + + public FileSystemDotNetProjectBuildConfigReader(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + public DotNetProjectBuildConfig Read(string directoryPath) + { + var buildConfig = new DotNetProjectBuildConfig(); + var solutionFiles = Directory.GetFiles(directoryPath, "*.sln", SearchOption.TopDirectoryOnly); + if (solutionFiles.Length == 1) + { + buildConfig.SlFilePath = solutionFiles.First(); + var configFile = GetClosestFile(directoryPath, _buildConfigName); + var configFileContent = File.ReadAllText(configFile); + buildConfig.GitRepository = _jsonSerializer.Deserialize(configFileContent); + + SetBranchNames(buildConfig.GitRepository); + + return buildConfig; + } + + var configFiles = Directory.GetFiles(directoryPath, _buildConfigName, SearchOption.TopDirectoryOnly); + if (configFiles.Length == 1) + { + var configFile = configFiles.First(); + var configFileContent = File.ReadAllText(configFile); + buildConfig.GitRepository = _jsonSerializer.Deserialize(configFileContent); + + SetBranchNames(buildConfig.GitRepository); + + return buildConfig; + } + + Console.WriteLine( + "There are more than 1 config (abp-build-config.json) file in the directory!" + ); + + throw new Exception("There is no solution file (*.sln) or " + _buildConfigName + " in the working directory !"); + } + + private void SetBranchNames(GitRepository gitRepository) + { + using (var repo = new Repository(string.Concat(gitRepository.RootPath, @"\.git"))) + { + gitRepository.BranchName = repo.Head.FriendlyName; + } + + foreach (var dependingRepository in gitRepository.DependingRepositories) + { + SetBranchNames(dependingRepository); + } + } + + private string GetClosestFile(string directoryPath, string fileName) + { + var directory = new DirectoryInfo(directoryPath); + var files = directory.GetFiles(fileName, SearchOption.TopDirectoryOnly); + if (files.Any() && files.Length == 1) + { + return files.First().FullName; + } + + do + { + directory = directory.Parent; + if (directory == null) + { + return string.Empty; + } + + files = directory.GetFiles(fileName, SearchOption.TopDirectoryOnly); + if (files.Any() && files.Length == 1) + { + return files.First().FullName; + } + } while (true); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs new file mode 100644 index 0000000000..0f6b2f19a7 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs @@ -0,0 +1,61 @@ +using System.IO; +using Newtonsoft.Json; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IO; + +namespace Volo.Abp.Cli.Build +{ + public class FileSystemRepositoryBuildStatusStore : IRepositoryBuildStatusStore, ITransientDependency + { + public GitRepositoryBuildStatus Get(string buildNamePrefix, GitRepository repository) + { + if (!Directory.Exists(CliPaths.Build)) + { + Directory.CreateDirectory(CliPaths.Build); + } + + var buildStatusFile = Path.Combine(CliPaths.Build, repository.GetUniqueName(buildNamePrefix)) + + ".json"; + + if (!File.Exists(buildStatusFile)) + { + return null; + } + + var buildStatusText = File.ReadAllText(buildStatusFile); + return JsonConvert.DeserializeObject(buildStatusText); + } + + public void Set(string buildNamePrefix, GitRepository repository, GitRepositoryBuildStatus status) + { + var existingRepositoryStatus = Get(buildNamePrefix, repository); + + var buildStatusFile = Path.Combine( + CliPaths.Build, + status.GetUniqueName(buildNamePrefix) + ) + ".json"; + + if (File.Exists(buildStatusFile)) + { + FileHelper.DeleteIfExists(buildStatusFile); + } + + if (existingRepositoryStatus != null) + { + existingRepositoryStatus.MergeWith(status); + + using (var file = File.CreateText(buildStatusFile)) + { + new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, existingRepositoryStatus); + } + } + else + { + using (var file = File.CreateText(buildStatusFile)) + { + new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, status); + } + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs new file mode 100644 index 0000000000..eb1acafd82 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + /// + /// Represents a source code repository + /// + public class GitRepository + { + /// + /// Name of the repository + /// + public string Name { get; set; } + + /// + /// Branch of the repository + /// + public string BranchName { get; set; } + + /// + /// Root path of the repository which contains .git folder + /// + public string RootPath { get; set; } + + public List DependingRepositories { get; set; } + + public List IgnoredDirectories { get; set; } + + public GitRepository(string name, string branchName, string rootPath) + { + Name = name; + BranchName = branchName; + RootPath = rootPath; + DependingRepositories = new List(); + IgnoredDirectories = new List(); + } + + public string GetUniqueName(string prefix) + { + var name = Name + "_" + BranchName; + foreach (var dependingRepository in DependingRepositories) + { + AddToUniqueName(dependingRepository, name); + } + + return (prefix.IsNullOrEmpty() ? "" : prefix + "_") + name.ToMd5(); + } + + private void AddToUniqueName(GitRepository gitRepository, string name) + { + name += "_" + gitRepository.Name + "_" + gitRepository.BranchName; + + foreach (var dependingRepository in gitRepository.DependingRepositories) + { + AddToUniqueName(dependingRepository, name); + } + } + + public string FindRepositoryOf(string csProjFilePath) + { + if (csProjFilePath.StartsWith(RootPath)) + { + return Name; + } + + foreach (var dependingRepository in DependingRepositories) + { + var name = FindRepositoryOfInternal(dependingRepository, csProjFilePath); + if (!string.IsNullOrEmpty(name)) + { + return name; + } + } + + return null; + } + + private string FindRepositoryOfInternal(GitRepository repository, string csProjFilePath) + { + if (csProjFilePath.StartsWith(repository.RootPath)) + { + return repository.Name; + } + + foreach (var dependingRepository in repository.DependingRepositories) + { + return FindRepositoryOfInternal(dependingRepository, csProjFilePath); + } + + return null; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs new file mode 100644 index 0000000000..1a60a04068 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.Cli.Build +{ + public class GitRepositoryBuildStatus + { + /// + /// Name of the repository + /// + public string RepositoryName { get; set; } + + /// + /// Branch of the repository + /// + public string BranchName { get; set; } + + /// + /// Last succeeded commitId of the repository + /// + public string CommitId { get; set; } + + /// + /// Build succeeded projects of this repository + /// + public List SucceedProjects { get; set; } + + /// + /// Build status of depending repositories + /// + public List DependingRepositories { get; set; } + + public GitRepositoryBuildStatus(string repositoryName, string branchName) + { + RepositoryName = repositoryName; + BranchName = branchName; + SucceedProjects = new List(); + DependingRepositories = new List(); + } + + public GitRepositoryBuildStatus GetSelfOrChild(string repositoryName) + { + if (RepositoryName == repositoryName) + { + return this; + } + + return GetChild(repositoryName); + } + + public GitRepositoryBuildStatus GetChild(string repositoryName) + { + foreach (var dependingRepository in DependingRepositories) + { + var child = GetChildInternal(dependingRepository, repositoryName); + if (child != null) + { + return child; + } + } + + return null; + } + + public string GetUniqueName(string prefix) + { + var name = RepositoryName + "_" + BranchName; + foreach (var dependingRepository in DependingRepositories) + { + AddToUniqueName(dependingRepository, name); + } + + return (prefix.IsNullOrEmpty() ? "" : prefix + "_") + name.ToMd5(); + } + + public void AddOrUpdateProjectStatus(DotNetProjectBuildStatus status) + { + var existingProjectStatus = SucceedProjects.FirstOrDefault(p => p.CsProjPath == status.CsProjPath); + if (existingProjectStatus != null) + { + existingProjectStatus.CommitId = status.CommitId; + } + else + { + SucceedProjects.Add(status); + } + } + + public void MergeWith(GitRepositoryBuildStatus newBuildStatus) + { + if (!newBuildStatus.CommitId.IsNullOrEmpty()) + { + CommitId = newBuildStatus.CommitId; + } + + foreach (var succeedProject in newBuildStatus.SucceedProjects) + { + AddOrUpdateProjectStatus(succeedProject); + } + + foreach (var dependingRepositoryBuildStatus in newBuildStatus.DependingRepositories) + { + var existingDependingRepositoryBuildStatus = GetChild(dependingRepositoryBuildStatus.RepositoryName); + var newDependingRepositoryBuildStatus = newBuildStatus.GetChild( + dependingRepositoryBuildStatus.RepositoryName + ); + + existingDependingRepositoryBuildStatus.MergeWith(newDependingRepositoryBuildStatus); + } + } + + private GitRepositoryBuildStatus GetChildInternal(GitRepositoryBuildStatus repositoryBuildStatus, + string repositoryName) + { + if (repositoryBuildStatus.RepositoryName == repositoryName) + { + return repositoryBuildStatus; + } + + foreach (var dependingRepository in repositoryBuildStatus.DependingRepositories) + { + var child = GetChildInternal(dependingRepository, repositoryName); + if (child != null) + { + return child; + } + } + + return null; + } + + private void AddToUniqueName(GitRepositoryBuildStatus gitRepository, string name) + { + name += "_" + gitRepository.RepositoryName + "_" + gitRepository.BranchName; + + foreach (var dependingRepository in gitRepository.DependingRepositories) + { + AddToUniqueName(dependingRepository, name); + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs new file mode 100644 index 0000000000..51c84f0f61 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs @@ -0,0 +1,24 @@ +using LibGit2Sharp; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Build +{ + public class GitRepositoryHelper : IGitRepositoryHelper, ITransientDependency + { + public string GetLastCommitId(GitRepository repository) + { + using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git"))) + { + return repo.Head.Tip.Id.ToString(); + } + } + + public string GetFriendlyName(GitRepository repository) + { + using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git"))) + { + return repo.Head.FriendlyName; + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs new file mode 100644 index 0000000000..ed97ca27c8 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + public interface IBuildProjectListSorter + { + List SortByDependencies( + List source, + IEqualityComparer comparer = null); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs new file mode 100644 index 0000000000..f9cf0b8254 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + public interface IBuildStatusGenerator + { + GitRepositoryBuildStatus Generate( + DotNetProjectBuildConfig buildConfig, + List changedProjects, + List buildSucceededProjects + ); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs new file mode 100644 index 0000000000..b381a073b4 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + public interface IChangedProjectFinder + { + List Find(DotNetProjectBuildConfig buildConfig); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs new file mode 100644 index 0000000000..54dd2bd5ac --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Cli.Build +{ + public interface IDotNetProjectBuildConfigReader + { + DotNetProjectBuildConfig Read(string directoryPath); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs new file mode 100644 index 0000000000..02f753c325 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + public interface IDotNetProjectBuilder + { + List Build(List projects, string arguments); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs new file mode 100644 index 0000000000..cc1ba65bc7 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Build +{ + public interface IDotNetProjectDependencyFiller + { + void Fill(List projects); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs new file mode 100644 index 0000000000..a1402c7dda --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Cli.Build +{ + public interface IGitRepositoryHelper + { + string GetLastCommitId(GitRepository repository); + + string GetFriendlyName(GitRepository repository); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs new file mode 100644 index 0000000000..99be400ec8 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Cli.Build +{ + public interface IRepositoryBuildStatusStore + { + GitRepositoryBuildStatus Get(string buildNamePrefix, GitRepository repository); + + void Set(string buildNamePrefix, GitRepository repository, GitRepositoryBuildStatus status); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs index eb6339b9da..2b422446ca 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs @@ -9,7 +9,8 @@ namespace Volo.Abp.Cli public static string Log => Path.Combine(AbpRootPath, "cli", "logs"); public static string Root => Path.Combine(AbpRootPath, "cli"); public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin"); - + public static string Build => Path.Combine(AbpRootPath, "build"); + private static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp"); } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs index 84643ed543..785d2ff3fc 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs @@ -20,13 +20,13 @@ namespace Volo.Abp.Cli public ILogger Logger { get; set; } protected ICommandLineArgumentParser CommandLineArgumentParser { get; } protected ICommandSelector CommandSelector { get; } - protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } protected NuGetService NuGetService { get; } public CliService( ICommandLineArgumentParser commandLineArgumentParser, ICommandSelector commandSelector, - IHybridServiceScopeFactory serviceScopeFactory, + IServiceScopeFactory serviceScopeFactory, NuGetService nugetService) { CommandLineArgumentParser = commandLineArgumentParser; @@ -73,7 +73,7 @@ namespace Volo.Abp.Cli { var assembly = typeof(CliService).Assembly; var toolPath = GetToolPath(assembly); - var currentCliVersion = await GetCurrentCliVersion(assembly); + var currentCliVersion = await GetCurrentCliVersionInternalAsync(assembly); var updateChannel = GetUpdateChannel(currentCliVersion); Logger.LogInformation($"Version {currentCliVersion} ({updateChannel})"); @@ -94,7 +94,7 @@ namespace Volo.Abp.Cli } } - private static string GetToolPath(Assembly assembly) + private string GetToolPath(Assembly assembly) { if (!assembly.Location.Contains(".store")) { @@ -104,7 +104,12 @@ namespace Volo.Abp.Cli return assembly.Location.Substring(0, assembly.Location.IndexOf(".store", StringComparison.Ordinal)); } - private static async Task GetCurrentCliVersion(Assembly assembly) + public async Task GetCurrentCliVersionAsync(Assembly assembly) + { + return await GetCurrentCliVersionInternalAsync(assembly); + } + + private async Task GetCurrentCliVersionInternalAsync(Assembly assembly) { SemanticVersion currentCliVersion = default; @@ -134,7 +139,7 @@ namespace Volo.Abp.Cli return currentCliVersion; } - private static UpdateChannel GetUpdateChannel(SemanticVersion currentCliVersion) + private UpdateChannel GetUpdateChannel(SemanticVersion currentCliVersion) { if (!currentCliVersion.IsPrerelease) { @@ -172,7 +177,7 @@ namespace Volo.Abp.Cli } } - private static bool IsGlobalTool(string toolPath) + private bool IsGlobalTool(string toolPath) { var globalPaths = new[] { @"%USERPROFILE%\.dotnet\tools\", "%HOME%/.dotnet/tools/", }; return globalPaths.Select(Environment.ExpandEnvironmentVariables).Contains(toolPath); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs index 5e690d8bb0..42a8d76b98 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs @@ -36,7 +36,8 @@ namespace Volo.Abp.Cli.Commands ); } - var withSourceCode = commandLineArgs.Options.ContainsKey("with-source-code"); + var withSourceCode = commandLineArgs.Options.ContainsKey(Options.SourceCode.Long); + var addSourceCodeToSolutionFile = withSourceCode && commandLineArgs.Options.ContainsKey("add-to-solution-file"); var skipDbMigrations = Convert.ToBoolean( commandLineArgs.Options.GetOrNull(Options.DbMigrations.Skip) ?? "false"); @@ -55,7 +56,8 @@ namespace Volo.Abp.Cli.Commands commandLineArgs.Options.GetOrNull(Options.StartupProject.Short, Options.StartupProject.Long), version, skipDbMigrations, - withSourceCode + withSourceCode, + addSourceCodeToSolutionFile ); } @@ -71,7 +73,8 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(" abp add-module [options]"); sb.AppendLine(""); sb.AppendLine("Options:"); - sb.AppendLine(" --with-source-code Downloads the source code of the module and adds it to your solution."); + sb.AppendLine(" --with-source-code Downloads the source code of the module to your solution folder."); + sb.AppendLine(" --add-to-solution-file Adds the downloaded module to your solution file. (only available when --with-source-code used)"); sb.AppendLine(" -s|--solution Specify the solution file explicitly."); sb.AppendLine(" --skip-db-migrations Specify if a new migration will be added or not."); sb.AppendLine(" -sp|--startup-project Relative path to the project folder of the startup project. Default value is the current folder."); @@ -158,6 +161,11 @@ namespace Volo.Abp.Cli.Commands public const string Short = "sp"; public const string Long = "startup-project"; } + + public static class SourceCode + { + public const string Long = "with-source-code"; + } } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs new file mode 100644 index 0000000000..6d28b39fad --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs @@ -0,0 +1,132 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.Build; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands +{ + public class BuildCommand : IConsoleCommand, ITransientDependency + { + public IDotNetProjectDependencyFiller DotNetProjectDependencyFiller { get; set; } + + public IChangedProjectFinder ChangedProjectFinder { get; set; } + + public IDotNetProjectBuilder DotNetProjectBuilder { get; set; } + + public IRepositoryBuildStatusStore RepositoryBuildStatusStore { get; set; } + + public IDotNetProjectBuildConfigReader DotNetProjectBuildConfigReader { get; set; } + + public IBuildStatusGenerator BuildStatusGenerator { get; set; } + + public IBuildProjectListSorter BuildProjectListSorter { get; set; } + + public Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + var sw = new Stopwatch(); + sw.Start(); + + var workingDirectory = commandLineArgs.Options.GetOrNull( + Options.WorkingDirectory.Short, + Options.WorkingDirectory.Long + ); + + var dotnetBuildArguments = commandLineArgs.Options.GetOrNull( + Options.DotnetBuildArguments.Short, + Options.DotnetBuildArguments.Long + ); + + var buildName = commandLineArgs.Options.GetOrNull( + Options.BuildName.Short, + Options.BuildName.Long + ); + + var forceBuild = commandLineArgs.Options.ContainsKey(Options.ForceBuild.Short) || + commandLineArgs.Options.ContainsKey(Options.ForceBuild.Long); + + var buildConfig = DotNetProjectBuildConfigReader.Read(workingDirectory ?? Directory.GetCurrentDirectory()); + buildConfig.BuildName = buildName; + buildConfig.ForceBuild = forceBuild; + + Console.WriteLine("Finding changed projects..."); + + var changedProjectFiles = ChangedProjectFinder.Find(buildConfig); + + var buildSucceededProjects = DotNetProjectBuilder.Build( + changedProjectFiles, + dotnetBuildArguments ?? "" + ); + + var buildStatus = BuildStatusGenerator.Generate( + buildConfig, + changedProjectFiles, + buildSucceededProjects + ); + + RepositoryBuildStatusStore.Set(buildName, buildConfig.GitRepository, buildStatus); + + sw.Stop(); + Console.WriteLine("Build operation is completed in " + sw.ElapsedMilliseconds + " (ms)"); + + return Task.CompletedTask; + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(""); + sb.AppendLine(" abp build [options]"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine(""); + sb.AppendLine("-wd|--working-directory (default: empty)"); + sb.AppendLine("-m |--max-parallel-builds (default: 1)"); + sb.AppendLine("-a |--dotnet-build-arguments (default: empty)"); + sb.AppendLine("-n |--build-name (default: empty)"); + sb.AppendLine("-f | --force (default: false)"); + sb.AppendLine(""); + sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); + + return sb.ToString(); + } + + public string GetShortDescription() + { + return "Builds a dotnet repository and dependent repositories or a solution."; + } + + public static class Options + { + public static class WorkingDirectory + { + public const string Short = "wd"; + public const string Long = "working-directory"; + } + + public static class DotnetBuildArguments + { + public const string Short = "a"; + public const string Long = "dotnet-build-arguments"; + } + + public static class BuildName + { + public const string Short = "n"; + public const string Long = "build-name"; + } + + public static class ForceBuild + { + public const string Short = "f"; + public const string Long = "force"; + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs index 72aedc51de..4f4aec853c 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs @@ -7,5 +7,10 @@ namespace Volo.Abp.Cli.Commands protected override string CommandName => Name; protected override string SchematicsCommandName => "proxy-add"; + + public GenerateProxyCommand(CliService cliService) + : base(cliService) + { + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs index 7cda59b999..5bf5041c22 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs @@ -2,14 +2,11 @@ using System.IO; using System.Text; using System.Threading.Tasks; -using ICSharpCode.SharpZipLib.Core; -using ICSharpCode.SharpZipLib.Zip; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Cli.Args; using Volo.Abp.Cli.Commands.Services; using Volo.Abp.Cli.ProjectBuilding; -using Volo.Abp.Cli.ProjectBuilding.Building; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Cli.Commands diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs index 70d5112e95..d59b8a9bb5 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs @@ -14,10 +14,10 @@ namespace Volo.Abp.Cli.Commands { public ILogger Logger { get; set; } protected AbpCliOptions AbpCliOptions { get; } - protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } public HelpCommand(IOptions cliOptions, - IHybridServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory) { ServiceScopeFactory = serviceScopeFactory; Logger = NullLogger.Instance; @@ -94,4 +94,4 @@ namespace Volo.Abp.Cli.Commands return "Show command line help. Write ` abp help `"; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs index 7898ae7abc..0ecd849ad1 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs @@ -2,7 +2,10 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json.Linq; +using NuGet.Versioning; using Volo.Abp.Cli.Args; using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; @@ -11,14 +14,23 @@ namespace Volo.Abp.Cli.Commands { public abstract class ProxyCommandBase : IConsoleCommand, ITransientDependency { + public CliService CliService { get; } + public ILogger Logger { get; set; } + protected abstract string CommandName { get; } protected abstract string SchematicsCommandName { get; } - public Task ExecuteAsync(CommandLineArgs commandLineArgs) + public ProxyCommandBase(CliService cliService) + { + CliService = cliService; + Logger = NullLogger.Instance; + } + + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) { CheckAngularJsonFile(); - CheckNgSchematics(); + await CheckNgSchematicsAsync(); var prompt = commandLineArgs.Options.ContainsKey("p") || commandLineArgs.Options.ContainsKey("prompt"); var defaultValue = prompt ? null : "__default"; @@ -51,11 +63,9 @@ namespace Volo.Abp.Cli.Commands } CmdHelper.RunCmd(commandBuilder.ToString()); - - return Task.CompletedTask; } - private void CheckNgSchematics() + private async Task CheckNgSchematicsAsync() { var packageJsonPath = $"package.json"; @@ -68,10 +78,10 @@ namespace Volo.Abp.Cli.Commands ); } - var schematicsPackageNode = + var schematicsVersion = (string) JObject.Parse(File.ReadAllText(packageJsonPath))["devDependencies"]?["@abp/ng.schematics"]; - if (schematicsPackageNode == null) + if (schematicsVersion == null) { throw new CliUsageException( "\"@abp/ng.schematics\" NPM package should be installed to the devDependencies before running this command!" + @@ -79,6 +89,20 @@ namespace Volo.Abp.Cli.Commands GetUsageInfo() ); } + + var parseError = SemanticVersion.TryParse(schematicsVersion.TrimStart('~', '^', 'v'), out var semanticSchematicsVersion); + if (parseError) + { + Logger.LogWarning("Couldn't determinate version of \"@abp/ng.schematics\" package."); + return; + } + + var cliVersion = await CliService.GetCurrentCliVersionAsync(typeof(CliService).Assembly); + if (semanticSchematicsVersion < cliVersion) + { + Logger.LogWarning("\"@abp/ng.schematics\" version is lower than ABP Cli version."); + return; + } } private void CheckAngularJsonFile() diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs index 7d31d5387a..85bbc3e15f 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs @@ -7,5 +7,10 @@ protected override string CommandName => Name; protected override string SchematicsCommandName => "proxy-remove"; + + public RemoveProxyCommand(CliService cliService) + : base(cliService) + { + } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs index 2d58474ed5..f614fbdd24 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs @@ -153,7 +153,7 @@ namespace Volo.Abp.Cli.Commands { if (preview) { - var latestVersion = await GetLatestSuiteVersioAsync(true); + var latestVersion = await GetLatestSuiteVersionAsync(true); if (latestVersion.IsPrerelease) { return $"--version {latestVersion.ToString()}"; @@ -163,7 +163,7 @@ namespace Volo.Abp.Cli.Commands return ""; } - private async Task GetLatestSuiteVersioAsync(bool preview) + private async Task GetLatestSuiteVersionAsync(bool preview) { return await _nuGetService.GetLatestVersionOrNullAsync(SuitePackageName, includeReleaseCandidates: preview); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/TranslateCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/TranslateCommand.cs index 39dbeafd3c..c3eeddc443 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/TranslateCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/TranslateCommand.cs @@ -255,8 +255,8 @@ namespace Volo.Abp.Cli.Commands return null; } - var culture = jObject.GetValue("culture"); - var texts = jObject.GetValue("texts"); + var culture = jObject.GetValue("culture") ?? jObject.GetValue("Culture") ; + var texts = jObject.GetValue("texts") ?? jObject.GetValue("Texts") ; if (culture == null || texts == null) { return null; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs index eb781c27b1..4230735680 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs @@ -12,7 +12,8 @@ namespace Volo.Abp.Cli.Licensing public string LicenseCode { get; set; } public string ErrorMessage { get; set; } public LicenseErrorType? ErrorType { get; set; } - + public LicenseType LicenseType { get; set; } + public enum LicenseErrorType { NotAuthenticated = 1, diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/LicenseType.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/LicenseType.cs new file mode 100644 index 0000000000..48bc818914 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/LicenseType.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.Cli.Licensing +{ + public enum LicenseType : byte + { + Personal = 1, + Team = 2, + Business = 3, + Enterprise = 4, + Personal_Discounted = 5, + Team_Discounted = 6, + Business_Discounted = 7, + Enterprise_Discounted = 8, + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs index 79f54eb8e2..1ecfc9e34d 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs @@ -75,16 +75,35 @@ namespace Volo.Abp.Cli.NuGet var responseContent = await responseMessage.Content.ReadAsStringAsync(); - var versions = JsonSerializer + List versions; + + if (!includeNightly && !includeReleaseCandidates) + { + versions = JsonSerializer .Deserialize(responseContent) .Versions .Select(SemanticVersion.Parse) .OrderByDescending(v=> v, new VersionComparer()).ToList(); - if (!includeNightly && !includeReleaseCandidates) - { versions = versions.Where(x => !x.IsPrerelease).ToList(); } + else if (!includeNightly && includeReleaseCandidates) + { + versions = JsonSerializer + .Deserialize(responseContent) + .Versions + .Where(v=> !v.Contains("-preview")) + .Select(SemanticVersion.Parse) + .OrderByDescending(v=> v, new VersionComparer()).ToList(); + } + else + { + versions = JsonSerializer + .Deserialize(responseContent) + .Versions + .Select(SemanticVersion.Parse) + .OrderByDescending(v=> v, new VersionComparer()).ToList(); + } return versions.Any() ? versions.Max() : null; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs index 106d7b5bcc..0462920fbb 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/AbpIoSourceCodeStore.cs @@ -14,6 +14,7 @@ using Volo.Abp.Cli.Http; using Volo.Abp.Cli.ProjectBuilding.Templates.App; using Volo.Abp.Cli.ProjectBuilding.Templates.Console; using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule; +using Volo.Abp.Cli.ProjectBuilding.Templates.Wpf; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.IO; @@ -244,7 +245,7 @@ namespace Volo.Abp.Cli.ProjectBuilding stringBuilder.AppendLine(cacheFile); } - var matches = Regex.Matches(stringBuilder.ToString(), $"({AppTemplate.TemplateName}|{AppProTemplate.TemplateName}|{ModuleTemplate.TemplateName}|{ModuleProTemplate.TemplateName}|{ConsoleTemplate.TemplateName})-(.+).zip"); + var matches = Regex.Matches(stringBuilder.ToString(), $"({AppTemplate.TemplateName}|{AppProTemplate.TemplateName}|{ModuleTemplate.TemplateName}|{ModuleProTemplate.TemplateName}|{ConsoleTemplate.TemplateName}|{WpfTemplate.TemplateName})-(.+).zip"); foreach (Match match in matches) { templateList.Add((match.Groups[1].Value, match.Groups[2].Value)); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs index 497a38c4b1..7b48c780f1 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs @@ -33,6 +33,12 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps private List RemoveProject(List solutionFileLines) { var projectKey = FindProjectKey(solutionFileLines); + + if (projectKey == null) + { + return solutionFileLines; + } + var newSolutionFileLines = new List(); var firstOccurence = true; @@ -67,7 +73,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps } } - throw new ApplicationException($"The solution file '{_solutionFilePath}' does not contain a project '{_projectName}'"); + return null; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateInfoProvider.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateInfoProvider.cs index d1c2e4e2ba..7a4df335fe 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateInfoProvider.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateInfoProvider.cs @@ -3,6 +3,7 @@ using Volo.Abp.Cli.ProjectBuilding.Building; using Volo.Abp.Cli.ProjectBuilding.Templates.App; using Volo.Abp.Cli.ProjectBuilding.Templates.Console; using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule; +using Volo.Abp.Cli.ProjectBuilding.Templates.Wpf; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Cli.ProjectBuilding @@ -28,6 +29,8 @@ namespace Volo.Abp.Cli.ProjectBuilding return new ModuleProTemplate(); case ConsoleTemplate.TemplateName: return new ConsoleTemplate(); + case WpfTemplate.TemplateName: + return new WpfTemplate(); default: throw new Exception("There is no template found with given name: " + name); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs index d309d4858a..8846473d96 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs @@ -25,6 +25,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App SwitchDatabaseProvider(context, steps); DeleteUnrelatedProjects(context, steps); RandomizeSslPorts(context, steps); + RandomizeStringEncryption(context, steps); UpdateNuGetConfig(context, steps); CleanupFolderHierarchy(context, steps); @@ -185,6 +186,11 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App ); } + private static void RandomizeStringEncryption(ProjectBuildContext context, List steps) + { + steps.Add(new RandomizeStringEncryptionStep()); + } + private static void UpdateNuGetConfig(ProjectBuildContext context, List steps) { steps.Add(new UpdateNuGetConfigStep("/aspnet-core/NuGet.Config")); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Console/ConsoleTemplate.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Console/ConsoleTemplate.cs index e2b49c00f7..67d67b2d9c 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Console/ConsoleTemplate.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Console/ConsoleTemplate.cs @@ -10,7 +10,7 @@ public ConsoleTemplate() : base(TemplateName) { - DocumentUrl = CliConsts.DocsLink + "/en/abp/latest/Getting-Started-Console-Application"; + DocumentUrl = CliConsts.DocsLink + "/en/abp/latest/Startup-Templates/Console"; } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/RandomizeStringEncryptionStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/RandomizeStringEncryptionStep.cs new file mode 100644 index 0000000000..6fc2bafc95 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/RandomizeStringEncryptionStep.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Text; +using Volo.Abp.Cli.ProjectBuilding.Building; + +namespace Volo.Abp.Cli.ProjectBuilding.Templates +{ + public class RandomizeStringEncryptionStep: ProjectBuildPipelineStep + { + public override void Execute(ProjectBuildContext context) + { + var appSettings = context.Files + .Where(x => !x.IsDirectory && x.Name.EndsWith("appSettings.json", StringComparison.InvariantCultureIgnoreCase)) + .Where(x => x.Content.IndexOf("StringEncryption", StringComparison.InvariantCultureIgnoreCase) >= 0) + .ToList(); + + const string defaultPassPhrase = "gsKnGZ041HLL4IM8"; + var randomPassPhrase = GetRandomString(defaultPassPhrase.Length); + foreach (var appSetting in appSettings) + { + appSetting.NormalizeLineEndings(); + + var appSettingLines = appSetting.GetLines(); + for (var i = 0; i < appSettingLines.Length; i++) + { + if (appSettingLines[i].Contains("DefaultPassPhrase") && appSettingLines[i].Contains(defaultPassPhrase)) + { + appSettingLines[i] = appSettingLines[i].Replace(defaultPassPhrase, randomPassPhrase); + } + } + + appSetting.SetLines(appSettingLines); + } + } + + private static string GetRandomString(int length) + { + const string letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + var builder = new StringBuilder(); + for (var i = 0; i < length; i++) + { + builder.Append(letters[RandomHelper.GetRandom(0, letters.Length)]); + } + return builder.ToString(); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Wpf/WpfTemplate.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Wpf/WpfTemplate.cs new file mode 100644 index 0000000000..b08a12daba --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Wpf/WpfTemplate.cs @@ -0,0 +1,16 @@ +namespace Volo.Abp.Cli.ProjectBuilding.Templates.Wpf +{ + public class WpfTemplate : WpfTemplateBase + { + /// + /// "wpf". + /// + public const string TemplateName = "wpf"; + + public WpfTemplate() + : base(TemplateName) + { + DocumentUrl = CliConsts.DocsLink + "/en/abp/latest/Startup-Templates/WPF"; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Wpf/WpfTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Wpf/WpfTemplateBase.cs new file mode 100644 index 0000000000..e7127bb23b --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Wpf/WpfTemplateBase.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using Volo.Abp.Cli.ProjectBuilding.Building; + +namespace Volo.Abp.Cli.ProjectBuilding.Templates.Wpf +{ + public class WpfTemplateBase: TemplateInfo + { + protected WpfTemplateBase([NotNull] string name) : + base(name) + { + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs index 890053049f..10274f3528 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs @@ -12,6 +12,7 @@ Web = 7, EntityFrameworkCore = 8, MongoDB = 9, - SignalR = 10 + SignalR = 10, + Blazor = 11 } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs index d4b3a62b94..0c02f8cb87 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs @@ -45,6 +45,8 @@ namespace Volo.Abp.Cli.ProjectModification return FindProjectEndsWith(projectFiles, assemblyNames, ".SignalR") ?? FindProjectEndsWith(projectFiles, assemblyNames, ".Web") ?? FindProjectEndsWith(projectFiles, assemblyNames, ".HttpApi.Host"); + case NuGetPackageTarget.Blazor: + return FindProjectEndsWith(projectFiles, assemblyNames, ".Blazor"); default: return null; } @@ -103,8 +105,8 @@ namespace Volo.Abp.Cli.ProjectModification private static string FindProjectEndsWith( string[] projectFiles, - string[] assemblyNames, - string postfix, + string[] assemblyNames, + string postfix, string excludePostfix = null) { for (var i = 0; i < assemblyNames.Length; i++) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs index eeb19e8621..7ed15f7b86 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs @@ -17,21 +17,29 @@ namespace Volo.Abp.Cli.ProjectModification Logger = NullLogger.Instance; } - public Task AddAsync(string directory, NpmPackageInfo npmPackage) + public Task AddAsync(string directory, NpmPackageInfo npmPackage, bool skipGulpCommand = false) { var packageJsonFilePath = Path.Combine(directory, "package.json"); - if (!File.Exists(packageJsonFilePath) || File.ReadAllText(packageJsonFilePath).Contains($"\"{npmPackage.Name}\"")) + if (!File.Exists(packageJsonFilePath) || + File.ReadAllText(packageJsonFilePath).Contains($"\"{npmPackage.Name}\"")) { return Task.CompletedTask; } Logger.LogInformation($"Installing '{npmPackage.Name}' package to the project '{packageJsonFilePath}'..."); + + using (DirectoryHelper.ChangeCurrentDirectory(directory)) { Logger.LogInformation("yarn add " + npmPackage.Name); CmdHelper.RunCmd("yarn add " + npmPackage.Name); + if (skipGulpCommand) + { + return Task.CompletedTask; + } + Logger.LogInformation("gulp"); CmdHelper.RunCmd("gulp"); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs index 4eabeaa846..a24b31e5f1 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs @@ -10,6 +10,61 @@ namespace Volo.Abp.Cli.ProjectModification { public class SolutionFileModifier : ITransientDependency { + public async Task RemoveProjectFromSolutionFileAsync(string solutionFile, string projectName) + { + var solutionFileContent = File.ReadAllText(solutionFile); + solutionFileContent.NormalizeLineEndings(); + var lines = solutionFileContent.Split(new[] {Environment.NewLine, "\n"}, StringSplitOptions.None); + File.WriteAllText(solutionFile, RemoveProject(lines.ToList(), projectName).JoinAsString(Environment.NewLine)); + } + + private List RemoveProject(List solutionFileLines, string projectName) + { + var projectKey = FindProjectKey(solutionFileLines, projectName); + + if (projectKey == null) + { + return solutionFileLines; + } + + var newSolutionFileLines = new List(); + var firstOccurence = true; + + for (var i = 0; i < solutionFileLines.Count; ++i) + { + if (solutionFileLines[i].Contains(projectKey)) + { + if (firstOccurence) + { + firstOccurence = false; + ++i; //Skip "EndProject" line too. + } + + continue; + } + + newSolutionFileLines.Add(solutionFileLines[i]); + } + + return newSolutionFileLines; + } + + private string FindProjectKey(List solutionFileLines, string projectName) + { + var projectNameWithQuotes = $"\"{projectName}\""; + foreach (var solutionFileLine in solutionFileLines) + { + if (solutionFileLine.Contains(projectNameWithQuotes)) + { + var curlyBracketStartIndex = solutionFileLine.LastIndexOf("{", StringComparison.OrdinalIgnoreCase); + var curlyBracketEndIndex = solutionFileLine.LastIndexOf("}", StringComparison.OrdinalIgnoreCase); + return solutionFileLine.Substring(curlyBracketStartIndex + 1, curlyBracketEndIndex - curlyBracketStartIndex - 1); + } + } + + return null; + } + public async Task AddModuleToSolutionFileAsync(ModuleWithMastersInfo module, string solutionFile) { await AddModule(module, solutionFile); @@ -19,7 +74,7 @@ namespace Volo.Abp.Cli.ProjectModification { var srcModuleFolderId = await AddNewFolderAndGetIdOrGetExistingId(solutionFile, module.Name, await AddNewFolderAndGetIdOrGetExistingId(solutionFile, "modules")); var testModuleFolderId = await AddNewFolderAndGetIdOrGetExistingId(solutionFile, module.Name + ".Tests", await AddNewFolderAndGetIdOrGetExistingId(solutionFile, "test")); - + var file = File.ReadAllText(solutionFile); var lines = file.Split(Environment.NewLine).ToList(); @@ -52,7 +107,7 @@ namespace Volo.Abp.Cli.ProjectModification + Environment.NewLine + "EndProject"; lines.InsertAfter(l => l.Trim().Equals("EndProject"), newProjectLine); - + var newPostSolutionLine = " {" + projectGuid + "}.Debug|Any CPU.ActiveCfg = Debug|Any CPU" + Environment.NewLine + " {" + projectGuid + "}.Debug|Any CPU.Build.0 = Debug|Any CPU" + Environment.NewLine + @@ -60,7 +115,7 @@ namespace Volo.Abp.Cli.ProjectModification " {" + projectGuid + "}.Release|Any CPU.Build.0 = Release|Any CPU"; lines.InsertAfter(l=>l.Contains("GlobalSection") && l.Contains("ProjectConfigurationPlatforms"), newPostSolutionLine); - + var newPreSolutionLine = " {"+ projectGuid + "} = {"+ parentFolderId + "}"; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs index 1b006c25b6..cc563f97cf 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs @@ -68,14 +68,16 @@ namespace Volo.Abp.Cli.ProjectModification string startupProject, string version, bool skipDbMigrations = false, - bool withSourceCode = false) + bool withSourceCode = false, + bool addSourceCodeToSolutionFile = false) { Check.NotNull(solutionFile, nameof(solutionFile)); Check.NotNull(moduleName, nameof(moduleName)); var module = await FindModuleInfoAsync(moduleName); - Logger.LogInformation($"Installing module '{module.Name}' to the solution '{Path.GetFileNameWithoutExtension(solutionFile)}'"); + Logger.LogInformation( + $"Installing module '{module.Name}' to the solution '{Path.GetFileNameWithoutExtension(solutionFile)}'"); var projectFiles = ProjectFinder.GetProjectFiles(solutionFile); @@ -85,16 +87,93 @@ namespace Volo.Abp.Cli.ProjectModification { var modulesFolderInSolution = Path.Combine(Path.GetDirectoryName(solutionFile), "modules"); await DownloadSourceCodesToSolutionFolder(module, modulesFolderInSolution, version); - await SolutionFileModifier.AddModuleToSolutionFileAsync(module, solutionFile); - await NugetPackageToLocalReferenceConverter.Convert(module, solutionFile); + await RemoveUnnecessaryProjectsAsync(Path.GetDirectoryName(solutionFile), module, projectFiles); + + if (addSourceCodeToSolutionFile) + { + await SolutionFileModifier.AddModuleToSolutionFileAsync(module, solutionFile); + } - await HandleAngularProject(modulesFolderInSolution, solutionFile); + await NugetPackageToLocalReferenceConverter.Convert(module, solutionFile); + await AddAngularSourceCode(modulesFolderInSolution, solutionFile); + } + else + { + await AddAngularPackages(solutionFile, module); } ModifyDbContext(projectFiles, module, startupProject, skipDbMigrations); } - private async Task HandleAngularProject(string modulesFolderInSolution, string solutionFilePath) + private async Task RemoveUnnecessaryProjectsAsync(string solutionDirectory, ModuleWithMastersInfo module, string[] projectFiles) + { + var moduleDirectory = Path.Combine(solutionDirectory, "modules", module.Name); + var moduleSolutionFile = Directory.GetFiles(moduleDirectory, "*.sln", SearchOption.TopDirectoryOnly).First(); + var isProjectTiered = await IsProjectTiered(projectFiles); + + if (!projectFiles.Any(p => p.EndsWith(".Blazor.csproj"))) + { + await RemoveProjectByTarget(module, moduleSolutionFile, NuGetPackageTarget.Blazor,isProjectTiered); + } + + if (!projectFiles.Any(p => p.EndsWith(".Web.csproj"))) + { + await RemoveProjectByTarget(module, moduleSolutionFile, NuGetPackageTarget.Web, isProjectTiered); + } + + if (!projectFiles.Any(p => p.EndsWith(".MongoDB.csproj"))) + { + await RemoveProjectByTarget(module, moduleSolutionFile, NuGetPackageTarget.MongoDB, isProjectTiered); + } + + if (!projectFiles.Any(p => p.EndsWith(".EntityFrameworkCore.csproj"))) + { + await RemoveProjectByTarget(module, moduleSolutionFile, NuGetPackageTarget.EntityFrameworkCore, isProjectTiered); + } + } + + private async Task RemoveProjectByTarget(ModuleWithMastersInfo module, string moduleSolutionFile, NuGetPackageTarget target, bool isTieredProject) + { + var packages = module.NugetPackages.Where(n => + (isTieredProject && n.TieredTarget != NuGetPackageTarget.Undefined + ? n.TieredTarget + : n.Target) == target + ).ToList(); + + foreach (var package in packages) + { + await SolutionFileModifier.RemoveProjectFromSolutionFileAsync(moduleSolutionFile, package.Name); + + var projectPath = Path.Combine(Path.GetDirectoryName(moduleSolutionFile), "src", package.Name); + if (Directory.Exists(projectPath)) + { + Directory.Delete(projectPath, true); + } + } + } + + private async Task AddAngularPackages(string solutionFilePath, ModuleWithMastersInfo module) + { + var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular"); + + if (!Directory.Exists(angularPath)) + { + return; + } + + var angularPackages = module.NpmPackages?.Where(p => p.ApplicationType.HasFlag(NpmApplicationType.Angular)) + .ToList(); + + if (!angularPackages.IsNullOrEmpty()) + { + foreach (var npmPackage in angularPackages) + { + await ProjectNpmPackageAdder.AddAsync(angularPath, npmPackage, true); + } + } + } + + private async Task AddAngularSourceCode(string modulesFolderInSolution, string solutionFilePath) { var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular"); @@ -121,7 +200,8 @@ namespace Volo.Abp.Cli.ProjectModification } } - private async Task DownloadSourceCodesToSolutionFolder(ModuleWithMastersInfo module, string modulesFolderInSolution, string version = null) + private async Task DownloadSourceCodesToSolutionFolder(ModuleWithMastersInfo module, + string modulesFolderInSolution, string version = null) { var targetModuleFolder = Path.Combine(modulesFolderInSolution, module.Name); @@ -187,7 +267,8 @@ namespace Volo.Abp.Cli.ProjectModification await ProjectNugetPackageAdder.AddAsync(targetProjectFile, nugetPackage); } - var mvcNpmPackages = module.NpmPackages?.Where(p => p.ApplicationType.HasFlag(NpmApplicationType.Mvc)).ToList(); + var mvcNpmPackages = module.NpmPackages?.Where(p => p.ApplicationType.HasFlag(NpmApplicationType.Mvc)) + .ToList(); if (!mvcNpmPackages.IsNullOrEmpty()) { @@ -211,7 +292,8 @@ namespace Volo.Abp.Cli.ProjectModification } } - protected void ModifyDbContext(string[] projectFiles, ModuleInfo module, string startupProject, bool skipDbMigrations = false) + protected void ModifyDbContext(string[] projectFiles, ModuleInfo module, string startupProject, + bool skipDbMigrations = false) { if (string.IsNullOrWhiteSpace(module.EfCoreConfigureMethodName)) { @@ -246,11 +328,13 @@ namespace Volo.Abp.Cli.ProjectModification if (dbContextFile == null) { - Logger.LogDebug($"{dbMigrationsProject} project doesn't have a class that is derived from \"AbpDbContext\"."); + Logger.LogDebug( + $"{dbMigrationsProject} project doesn't have a class that is derived from \"AbpDbContext\"."); return; } - var addedNewBuilder = DbContextFileBuilderConfigureAdder.Add(dbContextFile, module.EfCoreConfigureMethodName); + var addedNewBuilder = + DbContextFileBuilderConfigureAdder.Add(dbContextFile, module.EfCoreConfigureMethodName); if (!skipDbMigrations) { diff --git a/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj b/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj index 489ea99ac5..79760820d8 100644 --- a/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj +++ b/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj @@ -1,11 +1,11 @@ - + Exe - netcoreapp3.1 + net5.0 true abp @@ -18,8 +18,8 @@ - - + + diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs index 1b2d2c4652..88e73c67e8 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs @@ -24,7 +24,7 @@ namespace Microsoft.Extensions.DependencyInjection .FirstOrDefault(d => d.ServiceType == typeof(T)) ?.ImplementationInstance; } - + public static T GetSingletonInstance(this IServiceCollection services) { var service = services.GetSingletonInstanceOrNull(); @@ -165,5 +165,10 @@ namespace Microsoft.Extensions.DependencyInjection { return new Lazy(() => services.GetRequiredService(type), true); } + + public static IServiceProvider GetServiceProviderOrNull(this IServiceCollection services) + { + return services.GetObjectOrNull(); + } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs b/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs index 49e0d6dea6..958398e0c5 100644 --- a/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs @@ -1,4 +1,6 @@ -using System.Globalization; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; using System.Linq; namespace System @@ -9,7 +11,7 @@ namespace System public static class AbpObjectExtensions { /// - /// Used to simplify and beautify casting an object to a type. + /// Used to simplify and beautify casting an object to a type. /// /// Type to be casted /// Object to cast @@ -29,6 +31,11 @@ namespace System public static T To(this object obj) where T : struct { + if (typeof(T) == typeof(Guid)) + { + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(obj.ToString()); + } + return (T)Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); } @@ -42,7 +49,18 @@ namespace System { return list.Contains(item); } - + + /// + /// Check if an item is in the given enumerable. + /// + /// Item to check + /// Items + /// Type of the items + public static bool IsIn(this T item, IEnumerable items) + { + return items.Contains(item); + } + /// /// Can be used to conditionally perform a function /// on an object and return the modified or the original object. @@ -65,7 +83,7 @@ namespace System return obj; } - + /// /// Can be used to conditionally perform an action /// on an object and return the original object. diff --git a/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs b/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs index e7c600eff6..1cf60a2906 100644 --- a/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs @@ -55,14 +55,28 @@ namespace System return types.ToArray(); } + /// + /// Gets all base classes of this type. + /// + /// The type to get its base classes. + /// A type to stop going to the deeper base classes. This type will be be included in the returned array + /// True, to include the standard type in the returned array. + public static Type[] GetBaseClasses([NotNull] this Type type, Type stoppingType, bool includeObject = true) + { + Check.NotNull(type, nameof(type)); + + var types = new List(); + AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject, stoppingType); + return types.ToArray(); + } + private static void AddTypeAndBaseTypesRecursively( [NotNull] List types, - [CanBeNull] Type type, - bool includeObject) + [CanBeNull] Type type, + bool includeObject, + [CanBeNull] Type stoppingType = null) { - Check.NotNull(types, nameof(types)); - - if (type == null) + if (type == null || type == stoppingType) { return; } @@ -72,7 +86,7 @@ namespace System return; } - AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject); + AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject, stoppingType); types.Add(type); } } diff --git a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpCollectionExtensions.cs b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpCollectionExtensions.cs index 2a858f7626..e014ee9e21 100644 --- a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpCollectionExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpCollectionExtensions.cs @@ -105,5 +105,19 @@ namespace System.Collections.Generic return items; } + + /// + /// Removes all items from the collection those satisfy the given . + /// + /// Type of the items in the collection + /// The collection + /// Items to be removed from the list + public static void RemoveAll([NotNull] this ICollection source, IEnumerable items) + { + foreach (var item in items) + { + source.Remove(item); + } + } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs index ac71649793..23fed34f3c 100644 --- a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs @@ -188,18 +188,22 @@ namespace System.Collections.Generic /// The type of the members of values. /// A list of objects to sort /// Function to resolve the dependencies + /// Equality comparer for dependencies /// /// Returns a new list ordered by dependencies. /// If A depends on B, then B will come before than A in the resulting list. /// - public static List SortByDependencies(this IEnumerable source, Func> getDependencies) + public static List SortByDependencies( + this IEnumerable source, + Func> getDependencies, + IEqualityComparer comparer = null) { /* See: http://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp * http://en.wikipedia.org/wiki/Topological_sorting */ var sorted = new List(); - var visited = new Dictionary(); + var visited = new Dictionary(comparer); foreach (var item in source) { diff --git a/framework/src/Volo.Abp.Core/System/Linq/PredicateOperator.cs b/framework/src/Volo.Abp.Core/System/Linq/PredicateOperator.cs index 0732a63110..75849b24b6 100644 --- a/framework/src/Volo.Abp.Core/System/Linq/PredicateOperator.cs +++ b/framework/src/Volo.Abp.Core/System/Linq/PredicateOperator.cs @@ -1,5 +1,8 @@ -using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq.Expressions; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace System.Linq @@ -246,15 +249,6 @@ namespace System.Linq public bool TailCall => Predicate.TailCall; #endif -#if !(NET35 || WINDOWS_APP || NETSTANDARD || PORTABLE || PORTABLE40 || UAP) - /// - public void CompileToMethod(MethodBuilder method) { Predicate.CompileToMethod(method); } - - /// - public void CompileToMethod(MethodBuilder method, DebugInfoGenerator debugInfoGenerator) { Predicate.CompileToMethod(method, debugInfoGenerator); } - -#endif - #endregion #region Implement Expression methods and properties diff --git a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj index e1842d0447..4414eed1c6 100644 --- a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj +++ b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj @@ -16,15 +16,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationCreationOptions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationCreationOptions.cs index e4d7744ae8..f2e201d0aa 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationCreationOptions.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationCreationOptions.cs @@ -13,6 +13,9 @@ namespace Volo.Abp [NotNull] public PlugInSourceList PlugInSources { get; } + /// + /// The options in this property only take effect when IConfiguration not registered. + /// [NotNull] public AbpConfigurationBuilderOptions Configuration {get; } @@ -23,4 +26,4 @@ namespace Volo.Abp Configuration = new AbpConfigurationBuilderOptions(); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs new file mode 100644 index 0000000000..bca259d422 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace Volo.Abp.Content +{ + public interface IRemoteStreamContent + { + string ContentType { get; } + + long? ContentLength { get; } + + Stream GetStream(); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs new file mode 100644 index 0000000000..f217101cea --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs @@ -0,0 +1,23 @@ +using System.IO; + +namespace Volo.Abp.Content +{ + public class RemoteStreamContent : IRemoteStreamContent + { + private readonly Stream _stream; + + public RemoteStreamContent(Stream stream) + { + _stream = stream; + } + + public virtual string ContentType { get; set; } + + public virtual long? ContentLength => _stream.Length; + + public virtual Stream GetStream() + { + return _stream; + } + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs index 232b9a2ecd..e681b6d6f7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs @@ -12,9 +12,9 @@ namespace Volo.Abp.ExceptionHandling { public ILogger Logger { get; set; } - protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } - public ExceptionNotifier(IHybridServiceScopeFactory serviceScopeFactory) + public ExceptionNotifier(IServiceScopeFactory serviceScopeFactory) { ServiceScopeFactory = serviceScopeFactory; Logger = NullLogger.Instance; diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/IAsyncInitialize.cs b/framework/src/Volo.Abp.Core/Volo/Abp/IAsyncInitialize.cs new file mode 100644 index 0000000000..9c97c04a1e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/IAsyncInitialize.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Volo.Abp +{ + /// + /// IMPORTANT: THIS IS AN INTERNAL CLASS TO BE USED BY THE ABP FRAMEWORK. + /// IT WILL BE REMOVED IN THE FUTURE VERSIONS. DON'T USE IT! + /// + public interface IAsyncInitialize //TODO: Remove once we have async module initialization + { + Task InitializeAsync(); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs index fd47d8b859..132c198aa9 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs @@ -124,7 +124,7 @@ namespace Volo.Abp.Reflection ? customAttributes.Concat(declaringTypeCustomAttributes).Distinct() : customAttributes; } - + /// /// Gets value of a property by it's full path from given object /// @@ -134,7 +134,7 @@ namespace Volo.Abp.Reflection var currentType = objectType; var objectPath = currentType.FullName; var absolutePropertyPath = propertyPath; - if (absolutePropertyPath.StartsWith(objectPath)) + if (objectPath != null && absolutePropertyPath.StartsWith(objectPath)) { absolutePropertyPath = absolutePropertyPath.Replace(objectPath + ".", ""); } @@ -142,8 +142,16 @@ namespace Volo.Abp.Reflection foreach (var propertyName in absolutePropertyPath.Split('.')) { var property = currentType.GetProperty(propertyName); - value = property.GetValue(value, null); - currentType = property.PropertyType; + if (property != null) + { + value = property.GetValue(value, null); + currentType = property.PropertyType; + } + else + { + value = null; + break; + } } return value; diff --git a/framework/src/Volo.Abp.Dapper/Volo.Abp.Dapper.csproj b/framework/src/Volo.Abp.Dapper/Volo.Abp.Dapper.csproj index 4feb946600..a2fd749d0e 100644 --- a/framework/src/Volo.Abp.Dapper/Volo.Abp.Dapper.csproj +++ b/framework/src/Volo.Abp.Dapper/Volo.Abp.Dapper.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netstandard2.1 Volo.Abp.Dapper Volo.Abp.Dapper $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DataSeeder.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/DataSeeder.cs index c20badbeb7..cc282007de 100644 --- a/framework/src/Volo.Abp.Data/Volo/Abp/Data/DataSeeder.cs +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/DataSeeder.cs @@ -9,12 +9,12 @@ namespace Volo.Abp.Data //TODO: Create a Volo.Abp.Data.Seeding namespace? public class DataSeeder : IDataSeeder, ITransientDependency { - protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } protected AbpDataSeedOptions Options { get; } public DataSeeder( IOptions options, - IHybridServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory) { ServiceScopeFactory = serviceScopeFactory; Options = options.Value; @@ -36,4 +36,4 @@ namespace Volo.Abp.Data } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/es.json b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/es.json new file mode 100644 index 0000000000..4563876228 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/es.json @@ -0,0 +1,6 @@ +{ + "culture": "es", + "texts": { + "MaxResultCountExceededExceptionMessage": "{0} no puede tener más de {1}! Incremente {2}.{3} en el lado del servidor para obtener más resultados." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/fr.json b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/fr.json new file mode 100644 index 0000000000..772754e665 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/fr.json @@ -0,0 +1,6 @@ +{ + "culture": "fr", + "texts": { + "MaxResultCountExceededExceptionMessage": "{0} ne peut pas être plus que {1}! Augmentez {2}. {3} du côté du serveur pour permettre plus de résultats." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/hu.json b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/hu.json new file mode 100644 index 0000000000..6ec29c05e5 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/hu.json @@ -0,0 +1,6 @@ +{ + "culture": "hu", + "texts": { + "MaxResultCountExceededExceptionMessage": "{0} nem lehet több mint {1}! Csökkentse {2}.{3} a szerver oldalon, hogy több redeményt kapjon." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs index bf2be8e8f0..fffde41ba7 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs @@ -57,7 +57,7 @@ namespace Volo.Abp.Application.Services protected override Task MapToGetListOutputDtoAsync(TEntity entity) { - return base.MapToGetOutputDtoAsync(entity); + return MapToGetOutputDtoAsync(entity); } protected override TEntityDto MapToGetListOutputDto(TEntity entity) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo.Abp.Ddd.Domain.csproj b/framework/src/Volo.Abp.Ddd.Domain/Volo.Abp.Ddd.Domain.csproj index ab162e60c7..9fd8702f22 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo.Abp.Ddd.Domain.csproj +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo.Abp.Ddd.Domain.csproj @@ -18,6 +18,7 @@ + diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs index 19b857553f..87965924e4 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs @@ -1,6 +1,7 @@ using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.EventBus; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Guids; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; @@ -20,7 +21,8 @@ namespace Volo.Abp.Domain typeof(AbpThreadingModule), typeof(AbpTimingModule), typeof(AbpUnitOfWorkModule), - typeof(AbpObjectMappingModule) + typeof(AbpObjectMappingModule), + typeof(AbpExceptionHandlingModule) )] public class AbpDddDomainModule : AbpModule { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntityToEtoMapper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntityToEtoMapper.cs index 15a41bad9d..2ba6aaebee 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntityToEtoMapper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/EntityToEtoMapper.cs @@ -9,13 +9,13 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed { public class EntityToEtoMapper : IEntityToEtoMapper, ITransientDependency { - protected IHybridServiceScopeFactory HybridServiceScopeFactory { get; } - + protected IServiceScopeFactory HybridServiceScopeFactory { get; } + protected AbpDistributedEntityEventOptions Options { get; } public EntityToEtoMapper( IOptions options, - IHybridServiceScopeFactory hybridServiceScopeFactory) + IServiceScopeFactory hybridServiceScopeFactory) { HybridServiceScopeFactory = hybridServiceScopeFactory; Options = options.Value; @@ -51,4 +51,4 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IReadOnlyRepository.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IReadOnlyRepository.cs index 03aad9c048..499a1e58be 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IReadOnlyRepository.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IReadOnlyRepository.cs @@ -2,12 +2,15 @@ using System.Linq; using System.Linq.Expressions; using Volo.Abp.Domain.Entities; +using Volo.Abp.Linq; namespace Volo.Abp.Domain.Repositories { public interface IReadOnlyRepository : IQueryable, IReadOnlyBasicRepository where TEntity : class, IEntity { + IAsyncQueryableExecuter AsyncExecuter { get; } + IQueryable WithDetails(); IQueryable WithDetails(params Expression>[] propertySelectors); diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryAsyncExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryAsyncExtensions.cs new file mode 100644 index 0000000000..f6191a152f --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryAsyncExtensions.cs @@ -0,0 +1,441 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.Domain.Repositories +{ + public static class RepositoryAsyncExtensions + { + #region Contains + + public static Task ContainsAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] T item, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.ContainsAsync(repository, item, cancellationToken); + } + + #endregion + + #region Any/All + + public static Task AnyAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AnyAsync(repository, predicate, cancellationToken); + } + + public static Task AllAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AllAsync(repository, predicate, cancellationToken); + } + + #endregion + + #region Count/LongCount + + public static Task CountAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.CountAsync(repository, cancellationToken); + } + + public static Task CountAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.CountAsync(repository, predicate, cancellationToken); + } + + public static Task LongCountAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.LongCountAsync(repository, cancellationToken); + } + + public static Task LongCountAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.LongCountAsync(repository, predicate, cancellationToken); + } + + #endregion + + #region First/FirstOrDefault + + public static Task FirstAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.FirstAsync(repository, cancellationToken); + } + + public static Task FirstAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.FirstAsync(repository, predicate, cancellationToken); + } + + public static Task FirstOrDefaultAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.FirstOrDefaultAsync(repository, cancellationToken); + } + + public static Task FirstOrDefaultAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.FirstOrDefaultAsync(repository, predicate, cancellationToken); + } + + #endregion + + #region Last/LastOrDefault + + public static Task LastAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.LastAsync(repository, cancellationToken); + } + + public static Task LastAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.LastAsync(repository, predicate, cancellationToken); + } + + public static Task LastOrDefaultAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.LastOrDefaultAsync(repository, cancellationToken); + } + + public static Task LastOrDefaultAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.LastOrDefaultAsync(repository, predicate, cancellationToken); + } + + #endregion + + #region Single/SingleOrDefault + + public static Task SingleAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SingleAsync(repository, cancellationToken); + } + + public static Task SingleAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SingleAsync(repository, predicate, cancellationToken); + } + + public static Task SingleOrDefaultAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SingleOrDefaultAsync(repository, cancellationToken); + } + + public static Task SingleOrDefaultAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> predicate, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SingleOrDefaultAsync(repository, predicate, cancellationToken); + } + + #endregion + + #region Min + + public static Task MinAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.MinAsync(repository, cancellationToken); + } + + public static Task MinAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.MinAsync(repository, selector, cancellationToken); + } + + #endregion + + #region Max + + public static Task MaxAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.MaxAsync(repository, cancellationToken); + } + + public static Task MaxAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.MaxAsync(repository, selector, cancellationToken); + } + + #endregion + + #region Sum + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + public static Task SumAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.SumAsync(repository, selector, cancellationToken); + } + + #endregion + + #region Average + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + public static Task AverageAsync( + [NotNull] this IReadOnlyRepository repository, + [NotNull] Expression> selector, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.AverageAsync(repository, selector, cancellationToken); + } + + #endregion + + #region ToList/Array + + public static Task> ToListAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.ToListAsync(repository, cancellationToken); + } + + public static Task ToArrayAsync( + [NotNull] this IReadOnlyRepository repository, + CancellationToken cancellationToken = default) + where T : class, IEntity + { + return repository.AsyncExecuter.ToArrayAsync(repository, cancellationToken); + } + + #endregion + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs index bd7e486d29..fd66b293c9 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; +using Volo.Abp.Linq; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; @@ -19,6 +20,8 @@ namespace Volo.Abp.Domain.Repositories public ICurrentTenant CurrentTenant { get; set; } + public IAsyncQueryableExecuter AsyncExecuter { get; set; } + public IUnitOfWorkManager UnitOfWorkManager { get; set; } public virtual Type ElementType => GetQueryable().ElementType; @@ -50,12 +53,12 @@ namespace Volo.Abp.Domain.Repositories protected abstract IQueryable GetQueryable(); public abstract Task FindAsync( - Expression> predicate, + Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default); public async Task GetAsync( - Expression> predicate, + Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) { @@ -107,4 +110,4 @@ namespace Volo.Abp.Domain.Repositories await DeleteAsync(entity, autoSave, cancellationToken); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json index 33c59be8b5..da1d7fde95 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json @@ -18,6 +18,8 @@ "Description:Abp.Mailing.Smtp.Password": "La contraseña del ususario asociado a las credenciales.", "Description:Abp.Mailing.Smtp.Domain": "El dominio o equipo que valida las credenciales.", "Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient usa Secure Sockets Layer (SSL) para encriptar la conección.", - "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se envían con los request." + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se envían con los request.", + "TextTemplate:StandardEmailTemplates.Layout": "Plantilla de email por defecto", + "TextTemplate:StandardEmailTemplates.Message": "Plantilla de mensaje simple para emails" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/fr.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/fr.json new file mode 100644 index 0000000000..a5cdbb768d --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/fr.json @@ -0,0 +1,25 @@ +{ + "culture": "fr", + "texts": { + "DisplayName:Abp.Mailing.DefaultFromAddress": "Adresse par défaut", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Valeur par défaut du nom d'affichage", + "DisplayName:Abp.Mailing.Smtp.Host": "Hôte", + "DisplayName:Abp.Mailing.Smtp.Port": "Port", + "DisplayName:Abp.Mailing.Smtp.UserName": "Nom d'utilisateur", + "DisplayName:Abp.Mailing.Smtp.Password": "Mot de passe", + "DisplayName:Abp.Mailing.Smtp.Domain": "Domaine", + "DisplayName:Abp.Mailing.Smtp.EnableSsl": "Activer SSL", + "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Utiliser les informations d'identification par défaut", + "Description:Abp.Mailing.DefaultFromAddress": "L'adresse d'expédition par défaut", + "Description:Abp.Mailing.DefaultFromDisplayName": "La valeur par défaut du nom d'affichage", + "Description:Abp.Mailing.Smtp.Host": "Le nom ou l'adresse IP de l'hôte utilisé pour les transactions SMTP.", + "Description:Abp.Mailing.Smtp.Port": "Le port utilisé pour les transactions SMTP.", + "Description:Abp.Mailing.Smtp.UserName": "Nom d'utilisateur associé aux informations d'identification.", + "Description:Abp.Mailing.Smtp.Password": "Le mot de passe du nom d'utilisateur associé aux informations d'identification.", + "Description:Abp.Mailing.Smtp.Domain": "Le nom de domaine ou d'ordinateur qui vérifie les informations d'identification.", + "Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient utilise SSL (Secure Sockets Layer) pour crypter la connexion.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si les DefaultCredentials sont envoyés avec les demandes.", + "TextTemplate:StandardEmailTemplates.Layout": "Modèle de mise en page d'e-mail par défaut", + "TextTemplate:StandardEmailTemplates.Message": "Modèle de message simple pour les e-mails" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/hu.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/hu.json new file mode 100644 index 0000000000..f2dbce2961 --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/hu.json @@ -0,0 +1,25 @@ +{ + "culture": "hu", + "texts": { + "DisplayName:Abp.Mailing.DefaultFromAddress": "Alapértlemezett feladó címe", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Feladó alapértelmezett megjelenő neve", + "DisplayName:Abp.Mailing.Smtp.Host": "Kiszolgáló", + "DisplayName:Abp.Mailing.Smtp.Port": "Port", + "DisplayName:Abp.Mailing.Smtp.UserName": "Felhasználónév", + "DisplayName:Abp.Mailing.Smtp.Password": "Jelszó", + "DisplayName:Abp.Mailing.Smtp.Domain": "Domain", + "DisplayName:Abp.Mailing.Smtp.EnableSsl": "SSL engedélyezése", + "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Használja az alapértelmezett hitelesítő adatokat", + "Description:Abp.Mailing.DefaultFromAddress": "Az alapértlemezett feladó címe", + "Description:Abp.Mailing.DefaultFromDisplayName": "A feladó alapértelmezett megjelenő neve", + "Description:Abp.Mailing.Smtp.Host": "Az SMTP kiszolgáló neve vagy IP címe.", + "Description:Abp.Mailing.Smtp.Port": "Az SMTP forgalomhoz használt port.", + "Description:Abp.Mailing.Smtp.UserName": "A hitelesítő adatokhoz társított felhasználói név..", + "Description:Abp.Mailing.Smtp.Password": "A hitelesítő adatokhoz társított felhasználói név jelszava.", + "Description:Abp.Mailing.Smtp.Domain": "A hitelesítő adatokat igazoló tartomány vagy számítógép neve.", + "Description:Abp.Mailing.Smtp.EnableSsl": "Engedélyezze, ha a levelező programja Secure Sockets Layer (SSL) protokollt használ a kapcsolat titkosításához.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Alkalmazza, hogy a kérésekhez az alapértelmezett hitelesítést használja.", + "TextTemplate:StandardEmailTemplates.Layout": "Alapértelmezett e-mail elrendezési sablon", + "TextTemplate:StandardEmailTemplates.Message": "Egyszerű üzenet sablon az e-mailekhez" + } + } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hans.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hans.json index 0e2d25bff1..5f7343e771 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hans.json +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/zh-Hans.json @@ -18,6 +18,8 @@ "Description:Abp.Mailing.Smtp.Password": "凭据关联的用户名的密码.", "Description:Abp.Mailing.Smtp.Domain": "验证凭据的域名或计算机名.", "Description:Abp.Mailing.Smtp.EnableSsl": "指定 SmtpClient 是否使用安全套接字层 (SSL) 加密连接.", - "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "控制默认凭据是否随请求一起发送." + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "控制默认凭据是否随请求一起发送.", + "TextTemplate:StandardEmailTemplates.Layout": "默认电子邮件模板", + "TextTemplate:StandardEmailTemplates.Message": "电子邮件的简单消息模板" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo.Abp.EntityFrameworkCore.MySQL.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo.Abp.EntityFrameworkCore.MySQL.csproj index d565f6c114..7bf7b5d74f 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo.Abp.EntityFrameworkCore.MySQL.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo.Abp.EntityFrameworkCore.MySQL.csproj @@ -2,9 +2,9 @@ - + - netstandard2.0 + netstandard2.1 Volo.Abp.EntityFrameworkCore.MySQL Volo.Abp.EntityFrameworkCore.MySQL $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -21,5 +21,5 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Microsoft/EntityFrameworkCore/AbpOracleModelBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Microsoft/EntityFrameworkCore/AbpOracleModelBuilderExtensions.cs index 8feec1715b..23943873c3 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Microsoft/EntityFrameworkCore/AbpOracleModelBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Microsoft/EntityFrameworkCore/AbpOracleModelBuilderExtensions.cs @@ -1,13 +1,13 @@ -using Volo.Abp.EntityFrameworkCore; - -namespace Microsoft.EntityFrameworkCore -{ - public static class AbpOracleModelBuilderExtensions - { - public static void UseOracle( - this ModelBuilder modelBuilder) - { - modelBuilder.SetDatabaseProvider(EfCoreDatabaseProvider.Oracle); - } - } -} \ No newline at end of file +// using Volo.Abp.EntityFrameworkCore; +// +// namespace Microsoft.EntityFrameworkCore +// { +// public static class AbpOracleModelBuilderExtensions +// { +// public static void UseOracle( +// this ModelBuilder modelBuilder) +// { +// modelBuilder.SetDatabaseProvider(EfCoreDatabaseProvider.Oracle); +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj index 8043a1ff8e..f42981a9ae 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleDevartExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleDevartExtensions.cs index 482ee8b9a8..ba70772c9e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleDevartExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleDevartExtensions.cs @@ -1,26 +1,26 @@ -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using System; -using Devart.Data.Oracle.Entity; -using Volo.Abp.EntityFrameworkCore.DependencyInjection; - -namespace Volo.Abp.EntityFrameworkCore -{ - public static class AbpDbContextConfigurationContextOracleDevartExtensions - { - public static DbContextOptionsBuilder UseOracle( - [NotNull] this AbpDbContextConfigurationContext context, - [CanBeNull] Action oracleOptionsAction = null, - bool useExistingConnectionIfAvailable = false) - { - if (useExistingConnectionIfAvailable && context.ExistingConnection != null) - { - return context.DbContextOptions.UseOracle(context.ExistingConnection, oracleOptionsAction); - } - else - { - return context.DbContextOptions.UseOracle(context.ConnectionString, oracleOptionsAction); - } - } - } -} +// using JetBrains.Annotations; +// using Microsoft.EntityFrameworkCore; +// using System; +// using Devart.Data.Oracle.Entity; +// using Volo.Abp.EntityFrameworkCore.DependencyInjection; +// +// namespace Volo.Abp.EntityFrameworkCore +// { +// public static class AbpDbContextConfigurationContextOracleDevartExtensions +// { +// public static DbContextOptionsBuilder UseOracle( +// [NotNull] this AbpDbContextConfigurationContext context, +// [CanBeNull] Action oracleOptionsAction = null, +// bool useExistingConnectionIfAvailable = false) +// { +// if (useExistingConnectionIfAvailable && context.ExistingConnection != null) +// { +// return context.DbContextOptions.UseOracle(context.ExistingConnection, oracleOptionsAction); +// } +// else +// { +// return context.DbContextOptions.UseOracle(context.ConnectionString, oracleOptionsAction); +// } +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleDevartExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleDevartExtensions.cs index 20887ceb79..99cfc84b8e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleDevartExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleDevartExtensions.cs @@ -1,32 +1,32 @@ -using JetBrains.Annotations; -using System; -using Devart.Data.Oracle.Entity; - -namespace Volo.Abp.EntityFrameworkCore -{ - public static class AbpDbContextOptionsOracleDevartExtensions - { - public static void UseOracle( - [NotNull] this AbpDbContextOptions options, - [CanBeNull] Action oracleOptionsAction = null, - bool useExistingConnectionIfAvailable = false) - { - options.Configure(context => - { - context.UseOracle(oracleOptionsAction, useExistingConnectionIfAvailable); - }); - } - - public static void UseOracle( - [NotNull] this AbpDbContextOptions options, - [CanBeNull] Action oracleOptionsAction = null, - bool useExistingConnectionIfAvailable = false) - where TDbContext : AbpDbContext - { - options.Configure(context => - { - context.UseOracle(oracleOptionsAction, useExistingConnectionIfAvailable); - }); - } - } -} +// using JetBrains.Annotations; +// using System; +// using Devart.Data.Oracle.Entity; +// +// namespace Volo.Abp.EntityFrameworkCore +// { +// public static class AbpDbContextOptionsOracleDevartExtensions +// { +// public static void UseOracle( +// [NotNull] this AbpDbContextOptions options, +// [CanBeNull] Action oracleOptionsAction = null, +// bool useExistingConnectionIfAvailable = false) +// { +// options.Configure(context => +// { +// context.UseOracle(oracleOptionsAction, useExistingConnectionIfAvailable); +// }); +// } +// +// public static void UseOracle( +// [NotNull] this AbpDbContextOptions options, +// [CanBeNull] Action oracleOptionsAction = null, +// bool useExistingConnectionIfAvailable = false) +// where TDbContext : AbpDbContext +// { +// options.Configure(context => +// { +// context.UseOracle(oracleOptionsAction, useExistingConnectionIfAvailable); +// }); +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs index 7b5970bb59..3b1b9cc337 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs @@ -1,22 +1,22 @@ -using Volo.Abp.Guids; -using Volo.Abp.Modularity; - -namespace Volo.Abp.EntityFrameworkCore.Oracle.Devart -{ - [DependsOn( - typeof(AbpEntityFrameworkCoreModule) - )] - public class AbpEntityFrameworkCoreOracleDevartModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - if (options.DefaultSequentialGuidType == null) - { - options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; - } - }); - } - } -} \ No newline at end of file +// using Volo.Abp.Guids; +// using Volo.Abp.Modularity; +// +// namespace Volo.Abp.EntityFrameworkCore.Oracle.Devart +// { +// [DependsOn( +// typeof(AbpEntityFrameworkCoreModule) +// )] +// public class AbpEntityFrameworkCoreOracleDevartModule : AbpModule +// { +// public override void ConfigureServices(ServiceConfigurationContext context) +// { +// Configure(options => +// { +// if (options.DefaultSequentialGuidType == null) +// { +// options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; +// } +// }); +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj index be0537ae96..7611559008 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleExtensions.cs index a5b898b286..cb3a92d2cf 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextOracleExtensions.cs @@ -1,25 +1,25 @@ -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using System; -using Oracle.EntityFrameworkCore.Infrastructure; -using Volo.Abp.EntityFrameworkCore.DependencyInjection; - -namespace Volo.Abp.EntityFrameworkCore -{ - public static class AbpDbContextConfigurationContextOracleExtensions - { - public static DbContextOptionsBuilder UseOracle( - [NotNull] this AbpDbContextConfigurationContext context, - [CanBeNull] Action oracleOptionsAction = null) - { - if (context.ExistingConnection != null) - { - return context.DbContextOptions.UseOracle(context.ExistingConnection, oracleOptionsAction); - } - else - { - return context.DbContextOptions.UseOracle(context.ConnectionString, oracleOptionsAction); - } - } - } -} +// using JetBrains.Annotations; +// using Microsoft.EntityFrameworkCore; +// using System; +// using Oracle.EntityFrameworkCore.Infrastructure; +// using Volo.Abp.EntityFrameworkCore.DependencyInjection; +// +// namespace Volo.Abp.EntityFrameworkCore +// { +// public static class AbpDbContextConfigurationContextOracleExtensions +// { +// public static DbContextOptionsBuilder UseOracle( +// [NotNull] this AbpDbContextConfigurationContext context, +// [CanBeNull] Action oracleOptionsAction = null) +// { +// if (context.ExistingConnection != null) +// { +// return context.DbContextOptions.UseOracle(context.ExistingConnection, oracleOptionsAction); +// } +// else +// { +// return context.DbContextOptions.UseOracle(context.ConnectionString, oracleOptionsAction); +// } +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleExtensions.cs index f4817d5ab4..f5f9dc7f61 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsOracleExtensions.cs @@ -1,30 +1,30 @@ -using JetBrains.Annotations; -using System; -using Oracle.EntityFrameworkCore.Infrastructure; - -namespace Volo.Abp.EntityFrameworkCore -{ - public static class AbpDbContextOptionsOracleExtensions - { - public static void UseOracle( - [NotNull] this AbpDbContextOptions options, - [CanBeNull] Action oracleOptionsAction = null) - { - options.Configure(context => - { - context.UseOracle(oracleOptionsAction); - }); - } - - public static void UseOracle( - [NotNull] this AbpDbContextOptions options, - [CanBeNull] Action oracleOptionsAction = null) - where TDbContext : AbpDbContext - { - options.Configure(context => - { - context.UseOracle(oracleOptionsAction); - }); - } - } -} +// using JetBrains.Annotations; +// using System; +// using Oracle.EntityFrameworkCore.Infrastructure; +// +// namespace Volo.Abp.EntityFrameworkCore +// { +// public static class AbpDbContextOptionsOracleExtensions +// { +// public static void UseOracle( +// [NotNull] this AbpDbContextOptions options, +// [CanBeNull] Action oracleOptionsAction = null) +// { +// options.Configure(context => +// { +// context.UseOracle(oracleOptionsAction); +// }); +// } +// +// public static void UseOracle( +// [NotNull] this AbpDbContextOptions options, +// [CanBeNull] Action oracleOptionsAction = null) +// where TDbContext : AbpDbContext +// { +// options.Configure(context => +// { +// context.UseOracle(oracleOptionsAction); +// }); +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs index 3b7c801462..21fad9375a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs @@ -1,22 +1,22 @@ -using Volo.Abp.Guids; -using Volo.Abp.Modularity; - -namespace Volo.Abp.EntityFrameworkCore.Oracle -{ - [DependsOn( - typeof(AbpEntityFrameworkCoreModule) - )] - public class AbpEntityFrameworkCoreOracleModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - if (options.DefaultSequentialGuidType == null) - { - options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; - } - }); - } - } -} \ No newline at end of file +// using Volo.Abp.Guids; +// using Volo.Abp.Modularity; +// +// namespace Volo.Abp.EntityFrameworkCore.Oracle +// { +// [DependsOn( +// typeof(AbpEntityFrameworkCoreModule) +// )] +// public class AbpEntityFrameworkCoreOracleModule : AbpModule +// { +// public override void ConfigureServices(ServiceConfigurationContext context) +// { +// Configure(options => +// { +// if (options.DefaultSequentialGuidType == null) +// { +// options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; +// } +// }); +// } +// } +// } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj index 96de9fdd1c..8e325342d4 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netstandard2.1 Volo.Abp.EntityFrameworkCore.PostgreSql Volo.Abp.EntityFrameworkCore.PostgreSql $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj index 366958b213..ff0e06ebae 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netstandard2.1 Volo.Abp.EntityFrameworkCore.SqlServer Volo.Abp.EntityFrameworkCore.SqlServer $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj index 6a8fd26db2..010f419374 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netstandard2.1 Volo.Abp.EntityFrameworkCore.Sqlite Volo.Abp.EntityFrameworkCore.Sqlite $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj b/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj index f569b14da3..dc6e1f0534 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netstandard2.1 Volo.Abp.EntityFrameworkCore Volo.Abp.EntityFrameworkCore $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -21,8 +21,8 @@ - - + + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs index a019d29a49..2463062cde 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs @@ -5,32 +5,29 @@ using Volo.Abp.Domain.Repositories.EntityFrameworkCore; namespace Volo.Abp.Domain.Repositories { - //TODO: Should work for any IRepository implementation - public static class EfCoreRepositoryExtensions { - public static DbContext GetDbContext(this IReadOnlyBasicRepository repository) - where TEntity : class, IEntity + public static DbContext GetDbContext(this IReadOnlyBasicRepository repository) + where TEntity : class, IEntity { return repository.ToEfCoreRepository().DbContext; } - public static DbSet GetDbSet(this IReadOnlyBasicRepository repository) - where TEntity : class, IEntity + public static DbSet GetDbSet(this IReadOnlyBasicRepository repository) + where TEntity : class, IEntity { return repository.ToEfCoreRepository().DbSet; } - public static IEfCoreRepository ToEfCoreRepository(this IReadOnlyBasicRepository repository) - where TEntity : class, IEntity + public static IEfCoreRepository ToEfCoreRepository(this IReadOnlyBasicRepository repository) + where TEntity : class, IEntity { - var efCoreRepository = repository as IEfCoreRepository; - if (efCoreRepository == null) + if (repository is IEfCoreRepository efCoreRepository) { - throw new ArgumentException("Given repository does not implement " + typeof(IEfCoreRepository).AssemblyQualifiedName, nameof(repository)); + return efCoreRepository; } - return efCoreRepository; + throw new ArgumentException("Given repository does not implement " + typeof(IEfCoreRepository).AssemblyQualifiedName, nameof(repository)); } } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index c82912f0f1..9a61e7f8e3 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using System.Globalization; using System.Linq; @@ -22,6 +23,7 @@ using Volo.Abp.EntityFrameworkCore.EntityHistory; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.Guids; +using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectExtending; using Volo.Abp.Reflection; @@ -339,7 +341,20 @@ namespace Volo.Abp.EntityFrameworkCore { if (TypeHelper.IsPrimitiveExtended(entryProperty.Metadata.ClrType, includeEnums: true)) { - entryProperty.CurrentValue = Convert.ChangeType(entityProperty, entryProperty.Metadata.ClrType, CultureInfo.InvariantCulture); + var conversionType = entryProperty.Metadata.ClrType; + if (TypeHelper.IsNullable(conversionType)) + { + conversionType = conversionType.GetFirstGenericArgumentIfNullable(); + } + + if (conversionType == typeof(Guid)) + { + entryProperty.CurrentValue = TypeDescriptor.GetConverter(conversionType).ConvertFromInvariantString(entityProperty.ToString()); + } + else + { + entryProperty.CurrentValue = Convert.ChangeType(entityProperty, conversionType, CultureInfo.InvariantCulture); + } } } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DatabaseFacadeExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DatabaseFacadeExtensions.cs deleted file mode 100644 index 55bb9428e8..0000000000 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DatabaseFacadeExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; - -namespace Volo.Abp.EntityFrameworkCore -{ - public static class DatabaseFacadeExtensions - { - public static bool IsRelational(this DatabaseFacade database) - { -#pragma warning disable EF1001 // Internal EF Core API usage. - return ((IDatabaseFacadeDependenciesAccessor)database).Dependencies is IRelationalDatabaseFacadeDependencies; -#pragma warning restore EF1001 // Internal EF Core API usage. - } - } -} diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj b/framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj index 42b0293781..cbf8d78aea 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj @@ -2,7 +2,7 @@ - + netstandard2.0 diff --git a/framework/src/Volo.Abp.EventBus.Rebus/FodyWeavers.xml b/framework/src/Volo.Abp.EventBus.Rebus/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Rebus/FodyWeavers.xsd b/framework/src/Volo.Abp.EventBus.Rebus/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo.Abp.EventBus.Rebus.csproj b/framework/src/Volo.Abp.EventBus.Rebus/Volo.Abp.EventBus.Rebus.csproj new file mode 100644 index 0000000000..0a01911990 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo.Abp.EventBus.Rebus.csproj @@ -0,0 +1,26 @@ + + + + + + + netstandard2.0 + Volo.Abp.EventBus.Rebus + Volo.Abp.EventBus.Rebus + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs new file mode 100644 index 0000000000..9ebd91a6a5 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpEventBusRebusModule.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using Rebus.Handlers; +using Rebus.ServiceProvider; +using Volo.Abp.Modularity; + +namespace Volo.Abp.EventBus.Rebus +{ + [DependsOn( + typeof(AbpEventBusModule))] + public class AbpEventBusRebusModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var options = context.Services.ExecutePreConfiguredActions(); + + context.Services.AddTransient(typeof(IHandleMessages<>), typeof(RebusDistributedEventHandlerAdapter<>)); + + Configure(rebusOptions => + { + rebusOptions.Configurer = options.Configurer; + rebusOptions.Publish = options.Publish; + rebusOptions.InputQueueName = options.InputQueueName; + }); + + context.Services.AddRebus(configurer => + { + options.Configurer?.Invoke(configurer); + return configurer; + }); + + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + context.ServiceProvider.UseRebus(); + + context + .ServiceProvider + .GetRequiredService() + .Initialize(); + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpRebusEventBusOptions.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpRebusEventBusOptions.cs new file mode 100644 index 0000000000..8aaee7bd63 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/AbpRebusEventBusOptions.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Rebus.Bus; +using Rebus.Config; +using Rebus.Persistence.InMem; +using Rebus.Transport.InMem; + +namespace Volo.Abp.EventBus.Rebus +{ + public class AbpRebusEventBusOptions + { + [NotNull] + public string InputQueueName { get; set; } + + [NotNull] + public Action Configurer + { + get => _configurer; + set => _configurer = Check.NotNull(value, nameof(value)); + } + private Action _configurer; + + [NotNull] + public Func Publish + { + get => _publish; + set => _publish = Check.NotNull(value, nameof(value)); + } + private Func _publish; + + public AbpRebusEventBusOptions() + { + _publish = DefaultPublish; + _configurer = DefaultConfigurer; + } + + private async Task DefaultPublish(IBus bus, Type eventType, object eventData) + { + await bus.Advanced.Routing.Send(InputQueueName, eventData); + } + + private void DefaultConfigurer(RebusConfigurer configurer) + { + configurer.Subscriptions(s => s.StoreInMemory()); + configurer.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), InputQueueName)); + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs new file mode 100644 index 0000000000..ce3549646e --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Rebus.Bus; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; + +namespace Volo.Abp.EventBus.Rebus +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDistributedEventBus), typeof(RebusDistributedEventBus))] + public class RebusDistributedEventBus : EventBusBase, IDistributedEventBus, ISingletonDependency + { + protected IBus Rebus { get; } + + //TODO: Accessing to the List may not be thread-safe! + protected ConcurrentDictionary> HandlerFactories { get; } + protected ConcurrentDictionary EventTypes { get; } + protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; } + protected AbpRebusEventBusOptions AbpRebusEventBusOptions { get; } + + public RebusDistributedEventBus( + IServiceScopeFactory serviceScopeFactory, + ICurrentTenant currentTenant, + IBus rebus, + IOptions abpDistributedEventBusOptions, + IOptions abpEventBusRebusOptions) : + base(serviceScopeFactory, currentTenant) + { + Rebus = rebus; + AbpRebusEventBusOptions = abpEventBusRebusOptions.Value; + AbpDistributedEventBusOptions = abpDistributedEventBusOptions.Value; + + HandlerFactories = new ConcurrentDictionary>(); + EventTypes = new ConcurrentDictionary(); + } + + public void Initialize() + { + SubscribeHandlers(AbpDistributedEventBusOptions.Handlers); + } + + public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) + { + var handlerFactories = GetOrCreateHandlerFactories(eventType); + + if (factory.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(factory); + + if (handlerFactories.Count == 1) //TODO: Multi-threading! + { + Rebus.Subscribe(eventType); + } + + return new EventHandlerFactoryUnregistrar(this, eventType, factory); + } + + public override void Unsubscribe(Func action) + { + Check.NotNull(action, nameof(action)); + + GetOrCreateHandlerFactories(typeof(TEvent)) + .Locking(factories => + { + factories.RemoveAll( + factory => + { + if (!(factory is SingleInstanceHandlerFactory singleInstanceFactory)) + { + return false; + } + + if (!(singleInstanceFactory.HandlerInstance is ActionEventHandler actionHandler)) + { + return false; + } + + return actionHandler.Action == action; + }); + }); + + Rebus.Unsubscribe(typeof(TEvent)); + } + + public override void Unsubscribe(Type eventType, IEventHandler handler) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory handlerFactory && + handlerFactory.HandlerInstance == handler + ); + }); + + Rebus.Unsubscribe(eventType); + } + + public override void Unsubscribe(Type eventType, IEventHandlerFactory factory) + { + GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory)); + Rebus.Unsubscribe(eventType); + } + + public override void UnsubscribeAll(Type eventType) + { + GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); + Rebus.Unsubscribe(eventType); + } + + public IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class + { + return Subscribe(typeof(TEvent), handler); + } + + public override async Task PublishAsync(Type eventType, object eventData) + { + await AbpRebusEventBusOptions.Publish(Rebus, eventType, eventData); + } + + private List GetOrCreateHandlerFactories(Type eventType) + { + return HandlerFactories.GetOrAdd( + eventType, + type => + { + var eventName = EventNameAttribute.GetNameOrDefault(type); + EventTypes[eventName] = type; + return new List(); + } + ); + } + + protected override IEnumerable GetHandlerFactories(Type eventType) + { + var handlerFactoryList = new List(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) + ) + { + handlerFactoryList.Add( + new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); + } + + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) + { + //Should trigger same type + if (handlerEventType == targetEventType) + { + return true; + } + + //Should trigger for inherited types + if (handlerEventType.IsAssignableFrom(targetEventType)) + { + return true; + } + + return false; + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventHandlerAdapter.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventHandlerAdapter.cs new file mode 100644 index 0000000000..005226b885 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventHandlerAdapter.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Rebus.Handlers; + +namespace Volo.Abp.EventBus.Rebus +{ + public class RebusDistributedEventHandlerAdapter : IHandleMessages + { + protected RebusDistributedEventBus RebusDistributedEventBus { get; } + + public RebusDistributedEventHandlerAdapter(RebusDistributedEventBus rebusDistributedEventBus) + { + RebusDistributedEventBus = rebusDistributedEventBus; + } + + public async Task Handle(TEventData message) + { + await RebusDistributedEventBus.TriggerHandlersAsync(typeof(TEventData), message); + } + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/FodyWeavers.xml b/framework/src/Volo.Abp.ExceptionHandling/FodyWeavers.xml new file mode 100644 index 0000000000..bc5a74a236 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.ExceptionHandling/FodyWeavers.xsd b/framework/src/Volo.Abp.ExceptionHandling/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo.Abp.ExceptionHandling.csproj b/framework/src/Volo.Abp.ExceptionHandling/Volo.Abp.ExceptionHandling.csproj new file mode 100644 index 0000000000..d0aa06056e --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo.Abp.ExceptionHandling.csproj @@ -0,0 +1,25 @@ + + + + + + + netstandard2.0 + true + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs similarity index 93% rename from framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs rename to framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs index 339b249e49..6367bd24ff 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/DefaultExceptionToErrorInfoConverter.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using Localization.Resources.AbpUi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; @@ -10,6 +9,7 @@ using Volo.Abp.Authorization; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.ExceptionHandling; +using Volo.Abp.ExceptionHandling.Localization; using Volo.Abp.Http; using Volo.Abp.Localization; using Volo.Abp.Localization.ExceptionHandling; @@ -20,28 +20,25 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling public class DefaultExceptionToErrorInfoConverter : IExceptionToErrorInfoConverter, ITransientDependency { protected AbpExceptionLocalizationOptions LocalizationOptions { get; } - protected AbpExceptionHandlingOptions ExceptionHandlingOptions { get; } protected IStringLocalizerFactory StringLocalizerFactory { get; } - protected IStringLocalizer L { get; } + protected IStringLocalizer L { get; } protected IServiceProvider ServiceProvider { get; } public DefaultExceptionToErrorInfoConverter( IOptions localizationOptions, - IOptions exceptionHandlingOptions, IStringLocalizerFactory stringLocalizerFactory, - IStringLocalizer abpUiStringLocalizer, + IStringLocalizer stringLocalizer, IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; - ExceptionHandlingOptions = exceptionHandlingOptions.Value; StringLocalizerFactory = stringLocalizerFactory; - L = abpUiStringLocalizer; + L = stringLocalizer; LocalizationOptions = localizationOptions.Value; } - public RemoteServiceErrorInfo Convert(Exception exception) + public RemoteServiceErrorInfo Convert(Exception exception, bool includeSensitiveDetails) { - var errorInfo = CreateErrorInfoWithoutCode(exception); + var errorInfo = CreateErrorInfoWithoutCode(exception, includeSensitiveDetails); if (exception is IHasErrorCode hasErrorCodeException) { @@ -51,9 +48,9 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling return errorInfo; } - protected virtual RemoteServiceErrorInfo CreateErrorInfoWithoutCode(Exception exception) + protected virtual RemoteServiceErrorInfo CreateErrorInfoWithoutCode(Exception exception, bool includeSensitiveDetails) { - if (ExceptionHandlingOptions.SendExceptionsDetailsToClients) + if (includeSensitiveDetails) { return CreateDetailedErrorInfoFromException(exception); } @@ -179,10 +176,9 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling { var aggException = exception as AggregateException; - if (aggException.InnerException is IUserFriendlyException || - aggException.InnerException is AbpValidationException || - aggException.InnerException is EntityNotFoundException || + if (aggException.InnerException is AbpValidationException || aggException.InnerException is AbpAuthorizationException || + aggException.InnerException is EntityNotFoundException || aggException.InnerException is IBusinessException) { return aggException.InnerException; diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/IExceptionToErrorInfoConverter.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/IExceptionToErrorInfoConverter.cs similarity index 72% rename from framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/IExceptionToErrorInfoConverter.cs rename to framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/IExceptionToErrorInfoConverter.cs index a25206a918..3293be71ee 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/IExceptionToErrorInfoConverter.cs +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/AspNetCore/ExceptionHandling/IExceptionToErrorInfoConverter.cs @@ -4,7 +4,7 @@ using Volo.Abp.Http; namespace Volo.Abp.AspNetCore.ExceptionHandling { /// - /// This interface can be implemented to convert an object to an object. + /// This interface can be implemented to convert an object to an object. /// Implements Chain Of Responsibility pattern. /// public interface IExceptionToErrorInfoConverter @@ -13,7 +13,8 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling /// Converter method. /// /// The exception + /// Should include sensitive details to the error info? /// Error info or null - RemoteServiceErrorInfo Convert(Exception exception); + RemoteServiceErrorInfo Convert(Exception exception, bool includeSensitiveDetails); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Domain/Entities/EntityNotFoundException.cs similarity index 100% rename from framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs rename to framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Domain/Entities/EntityNotFoundException.cs diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/AbpExceptionHandlingModule.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/AbpExceptionHandlingModule.cs new file mode 100644 index 0000000000..12ed60299d --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/AbpExceptionHandlingModule.cs @@ -0,0 +1,33 @@ +using Volo.Abp.ExceptionHandling.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace Volo.Abp.ExceptionHandling +{ + /* TODO: This package is introduced in a later time by gathering + * classes from multiple packages. + * So, didn't change the original namespaces of the types to not introduce breaking changes. + * We will re-design this package in a later time, probably with v5.0. + */ + [DependsOn( + typeof(AbpLocalizationModule) + )] + public class AbpExceptionHandlingModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add("en") + .AddVirtualJson("/Volo/Abp/ExceptionHandling/Localization"); + }); + } + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/AbpExceptionHandlingResource.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/AbpExceptionHandlingResource.cs new file mode 100644 index 0000000000..fe73b72aa1 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/AbpExceptionHandlingResource.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Localization; + +namespace Volo.Abp.ExceptionHandling.Localization +{ + [LocalizationResourceName("AbpExceptionHandling")] + public class AbpExceptionHandlingResource + { + + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json new file mode 100644 index 0000000000..bd2a274d94 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json @@ -0,0 +1,23 @@ +{ + "culture": "ar", + "texts": { + "InternalServerErrorMessage": "حدث خطأ داخلي أثناء طلبك!", + "ValidationErrorMessage": "طلبك غير صحيح!", + "ValidationNarrativeErrorMessageTitle": "تم الكشف عن الأخطاء التالية أثناء التحقق .", + "DefaultErrorMessage": "حدث خطأ!", + "DefaultErrorMessageDetail": "لم يتم إرسال تفاصيل الخطأ بواسطة الخادم.", + "DefaultErrorMessage401": "أنت غير مصدق!", + "DefaultErrorMessage401Detail": "يجب عليك تسجيل الدخول لأداء هذه العملية.", + "DefaultErrorMessage403": "أنك غير مخول!", + "DefaultErrorMessage403Detail": "لا يسمح لك بإجراء هذه العملية!", + "DefaultErrorMessage404": "المورد غير موجود!", + "DefaultErrorMessage404Detail": "لم يتم العثور على المورد المطلوب على الخادم!", + "EntityNotFoundErrorMessage": "لا يوجد كيان {0} بالمعرف = {1}!", + "Error": "خطأ", + "UnhandledException": "استثناء غير معالج!", + "401Message": "غير مصرح", + "403Message": "ممنوع", + "404Message": "الصفحة غير موجودة", + "500Message": "خطأ في الخادم الداخلي" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json new file mode 100644 index 0000000000..87c4fe6545 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/cs.json @@ -0,0 +1,23 @@ +{ + "culture": "cs", + "texts": { + "InternalServerErrorMessage": "Během požadavku se vyskytla vnitřní chyba!", + "ValidationErrorMessage": "Neplatný požadavek!", + "ValidationNarrativeErrorMessageTitle": "Během ověřování byly zjištěny následující chyby.", + "DefaultErrorMessage": "Vyskytla se chyba!", + "DefaultErrorMessageDetail": "Server neposkytl detaily k chybě.", + "DefaultErrorMessage401": "Nejste přihlášeni!", + "DefaultErrorMessage401Detail": "K vykonání této operace se musíte nejprve přihlásit.", + "DefaultErrorMessage403": "Nemáte oprávnění!", + "DefaultErrorMessage403Detail": "Není vám dovoleno vykonat tuto operaci!", + "DefaultErrorMessage404": "Prostředek nenalezen!", + "DefaultErrorMessage404Detail": "Vyžádaný prostředek nebyl na serveru nalezen!", + "EntityNotFoundErrorMessage": "Neexistující entita {0} s id = {1}!", + "Error": "Chyba", + "UnhandledException": "Neošetřená výjimka!", + "401Message": "Neoprávněno", + "403Message": "Zakázáno", + "404Message": "Stránka nenalezena", + "500Message": "Interní chyba serveru" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json new file mode 100644 index 0000000000..585e858cb3 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/de.json @@ -0,0 +1,23 @@ +{ + "culture": "de", + "texts": { + "InternalServerErrorMessage": "Während Ihrer Anfrage ist ein interner Fehler aufgetreten!", + "ValidationErrorMessage": "Ihre Anfrage ist nicht gültig!", + "ValidationNarrativeErrorMessageTitle": "Die folgenden Fehler wurden bei der Validierung entdeckt.", + "DefaultErrorMessage": "Ein Fehler ist aufgetreten!", + "DefaultErrorMessageDetail": "Es wurden keine Fehlerdetails vom Server gesendet.", + "DefaultErrorMessage401": "Sie sind nicht authentifiziert.", + "DefaultErrorMessage401Detail": "Sie sollten sich anmelden, um diese Operation durchzuführen.", + "DefaultErrorMessage403": "Sie sind nicht autorisiert!", + "DefaultErrorMessage403Detail": "Es ist Ihnen nicht erlaubt, diese Operation durchzuführen!", + "DefaultErrorMessage404": "Ressource nicht gefunden!", + "DefaultErrorMessage404Detail": "Die angeforderte Ressource konnte auf dem Server nicht gefunden werden!", + "EntityNotFoundErrorMessage": "Es gibt keine Entität {0} mit id = {1}!", + "Error": "Fehler", + "UnhandledException": "Unerwartete Ausnahme!", + "401Message": "Unauthorisiert", + "403Message": "Verboten", + "404Message": "Seite nicht gefunden", + "500Message": "Interner Server Fehler" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json new file mode 100644 index 0000000000..62b599b7d1 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/en.json @@ -0,0 +1,25 @@ +{ + "culture": "en", + "texts": { + "InternalServerErrorMessage": "An internal error occurred during your request!", + "ValidationErrorMessage": "Your request is not valid!", + "ValidationNarrativeErrorMessageTitle": "The following errors were detected during validation.", + "DefaultErrorMessage": "An error has occurred!", + "DefaultErrorMessageDetail": "Error detail not sent by server.", + "DefaultErrorMessage401": "You are not authenticated!", + "DefaultErrorMessage401Detail": "You should sign in to perform this operation.", + "DefaultErrorMessage403": "You are not authorized!", + "DefaultErrorMessage403Detail": "You are not allowed to perform this operation!", + "DefaultErrorMessage404": "Resource not found!", + "DefaultErrorMessage404Detail": "The resource requested could not found on the server!", + "EntityNotFoundErrorMessage": "There is no entity {0} with id = {1}!", + "Error": "Error", + "UnhandledException": "Unhandled exception!", + "401Message": "Unauthorized", + "403Message": "Forbidden", + "404Message": "Page not found", + "500Message": "Internal Server Error", + "403MessageDetail": "You are not authorized to perform this operation!", + "404MessageDetail": "Sorry, there's nothing at this address." + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json new file mode 100644 index 0000000000..b4ac4139b1 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/es.json @@ -0,0 +1,23 @@ +{ + "culture": "es", + "texts": { + "InternalServerErrorMessage": "Ocurrió un error interno en su pedido!", + "ValidationErrorMessage": "Su pedido no es válido!", + "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.", + "DefaultErrorMessage": "Ocurrió un error!", + "DefaultErrorMessageDetail": "El servidor no envió el detalle del error.", + "DefaultErrorMessage401": "Usted no ha iniciado sessión!", + "DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.", + "DefaultErrorMessage403": "Usted no está autorizado!", + "DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!", + "DefaultErrorMessage404": "No se encontró el recurso!", + "DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!", + "EntityNotFoundErrorMessage": "No hay una entidad {0} con el id = {1}!", + "Error": "Error", + "UnhandledException": "Excepción no controlada!", + "401Message": "No autorizado", + "403Message": "Prohibido", + "404Message": "La página no existe", + "500Message": "Error interno del servidor" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json new file mode 100644 index 0000000000..f1c9713e65 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/fr.json @@ -0,0 +1,23 @@ +{ + "culture": "fr", + "texts": { + "InternalServerErrorMessage": "Une erreur interne s'est produite lors de votre demande!", + "ValidationErrorMessage": "Votre demande n'est pas valide!", + "ValidationNarrativeErrorMessageTitle": "Les erreurs suivantes ont été détectées lors de la validation.", + "DefaultErrorMessage": "Une erreur est survenue!", + "DefaultErrorMessageDetail": "Détails de l'erreur non envoyés par le serveur.", + "DefaultErrorMessage401": "Vous n'êtes pas authentifié!", + "DefaultErrorMessage401Detail": "Vous devez vous connecter pour effectuer cette opération.", + "DefaultErrorMessage403": "Vous n'êtes pas autorisé!", + "DefaultErrorMessage403Detail": "Vous n'êtes pas autorisé à effectuer cette opération!", + "DefaultErrorMessage404": "Ressource introuvable!", + "DefaultErrorMessage404Detail": "La ressource demandée est introuvable sur le serveur!", + "EntityNotFoundErrorMessage": "Il n'y a pas d'entité {0} avec id = {1}!", + "Error": "Erreur", + "UnhandledException": "Exception non-gérée!", + "401Message": "Non autorisé", + "403Message": "Interdit", + "404Message": "Page introuvable", + "500Message": "Erreur Interne du Serveur" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json new file mode 100644 index 0000000000..18ef0e4be1 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/hu.json @@ -0,0 +1,23 @@ +{ + "culture": "hu", + "texts": { + "InternalServerErrorMessage": "Belső hiba történt a kérése során!", + "ValidationErrorMessage": "Kérése érvénytelen!", + "ValidationNarrativeErrorMessageTitle": "A validálás során a következő hibákat észleltük.", + "DefaultErrorMessage": "Hiba történt!", + "DefaultErrorMessageDetail": "A hiba részleteit nem a szerver küldte el.", + "DefaultErrorMessage401": "Nincs hitelesítve!", + "DefaultErrorMessage401Detail": "A művelet végrehajtásához be kell jelentkeznie.", + "DefaultErrorMessage403": "Ön nem jogosult!", + "DefaultErrorMessage403Detail": "Ezt a műveletet nem hajthatja végre!", + "DefaultErrorMessage404": "Az erőforrás nem található!", + "DefaultErrorMessage404Detail": "A kért erőforrás nem található a szerveren", + "EntityNotFoundErrorMessage": "Nincs {0} elem, amelynek id értéke {1}!", + "Error": "Nincs {0} elem, amelynek id értéke {1}!", + "UnhandledException": "Nem kezelt kivétel!", + "401Message": "Jogosulatlan", + "403Message": "Tiltott", + "404Message": "Az oldal nem található", + "500Message": "Belső Szerverhiba" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json new file mode 100644 index 0000000000..f96c1f9a9c --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/nl.json @@ -0,0 +1,23 @@ +{ + "culture": "nl", + "texts": { + "InternalServerErrorMessage": "Er is een interne fout opgetreden tijdens uw verzoek!", + "ValidationErrorMessage": "Uw verzoek is niet geldig!", + "ValidationNarrativeErrorMessageTitle": "Tijdens de validatie zijn de volgende fouten gedetecteerd.", + "DefaultErrorMessage": "er is een fout opgetreden!", + "DefaultErrorMessageDetail": "Foutdetails niet verzonden door server.", + "DefaultErrorMessage401": "U bent niet geverifieerd!", + "DefaultErrorMessage401Detail": "U moet inloggen om deze bewerking uit te voeren.", + "DefaultErrorMessage403": "U bent niet geautoriseerd!", + "DefaultErrorMessage403Detail": "U mag deze bewerking niet uitvoeren!", + "DefaultErrorMessage404": "Bron niet gevonden!", + "DefaultErrorMessage404Detail": "De gevraagde bron kan niet worden gevonden op de server!", + "EntityNotFoundErrorMessage": "Er is geen entiteit {0} met id = {1}!", + "Error": "Fout", + "UnhandledException": "Onverwerkte uitzondering!", + "401Message": "Ongeautoriseerd", + "403Message": "Verboden", + "404Message": "Pagina niet gevonden", + "500Message": "Interne Server Fout" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json new file mode 100644 index 0000000000..74105cb1d4 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pl-PL.json @@ -0,0 +1,18 @@ +{ + "culture": "pl-PL", + "texts": { + "InternalServerErrorMessage": "Błąd wewnętrzny serwera podczas przetwarzania żądania!", + "ValidationErrorMessage": "Twoje żądanie jest niepoprawnie!", + "ValidationNarrativeErrorMessageTitle": "Wykryto następujące błędy podczas walidacji.", + "DefaultErrorMessage": "Wystąpił błąd!", + "DefaultErrorMessageDetail": "Treść błędu nie została wysłana z serwera.", + "DefaultErrorMessage401": "Nie jesteś zalogowany!", + "DefaultErrorMessage401Detail": "Musisz się zalogować, aby wykonać tą operację.", + "DefaultErrorMessage403": "Nie jesteś zautentykowany!", + "DefaultErrorMessage403Detail": "Nie masz uprawnień do wykonania tej operacji!", + "DefaultErrorMessage404": "Nie znaleziono zasobu!", + "DefaultErrorMessage404Detail": "Nie znaleziono zasobu z żądania na serwerze!", + "EntityNotFoundErrorMessage": "Nie istnieje encja {0} z id = {1}!", + "Error": "Błąd" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json new file mode 100644 index 0000000000..cfee21e1b0 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/pt-BR.json @@ -0,0 +1,23 @@ +{ + "culture": "pt-BR", + "texts": { + "InternalServerErrorMessage": "Um erro interno ocorreu durante a requisição!", + "ValidationErrorMessage": "Sua requisição não é válida!", + "ValidationNarrativeErrorMessageTitle": "Os seguintes erros foram detectados durante a validação:", + "DefaultErrorMessage": "Ocorreu um erro!", + "DefaultErrorMessageDetail": "Os detalhes do erro não foram enviados pelo servidor.", + "DefaultErrorMessage401": "Você não etá autenticado!", + "DefaultErrorMessage401Detail": "Você deve autenticar-se para proceder com esta operação.", + "DefaultErrorMessage403": "Você não possui autorização!", + "DefaultErrorMessage403Detail": "Você não tem permissão para executar esta operação!", + "DefaultErrorMessage404": "Recurso não encontrado!", + "DefaultErrorMessage404Detail": "O recurso requisitado não pode ser encontrado pelo servidor!", + "EntityNotFoundErrorMessage": "Não existe uma entidade {0} com código = {1}!", + "Error": "Erro", + "UnhandledException": "Exceção não tratada!", + "401Message": "Autenticação inválida", + "403Message": "Não autorizado", + "404Message": "Página não encontrada", + "500Message": "Erro interno do servidor" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json new file mode 100644 index 0000000000..28707de134 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ru.json @@ -0,0 +1,23 @@ +{ + "culture": "ru", + "texts": { + "InternalServerErrorMessage": "Во время запроса произошла внутренняя ошибка!", + "ValidationErrorMessage": "Ваш запрос недействителен!", + "ValidationNarrativeErrorMessageTitle": "При проверке были обнаружены следующие ошибки.", + "DefaultErrorMessage": "Произошла ошибка!", + "DefaultErrorMessageDetail": "Сведения об ошибке не были предоставлены сервером.", + "DefaultErrorMessage401": "Вы не авторизированы!", + "DefaultErrorMessage401Detail": "Вы должны Войти, чтобы выполнить эту операцию.", + "DefaultErrorMessage403": "Вы не авторизованы!", + "DefaultErrorMessage403Detail": "У вас нет доступа к выполнению этой операции!", + "DefaultErrorMessage404": "Ресурс не найден!", + "DefaultErrorMessage404Detail": "Запрошенный ресурс не удалось найти на сервере!", + "EntityNotFoundErrorMessage": "Нет объекта {0} с id = {1}!", + "Error": "Ошибка", + "UnhandledException": "Непредвиденная ошибка!", + "401Message": "Неавторизованный", + "403Message": "В доступе отказано", + "404Message": "Страница не найдена", + "500Message": "Внутренняя ошибка сервера" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json new file mode 100644 index 0000000000..68394087a0 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/sl.json @@ -0,0 +1,23 @@ +{ + "culture": "sl", + "texts": { + "InternalServerErrorMessage": "Zgodila se je napaka na strežniku!", + "ValidationErrorMessage": "Vaš zahtevek ni veljaven!", + "ValidationNarrativeErrorMessageTitle": "Pri preverjanju so bile zaznane sledeče napake.", + "DefaultErrorMessage": "Zgodila se je napaka!", + "DefaultErrorMessageDetail": "Strežnik ni poslal podrobnosti o napaki.", + "DefaultErrorMessage401": "Niste prijavljeni!", + "DefaultErrorMessage401Detail": "Za izvedbo te operacije se morate prijaviti.", + "DefaultErrorMessage403": "Nimate pravic!", + "DefaultErrorMessage403Detail": "Nimate dovoljenja za izvedbo te operacije!", + "DefaultErrorMessage404": "Vir ni bil najden!", + "DefaultErrorMessage404Detail": "Zahtevanega vira ni bilo mogoče najti na strežniku!", + "EntityNotFoundErrorMessage": "Ni entitete {0} z id-jem = {1}!", + "Error": "Napaka", + "UnhandledException": "Neobravnavana napaka!", + "401Message": "Nepooblaščeno", + "403Message": "Prepovedano", + "404Message": "Strani ni mogoče najti", + "500Message": "Napaka na strani strežnika" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json new file mode 100644 index 0000000000..da199b2707 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/tr.json @@ -0,0 +1,25 @@ +{ + "culture": "tr", + "texts": { + "InternalServerErrorMessage": "Sayfa işlenirken sunucu tarafında beklenmedik bir hata oluştu!", + "ValidationErrorMessage": "İşleminiz geçerli değil!", + "ValidationNarrativeErrorMessageTitle": "Aşağıdaki hatalar doğrulama sırasında tespit edilmiştir.", + "DefaultErrorMessage": "Bir hata oluştu!", + "DefaultErrorMessageDetail": "Hatanın ne olduğu sunucu tarafından gönderilmedi.", + "DefaultErrorMessage401": "Giriş yapılmamış!", + "DefaultErrorMessage401Detail": "Bu işlemi gerçekleştirmek için uygulamaya giriş yapmalısınız.", + "DefaultErrorMessage403": "İzinsiz işlem!", + "DefaultErrorMessage403Detail": "Bu işlemi gerçekleştirmek için gereken izne sahip değilsiniz.", + "DefaultErrorMessage404": "Kaynak bulunamadı!", + "DefaultErrorMessage404Detail": "İstenilen kaynak sunucuda bulunamadı.", + "EntityNotFoundErrorMessage": "Id değeri {1} olan {0} türünden bir nesne bulunamadı!", + "Error": "Hata", + "UnhandledException": "Yakalanmamış hata!", + "401Message": "Yetkisiz İşlem", + "403Message": "İzinsiz", + "404Message": "Sayfa bulunamadı", + "500Message": "Sunucu tarafında hata", + "403MessageDetail": "Bu işlemi gerçekleştirmek için gereken yetkiye sahip değilsiniz!", + "404MessageDetail": "Üzgünüz, bu adreste bir şey yok." + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json new file mode 100644 index 0000000000..245d9f52be --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/vi.json @@ -0,0 +1,18 @@ +{ + "culture": "vi", + "texts": { + "InternalServerErrorMessage": "Có một lỗi nội bộ xảy ra trong quá trình thực hiện yêu cầu của bạn!", + "ValidationErrorMessage": "Yêu cầu của bạn không hợp lệ!", + "ValidationNarrativeErrorMessageTitle": "Các lỗi sau đây đã được phát hiện trong quá trình xác nhận", + "DefaultErrorMessage": "Một lỗi đã xảy ra", + "DefaultErrorMessageDetail": "Chi tiết lỗi không được gửi bởi máy chủ", + "DefaultErrorMessage401": "Bạn chưa được xác thực", + "DefaultErrorMessage401Detail": "Bạn cần đăng nhập để thực hiện thao tác này.", + "DefaultErrorMessage403": "Bạn không được phép!", + "DefaultErrorMessage403Detail": "Bạn không được phép thực hiện thao tác này!", + "DefaultErrorMessage404": "Tài nguyên không tìm thấy!", + "DefaultErrorMessage404Detail": "Tài nguyên được yêu cầu không được tìm thấy trên máy chủ!", + "EntityNotFoundErrorMessage": "Không có thực thể nào {0} với id = {1}!", + "Error": "Lỗi" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json new file mode 100644 index 0000000000..e9843def1b --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json @@ -0,0 +1,23 @@ +{ + "culture": "zh-Hans", + "texts": { + "InternalServerErrorMessage": "对不起,在处理你的请求期间,产生了一个服务器内部错误!", + "ValidationErrorMessage": "你的请求无效!", + "ValidationNarrativeErrorMessageTitle": "验证时发现以下错误.", + "DefaultErrorMessage": "发生错误!", + "DefaultErrorMessageDetail": "服务器未发送错误的详细信息.", + "DefaultErrorMessage401": "未通过身份验证!", + "DefaultErrorMessage401Detail": "你需要进行身份认证(登录)后再执行此操作.", + "DefaultErrorMessage403": "你没有权限!", + "DefaultErrorMessage403Detail": "你不能执行此操作!", + "DefaultErrorMessage404": "未找到资源!", + "DefaultErrorMessage404Detail": "未在服务中找到请求的资源!", + "EntityNotFoundErrorMessage": "实体 {0} 不存在,id = {1}!", + "Error": "错误", + "UnhandledException": "未处理的异常!", + "401Message": "未授权", + "403Message": "禁止访问", + "404Message": "网页未找到", + "500Message": "内部服务器错误" + } +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json new file mode 100644 index 0000000000..bf02b7f053 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "culture": "zh-Hant", + "texts": { + "InternalServerErrorMessage": "對不起,在處理你的請求期間,產生了一個伺服器內部錯誤!", + "ValidationErrorMessage": "你的請求無效!", + "ValidationNarrativeErrorMessageTitle": "驗證時發現以下錯誤.", + "DefaultErrorMessage": "發生錯誤!", + "DefaultErrorMessageDetail": "伺服器未發送錯誤的詳細信息.", + "DefaultErrorMessage401": "未通過身份驗證!", + "DefaultErrorMessage401Detail": "你需要進行身份認證(登入)後再執行此操作.", + "DefaultErrorMessage403": "你沒有權限!", + "DefaultErrorMessage403Detail": "你不能執行此操作!", + "DefaultErrorMessage404": "未找到資源!", + "DefaultErrorMessage404Detail": "未在服務中找到請求的資源!", + "EntityNotFoundErrorMessage": "實體 {0} 不存在,id = {1}!", + "Error": "錯誤", + "UnhandledException": "未處理的異常!", + "401Message": "未授權", + "403Message": "禁止訪問", + "404Message": "網頁未找到", + "500Message": "內部伺服器錯誤" + } +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorInfo.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceErrorInfo.cs similarity index 97% rename from framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorInfo.cs rename to framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceErrorInfo.cs index 287b423c20..c54ba24405 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorInfo.cs +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceErrorInfo.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Collections.Generic; namespace Volo.Abp.Http { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorResponse.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceErrorResponse.cs similarity index 100% rename from framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceErrorResponse.cs rename to framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceErrorResponse.cs diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceValidationErrorInfo.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceValidationErrorInfo.cs similarity index 100% rename from framework/src/Volo.Abp.Http/Volo/Abp/Http/RemoteServiceValidationErrorInfo.cs rename to framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Http/RemoteServiceValidationErrorInfo.cs diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj index 1518da9045..81a3d0ab9e 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + net5.0 Volo.Abp.Http.Client.IdentityModel.Web Volo.Abp.Http.Client.IdentityModel.Web $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo.Abp.Http.Client.IdentityModel.WebAssembly.csproj b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo.Abp.Http.Client.IdentityModel.WebAssembly.csproj index 36838f5149..95270edc7c 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo.Abp.Http.Client.IdentityModel.WebAssembly.csproj +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo.Abp.Http.Client.IdentityModel.WebAssembly.csproj @@ -4,7 +4,7 @@ - netstandard2.1 + net5.0 Volo.Abp.Http.Client.IdentityModel.WebAssembly Volo.Abp.Http.Client.IdentityModel.WebAssembly $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; @@ -15,9 +15,9 @@ - + - + diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs index 3110c36e26..3276a97313 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs @@ -1,5 +1,6 @@ -using System; +using IdentityModel; using Volo.Abp.Modularity; +using Volo.Abp.Security.Claims; namespace Volo.Abp.Http.Client.IdentityModel.WebAssembly { @@ -8,6 +9,14 @@ namespace Volo.Abp.Http.Client.IdentityModel.WebAssembly )] public class AbpHttpClientIdentityModelWebAssemblyModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + AbpClaimTypes.UserName = JwtClaimTypes.PreferredUserName; + AbpClaimTypes.Name = JwtClaimTypes.GivenName; + AbpClaimTypes.SurName = JwtClaimTypes.FamilyName; + AbpClaimTypes.UserId = JwtClaimTypes.Subject; + AbpClaimTypes.Role = JwtClaimTypes.Role; + AbpClaimTypes.Email = JwtClaimTypes.Email; + } } } diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs index c86fc095c7..0e0b659ef1 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs @@ -20,7 +20,7 @@ namespace Volo.Abp.Http.Client.IdentityModel.WebAssembly AccessTokenProvider = accessTokenProvider; } - public override async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context) + public async override Task Authenticate(RemoteServiceHttpClientAuthenticateContext context) { if (context.RemoteService.GetUseCurrentAccessToken() != false) { diff --git a/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj b/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj index 75dbce3b03..a96f3afd4a 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj +++ b/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj @@ -1,4 +1,4 @@ - + @@ -15,11 +15,12 @@ - + + diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs index e4e4f3e802..37eefa8f5a 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs @@ -6,6 +6,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; using Volo.Abp.Validation; using Polly; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.Http.Client { @@ -14,7 +15,8 @@ namespace Volo.Abp.Http.Client typeof(AbpCastleCoreModule), typeof(AbpThreadingModule), typeof(AbpMultiTenancyModule), - typeof(AbpValidationModule) + typeof(AbpValidationModule), + typeof(AbpExceptionHandlingModule) )] public class AbpHttpClientModule : AbpModule { diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs new file mode 100644 index 0000000000..b99cec8a51 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs @@ -0,0 +1,16 @@ +using System.IO; +using Volo.Abp.Content; + +namespace Volo.Abp.Http.Client.Content +{ + internal class ReferencedRemoteStreamContent : RemoteStreamContent + { + private readonly object[] _references; + + public ReferencedRemoteStreamContent(Stream stream, params object[] references) + : base(stream) + { + this._references = references; + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs index 5313457ba7..321d538044 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -10,9 +10,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Volo.Abp.Content; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; using Volo.Abp.Http.Client.Authentication; +using Volo.Abp.Http.Client.Content; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; @@ -103,17 +105,26 @@ namespace Volo.Abp.Http.Client.DynamicProxying private async Task MakeRequestAndGetResultAsync(IAbpMethodInvocation invocation) { - var responseAsString = await MakeRequestAsync(invocation); + var responseContent = await MakeRequestAsync(invocation); + if (typeof(T) == typeof(IRemoteStreamContent)) + { + /* returning a class that holds a reference to response + * content just to be sure that GC does not dispose of + * it before we finish doing our work with the stream */ + return (T)((object)new ReferencedRemoteStreamContent(await responseContent.ReadAsStreamAsync(), responseContent)); + } + + var stringContent = await responseContent.ReadAsStringAsync(); if (typeof(T) == typeof(string)) { - return (T)Convert.ChangeType(responseAsString, typeof(T)); + return (T)(object)stringContent; } - return JsonSerializer.Deserialize(responseAsString); + return JsonSerializer.Deserialize(await responseContent.ReadAsStringAsync()); } - private async Task MakeRequestAsync(IAbpMethodInvocation invocation) + private async Task MakeRequestAsync(IAbpMethodInvocation invocation) { var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}."); var remoteServiceConfig = AbpRemoteServiceOptions.RemoteServices.GetConfigurationOrDefault(clientConfig.RemoteServiceName); @@ -140,14 +151,16 @@ namespace Volo.Abp.Http.Client.DynamicProxying ) ); - var response = await client.SendAsync(requestMessage, GetCancellationToken()); + var response = await client.SendAsync(requestMessage, + HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/, + GetCancellationToken()); if (!response.IsSuccessStatusCode) { await ThrowExceptionForResponseAsync(response); } - return await response.Content.ReadAsStringAsync(); + return response.Content; } private ApiVersionInfo GetApiVersionInfo(ActionApiDescriptionModel action) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs index afc5f9e4f0..7aacfaad02 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using System.Text; using JetBrains.Annotations; +using Volo.Abp.Content; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; @@ -12,24 +13,20 @@ namespace Volo.Abp.Http.Client.DynamicProxying public static class RequestPayloadBuilder { [CanBeNull] - public static HttpContent BuildContent(ActionApiDescriptionModel action,IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) + public static HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) { var body = GenerateBody(action, methodArguments, jsonSerializer); if (body != null) { - return new StringContent(body, Encoding.UTF8, MimeTypes.Application.Json); + return body; } body = GenerateFormPostData(action, methodArguments); - if (body != null) - { - return new StringContent(body, Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded); - } - return null; + return body; } - private static string GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) + private static HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) { var parameters = action .Parameters @@ -54,10 +51,20 @@ namespace Volo.Abp.Http.Client.DynamicProxying return null; } - return jsonSerializer.Serialize(value); + if (value is IRemoteStreamContent remoteStreamContent) + { + var content = new StreamContent(remoteStreamContent.GetStream()); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(remoteStreamContent.ContentType); + content.Headers.ContentLength = remoteStreamContent.ContentLength; + return content; + } + else + { + return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json); + } } - private static string GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) + private static HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) { var parameters = action .Parameters @@ -86,7 +93,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying isFirstParam = false; } - return postDataBuilder.ToString(); + return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded); } } } diff --git a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj index b96da9be3e..eb5bc35428 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj +++ b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj @@ -16,7 +16,7 @@ - + diff --git a/framework/src/Volo.Abp.Json/Volo.Abp.Json.csproj b/framework/src/Volo.Abp.Json/Volo.Abp.Json.csproj index 8af6ecc68f..4e891a687d 100644 --- a/framework/src/Volo.Abp.Json/Volo.Abp.Json.csproj +++ b/framework/src/Volo.Abp.Json/Volo.Abp.Json.csproj @@ -17,7 +17,7 @@ - + diff --git a/framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj b/framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj index e91f42d7b2..2a0b576f98 100644 --- a/framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj +++ b/framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj @@ -2,12 +2,12 @@ - + netstandard2.0 - + diff --git a/framework/src/Volo.Abp.Ldap/Volo.Abp.Ldap.csproj b/framework/src/Volo.Abp.Ldap/Volo.Abp.Ldap.csproj index 849fe3ce3d..277ac1c24b 100644 --- a/framework/src/Volo.Abp.Ldap/Volo.Abp.Ldap.csproj +++ b/framework/src/Volo.Abp.Ldap/Volo.Abp.Ldap.csproj @@ -17,12 +17,12 @@ - + - + diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/ar.json b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/ar.json new file mode 100644 index 0000000000..0fa580a24b --- /dev/null +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/ar.json @@ -0,0 +1,15 @@ +{ + "culture": "ar", + "texts": { + "DisplayName:Abp.Ldap.ServerHost": "اسم الخادم", + "Description:Abp.Ldap.ServerHost": "اسم الخادم", + "DisplayName:Abp.Ldap.ServerPort": "منفذ الخادم", + "Description:Abp.Ldap.ServerPort": "منفذ الخادم", + "DisplayName:Abp.Ldap.BaseDc": "مكون المجال الأساسي", + "Description:Abp.Ldap.BaseDc": "مكون المجال الأساسي", + "DisplayName:Abp.Ldap.UserName": "اسم المستخدم", + "Description:Abp.Ldap.UserName": "اسم المستخدم", + "DisplayName:Abp.Ldap.Password": "كلمة السر", + "Description:Abp.Ldap.Password": "كلمة السر" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/es.json b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/es.json new file mode 100644 index 0000000000..365856425e --- /dev/null +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/es.json @@ -0,0 +1,19 @@ +{ + "culture": "es", + "texts": { + "DisplayName:Abp.Ldap.ServerHost": "Server host", + "Description:Abp.Ldap.ServerHost": "Servidor host", + + "DisplayName:Abp.Ldap.ServerPort": "Server port", + "Description:Abp.Ldap.ServerPort": "Puerto del servidor host", + + "DisplayName:Abp.Ldap.BaseDc": "Base domain component", + "Description:Abp.Ldap.BaseDc": "Dominio base", + + "DisplayName:Abp.Ldap.UserName": "Username", + "Description:Abp.Ldap.UserName": "Nombre de usuario", + + "DisplayName:Abp.Ldap.Password": "Password", + "Description:Abp.Ldap.Password": "Contraseña" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/fr.json b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/fr.json new file mode 100644 index 0000000000..adcded679d --- /dev/null +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/fr.json @@ -0,0 +1,15 @@ +{ + "culture": "fr", + "texts": { + "DisplayName:Abp.Ldap.ServerHost": "Hôte du serveur", + "Description:Abp.Ldap.ServerHost": "Hôte du serveur", + "DisplayName:Abp.Ldap.ServerPort": "Port de serveur", + "Description:Abp.Ldap.ServerPort": "Port de serveur", + "DisplayName:Abp.Ldap.BaseDc": "Composant de domaine de base", + "Description:Abp.Ldap.BaseDc": "Composant de domaine de base", + "DisplayName:Abp.Ldap.UserName": "Nom d'utilisateur", + "Description:Abp.Ldap.UserName": "Nom d'utilisateur", + "DisplayName:Abp.Ldap.Password": "Mot de passe", + "Description:Abp.Ldap.Password": "Mot de passe" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/hu.json b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/hu.json new file mode 100644 index 0000000000..1b240d9b29 --- /dev/null +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/Localization/hu.json @@ -0,0 +1,20 @@ +{ + "culture": "hu", + "texts": { + "DisplayName:Abp.Ldap.ServerHost": "Szerver host", + "Description:Abp.Ldap.ServerHost": "Az LDAP ksizolgáló szerver hostneve vagy IP címe", + + "DisplayName:Abp.Ldap.ServerPort": "Szerver port", + "Description:Abp.Ldap.ServerPort": "LDAP kommunikáció portja", + + "DisplayName:Abp.Ldap.BaseDc": "Bázis tartomány-komponens", + "Description:Abp.Ldap.BaseDc": "Bázis tartomány-komponens", + + "DisplayName:Abp.Ldap.UserName": "Felhasználónév", + "Description:Abp.Ldap.UserName": "Felhasználónév", + + "DisplayName:Abp.Ldap.Password": "Jelszó", + "Description:Abp.Ldap.Password": "Jelszó" + } + } + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj index 88c3bf0e81..df04ab95df 100644 --- a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj +++ b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj @@ -13,12 +13,12 @@ false - + - + diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs index 7d9056ca4d..4979b87ce6 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -31,7 +31,7 @@ namespace Volo.Abp.Localization includeParentCultures ); } - + public IEnumerable GetAllStrings(bool includeParentCultures, bool includeBaseLocalizers) { return GetAllStrings( @@ -41,12 +41,6 @@ namespace Volo.Abp.Localization ); } - [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] - public IStringLocalizer WithCulture(CultureInfo culture) - { - return new CultureWrapperStringLocalizer(culture.Name, this); - } - protected virtual LocalizedString GetLocalizedStringFormatted(string name, params object[] arguments) { return GetLocalizedStringFormatted(name, CultureInfo.CurrentUICulture.Name, arguments); @@ -126,7 +120,7 @@ namespace Volo.Abp.Localization } protected virtual IReadOnlyList GetAllStrings( - string cultureName, + string cultureName, bool includeParentCultures = true, bool includeBaseLocalizers = true) { @@ -177,7 +171,7 @@ namespace Volo.Abp.Localization return allStrings.Values.ToImmutableList(); } - + public class CultureWrapperStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance { private readonly string _cultureName; @@ -198,16 +192,10 @@ namespace Volo.Abp.Localization return _innerLocalizer.GetAllStrings(_cultureName, includeParentCultures); } - [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] - public IStringLocalizer WithCulture(CultureInfo culture) - { - return new CultureWrapperStringLocalizer(culture.Name, _innerLocalizer); - } - public IEnumerable GetAllStrings(bool includeParentCultures, bool includeBaseLocalizers) { return _innerLocalizer.GetAllStrings(_cultureName, includeParentCultures, includeBaseLocalizers); } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/fr.json b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/fr.json new file mode 100644 index 0000000000..a8b5c1bb5f --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/fr.json @@ -0,0 +1,7 @@ +{ + "culture": "fr", + "texts": { + "DisplayName:Abp.Localization.DefaultLanguage": "Langage par défaut", + "Description:Abp.Localization.DefaultLanguage": "La langue par défaut de l'application." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/hu.json b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/hu.json new file mode 100644 index 0000000000..d355c69ee3 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/hu.json @@ -0,0 +1,7 @@ +{ + "culture": "hu", + "texts": { + "DisplayName:Abp.Localization.DefaultLanguage": "Alapértelmezett nyelv", + "Description:Abp.Localization.DefaultLanguage": "Az alkalmazás alapértelmezett nyelve." + } + } \ No newline at end of file diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ActionTenantResolveContributor.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ActionTenantResolveContributor.cs index c73075107e..0af00d647e 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ActionTenantResolveContributor.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ActionTenantResolveContributor.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using JetBrains.Annotations; namespace Volo.Abp.MultiTenancy @@ -18,9 +19,10 @@ namespace Volo.Abp.MultiTenancy _resolveAction = resolveAction; } - public override void Resolve(ITenantResolveContext context) + public override Task ResolveAsync(ITenantResolveContext context) { _resolveAction(context); + return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentTenant.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentTenant.cs index 4cd6ae9527..cd5fb5c3b3 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentTenant.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentTenant.cs @@ -1,4 +1,4 @@ -using System; +using System; using Volo.Abp.DependencyInjection; namespace Volo.Abp.MultiTenancy diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentUserTenantResolveContributor.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentUserTenantResolveContributor.cs index d326421682..002a31bc6e 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentUserTenantResolveContributor.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/CurrentUserTenantResolveContributor.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Users; @@ -9,16 +10,16 @@ namespace Volo.Abp.MultiTenancy public override string Name => ContributorName; - public override void Resolve(ITenantResolveContext context) + public override Task ResolveAsync(ITenantResolveContext context) { var currentUser = context.ServiceProvider.GetRequiredService(); - if (currentUser.IsAuthenticated != true) + if (currentUser.IsAuthenticated) { - return; + context.Handled = true; + context.TenantIdOrName = currentUser.TenantId?.ToString(); } - context.Handled = true; - context.TenantIdOrName = currentUser.TenantId?.ToString(); + return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantConfigurationProvider.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantConfigurationProvider.cs new file mode 100644 index 0000000000..010a10791a --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantConfigurationProvider.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.MultiTenancy +{ + public interface ITenantConfigurationProvider + { + Task GetAsync(bool saveResolveResult = false); + } +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolveContributor.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolveContributor.cs index a79cccfe66..c6d067044d 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolveContributor.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolveContributor.cs @@ -1,9 +1,11 @@ +using System.Threading.Tasks; + namespace Volo.Abp.MultiTenancy { public interface ITenantResolveContributor { string Name { get; } - void Resolve(ITenantResolveContext context); + Task ResolveAsync(ITenantResolveContext context); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolver.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolver.cs index b9ce83b0b9..7ab0e40366 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolver.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantResolver.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using JetBrains.Annotations; namespace Volo.Abp.MultiTenancy @@ -11,6 +12,6 @@ namespace Volo.Abp.MultiTenancy /// Tenant id, unique name or null (if could not resolve). /// [NotNull] - TenantResolveResult ResolveTenantIdOrName(); + Task ResolveTenantIdOrNameAsync(); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantConfigurationProvider.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantConfigurationProvider.cs new file mode 100644 index 0000000000..81a9c72042 --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantConfigurationProvider.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.MultiTenancy +{ + public class TenantConfigurationProvider : ITenantConfigurationProvider, ITransientDependency + { + protected virtual ITenantResolver TenantResolver { get; } + protected virtual ITenantStore TenantStore { get; } + protected virtual ITenantResolveResultAccessor TenantResolveResultAccessor { get; } + + public TenantConfigurationProvider( + ITenantResolver tenantResolver, + ITenantStore tenantStore, + ITenantResolveResultAccessor tenantResolveResultAccessor) + { + TenantResolver = tenantResolver; + TenantStore = tenantStore; + TenantResolveResultAccessor = tenantResolveResultAccessor; + } + + public virtual async Task GetAsync(bool saveResolveResult = false) + { + var resolveResult = await TenantResolver.ResolveTenantIdOrNameAsync(); + + if (saveResolveResult) + { + TenantResolveResultAccessor.Result = resolveResult; + } + + TenantConfiguration tenant = null; + if (resolveResult.TenantIdOrName != null) + { + tenant = await FindTenantAsync(resolveResult.TenantIdOrName); + + if (tenant == null) + { + throw new BusinessException( + code: "Volo.AbpIo.MultiTenancy:010001", + message: "Tenant not found!", + details: "There is no tenant with the tenant id or name: " + resolveResult.TenantIdOrName + ); + } + } + + return tenant; + } + + protected virtual async Task FindTenantAsync(string tenantIdOrName) + { + if (Guid.TryParse(tenantIdOrName, out var parsedTenantId)) + { + return await TenantStore.FindAsync(parsedTenantId); + } + else + { + return await TenantStore.FindAsync(tenantIdOrName); + } + } + } +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolveContributorBase.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolveContributorBase.cs index 8fcc22059d..f0f65b4c0f 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolveContributorBase.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolveContributorBase.cs @@ -1,10 +1,11 @@ +using System.Threading.Tasks; + namespace Volo.Abp.MultiTenancy { public abstract class TenantResolveContributorBase : ITenantResolveContributor { public abstract string Name { get; } - //TODO: We can make this async - public abstract void Resolve(ITenantResolveContext context); + public abstract Task ResolveAsync(ITenantResolveContext context); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs index 202ab4c293..5f3a97be2d 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs @@ -1,6 +1,5 @@ using System; -using System.Linq; -using JetBrains.Annotations; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; @@ -18,7 +17,7 @@ namespace Volo.Abp.MultiTenancy _options = options.Value; } - public TenantResolveResult ResolveTenantIdOrName() + public virtual async Task ResolveTenantIdOrNameAsync() { var result = new TenantResolveResult(); @@ -28,7 +27,7 @@ namespace Volo.Abp.MultiTenancy foreach (var tenantResolver in _options.TenantResolvers) { - tenantResolver.Resolve(context); + await tenantResolver.ResolveAsync(context); result.AppliedResolvers.Add(tenantResolver.Name); @@ -43,4 +42,4 @@ namespace Volo.Abp.MultiTenancy return result; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj b/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj index 4b8542101a..19f549b36a 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj +++ b/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj @@ -1,4 +1,4 @@ - + @@ -19,4 +19,4 @@ - \ No newline at end of file + diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs index 6abb350328..22b135c3d0 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using Volo.Abp.DynamicProxy; +using Volo.Abp.Localization; using Volo.Abp.ObjectExtending; using Volo.Abp.Reflection; @@ -32,15 +34,26 @@ namespace Volo.Abp.Data if (TypeHelper.IsPrimitiveExtended(typeof(TProperty), includeEnums: true)) { - return (TProperty)Convert.ChangeType(value, typeof(TProperty), CultureInfo.InvariantCulture); + var conversionType = typeof(TProperty); + if (TypeHelper.IsNullable(conversionType)) + { + conversionType = conversionType.GetFirstGenericArgumentIfNullable(); + } + + if (conversionType == typeof(Guid)) + { + return (TProperty)TypeDescriptor.GetConverter(conversionType).ConvertFromInvariantString(value.ToString()); + } + + return (TProperty)Convert.ChangeType(value, conversionType, CultureInfo.InvariantCulture); } throw new AbpException("GetProperty does not support non-primitive types. Use non-generic GetProperty method and handle type casting manually."); } public static TSource SetProperty( - this TSource source, - string name, + this TSource source, + string name, object value, bool validate = true) where TSource : IHasExtraProperties @@ -96,4 +109,4 @@ namespace Volo.Abp.Data ((IHasExtraProperties) source).SetDefaultsForExtraProperties(objectType); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Quartz/Volo.Abp.Quartz.csproj b/framework/src/Volo.Abp.Quartz/Volo.Abp.Quartz.csproj index 00da6b31b6..198d2479e4 100644 --- a/framework/src/Volo.Abp.Quartz/Volo.Abp.Quartz.csproj +++ b/framework/src/Volo.Abp.Quartz/Volo.Abp.Quartz.csproj @@ -1,4 +1,4 @@ - + diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/AbpSecurityModule.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/AbpSecurityModule.cs index f62a9566c4..b1cdcb1097 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/AbpSecurityModule.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/AbpSecurityModule.cs @@ -1,9 +1,45 @@ -using Volo.Abp.Modularity; +using System; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; +using Volo.Abp.Security.Encryption; namespace Volo.Abp.Security { public class AbpSecurityModule : AbpModule { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + context.Services.Configure(options => + { + var keySize = configuration["StringEncryption:KeySize"]; + if (!keySize.IsNullOrWhiteSpace()) + { + if (int.TryParse(keySize, out var intValue)) + { + options.Keysize = intValue; + } + } + var defaultPassPhrase = configuration["StringEncryption:DefaultPassPhrase"]; + if (!defaultPassPhrase.IsNullOrWhiteSpace()) + { + options.DefaultPassPhrase = defaultPassPhrase; + } + + var initVectorBytes = configuration["StringEncryption:InitVectorBytes"]; + if (!initVectorBytes.IsNullOrWhiteSpace()) + { + options.InitVectorBytes = Encoding.ASCII.GetBytes(initVectorBytes);; + } + + var defaultSalt = configuration["StringEncryption:DefaultSalt"]; + if (!defaultSalt.IsNullOrWhiteSpace()) + { + options.DefaultSalt = Encoding.ASCII.GetBytes(defaultSalt);; + } + }); + } } } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorExtensions.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorExtensions.cs new file mode 100644 index 0000000000..63f83f8a5a --- /dev/null +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; + +namespace Volo.Abp.Security.Claims +{ + public static class CurrentPrincipalAccessorExtensions + { + public static IDisposable Change(this ICurrentPrincipalAccessor currentPrincipalAccessor, Claim claim) + { + return currentPrincipalAccessor.Change(new[] {claim}); + } + + public static IDisposable Change(this ICurrentPrincipalAccessor currentPrincipalAccessor, IEnumerable claims) + { + return currentPrincipalAccessor.Change(new ClaimsIdentity(claims)); + } + + public static IDisposable Change(this ICurrentPrincipalAccessor currentPrincipalAccessor, ClaimsIdentity claimsIdentity) + { + return currentPrincipalAccessor.Change(new ClaimsPrincipal(claimsIdentity)); + } + } +} diff --git a/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/Binary/BinarySerializationHelper.cs b/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/Binary/BinarySerializationHelper.cs deleted file mode 100644 index 31a9ff280b..0000000000 --- a/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/Binary/BinarySerializationHelper.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.IO; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; - -namespace Volo.Abp.Serialization.Binary -{ - /// - /// This class is used to simplify serialization/deserialization operations. - /// Uses .NET binary serialization. - /// - public static class BinarySerializationHelper - { - /// - /// Serializes an object and returns as a byte array. - /// - /// object to be serialized - /// bytes of object - public static byte[] Serialize(object obj) - { - using (var memoryStream = new MemoryStream()) - { - Serialize(obj, memoryStream); - return memoryStream.ToArray(); - } - } - - /// - /// Serializes an object into a stream. - /// - /// object to be serialized - /// Stream to serialize in - /// bytes of object - public static void Serialize(object obj, Stream stream) - { - CreateBinaryFormatter().Serialize(stream, obj); - } - - /// - /// Deserializes an object from given byte array. - /// - /// The byte array that contains object - /// deserialized object - public static object Deserialize(byte[] bytes) - { - using (var memoryStream = new MemoryStream(bytes)) - { - return Deserialize(memoryStream); - } - } - - /// - /// Deserializes an object from given stream. - /// - /// The stream that contains object - /// deserialized object - public static object Deserialize(Stream stream) - { - return CreateBinaryFormatter().Deserialize(stream); - } - - /// - /// Deserializes an object from given byte array. - /// Difference from is that; this method can also deserialize - /// types that are defined in dynamically loaded assemblies (like PlugIns). - /// - /// The byte array that contains object - /// deserialized object - public static object DeserializeExtended(byte[] bytes) - { - using (var memoryStream = new MemoryStream(bytes)) - { - return CreateBinaryFormatter(true).Deserialize(memoryStream); - } - } - - /// - /// Deserializes an object from given stream. - /// Difference from is that; this method can also deserialize - /// types that are defined in dynamically loaded assemblies (like PlugIns). - /// - /// The stream that contains object - /// deserialized object - public static object DeserializeExtended(Stream stream) - { - return CreateBinaryFormatter(true).Deserialize(stream); - } - - private static BinaryFormatter CreateBinaryFormatter(bool extended = false) - { - if (extended) - { - return new BinaryFormatter - { - //TODO: AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple, - Binder = new ExtendedSerializationBinder() - }; - } - else - { - return new BinaryFormatter(); - } - } - - /// - /// This class is used in deserializing to allow deserializing objects that are defined - /// in assemlies that are load in runtime (like PlugIns). - /// - private sealed class ExtendedSerializationBinder : SerializationBinder - { - public override Type BindToType(string assemblyName, string typeName) - { - var toAssemblyName = assemblyName.Split(',')[0]; - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (assembly.FullName.Split(',')[0] == toAssemblyName) - { - return assembly.GetType(typeName); - } - } - - return Type.GetType(string.Format("{0}, {1}", typeName, assemblyName)); - } - } - } -} diff --git a/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/DefaultObjectSerializer.cs b/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/DefaultObjectSerializer.cs index 009fab9fb2..f016fe9773 100644 --- a/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/DefaultObjectSerializer.cs +++ b/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/DefaultObjectSerializer.cs @@ -1,8 +1,8 @@ using System; +using System.Text.Json; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; -using Volo.Abp.Serialization.Binary; namespace Volo.Abp.Serialization { @@ -58,12 +58,12 @@ namespace Volo.Abp.Serialization protected virtual byte[] AutoSerialize(T obj) { - return BinarySerializationHelper.Serialize(obj); + return JsonSerializer.SerializeToUtf8Bytes(obj); } protected virtual T AutoDeserialize(byte[] bytes) { - return (T) BinarySerializationHelper.DeserializeExtended(bytes); + return JsonSerializer.Deserialize(bytes); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/ISettingDefinitionContext.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/ISettingDefinitionContext.cs index 220e40fe59..59d7acda98 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/ISettingDefinitionContext.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/ISettingDefinitionContext.cs @@ -1,9 +1,13 @@ -namespace Volo.Abp.Settings +using System.Collections.Generic; + +namespace Volo.Abp.Settings { public interface ISettingDefinitionContext { SettingDefinition GetOrNull(string name); + + IReadOnlyList GetAll(); void Add(params SettingDefinition[] definitions); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs index 6f5372a4cf..5ef67cbc21 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs @@ -35,7 +35,7 @@ namespace Volo.Abp.Settings //TODO: How to implement setting.IsInherited? var value = await GetOrNullValueFromProvidersAsync(providers, setting); - if (setting.IsEncrypted) + if (value != null && setting.IsEncrypted) { value = SettingEncryptionService.Decrypt(setting, value); } @@ -84,4 +84,4 @@ namespace Volo.Abp.Settings return null; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Swashbuckle/FodyWeavers.xml b/framework/src/Volo.Abp.Swashbuckle/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Swashbuckle/FodyWeavers.xsd b/framework/src/Volo.Abp.Swashbuckle/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Swashbuckle/Microsoft/AspNetCore/Builder/AbpSwaggerUIBuilderExtensions.cs b/framework/src/Volo.Abp.Swashbuckle/Microsoft/AspNetCore/Builder/AbpSwaggerUIBuilderExtensions.cs new file mode 100644 index 0000000000..4ea501626b --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/Microsoft/AspNetCore/Builder/AbpSwaggerUIBuilderExtensions.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Swashbuckle.AspNetCore.SwaggerUI; +using Volo.Abp; +using Volo.Abp.Swashbuckle; + +namespace Microsoft.AspNetCore.Builder +{ + public static class AbpSwaggerUIBuilderExtensions + { + public static IApplicationBuilder UseAbpSwaggerUI( + this IApplicationBuilder app, + Action setupAction = null) + { + var resolver = app.ApplicationServices.GetService(); + + return app.UseSwaggerUI(options => + { + options.InjectJavascript("/libs/abp/core/abp.js"); + options.InjectJavascript("/swagger/ui/abp.swagger.js"); + options.IndexStream = () => resolver.Resolver(); + + setupAction?.Invoke(options); + }); + } + } +} diff --git a/framework/src/Volo.Abp.Swashbuckle/Volo.Abp.Swashbuckle.csproj b/framework/src/Volo.Abp.Swashbuckle/Volo.Abp.Swashbuckle.csproj new file mode 100644 index 0000000000..bbd09c5fed --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/Volo.Abp.Swashbuckle.csproj @@ -0,0 +1,30 @@ + + + + + + + netstandard2.0 + Volo.Abp.Swashbuckle + Volo.Abp.Swashbuckle + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleModule.cs b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleModule.cs new file mode 100644 index 0000000000..949ac2ca2d --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleModule.cs @@ -0,0 +1,17 @@ +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace Volo.Abp.Swashbuckle +{ + [DependsOn(typeof(AbpVirtualFileSystemModule))] + public class AbpSwashbuckleModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + } + } +} diff --git a/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/ISwaggerHtmlResolver.cs b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/ISwaggerHtmlResolver.cs new file mode 100644 index 0000000000..8609acf418 --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/ISwaggerHtmlResolver.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Volo.Abp.Swashbuckle +{ + public interface ISwaggerHtmlResolver + { + Stream Resolver(); + } +} diff --git a/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/SwaggerHtmlResolver.cs b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/SwaggerHtmlResolver.cs new file mode 100644 index 0000000000..7d80f5477f --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/SwaggerHtmlResolver.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Reflection; +using System.Text; +using Swashbuckle.AspNetCore.SwaggerUI; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Swashbuckle +{ + public class SwaggerHtmlResolver : ISwaggerHtmlResolver, ITransientDependency + { + public virtual Stream Resolver() + { + var stream = typeof(SwaggerUIOptions).GetTypeInfo().Assembly + .GetManifestResourceStream("Swashbuckle.AspNetCore.SwaggerUI.index.html"); + + var html = new StreamReader(stream) + .ReadToEnd() + .Replace("SwaggerUIBundle(configObject)", "abp.SwaggerUIBundle(configObject)"); + + return new MemoryStream(Encoding.UTF8.GetBytes(html)); + } + } +} diff --git a/framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js b/framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js new file mode 100644 index 0000000000..b1b9614640 --- /dev/null +++ b/framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js @@ -0,0 +1,17 @@ +var abp = abp || {}; + +(function () { + abp.SwaggerUIBundle = function (configObject) { + configObject.requestInterceptor = function (request) { + var token = abp.auth.getToken(); + request.headers.Authorization = token ? "Bearer " + token : null; + var antiForgeryToken = abp.security.antiForgery.getToken(); + if (antiForgeryToken) { + request.headers[abp.security.antiForgery.tokenHeaderName] = antiForgeryToken; + } + return request; + }; + + return SwaggerUIBundle(configObject); + } +})(); diff --git a/framework/src/Volo.Abp.TextTemplating/Volo.Abp.TextTemplating.csproj b/framework/src/Volo.Abp.TextTemplating/Volo.Abp.TextTemplating.csproj index 41bba98866..7885cefbaf 100644 --- a/framework/src/Volo.Abp.TextTemplating/Volo.Abp.TextTemplating.csproj +++ b/framework/src/Volo.Abp.TextTemplating/Volo.Abp.TextTemplating.csproj @@ -1,4 +1,4 @@ - + diff --git a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/TemplateContentProvider.cs b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/TemplateContentProvider.cs index 55a3979e21..e084abeb05 100644 --- a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/TemplateContentProvider.cs +++ b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/TemplateContentProvider.cs @@ -12,13 +12,13 @@ namespace Volo.Abp.TextTemplating { public class TemplateContentProvider : ITemplateContentProvider, ITransientDependency { - public IHybridServiceScopeFactory ServiceScopeFactory { get; } + public IServiceScopeFactory ServiceScopeFactory { get; } public AbpTextTemplatingOptions Options { get; } private readonly ITemplateDefinitionManager _templateDefinitionManager; public TemplateContentProvider( ITemplateDefinitionManager templateDefinitionManager, - IHybridServiceScopeFactory serviceScopeFactory, + IServiceScopeFactory serviceScopeFactory, IOptions options) { ServiceScopeFactory = serviceScopeFactory; @@ -167,4 +167,4 @@ namespace Volo.Abp.TextTemplating return null; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/es.json b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/es.json new file mode 100644 index 0000000000..a23fccc1c4 --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/es.json @@ -0,0 +1,7 @@ +{ + "culture": "es", + "texts": { + "DisplayName:Abp.Timing.Timezone": "Zona horaria", + "Description:Abp.Timing.Timezone": "Zona horaria de la Aplicacion" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/fr.json b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/fr.json new file mode 100644 index 0000000000..b36fbcdcfb --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/fr.json @@ -0,0 +1,7 @@ +{ + "culture": "fr", + "texts": { + "DisplayName:Abp.Timing.Timezone": "Fuseau horaire", + "Description:Abp.Timing.Timezone": "Fuseau horaire de l'application" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/hu.json b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/hu.json new file mode 100644 index 0000000000..7902d093a8 --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/hu.json @@ -0,0 +1,8 @@ +{ + "culture": "hu", + "texts": { + "DisplayName:Abp.Timing.Timezone": "Időzóna", + "Description:Abp.Timing.Timezone": "Alkalmazás időzónája" + } + } + \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/fr.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/fr.json new file mode 100644 index 0000000000..1fb2e26760 --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/fr.json @@ -0,0 +1,6 @@ +{ + "culture": "fr", + "texts": { + "Menu:Administration": "Administration" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/hu.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/hu.json new file mode 100644 index 0000000000..c391e23db2 --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/hu.json @@ -0,0 +1,6 @@ +{ + "culture": "hu", + "texts": { + "Menu:Administration": "Adminisztráció" + } + } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json index ec51f85510..37b538ce89 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json @@ -1,22 +1,10 @@ { "culture": "ar", "texts": { - "InternalServerErrorMessage": "حدث خطأ داخلي أثناء طلبك!", - "ValidationErrorMessage": "طلبك غير صحيح!", - "ValidationNarrativeErrorMessageTitle": "تم الكشف عن الأخطاء التالية أثناء التحقق .", - "DefaultErrorMessage": "حدث خطأ!", - "DefaultErrorMessageDetail": "لم يتم إرسال تفاصيل الخطأ بواسطة الخادم.", - "DefaultErrorMessage401": "أنت غير مصدق!", - "DefaultErrorMessage401Detail": "يجب عليك تسجيل الدخول لأداء هذه العملية.", - "DefaultErrorMessage403": "أنك غير مخول!", - "DefaultErrorMessage403Detail": "لا يسمح لك بإجراء هذه العملية!", - "DefaultErrorMessage404": "المورد غير موجود!", - "DefaultErrorMessage404Detail": "لم يتم العثور على المورد المطلوب على الخادم!", - "EntityNotFoundErrorMessage": "لا يوجد كيان {0} بالمعرف = {1}!", "Languages": "اللغات", - "Error": "خطأ", "AreYouSure": "هل أنت متأكد؟", "Cancel": "إلغاء", + "Clear": "تنظيف", "Yes": "نعم", "No": "لا", "Ok": "حسنا", @@ -54,13 +42,8 @@ "ChangePassword": "تغيير كلمة السر", "PersonalInfo": "ملفي الشخصي", "AreYouSureYouWantToCancelEditingWarningMessage": "لم تحفظ التغييرات.", - "UnhandledException": "استثناء غير معالج!", - "401Message": "غير مصرح", - "403Message": "ممنوع", - "404Message": "الصفحة غير موجودة", - "500Message": "خطأ في الخادم الداخلي", "GoHomePage": "اذهب الى الصفحة الرئيسية", "GoBack": "عد", "Search": "بحث" } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json index b42cc1ebe3..85cf515095 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json @@ -1,20 +1,7 @@ { "culture": "cs", "texts": { - "InternalServerErrorMessage": "Během požadavku se vyskytla vnitřní chyba!", - "ValidationErrorMessage": "Neplatný požadavek!", - "ValidationNarrativeErrorMessageTitle": "Během ověřování byly zjištěny následující chyby.", - "DefaultErrorMessage": "Vyskytla se chyba!", - "DefaultErrorMessageDetail": "Server neposkytl detaily k chybě.", - "DefaultErrorMessage401": "Nejste přihlášeni!", - "DefaultErrorMessage401Detail": "K vykonání této operace se musíte nejprve přihlásit.", - "DefaultErrorMessage403": "Nemáte oprávnění!", - "DefaultErrorMessage403Detail": "Není vám dovoleno vykonat tuto operaci!", - "DefaultErrorMessage404": "Prostředek nenalezen!", - "DefaultErrorMessage404Detail": "Vyžádaný prostředek nebyl na serveru nalezen!", - "EntityNotFoundErrorMessage": "Neexistující entita {0} s id = {1}!", "Languages": "Jazyky", - "Error": "Chyba", "AreYouSure": "Jste si jisti?", "Cancel": "Zrušit", "Yes": "Ano", @@ -54,11 +41,6 @@ "ChangePassword": "Změnit heslo", "PersonalInfo": "Můj profil", "AreYouSureYouWantToCancelEditingWarningMessage": "Máte neuložené změny.", - "UnhandledException": "Neošetřená výjimka!", - "401Message": "Neoprávněno", - "403Message": "Zakázáno", - "404Message": "Stránka nenalezena", - "500Message": "Interní chyba serveru", "GoHomePage": "Přejít na domovskou stránku", "GoBack": "Jít zpět", "Search": "Vyhledávání" diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json index 9727a11823..64bb0f99b5 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json @@ -1,20 +1,7 @@ { "culture": "de", "texts": { - "InternalServerErrorMessage": "Während Ihrer Anfrage ist ein interner Fehler aufgetreten!", - "ValidationErrorMessage": "Ihre Anfrage ist nicht gültig!", - "ValidationNarrativeErrorMessageTitle": "Die folgenden Fehler wurden bei der Validierung entdeckt.", - "DefaultErrorMessage": "Ein Fehler ist aufgetreten!", - "DefaultErrorMessageDetail": "Es wurden keine Fehlerdetails vom Server gesendet.", - "DefaultErrorMessage401": "Sie sind nicht authentifiziert.", - "DefaultErrorMessage401Detail": "Sie sollten sich anmelden, um diese Operation durchzuführen.", - "DefaultErrorMessage403": "Sie sind nicht autorisiert!", - "DefaultErrorMessage403Detail": "Es ist Ihnen nicht erlaubt, diese Operation durchzuführen!", - "DefaultErrorMessage404": "Ressource nicht gefunden!", - "DefaultErrorMessage404Detail": "Die angeforderte Ressource konnte auf dem Server nicht gefunden werden!", - "EntityNotFoundErrorMessage": "Es gibt keine Entität {0} mit id = {1}!", "Languages": "Sprachen", - "Error": "Fehler", "AreYouSure": "Sind Sie sicher?", "Cancel": "Abbrechen", "Yes": "Ja", @@ -52,11 +39,6 @@ "ChangePassword": "Passwort ändern", "PersonalInfo": "Mein Profil", "AreYouSureYouWantToCancelEditingWarningMessage": "Sie haben ungespeicherte Änderungen.", - "UnhandledException": "Unerwartete Ausnahme!", - "401Message": "Unauthorisiert", - "403Message": "Verboten", - "404Message": "Seite nicht gefunden", - "500Message": "Interner Server Fehler", "GoHomePage": "Zur Startseite", "GoBack": "Zurück" } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json index e0cdee07af..72c53e8adf 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json @@ -1,20 +1,7 @@ { "culture": "en", "texts": { - "InternalServerErrorMessage": "An internal error occurred during your request!", - "ValidationErrorMessage": "Your request is not valid!", - "ValidationNarrativeErrorMessageTitle": "The following errors were detected during validation.", - "DefaultErrorMessage": "An error has occurred!", - "DefaultErrorMessageDetail": "Error detail not sent by server.", - "DefaultErrorMessage401": "You are not authenticated!", - "DefaultErrorMessage401Detail": "You should sign in to perform this operation.", - "DefaultErrorMessage403": "You are not authorized!", - "DefaultErrorMessage403Detail": "You are not allowed to perform this operation!", - "DefaultErrorMessage404": "Resource not found!", - "DefaultErrorMessage404Detail": "The resource requested could not found on the server!", - "EntityNotFoundErrorMessage": "There is no entity {0} with id = {1}!", "Languages": "Languages", - "Error": "Error", "AreYouSure": "Are you sure?", "Cancel": "Cancel", "Clear": "Clear", @@ -55,15 +42,11 @@ "ChangePassword": "Change password", "PersonalInfo": "My profile", "AreYouSureYouWantToCancelEditingWarningMessage": "You have unsaved changes.", - "UnhandledException": "Unhandled exception!", - "401Message": "Unauthorized", - "403Message": "Forbidden", - "404Message": "Page not found", - "500Message": "Internal Server Error", "GoHomePage": "Go to the homepage", "GoBack": "Go back", "Search": "Search", "ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!", - "ItemWillBeDeletedMessage": "This item will be deleted!" + "ItemWillBeDeletedMessage": "This item will be deleted!", + "ManageYourAccount": "Manage your account" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index 69ac5dfc56..56d6b117cf 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -1,22 +1,10 @@ { "culture": "es", "texts": { - "InternalServerErrorMessage": "Ocurrió un error interno en su pedido!", - "ValidationErrorMessage": "Su pedido no es válido!", - "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.", - "DefaultErrorMessage": "Ocurrió un error!", - "DefaultErrorMessageDetail": "El servidor no envió el detalle del error.", - "DefaultErrorMessage401": "Usted no ha iniciado sessión!", - "DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.", - "DefaultErrorMessage403": "Usted no está autorizado!", - "DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!", - "DefaultErrorMessage404": "No se encontró el recurso!", - "DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!", - "EntityNotFoundErrorMessage": "No hay una entidad {0} con el id = {1}!", "Languages": "Lenguajes", - "Error": "Error", "AreYouSure": "¿Está seguro?", "Cancel": "Cancelar", + "Clear": "Limpiar", "Yes": "Si", "No": "No", "Ok": "Aceptar", @@ -47,18 +35,18 @@ "PagerInfoEmpty": "Mostrando desde 0 hasta 0 de 0 registros", "PagerInfoFiltered": "(filtrado de un total de _MAX_ registros)", "NoDataAvailableInDatatable": "No hay datos", + "Total": "total", + "Selected": "seleccionado", "PagerShowMenuEntries": "Mostrar _MENU_ registros", "DatatableActionDropdownDefaultText": "Acciones", "ChangePassword": "Cambiar contraseña", "PersonalInfo": "My perfil", "AreYouSureYouWantToCancelEditingWarningMessage": "Tiene cambios sin guardar.", - "UnhandledException": "Excepción no controlada!", - "401Message": "No autorizado", - "403Message": "Prohibido", - "404Message": "La página no existe", - "500Message": "Error interno del servidor", "GoHomePage": "Ir a la página principal", "GoBack": "Atras", - "Search": "Buscar" + "Search": "Buscar", + "ItemWillBeDeletedMessageWithFormat": "Se eliminarán {0}!", + "ItemWillBeDeletedMessage": "Este itém será eliminado!", + "ManageYourAccount": "Administre su cuenta" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json new file mode 100644 index 0000000000..8084270436 --- /dev/null +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json @@ -0,0 +1,51 @@ +{ + "culture": "fr", + "texts": { + "Languages": "Langues", + "AreYouSure": "Êtes-vous sûr?", + "Cancel": "Annuler", + "Clear": "Clair", + "Yes": "Oui", + "No": "Non", + "Ok": "D'accord", + "Close": "Fermer", + "Save": "Enregistrer", + "SavingWithThreeDot": "Enregistrement...", + "Actions": "Actions", + "Delete": "Supprimer", + "Edit": "Modifier", + "Refresh": "Actualiser", + "Language": "Langue", + "LoadMore": "Charger plus", + "ProcessingWithThreeDot": "Traitement...", + "LoadingWithThreeDot": "Chargement...", + "Welcome": "Bienvenue", + "Login": "Se connecter", + "Register": "S'inscrire", + "Logout": "Se déconnecter", + "Submit": "Envoyer", + "Back": "Précédent", + "PagerSearch": "Recherche", + "PagerNext": "prochain", + "PagerPrevious": "Précédent", + "PagerFirst": "Première", + "PagerLast": "Dernier", + "PagerInfo": "Affichage de _START_ à _END_ entrées sur _TOTAL_", + "PagerInfo{0}{1}{2}": "Affichage de {0} à {1} sur {2} entrées", + "PagerInfoEmpty": "Affichage de 0 à 0 sur 0 entrées", + "PagerInfoFiltered": "(filtré à partir des entrées _MAX_ totale)", + "NoDataAvailableInDatatable": "Aucune donnée disponible", + "Total": "Total", + "Selected": "Sélectionné", + "PagerShowMenuEntries": "Afficher les entrées _MENU_", + "DatatableActionDropdownDefaultText": "Actions", + "ChangePassword": "Changer le mot de passe", + "PersonalInfo": "Mon profil", + "AreYouSureYouWantToCancelEditingWarningMessage": "Vous avez des changements non enregistrés.", + "GoHomePage": "Aller à la page d'accueil", + "GoBack": "Retour", + "Search": "Recherche", + "ItemWillBeDeletedMessageWithFormat": "{0} sera supprimé!", + "ItemWillBeDeletedMessage": "Cet objet va être supprimé!" + } +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json new file mode 100644 index 0000000000..ed63fb50d0 --- /dev/null +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json @@ -0,0 +1,52 @@ +{ + "culture": "hu", + "texts": { + "Languages": "Nyelvek", + "AreYouSure": "Biztos benne?", + "Cancel": "Mégsem", + "Clear": "Tisztítás", + "Yes": "Igen", + "No": "Nem", + "Ok": "Rendben", + "Close": "Bezárás", + "Save": "Mentés", + "SavingWithThreeDot": "Mentés...", + "Actions": "Műveletek", + "Delete": "Törlés", + "Edit": "Szerkesztés", + "Refresh": "Frissítés", + "Language": "Nyelv", + "LoadMore": "Több betöltése", + "ProcessingWithThreeDot": "Feldolgozás...", + "LoadingWithThreeDot": "Töltés...", + "Welcome": "Üdvözlöm", + "Login": "Bejelentkezés", + "Register": "Regisztráció", + "Logout": "Kijelentkezés", + "Submit": "Beküldés", + "Back": "Vissza", + "PagerSearch": "Keresés", + "PagerNext": "Következő", + "PagerPrevious": "Előző", + "PagerFirst": "Első", + "PagerLast": "Utolsó", + "PagerInfo": "Látható _START_ -tól _END_ -ig _TOTAL_ elemből", + "PagerInfo{0}{1}{2}": "Látható {0}-tól {1}-ig {2} elemből", + "PagerInfoEmpty": "Látható 0-tól 0-ig 0 elemből", + "PagerInfoFiltered": "(szűrve _MAX_ elemből)", + "NoDataAvailableInDatatable": "Nem elérhető adat", + "Total": "összesen", + "Selected": "kiválasztott", + "PagerShowMenuEntries": "Megjelenítve _MENU_ elem", + "DatatableActionDropdownDefaultText": "Műveletek", + "ChangePassword": "Jelszó véltoztatás", + "PersonalInfo": "Saját profil", + "AreYouSureYouWantToCancelEditingWarningMessage": "Vannak mentetlen változtatásai.", + "GoHomePage": "Kezdőlapra lépés", + "GoBack": "Menjen vissza", + "Search": "Keresés", + "ItemWillBeDeletedMessageWithFormat": "{0} törlésre kerül!", + "ItemWillBeDeletedMessage": "Ez az elem törlődik!", + "ManageYourAccount": "Kezelje fiókját" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json index 6198aa360f..c65715f8a5 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json @@ -1,20 +1,7 @@ { "culture": "nl", "texts": { - "InternalServerErrorMessage": "Er is een interne fout opgetreden tijdens uw verzoek!", - "ValidationErrorMessage": "Uw verzoek is niet geldig!", - "ValidationNarrativeErrorMessageTitle": "Tijdens de validatie zijn de volgende fouten gedetecteerd.", - "DefaultErrorMessage": "er is een fout opgetreden!", - "DefaultErrorMessageDetail": "Foutdetails niet verzonden door server.", - "DefaultErrorMessage401": "U bent niet geverifieerd!", - "DefaultErrorMessage401Detail": "U moet inloggen om deze bewerking uit te voeren.", - "DefaultErrorMessage403": "U bent niet geautoriseerd!", - "DefaultErrorMessage403Detail": "U mag deze bewerking niet uitvoeren!", - "DefaultErrorMessage404": "Bron niet gevonden!", - "DefaultErrorMessage404Detail": "De gevraagde bron kan niet worden gevonden op de server!", - "EntityNotFoundErrorMessage": "Er is geen entiteit {0} met id = {1}!", "Languages": "Talen", - "Error": "Fout", "AreYouSure": "Bent u zeker?", "Cancel": "Annuleren", "Yes": "Ja", @@ -52,11 +39,6 @@ "ChangePassword": "Verander wachtwoord", "PersonalInfo": "Mijn profiel", "AreYouSureYouWantToCancelEditingWarningMessage": "U heeft nog niet-opgeslagen wijzigingen.", - "UnhandledException": "Onverwerkte uitzondering!", - "401Message": "Ongeautoriseerd", - "403Message": "Verboden", - "404Message": "Pagina niet gevonden", - "500Message": "Interne Server Fout", "GoHomePage": "Ga naar de homepage", "GoBack": "Ga terug", "Search": "Zoeken" diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json index 1be12a51cf..7c6738e0bd 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json @@ -1,19 +1,6 @@ { "culture": "pl-PL", "texts": { - "InternalServerErrorMessage": "Błąd wewnętrzny serwera podczas przetwarzania żądania!", - "ValidationErrorMessage": "Twoje żądanie jest niepoprawnie!", - "ValidationNarrativeErrorMessageTitle": "Wykryto następujące błędy podczas walidacji.", - "DefaultErrorMessage": "Wystąpił błąd!", - "DefaultErrorMessageDetail": "Treść błędu nie została wysłana z serwera.", - "DefaultErrorMessage401": "Nie jesteś zalogowany!", - "DefaultErrorMessage401Detail": "Musisz się zalogować, aby wykonać tą operację.", - "DefaultErrorMessage403": "Nie jesteś zautentykowany!", - "DefaultErrorMessage403Detail": "Nie masz uprawnień do wykonania tej operacji!", - "DefaultErrorMessage404": "Nie znaleziono zasobu!", - "DefaultErrorMessage404Detail": "Nie znaleziono zasobu z żądania na serwerze!", - "EntityNotFoundErrorMessage": "Nie istnieje encja {0} z id = {1}!", - "Error": "Błąd", "AreYouSure": "Czy jesteś pewien?", "Cancel": "Anuluj", "Yes": "Tak", @@ -49,4 +36,4 @@ "PersonalInfo": "Mój profil", "AreYouSureYouWantToCancelEditingWarningMessage": "Masz niezapisane zmiany." } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json index 9d69ba1ef8..0aa20797c9 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json @@ -1,19 +1,6 @@ { "culture": "pt-BR", "texts": { - "InternalServerErrorMessage": "Um erro interno ocorreu durante a requisição!", - "ValidationErrorMessage": "Sua requisição não é válida!", - "ValidationNarrativeErrorMessageTitle": "Os seguintes erros foram detectados durante a validação:", - "DefaultErrorMessage": "Ocorreu um erro!", - "DefaultErrorMessageDetail": "Os detalhes do erro não foram enviados pelo servidor.", - "DefaultErrorMessage401": "Você não etá autenticado!", - "DefaultErrorMessage401Detail": "Você deve autenticar-se para proceder com esta operação.", - "DefaultErrorMessage403": "Você não possui autorização!", - "DefaultErrorMessage403Detail": "Você não tem permissão para executar esta operação!", - "DefaultErrorMessage404": "Recurso não encontrado!", - "DefaultErrorMessage404Detail": "O recurso requisitado não pode ser encontrado pelo servidor!", - "EntityNotFoundErrorMessage": "Não existe uma entidade {0} com código = {1}!", - "Error": "Erro", "AreYouSure": "Você tem certeza?", "Cancel": "Cancelar", "Yes": "Sim", @@ -48,13 +35,8 @@ "ChangePassword": "Alterar Senha", "PersonalInfo": "Perfil", "AreYouSureYouWantToCancelEditingWarningMessage": "Há alterações não salvas.", - "UnhandledException": "Exceção não tratada!", - "401Message": "Autenticação inválida", - "403Message": "Não autorizado", - "404Message": "Página não encontrada", - "500Message": "Erro interno do servidor", "GoHomePage": "Voltar à página principal", "GoBack": "Voltar", "Search": "Procurar" } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json index 12ccca117b..7b4014ebef 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json @@ -1,20 +1,7 @@ { "culture": "ru", "texts": { - "InternalServerErrorMessage": "Во время запроса произошла внутренняя ошибка!", - "ValidationErrorMessage": "Ваш запрос недействителен!", - "ValidationNarrativeErrorMessageTitle": "При проверке были обнаружены следующие ошибки.", - "DefaultErrorMessage": "Произошла ошибка!", - "DefaultErrorMessageDetail": "Сведения об ошибке не были предоставлены сервером.", - "DefaultErrorMessage401": "Вы не авторизированы!", - "DefaultErrorMessage401Detail": "Вы должны Войти, чтобы выполнить эту операцию.", - "DefaultErrorMessage403": "Вы не авторизованы!", - "DefaultErrorMessage403Detail": "У вас нет доступа к выполнению этой операции!", - "DefaultErrorMessage404": "Ресурс не найден!", - "DefaultErrorMessage404Detail": "Запрошенный ресурс не удалось найти на сервере!", - "EntityNotFoundErrorMessage": "Нет объекта {0} с id = {1}!", "Languages": "Языки", - "Error": "Ошибка", "AreYouSure": "Вы уверены?", "Cancel": "Отмена", "Yes": "Да", @@ -51,11 +38,6 @@ "ChangePassword": "Изменить пароль", "PersonalInfo": "Мой профиль", "AreYouSureYouWantToCancelEditingWarningMessage": "У вас есть несохраненные изменения.", - "UnhandledException": "Непредвиденная ошибка!", - "401Message": "Неавторизованный", - "403Message": "В доступе отказано", - "404Message": "Страница не найдена", - "500Message": "Внутренняя ошибка сервера", "GoHomePage": "Вернуться на главную страницу", "GoBack": "Вернуться назад", "Search": "поиск" diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json index 2bbdb2c09d..2b285d97e4 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json @@ -1,20 +1,7 @@ { "culture": "sl", "texts": { - "InternalServerErrorMessage": "Zgodila se je napaka na strežniku!", - "ValidationErrorMessage": "Vaš zahtevek ni veljaven!", - "ValidationNarrativeErrorMessageTitle": "Pri preverjanju so bile zaznane sledeče napake.", - "DefaultErrorMessage": "Zgodila se je napaka!", - "DefaultErrorMessageDetail": "Strežnik ni poslal podrobnosti o napaki.", - "DefaultErrorMessage401": "Niste prijavljeni!", - "DefaultErrorMessage401Detail": "Za izvedbo te operacije se morate prijaviti.", - "DefaultErrorMessage403": "Nimate pravic!", - "DefaultErrorMessage403Detail": "Nimate dovoljenja za izvedbo te operacije!", - "DefaultErrorMessage404": "Vir ni bil najden!", - "DefaultErrorMessage404Detail": "Zahtevanega vira ni bilo mogoče najti na strežniku!", - "EntityNotFoundErrorMessage": "Ni entitete {0} z id-jem = {1}!", "Languages": "Jeziki", - "Error": "Napaka", "AreYouSure": "Ali ste prepričani?", "Cancel": "Prekliči", "Yes": "Da", @@ -52,11 +39,6 @@ "ChangePassword": "Zamenjaj geslo", "PersonalInfo": "Moj profil", "AreYouSureYouWantToCancelEditingWarningMessage": "Imate neshranjene spremembe.", - "UnhandledException": "Neobravnavana napaka!", - "401Message": "Nepooblaščeno", - "403Message": "Prepovedano", - "404Message": "Strani ni mogoče najti", - "500Message": "Napaka na strani strežnika", "GoHomePage": "Pojdi na osnovno stran", "GoBack": "Nazaj", "Search": "Iskanje" diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json index db803e01e2..630902d99e 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json @@ -1,20 +1,7 @@ { "culture": "tr", "texts": { - "InternalServerErrorMessage": "Sayfa işlenirken sunucu tarafında beklenmedik bir hata oluştu!", - "ValidationErrorMessage": "İşleminiz geçerli değil!", - "ValidationNarrativeErrorMessageTitle": "Aşağıdaki hatalar doğrulama sırasında tespit edilmiştir.", - "DefaultErrorMessage": "Bir hata oluştu!", - "DefaultErrorMessageDetail": "Hatanın ne olduğu sunucu tarafından gönderilmedi.", - "DefaultErrorMessage401": "Giriş yapılmamış!", - "DefaultErrorMessage401Detail": "Bu işlemi gerçekleştirmek için uygulamaya giriş yapmalısınız.", - "DefaultErrorMessage403": "İzinsiz işlem!", - "DefaultErrorMessage403Detail": "Bu işlemi gerçekleştirmek için gereken izne sahip değilsiniz.", - "DefaultErrorMessage404": "Kaynak bulunamadı!", - "DefaultErrorMessage404Detail": "İstenilen kaynak sunucuda bulunamadı.", - "EntityNotFoundErrorMessage": "Id değeri {1} olan {0} türünden bir nesne bulunamadı!", "Languages": "Diller", - "Error": "Hata", "AreYouSure": "Emin misiniz?", "Cancel": "Vazgeç", "Clear": "Temizle", @@ -55,15 +42,11 @@ "ChangePassword": "Şifre değiştir", "PersonalInfo": "Profilim", "AreYouSureYouWantToCancelEditingWarningMessage": "Kaydedilmemiş değişiklikler var.", - "UnhandledException": "Yakalanmamış hata!", - "401Message": "Yetkisiz", - "403Message": "Yasak", - "404Message": "Sayfa bulunamadı", - "500Message": "Sunucu tarafında hata", "GoHomePage": "Ana sayfaya git", "GoBack": "Geri dön", "Search": "Arama", "ItemWillBeDeletedMessageWithFormat": "{0} silinecektir!", - "ItemWillBeDeletedMessage": "Bu nesne silinecektir!" + "ItemWillBeDeletedMessage": "Bu nesne silinecektir!", + "ManageYourAccount": "Hesap yönetimi" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json index 00f6c8f195..239622e438 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json @@ -1,19 +1,6 @@ { "culture": "vi", "texts": { - "InternalServerErrorMessage": "Có một lỗi nội bộ xảy ra trong quá trình thực hiện yêu cầu của bạn!", - "ValidationErrorMessage": "Yêu cầu của bạn không hợp lệ!", - "ValidationNarrativeErrorMessageTitle": "Các lỗi sau đây đã được phát hiện trong quá trình xác nhận", - "DefaultErrorMessage": "Một lỗi đã xảy ra", - "DefaultErrorMessageDetail": "Chi tiết lỗi không được gửi bởi máy chủ", - "DefaultErrorMessage401": "Bạn chưa được xác thực", - "DefaultErrorMessage401Detail": "Bạn cần đăng nhập để thực hiện thao tác này.", - "DefaultErrorMessage403": "Bạn không được phép!", - "DefaultErrorMessage403Detail": "Bạn không được phép thực hiện thao tác này!", - "DefaultErrorMessage404": "Tài nguyên không tìm thấy!", - "DefaultErrorMessage404Detail": "Tài nguyên được yêu cầu không được tìm thấy trên máy chủ!", - "EntityNotFoundErrorMessage": "Không có thực thể nào {0} với id = {1}!", - "Error": "Lỗi", "AreYouSure": "Bạn có chắc không ?", "Cancel": "Hủy bỏ", "Yes": "Đồng ý", diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json index 77fae4351a..59a680bb81 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json @@ -1,24 +1,13 @@ { "culture": "zh-Hans", "texts": { - "InternalServerErrorMessage": "对不起,在处理你的请求期间,产生了一个服务器内部错误!", - "ValidationErrorMessage": "你的请求无效!", - "ValidationNarrativeErrorMessageTitle": "验证时发现以下错误.", - "DefaultErrorMessage": "发生错误!", - "DefaultErrorMessageDetail": "服务器未发送错误的详细信息.", - "DefaultErrorMessage401": "未通过身份验证!", - "DefaultErrorMessage401Detail": "你需要进行身份认证(登录)后再执行此操作.", - "DefaultErrorMessage403": "你没有权限!", - "DefaultErrorMessage403Detail": "你不能执行此操作!", - "DefaultErrorMessage404": "未找到资源!", - "DefaultErrorMessage404Detail": "未在服务中找到请求的资源!", - "EntityNotFoundErrorMessage": "实体 {0} 不存在,id = {1}!", "Languages": "语言", - "Error": "错误", "AreYouSure": "你确定吗?", "Cancel": "取消", + "Clear": "清楚", "Yes": "是", "No": "否", + "Ok": "Ok", "Close": "关闭", "Save": "保存", "SavingWithThreeDot": "保存中...", @@ -26,6 +15,8 @@ "Delete": "删除", "Edit": "编辑", "Refresh": "刷新", + "Language": "语言", + "LoadMore": "加载更多", "ProcessingWithThreeDot": "处理中...", "LoadingWithThreeDot": "加载中...", "Welcome": "欢迎", @@ -44,18 +35,18 @@ "PagerInfoEmpty": "显示0个条目中的0到0", "PagerInfoFiltered": "(从 _MAX_ 总条目中过滤掉)", "NoDataAvailableInDatatable": "表中没有数据", + "Total": "总计", + "Selected": "已选", "PagerShowMenuEntries": "显示 _MENU_ 条数据", "DatatableActionDropdownDefaultText": "操作", "ChangePassword": "修改密码", "PersonalInfo": "个人信息", "AreYouSureYouWantToCancelEditingWarningMessage": "你有未保存的更改.", - "UnhandledException": "未处理的异常!", - "401Message": "未授权", - "403Message": "禁止访问", - "404Message": "网页未找到", - "500Message": "内部服务器错误", "GoHomePage": "返回主页", "GoBack": "返回", - "Search" : "搜索" + "Search": "搜索", + "ItemWillBeDeletedMessageWithFormat": "{0} 将被删除!", + "ItemWillBeDeletedMessage": "此项将被删除!", + "ManageYourAccount": "管理你的账户" } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json index 496e6e3e9d..ab529791be 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json @@ -1,22 +1,10 @@ { "culture": "zh-Hant", "texts": { - "InternalServerErrorMessage": "對不起,在處理你的請求期間,產生了一個伺服器內部錯誤!", - "ValidationErrorMessage": "你的請求無效!", - "ValidationNarrativeErrorMessageTitle": "驗證時發現以下錯誤.", - "DefaultErrorMessage": "發生錯誤!", - "DefaultErrorMessageDetail": "伺服器未發送錯誤的詳細信息.", - "DefaultErrorMessage401": "未通過身份驗證!", - "DefaultErrorMessage401Detail": "你需要進行身份認證(登入)後再執行此操作.", - "DefaultErrorMessage403": "你沒有權限!", - "DefaultErrorMessage403Detail": "你不能執行此操作!", - "DefaultErrorMessage404": "未找到資源!", - "DefaultErrorMessage404Detail": "未在服務中找到請求的資源!", - "EntityNotFoundErrorMessage": "實體 {0} 不存在,id = {1}!", "Languages": "語系", - "Error": "錯誤", "AreYouSure": "你確定嗎?", "Cancel": "取消", + "Clear": "清除", "Yes": "是", "No": "否", "Ok": "好", @@ -54,13 +42,11 @@ "ChangePassword": "修改密碼", "PersonalInfo": "個人資料", "AreYouSureYouWantToCancelEditingWarningMessage": "你有未保存的更改", - "UnhandledException": "未處理的異常!", - "401Message": "未授權", - "403Message": "禁止訪問", - "404Message": "網頁未找到", - "500Message": "內部伺服器錯誤", "GoHomePage": "返回首頁", "GoBack": "返回", - "Search": "查詢" + "Search": "查詢", + "ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!", + "ItemWillBeDeletedMessage": "This item will be deleted!", + "ManageYourAccount": "Manage your account" } } diff --git a/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj b/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj index acff7d004b..7cf2afccb1 100644 --- a/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj +++ b/framework/src/Volo.Abp.UI/Volo.Abp.UI.csproj @@ -18,9 +18,9 @@ - + - + diff --git a/framework/src/Volo.Abp.UI/Volo/Abp/Ui/AbpUiModule.cs b/framework/src/Volo.Abp.UI/Volo/Abp/Ui/AbpUiModule.cs index 90a9aa45f6..5191063f98 100644 --- a/framework/src/Volo.Abp.UI/Volo/Abp/Ui/AbpUiModule.cs +++ b/framework/src/Volo.Abp.UI/Volo/Abp/Ui/AbpUiModule.cs @@ -1,5 +1,6 @@ using Localization.Resources.AbpUi; -using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.ExceptionHandling.Localization; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; @@ -7,7 +8,7 @@ using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.UI { [DependsOn( - typeof(AbpLocalizationModule) + typeof(AbpExceptionHandlingModule) )] public class AbpUiModule : AbpModule { @@ -22,6 +23,7 @@ namespace Volo.Abp.UI { options.Resources .Add("en") + .AddBaseTypes(typeof(AbpExceptionHandlingResource)) .AddVirtualJson("/Localization/Resources/AbpUi"); }); } diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs index 0a1fcc7a9f..3a3ef1fbd6 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs @@ -7,12 +7,12 @@ namespace Volo.Abp.Uow { public IUnitOfWork Current => GetCurrentUnitOfWork(); - private readonly IHybridServiceScopeFactory _serviceScopeFactory; + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IAmbientUnitOfWork _ambientUnitOfWork; public UnitOfWorkManager( - IAmbientUnitOfWork ambientUnitOfWork, - IHybridServiceScopeFactory serviceScopeFactory) + IAmbientUnitOfWork ambientUnitOfWork, + IServiceScopeFactory serviceScopeFactory) { _ambientUnitOfWork = ambientUnitOfWork; _serviceScopeFactory = serviceScopeFactory; @@ -38,7 +38,7 @@ namespace Volo.Abp.Uow { Check.NotNull(reservationName, nameof(reservationName)); - if (!requiresNew && + if (!requiresNew && _ambientUnitOfWork.UnitOfWork != null && _ambientUnitOfWork.UnitOfWork.IsReservedFor(reservationName)) { @@ -122,4 +122,4 @@ namespace Volo.Abp.Uow } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Validation.Abstractions/Volo.Abp.Validation.Abstractions.csproj b/framework/src/Volo.Abp.Validation.Abstractions/Volo.Abp.Validation.Abstractions.csproj index 2ed6955187..32ada2ce09 100644 --- a/framework/src/Volo.Abp.Validation.Abstractions/Volo.Abp.Validation.Abstractions.csproj +++ b/framework/src/Volo.Abp.Validation.Abstractions/Volo.Abp.Validation.Abstractions.csproj @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs index 797721bea3..25b59fbbcf 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/DataAnnotationObjectValidationContributor.cs @@ -45,12 +45,18 @@ namespace Volo.Abp.Validation AddErrors(errors, validatingObject); //Validate items of enumerable - if (validatingObject is IEnumerable) + if (validatingObject is IEnumerable enumerable) { - if (!(validatingObject is IQueryable)) + if (!(enumerable is IQueryable)) { - foreach (var item in (validatingObject as IEnumerable)) + foreach (var item in enumerable) { + //Do not recursively validate for primitive objects + if (TypeHelper.IsPrimitiveExtended(item.GetType())) + { + break; + } + ValidateObjectRecursively(errors, item, currentDepth + 1); } } @@ -124,4 +130,4 @@ namespace Volo.Abp.Validation } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/fr.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/fr.json new file mode 100644 index 0000000000..558bc9ca11 --- /dev/null +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/fr.json @@ -0,0 +1,34 @@ +{ + "culture": "fr", + "texts": { + "'{0}' and '{1}' do not match.": "'{0}' et '{1}' ne correspondent pas.", + "The {0} field is not a valid credit card number.": "Le champ {0} n'est pas un numéro de carte de crédit valide.", + "{0} is not valid.": "{0} n'est pas valide.", + "The {0} field is not a valid e-mail address.": "Le champ {0} n'est pas une adresse e-mail valide.", + "The {0} field only accepts files with the following extensions: {1}": "Le champ {0} accepte uniquement les fichiers avec les extensions suivantes: {1}", + "The field {0} must be a string or array type with a maximum length of '{1}'.": "Le champ {0} doit être une chaîne ou un type de tableau d'une longueur maximale de '{1}'.", + "The field {0} must be a string or array type with a minimum length of '{1}'.": "Le champ {0} doit être une chaîne ou un type de tableau d'une longueur minimale de '{1}'.", + "The {0} field is not a valid phone number.": "Le champ {0} n'est pas un numéro de téléphone valide.", + "The field {0} must be between {1} and {2}.": "Le champ {0} doit être compris entre {1} et {2}.", + "The field {0} must match the regular expression '{1}'.": "Le champ {0} ne correspond pas au format demandé.", + "The {0} field is required.": "Le champ {0} est obligatoire.", + "The field {0} must be a string with a maximum length of {1}.": "Le champ {0} doit être une chaîne d'une longueur maximale de {1}.", + "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Le champ {0} doit être une chaîne d'une longueur minimale de {2} et d'une longueur maximale de {1}.", + "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Le champ {0} n'est pas une URL http, https ou ftp complète valide.", + "The field {0} is invalid.": "Le champ {0} n'est pas valide.", + "ThisFieldIsNotAValidCreditCardNumber.": "Ce champ n'est pas un numéro de carte de crédit valide.", + "ThisFieldIsNotValid.": "Ce champ n'est pas valide.", + "ThisFieldIsNotAValidEmailAddress.": "Ce champ n'est pas une adresse e-mail valide.", + "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "Ce champ accepte uniquement les fichiers avec les extensions suivantes: {0}", + "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}": "Ce champ doit être une chaîne ou un type de tableau d'une longueur maximale de '{0}'.", + "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "Ce champ doit être un type de chaîne ou de tableau d'une longueur minimale de '{0}'.", + "ThisFieldIsNotAValidPhoneNumber.": "Ce champ n'est pas un numéro de téléphone valide.", + "ThisFieldMustBeBetween{0}And{1}": "Ce champ doit être compris entre {0} et {1}.", + "ThisFieldMustMatchTheRegularExpression{0}": "Ce champ doit correspondre à l'expression régulière '{0}'.", + "ThisFieldIsRequired.": "Ce champ est requis.", + "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "Ce champ doit être une chaîne d'une longueur maximale de {0}.", + "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "Ce champ doit être une chaîne d'une longueur minimale de {1} et d'une longueur maximale de {0}.", + "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "Ce champ n'est pas une URL http, https ou ftp complète valide.", + "ThisFieldIsInvalid.": "Ce champ n'est pas valide." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/hu.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/hu.json new file mode 100644 index 0000000000..b956b72f96 --- /dev/null +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/hu.json @@ -0,0 +1,34 @@ +{ + "culture": "hu", + "texts": { + "'{0}' and '{1}' do not match.": "'{0}' és '{1}' nem egyforma.", + "The {0} field is not a valid credit card number.": "A {0} érvényes bankkártya szám.", + "{0} is not valid.": "{0} nem érvényes.", + "The {0} field is not a valid e-mail address.": "A {0} nem érvényes email cím.", + "The {0} field only accepts files with the following extensions: {1}": "A {0} mező csak olyan fileokat tartalmazhat melynek kiterjesztése ezek: {1}", + "The field {0} must be a string or array type with a maximum length of '{1}'.": "A {0} mező szöveg vagy felsorolás típusú lehet '{1}' maximum hosszal.", + "The field {0} must be a string or array type with a minimum length of '{1}'.": "A {0} mező szöveg vagy felsorolás típusú lehet '{1}' minimum hosszal.", + "The {0} field is not a valid phone number.": "A {0} nem érvényes telefonszám.", + "The field {0} must be between {1} and {2}.": "A {0} értéke {1} és {2} között kell legyen.", + "The field {0} must match the regular expression '{1}'.": "A {0} nem megfelelő a szükséges formátumnak.", + "The {0} field is required.": "A {0} kötelező.", + "The field {0} must be a string with a maximum length of {1}.": "A {0} szövegnek kell lennie maximum {1} hosszan.", + "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "A {0} mezőnek szöveget kell tartalmaznia minimum {2} és maximum {1} hosszan.", + "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "A {0} mező nem érvényes fully-qualified http, https, vagy ftp URL cím.", + "The field {0} is invalid.": "A {0} mező nem érvényes.", + "ThisFieldIsNotAValidCreditCardNumber.": "A mező nem érvényes bankkártya számot tartalmaz.", + "ThisFieldIsNotValid.": "A mező nem érvényes.", + "ThisFieldIsNotAValidEmailAddress.": "A mező nem érvényes email cím.", + "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "A mező csak fileneveket fogad a következő kiterjesztésekkel: {0}", + "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}": "A mezőnek szöveg vagy felsorolás tpusúnak kell lennie maximum '{0}' hosszal.", + "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "A mezőnek szöveg vagy felsorolás tpusúnak kell lennie minimum '{0}' hosszal.", + "ThisFieldIsNotAValidPhoneNumber.": "A mező nem érvényes telefonszám.", + "ThisFieldMustBeBetween{0}And{1}": "A mező értéke {0} és {1} között kell lennie.", + "ThisFieldMustMatchTheRegularExpression{0}": "A mezőnek meg kell felelnie a következő reguláris kifejezésnek '{0}'.", + "ThisFieldIsRequired.": "A mező szükséges.", + "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "A mezőnek szöveg tpusúnak kell lennie maximum {0} hosszal.", + "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "A mezőnek szöveget kell tartalmaznia minimum {1} és maximum {0} hosszan.", + "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "A mező nem érvényes fully-qualified http, https, vagy ftp URL cím.", + "ThisFieldIsInvalid.": "A mező nem érvényes." + } + } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/zh-Hans.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/zh-Hans.json index 1f40a603e8..0a41ce1ed5 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/zh-Hans.json +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/zh-Hans.json @@ -27,7 +27,7 @@ "ThisFieldMustMatchTheRegularExpression{0}": "字段必须匹配正则表达式'{0}'.", "ThisFieldIsRequired.": "字段不可为空.", "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "字段必须是长度为{0}的字符串.", - "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "字段必须是最小长度为{1}并且最大长度{*}的字符串.", + "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "字段必须是最小长度为{1}并且最大长度{0}的字符串.", "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "字段{0}不是有效的完全限定的http,https或ftp URL.", "ThisFieldIsInvalid.": "该字段无效." } diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidator.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidator.cs index 784c211061..d8e0c60b65 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidator.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/ObjectValidator.cs @@ -9,10 +9,10 @@ namespace Volo.Abp.Validation { public class ObjectValidator : IObjectValidator, ITransientDependency { - protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } protected AbpValidationOptions Options { get; } - public ObjectValidator(IOptions options, IHybridServiceScopeFactory serviceScopeFactory) + public ObjectValidator(IOptions options, IServiceScopeFactory serviceScopeFactory) { ServiceScopeFactory = serviceScopeFactory; Options = options.Value; @@ -56,7 +56,7 @@ namespace Volo.Abp.Validation { foreach (var contributorType in Options.ObjectValidationContributors) { - var contributor = (IObjectValidationContributor) + var contributor = (IObjectValidationContributor) scope.ServiceProvider.GetRequiredService(contributorType); contributor.AddErrors(context); } @@ -65,4 +65,4 @@ namespace Volo.Abp.Validation return context.Errors; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj b/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj index 09af40a2c7..748524acd2 100644 --- a/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj +++ b/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/framework/test/AbpTestBase/AbpTestBase.csproj b/framework/test/AbpTestBase/AbpTestBase.csproj index a1b2dcd89b..10e5b5d74a 100644 --- a/framework/test/AbpTestBase/AbpTestBase.csproj +++ b/framework/test/AbpTestBase/AbpTestBase.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + net5.0 AbpTestBase AbpTestBase diff --git a/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj b/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj index 4edbb748b9..cbb90d7379 100644 --- a/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj +++ b/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net5.0 @@ -18,7 +18,7 @@ - + diff --git a/framework/test/Volo.Abp.AspNetCore.Authentication.OAuth.Tests/Volo.Abp.AspNetCore.Authentication.OAuth.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Authentication.OAuth.Tests/Volo.Abp.AspNetCore.Authentication.OAuth.Tests.csproj index 0d170148d4..0a3603d7c0 100644 --- a/framework/test/Volo.Abp.AspNetCore.Authentication.OAuth.Tests/Volo.Abp.AspNetCore.Authentication.OAuth.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Authentication.OAuth.Tests/Volo.Abp.AspNetCore.Authentication.OAuth.Tests.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + net5.0 latest Volo.Abp.AspNetCore.Authentication.OAuth.Tests Volo.Abp.AspNetCore.Authentication.OAuth.Tests diff --git a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo.Abp.AspNetCore.MultiTenancy.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo.Abp.AspNetCore.MultiTenancy.Tests.csproj index 9e7bf68457..e5c8f6a398 100644 --- a/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo.Abp.AspNetCore.MultiTenancy.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.MultiTenancy.Tests/Volo.Abp.AspNetCore.MultiTenancy.Tests.csproj @@ -1,9 +1,9 @@ - + - netcoreapp3.1 + net5.0 Volo.Abp.AspNetCore.MultiTenancy.Tests Volo.Abp.AspNetCore.MultiTenancy.Tests diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj index c6450e6df1..200e8e45a4 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + net5.0 $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; Volo.Abp.AspNetCore.Mvc.Tests Volo.Abp.AspNetCore.Mvc.Tests diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs index c666aeaa44..f569283415 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs @@ -87,6 +87,7 @@ namespace Volo.Abp.AspNetCore.Mvc ).AddVirtualJson("/Volo/Abp/AspNetCore/Mvc/Localization/Resource"); options.Languages.Add(new LanguageInfo("en", "en", "English")); + options.Languages.Add(new LanguageInfo("hu", "hu", "Magyar")); options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe")); }); diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/es.json b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/es.json new file mode 100644 index 0000000000..baf2d0c45b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/es.json @@ -0,0 +1,7 @@ +{ + "culture": "es", + "texts": { + "BirthDate": "Fecha de nacimiento", + "Value1": "Valor uno" + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/fr.json b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/fr.json new file mode 100644 index 0000000000..dfc8dce92a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/fr.json @@ -0,0 +1,7 @@ +{ + "culture": "fr", + "texts": { + "BirthDate": "Date de naissance", + "Value1": "Valeur 1" + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/hu.json b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/hu.json new file mode 100644 index 0000000000..3aba5802f0 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/hu.json @@ -0,0 +1,7 @@ +{ + "culture": "hu", + "texts": { + "BirthDate": "Születési dátum", + "Value1": "Első érték" + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs index f2ab8fd3d0..698723a34a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs @@ -43,8 +43,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding response.StatusCode.ShouldBe(HttpStatusCode.OK); var resultAsString = await response.Content.ReadAsStringAsync(); - //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local. - resultAsString.ShouldBe(DateTimeKind.Local.ToString().ToLower()); + //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC + //https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times + resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower()); } [Fact] @@ -56,8 +57,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding response.StatusCode.ShouldBe(HttpStatusCode.OK); var resultAsString = await response.Content.ReadAsStringAsync(); - //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local. - resultAsString.ShouldBe(DateTimeKind.Local.ToString().ToLower()); + //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC + //https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times + resultAsString.ShouldBe(DateTimeKind.Utc.ToString().ToLower()); } [Fact] @@ -71,9 +73,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding response.StatusCode.ShouldBe(HttpStatusCode.OK); var resultAsString = await response.Content.ReadAsStringAsync(); - //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local. + //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is UTC + //https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50?view=aspnetcore-3.1&tabs=visual-studio#datetime-values-are-model-bound-as-utc-times resultAsString.ShouldBe( - $"local_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_local"); + $"utc_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_utc"); } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs index a066622748..f30dc082aa 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs @@ -10,7 +10,10 @@ namespace Volo.Abp.AspNetCore.Mvc.Utils new[] { "p1", "p2", "p3", "p4", "p5" }, new[] { "p1", "p2", "p2", "p2", "p3", "p4", "p5", "p5" }) ] - public void Should_Find_Correct_Items(string[] sourceArray, string[] destinationArray, string[] expectedArray) + public void Should_Find_Correct_Items( + string[] sourceArray, + string[] destinationArray, + string[] expectedArray) { var result = ArrayMatcher.Match(sourceArray, destinationArray); Assert.Equal(expectedArray, result); diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests.csproj index f1fa33a3b3..f3976e2e1f 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + net5.0 $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Tests diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile index 41b878cf7a..b87da6d564 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base +FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base WORKDIR /app EXPOSE 80 COPY bin/Release/publish . diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Buttons.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Buttons.cshtml index e58817d91c..c3aa9c0c92 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Buttons.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Buttons.cshtml @@ -295,6 +295,9 @@
  • busy-text: Sets "data-busy-text" attribute. Default value is localization of "ProcessingWithThreeDot" string.
  • +
  • + busy-text-is-html: Sets "data-busy-is-html" attribute. Default value is "false". +
  • \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Carousel.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Carousel.cshtml index 3763cd2601..4005b9d3d2 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Carousel.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Carousel.cshtml @@ -2,6 +2,11 @@ @model Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components.CarouselsModel @{ ViewData["Title"] = "Carousels"; + + var firstSlide = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgODAwIDQwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4jaG9sZGVyXzE2MzQzY2ZiNzEyIHRleHQgeyBmaWxsOiM0NDQ7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtZmFtaWx5OkhlbHZldGljYSwgbW9ub3NwYWNlO2ZvbnQtc2l6ZTo0MHB0IH0gPC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNjM0M2NmYjcxMiI+PHJlY3Qgd2lkdGg9IjgwMCIgaGVpZ2h0PSI0MDAiIGZpbGw9IiM2NjYiPjwvcmVjdD48Zz48dGV4dCB4PSIyNDcuMjkxNjcxNzUyOTI5NyIgeT0iMjE4LjMiPkZpcnN0IHNsaWRlPC90ZXh0PjwvZz48L2c+PC9zdmc+"; + var secondSlide = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgODAwIDQwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4jaG9sZGVyXzE2MzQzY2ZiNzEyIHRleHQgeyBmaWxsOiM0NDQ7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtZmFtaWx5OkhlbHZldGljYSwgbW9ub3NwYWNlO2ZvbnQtc2l6ZTo0MHB0IH0gPC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNjM0M2NmYjcxMiI+PHJlY3Qgd2lkdGg9IjgwMCIgaGVpZ2h0PSI0MDAiIGZpbGw9IiM2NjYiPjwvcmVjdD48Zz48dGV4dCB4PSIyNDcuMjkxNjcxNzUyOTI5NyIgeT0iMjE4LjMiPlNlY29uZCBzbGlkZTwvdGV4dD48L2c+PC9nPjwvc3ZnPg=="; + var thirdSlide = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgODAwIDQwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4jaG9sZGVyXzE2MzQzY2ZiNzE0IHRleHQgeyBmaWxsOiMzMzM7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtZmFtaWx5OkhlbHZldGljYSwgbW9ub3NwYWNlO2ZvbnQtc2l6ZTo0MHB0IH0gPC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNjM0M2NmYjcxNCI+PHJlY3Qgd2lkdGg9IjgwMCIgaGVpZ2h0PSI0MDAiIGZpbGw9IiM1NTUiPjwvcmVjdD48Zz48dGV4dCB4PSIyNzYuOTc0OTk4NDc0MTIxMSIgeT0iMjE4LjMiPlRoaXJkIHNsaWRlPC90ZXh0PjwvZz48L2c+PC9zdmc+"; + } @section styles { @@ -31,11 +36,10 @@
    - - - - + + +
    @@ -76,9 +80,9 @@
    - - - + + +
    @@ -127,9 +131,9 @@
    - - - + + +
    @@ -183,7 +187,7 @@
    - +
    @@ -234,9 +238,9 @@
    - - - + + +
    diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.csproj index ccbffb47f4..f4f91b2629 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.csproj @@ -1,14 +1,14 @@ - + - netcoreapp3.1 + net5.0 true - + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json index a37af33e79..9bd999dd53 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json @@ -3,7 +3,7 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.shared": "^3.1.2", + "@abp/aspnetcore.mvc.ui.theme.shared": "^3.3.0", "highlight.js": "^9.13.1" }, "devDependencies": {} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/core/abp.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/core/abp.js index 20bd92147a..72af4bd106 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/core/abp.js +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/core/abp.js @@ -689,7 +689,7 @@ var abp = abp || {}; abp.security.antiForgery = abp.security.antiForgery || {}; abp.security.antiForgery.tokenCookieName = 'XSRF-TOKEN'; - abp.security.antiForgery.tokenHeaderName = 'X-XSRF-TOKEN'; + abp.security.antiForgery.tokenHeaderName = 'RequestVerificationToken'; abp.security.antiForgery.getToken = function () { return abp.utils.getCookieValue(abp.security.antiForgery.tokenCookieName); diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock index 5d59728037..ab68399880 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock @@ -2,30 +2,30 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.shared@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.1.2.tgz#88ea020b1d26f3e551ff81b96a848c700bed97ca" - integrity sha512-CjqinzcmES96dJ+IzEDSYO+uJE3Z3k3CZ5jGfgg97N0rYCTkt9LzYpuFHGeX4W+qI2qMurr5jstgsuLQY0cCzg== - dependencies: - "@abp/aspnetcore.mvc.ui" "~3.1.2" - "@abp/bootstrap" "~3.1.2" - "@abp/bootstrap-datepicker" "~3.1.2" - "@abp/datatables.net-bs4" "~3.1.2" - "@abp/font-awesome" "~3.1.2" - "@abp/jquery-form" "~3.1.2" - "@abp/jquery-validation-unobtrusive" "~3.1.2" - "@abp/lodash" "~3.1.2" - "@abp/luxon" "~3.1.2" - "@abp/malihu-custom-scrollbar-plugin" "~3.1.2" - "@abp/select2" "~3.1.2" - "@abp/sweetalert" "~3.1.2" - "@abp/timeago" "~3.1.2" - "@abp/toastr" "~3.1.2" - -"@abp/aspnetcore.mvc.ui@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.1.2.tgz#4689ff7e4b722803f2133856f2fb39171bbfd752" - integrity sha512-jlBR+IgKuNGCerHbx6BEn2f1Xf9Vi9Dgge1/5VYzqu2MVk7dHRXHE7WYkZqpJuQKZtGMxF3c8ul2/u6uGlkmqw== +"@abp/aspnetcore.mvc.ui.theme.shared@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.3.0.tgz#6c998bad95e46905e5a1487ab2370905cdf4376a" + integrity sha512-Tt6SK3EUj8jfZKSzeIVkRuWyAGRoDTqeoS5VVkNZLoIfLzwiimHw2o2AlPRtCMa0jqLropHtaT/kNl+EU24AZw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~3.3.0" + "@abp/bootstrap" "~3.3.0" + "@abp/bootstrap-datepicker" "~3.3.0" + "@abp/datatables.net-bs4" "~3.3.0" + "@abp/font-awesome" "~3.3.0" + "@abp/jquery-form" "~3.3.0" + "@abp/jquery-validation-unobtrusive" "~3.3.0" + "@abp/lodash" "~3.3.0" + "@abp/luxon" "~3.3.0" + "@abp/malihu-custom-scrollbar-plugin" "~3.3.0" + "@abp/select2" "~3.3.0" + "@abp/sweetalert" "~3.3.0" + "@abp/timeago" "~3.3.0" + "@abp/toastr" "~3.3.0" + +"@abp/aspnetcore.mvc.ui@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.3.0.tgz#07f7d0f7f689a3ac75a69e12681ebd3d08fa0644" + integrity sha512-EbUISgnR8qHYdu/tSro8QjUWj17M6gOkugMI7N0fiPNbtLHgKG2GHWxIsVEf5ic1O2j/PWKOQ+FPBLZ4v3jFrw== dependencies: ansi-colors "^4.1.1" extend-object "^1.0.0" @@ -34,144 +34,145 @@ path "^0.12.7" rimraf "^3.0.2" -"@abp/bootstrap-datepicker@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.1.2.tgz#90ed84f4345bf8e47c0a05511fab9c63a5a3424c" - integrity sha512-sY9jXReDXCNzuSoNCiSkUD9cbdj9W/AR/glOsCb29zC7GX3XLEykuT42b8W6Kl6prS5NgfmXaX2pZOuyv0KBeQ== +"@abp/bootstrap-datepicker@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.3.0.tgz#d7d11e591818a6ec654127d88602cfdfddf81b1f" + integrity sha512-PUsS48o5Cwufn8O8d34UBsjLF6/HrgdhsGTUzrVIhGapiyuYD/QrvqWYWRUj1Jeeb4nrsRVem2DnM1isy+CUCA== dependencies: bootstrap-datepicker "^1.9.0" -"@abp/bootstrap@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.1.2.tgz#d80e14d0cfa0b3213fafecf275520eddd9b846e2" - integrity sha512-neI766oMxwrwrsQc477koPXaT2/yy1mG4djGvnC+g2DgLeqxU7ye6dt6Lf/rKmQuZD9jtz28920wPN3N4N/HrQ== +"@abp/bootstrap@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.3.0.tgz#1314549e7a940c58fdff521a9ddf153f3414fffa" + integrity sha512-CVjSwbZPjio0VJWxdo+PRkvUYGtv51DgVrqjgHK7m7tGFwHAK38cDHAl+mqmIp7dgPG1Z+E0o41JteT3Za1Bkw== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" bootstrap "^4.5.0" bootstrap-v4-rtl "4.4.1-2" -"@abp/core@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.1.2.tgz#1d204b2f6eb3c04e036a30b416ecf77d5ce51a47" - integrity sha512-Tb/E8VHTESFoioj0gO1PB5AxZB5aIZSgAS4/V5GFne4Z399DPwkX8+XJIIRtQVzlFySAdoA37Emml/7eHCvMMQ== +"@abp/core@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.3.0.tgz#c88c28df19f96349b9a32b7ebe402e6d267c34e5" + integrity sha512-y9P86zPAm55fAyKwDaoxv+IrApA339aaGEpN9364upeeNBsO8mYf9hoM136VovQaWNIABFHRD7R+9KskwPAuNA== dependencies: - "@abp/utils" "^3.1.2" + "@abp/utils" "^3.3.0" -"@abp/datatables.net-bs4@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.1.2.tgz#23530f20d01776c34669c7d15df5ecd19beb6c95" - integrity sha512-dbJOTSkiyYHMW3Og9kQFqcSONNE+ElEHgfO/sfIT4Aj9hFpNHkGBTnaDTYXJ22ViFcSMWOTynvg6u+I3ZHe8qw== +"@abp/datatables.net-bs4@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.3.0.tgz#e4cc85f3c1c195d9e8b07532eb7c53619f40a787" + integrity sha512-StuwT8jGfDWGNXmYgRELuErXxSE+qjVoiSAkd27mZqDbYSTipq4OZSAHr6WNmoqMgP6QlCgjGgXcz4Pr8Pxuaw== dependencies: - "@abp/datatables.net" "~3.1.2" + "@abp/datatables.net" "~3.3.0" datatables.net-bs4 "^1.10.21" -"@abp/datatables.net@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.1.2.tgz#34a0718bdac6802336484c4011067cf665cdd413" - integrity sha512-TJWp/+sMZIQqEy1F7Zb4/kSvrUQoC3AfG5qrSFIRmKyZMEsuUzTLugtfYWUi0BKEHdc454ErWrGuD1MCMIzEsQ== +"@abp/datatables.net@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.3.0.tgz#a2b2cedc06beb325e62ce6faba899c935564b9a7" + integrity sha512-y0RGrLxI7VgWFAFtHAraDnrEJ5pkUIJ3zxEScS43RUbArhxk+5pIpxQbmU0WgHpk72H7WXdqtL486Kw8PSj1Nw== dependencies: - "@abp/jquery" "~3.1.2" + "@abp/jquery" "~3.3.0" datatables.net "^1.10.21" -"@abp/font-awesome@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.1.2.tgz#1b14dc64cfe5f903bbcc25a4d2c88df54b561e87" - integrity sha512-33C0lAioBWcIs5wU+M7+SF06+Yvoq9BKVDjPx1R8TndphOfXU6IRneZvHDCLig76EY/BNcEYEREbv33Wqnie9Q== +"@abp/font-awesome@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.3.0.tgz#f9307334c34309456c47cc407fbe079821ace45e" + integrity sha512-7/uZ2qXooHQZG35F6ERKsjI8m0gn8/BgE2cOB7Zhu5KRtSTOHOOXX4YWEfoHYLEMuo3xKTTJ2gHM3jiCJEpVtg== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" "@fortawesome/fontawesome-free" "^5.13.0" -"@abp/jquery-form@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.1.2.tgz#8d516356e443e0ae8f1367ce3090e84a5afc2867" - integrity sha512-UCWZLnu7qETSeX69Wui0VtMpEiPrRv2+JsoJKsS3XcqXehNGJktMc5H5sNCDgPaQXzE5SY6hWnfLBj88014T6g== +"@abp/jquery-form@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.3.0.tgz#43fc5655b4e3cac7c0558fb36dd9ef188fe64412" + integrity sha512-fH4IPf/cU3ojWtVsPZjtymzK0xWqqgmvnSk5zy4AB4MOCKyYtPrn5OAMUvq8A4uZ0mGIRAQ8Z72wPKZA1rws5A== dependencies: - "@abp/jquery" "~3.1.2" + "@abp/jquery" "~3.3.0" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.1.2.tgz#64e6c2f287c21a4a569f4152936e84afa4a66c50" - integrity sha512-soZjLE2ygApecWs9DqKh7hRkf067esvLXSQOwUvw5WdeHOWccgAu/GxzAeFzwNA3CpfjFGoHqNNeSnETLLVEUQ== +"@abp/jquery-validation-unobtrusive@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.3.0.tgz#0840f3114626a1d6efff54f4791776ebfdf56e76" + integrity sha512-foKM9FDAY2RZGz5EQ2FcQt/FiL8sMc+uwd+2KeAdIejLyPgeSH/lLio6agd2Xun4e/xlBJB185Ga1eKcdHIGAw== dependencies: - "@abp/jquery-validation" "~3.1.2" + "@abp/jquery-validation" "~3.3.0" jquery-validation-unobtrusive "^3.2.11" -"@abp/jquery-validation@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.1.2.tgz#a34fea85a67e2552d9ee278ecc22b5740bbcc5eb" - integrity sha512-RrzdY3nay9/OHpd58DyJ9oYnwr3++6gJoI135QMYz+otlJnUjrM6bFfqSeWQaetvbiCVy7nOVYVyqIFO7KgQUw== +"@abp/jquery-validation@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.3.0.tgz#2199968638d7bab9bfb6ba5e9f27d9d607c09a24" + integrity sha512-jxw274hx8i9+dkRrDz7aUpWSGESnJ2wuA6e3IAQIlubFf+7q0o4vosXD5bubinn4qaRGcSiYC15armfefgmFCw== dependencies: - "@abp/jquery" "~3.1.2" + "@abp/jquery" "~3.3.0" jquery-validation "^1.19.2" -"@abp/jquery@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.1.2.tgz#f890d872d2d0287e7c195801f8c51c88c64602a0" - integrity sha512-Pk3Ybx/S88rFThDUlziiKIq7uKwiu2J1xYuzJTlP+xc1IgIcJNkBONl07KFdoHvctOtFtdf4mVN7SGYmNePK4g== +"@abp/jquery@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.3.0.tgz#b99a9436bf3c2394917e761a2dcdfe4f1af9ff3d" + integrity sha512-WrjDQCBOi83kjtMYjXCJVWSbnFxKLW/KtDADkjTF7TxIGccsNWx6E8npzdCEnDnlOJ5Rk9+1QIu07pWedOjBSg== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" jquery "~3.5.1" -"@abp/lodash@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.1.2.tgz#07ab3001e463f1c452865ff98e051251322c7061" - integrity sha512-0XMWCKQJyPVpn22OVEYlQR7+zEn1yz4sTb5ilUFhLOSTHXEOJXNVzxEYlGYCPUjxGZFvZH+vdeurSjXdIOOB5w== +"@abp/lodash@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.3.0.tgz#5402a779ef1e8ecd585644a68607425587560305" + integrity sha512-QFm9hpWs0/J5TFFiE7vO5oxTsLgMcibmgg/CxYvmQZXhHGZpx3mdoAzC48CvRu8quxr6Xo8pIxlCyYXniR6/Dw== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" lodash "^4.17.15" -"@abp/luxon@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.1.2.tgz#ed3adfd1e7e46939c2ac456d44d9987b0d923334" - integrity sha512-fZj3yIT1H+I6vcRxMONw9VRYLWfvzdzLeDZ1xlkXsC4NqI7cK6vchYNTqZlTg4zH6DNYNg6+WOoxQ8ao8a00QA== +"@abp/luxon@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.3.0.tgz#2d669786308df997c6738dd205363edaae670d15" + integrity sha512-1oSUuzgehSBxaxJLfD06ktXK4jisILPKFnEaKCBuPiKiQSDFvdQbepHAYJcpU1Jye6sVrheUKDUyJhnSwDb+0w== dependencies: + "@abp/core" "~3.3.0" luxon "^1.24.1" -"@abp/malihu-custom-scrollbar-plugin@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.1.2.tgz#3556912136a85289b0b237e5bb7368b08f5efac8" - integrity sha512-b0uU/PwNOQaAurEkEZ4e4103pGomeKduo3Rg0hsyN7eeUrTzNw2tq1AQlgGRxw2v1pzyOvzNd0v14jyERE947g== +"@abp/malihu-custom-scrollbar-plugin@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.3.0.tgz#3bb48e0d68c0b76f3460eb19706927af2168a6c3" + integrity sha512-44JvJOfxJ5ihfNCPyc5U8D8N8CmLh8UBMkzjH7YJQZJO+7PGgjx+uEqbQhTtBDftUz5C5oFkGoQ7i2pil7YSYQ== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/select2@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.1.2.tgz#05997301b2800249ed560c4d24c9bfcfee68f99a" - integrity sha512-PyIiCGkvRUEfSyVF4M7p4BzHgaUG7lysYPop0R1tGiJcEVSxkXvhCJnEODPYiqvVjdFFKVz8Ni1R+xpEIEL/2w== +"@abp/select2@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.3.0.tgz#207460dd128a1620c332af5eea2df88bfdfd0520" + integrity sha512-dbOnzQVARBQp/ynJvNyJqE1ArtsWCV8nGMUablB1ILPonsJy+0ADJ+GXWa1nIsHdj3zPW0MLi8pdqpepRCTAFg== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" select2 "^4.0.13" -"@abp/sweetalert@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.1.2.tgz#18b062b0d5c5d5c78b4f8d13186587ab87d46464" - integrity sha512-MJXLyrqPgxiy5NViyVvo6iasDoO3Jfdb3+4Jf+5PFlR7pZd1MmhmvNWn/rc5yFcCwOKvJkXVaWYaT9UsvZPxIA== +"@abp/sweetalert@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.3.0.tgz#457a05af37819fc54082254022551ed0233c471e" + integrity sha512-6KXwPVC1zwNwlZBS9x0uPLTmieu3nmBp0G+ZH1aP+GTMphSa0ejI7nuPFqUeqfEMg+an1yj51SKKUfKtmlx6Zg== dependencies: - "@abp/core" "~3.1.2" + "@abp/core" "~3.3.0" sweetalert "^2.1.2" -"@abp/timeago@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.1.2.tgz#120cdd99a9dde1fbd1276571fdc5f415eee2941e" - integrity sha512-C3d1RLMEUBctPA/ASpqt9+gumsziYu0De7PWdmCmkPC7q23JpZaOmvWffaTpP2QoInyQXUPo51obOmEaC23InQ== +"@abp/timeago@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.3.0.tgz#f0da23577630cec21255fc9c71dd2f7a3a3a3388" + integrity sha512-f5sb4vxgGPyIzTj6lxDzUx/qJiC8pAqWPasdxHDMzhKlkRuFWO+GfblR5DC/sj9fQPcyV8+yX/VQPY3JW8i62Q== dependencies: - "@abp/jquery" "~3.1.2" + "@abp/jquery" "~3.3.0" timeago "^1.6.7" -"@abp/toastr@~3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.1.2.tgz#3f8838423559482b8791cd7ce5bb6ff3a8f3722a" - integrity sha512-Omb/7Mkv73hRunVfWbRJxlNoTC3cqKJACB+pAcB5rb+rCTiR3c9HaSf1C7ljlofpxY/AKMN+JfJZP9VmgH2e4g== +"@abp/toastr@~3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.3.0.tgz#c4f7d761eb4d93e993cb8e8cb54a0d670b20bdd3" + integrity sha512-GGqtVUCX8WalpkCnhiYUs+XxxBnhxGOLe87qFe/2tr0XNDJ3BSHXxiKynDetjlNAq/Qnf4U5P4Eg9FnHWFdWZg== dependencies: - "@abp/jquery" "~3.1.2" + "@abp/jquery" "~3.3.0" toastr "^2.1.4" -"@abp/utils@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.1.2.tgz#e9bca8f6133dc2af587c45825e734d2accabcd96" - integrity sha512-wnbHGQDGkQYazez1YSXAZ0ZDbsIZZr2ZN8V4LtOSAfzmYhPhFZ5qftoM0xoxlbmsuLLX6QtiPvxU35kEmlZPyg== +"@abp/utils@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.3.0.tgz#44ccacf4c415227e94981d907017de1c6f7225bb" + integrity sha512-/JIcygVJu/Ob1G5IicL2XgOVjIIePC2t3QzgEbwaq6NDGMHiOm2quyS8Lj7TfjBeUFTJhCFEbXJMq0lGQ+KMww== dependencies: just-compare "^1.3.0" diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo.Abp.AspNetCore.Mvc.UI.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo.Abp.AspNetCore.Mvc.UI.Tests.csproj index 92e5e21867..babe8a4650 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo.Abp.AspNetCore.Mvc.UI.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Tests/Volo.Abp.AspNetCore.Mvc.UI.Tests.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + net5.0 $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; Volo.Abp.AspNetCore.Mvc.UI.Tests Volo.Abp.AspNetCore.Mvc.UI.Tests diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj index 4c328a670f..606c1a5122 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj @@ -3,7 +3,7 @@ - netcoreapp3.1 + net5.0 true @@ -15,7 +15,7 @@ - + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json index 9e281c4b1f..028b3e8535 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json @@ -3,7 +3,8 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^3.1.2" + "@abp/aspnetcore.mvc.ui.theme.basic": "^3.3.0", + "@abp/prismjs": "^3.3.0" }, "devDependencies": {} } \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js index 20bd92147a..72af4bd106 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js @@ -689,7 +689,7 @@ var abp = abp || {}; abp.security.antiForgery = abp.security.antiForgery || {}; abp.security.antiForgery.tokenCookieName = 'XSRF-TOKEN'; - abp.security.antiForgery.tokenHeaderName = 'X-XSRF-TOKEN'; + abp.security.antiForgery.tokenHeaderName = 'RequestVerificationToken'; abp.security.antiForgery.getToken = function () { return abp.utils.getCookieValue(abp.security.antiForgery.tokenCookieName); diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/clipboard/clipboard.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/clipboard/clipboard.js new file mode 100644 index 0000000000..0240e3f738 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/clipboard/clipboard.js @@ -0,0 +1,973 @@ +/*! + * clipboard.js v2.0.6 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["ClipboardJS"] = factory(); + else + root["ClipboardJS"] = factory(); +})(this, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 6); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +function select(element) { + var selectedText; + + if (element.nodeName === 'SELECT') { + element.focus(); + + selectedText = element.value; + } + else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { + var isReadOnly = element.hasAttribute('readonly'); + + if (!isReadOnly) { + element.setAttribute('readonly', ''); + } + + element.select(); + element.setSelectionRange(0, element.value.length); + + if (!isReadOnly) { + element.removeAttribute('readonly'); + } + + selectedText = element.value; + } + else { + if (element.hasAttribute('contenteditable')) { + element.focus(); + } + + var selection = window.getSelection(); + var range = document.createRange(); + + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + + selectedText = selection.toString(); + } + + return selectedText; +} + +module.exports = select; + + +/***/ }), +/* 1 */ +/***/ (function(module, exports) { + +function E () { + // Keep this empty so it's easier to inherit from + // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3) +} + +E.prototype = { + on: function (name, callback, ctx) { + var e = this.e || (this.e = {}); + + (e[name] || (e[name] = [])).push({ + fn: callback, + ctx: ctx + }); + + return this; + }, + + once: function (name, callback, ctx) { + var self = this; + function listener () { + self.off(name, listener); + callback.apply(ctx, arguments); + }; + + listener._ = callback + return this.on(name, listener, ctx); + }, + + emit: function (name) { + var data = [].slice.call(arguments, 1); + var evtArr = ((this.e || (this.e = {}))[name] || []).slice(); + var i = 0; + var len = evtArr.length; + + for (i; i < len; i++) { + evtArr[i].fn.apply(evtArr[i].ctx, data); + } + + return this; + }, + + off: function (name, callback) { + var e = this.e || (this.e = {}); + var evts = e[name]; + var liveEvents = []; + + if (evts && callback) { + for (var i = 0, len = evts.length; i < len; i++) { + if (evts[i].fn !== callback && evts[i].fn._ !== callback) + liveEvents.push(evts[i]); + } + } + + // Remove event from queue to prevent memory leak + // Suggested by https://github.com/lazd + // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910 + + (liveEvents.length) + ? e[name] = liveEvents + : delete e[name]; + + return this; + } +}; + +module.exports = E; +module.exports.TinyEmitter = E; + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +var is = __webpack_require__(3); +var delegate = __webpack_require__(4); + +/** + * Validates all params and calls the right + * listener function based on its target type. + * + * @param {String|HTMLElement|HTMLCollection|NodeList} target + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listen(target, type, callback) { + if (!target && !type && !callback) { + throw new Error('Missing required arguments'); + } + + if (!is.string(type)) { + throw new TypeError('Second argument must be a String'); + } + + if (!is.fn(callback)) { + throw new TypeError('Third argument must be a Function'); + } + + if (is.node(target)) { + return listenNode(target, type, callback); + } + else if (is.nodeList(target)) { + return listenNodeList(target, type, callback); + } + else if (is.string(target)) { + return listenSelector(target, type, callback); + } + else { + throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList'); + } +} + +/** + * Adds an event listener to a HTML element + * and returns a remove listener function. + * + * @param {HTMLElement} node + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listenNode(node, type, callback) { + node.addEventListener(type, callback); + + return { + destroy: function() { + node.removeEventListener(type, callback); + } + } +} + +/** + * Add an event listener to a list of HTML elements + * and returns a remove listener function. + * + * @param {NodeList|HTMLCollection} nodeList + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listenNodeList(nodeList, type, callback) { + Array.prototype.forEach.call(nodeList, function(node) { + node.addEventListener(type, callback); + }); + + return { + destroy: function() { + Array.prototype.forEach.call(nodeList, function(node) { + node.removeEventListener(type, callback); + }); + } + } +} + +/** + * Add an event listener to a selector + * and returns a remove listener function. + * + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @return {Object} + */ +function listenSelector(selector, type, callback) { + return delegate(document.body, selector, type, callback); +} + +module.exports = listen; + + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +/** + * Check if argument is a HTML element. + * + * @param {Object} value + * @return {Boolean} + */ +exports.node = function(value) { + return value !== undefined + && value instanceof HTMLElement + && value.nodeType === 1; +}; + +/** + * Check if argument is a list of HTML elements. + * + * @param {Object} value + * @return {Boolean} + */ +exports.nodeList = function(value) { + var type = Object.prototype.toString.call(value); + + return value !== undefined + && (type === '[object NodeList]' || type === '[object HTMLCollection]') + && ('length' in value) + && (value.length === 0 || exports.node(value[0])); +}; + +/** + * Check if argument is a string. + * + * @param {Object} value + * @return {Boolean} + */ +exports.string = function(value) { + return typeof value === 'string' + || value instanceof String; +}; + +/** + * Check if argument is a function. + * + * @param {Object} value + * @return {Boolean} + */ +exports.fn = function(value) { + var type = Object.prototype.toString.call(value); + + return type === '[object Function]'; +}; + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +var closest = __webpack_require__(5); + +/** + * Delegates event to a selector. + * + * @param {Element} element + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @param {Boolean} useCapture + * @return {Object} + */ +function _delegate(element, selector, type, callback, useCapture) { + var listenerFn = listener.apply(this, arguments); + + element.addEventListener(type, listenerFn, useCapture); + + return { + destroy: function() { + element.removeEventListener(type, listenerFn, useCapture); + } + } +} + +/** + * Delegates event to a selector. + * + * @param {Element|String|Array} [elements] + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @param {Boolean} useCapture + * @return {Object} + */ +function delegate(elements, selector, type, callback, useCapture) { + // Handle the regular Element usage + if (typeof elements.addEventListener === 'function') { + return _delegate.apply(null, arguments); + } + + // Handle Element-less usage, it defaults to global delegation + if (typeof type === 'function') { + // Use `document` as the first parameter, then apply arguments + // This is a short way to .unshift `arguments` without running into deoptimizations + return _delegate.bind(null, document).apply(null, arguments); + } + + // Handle Selector-based usage + if (typeof elements === 'string') { + elements = document.querySelectorAll(elements); + } + + // Handle Array-like based usage + return Array.prototype.map.call(elements, function (element) { + return _delegate(element, selector, type, callback, useCapture); + }); +} + +/** + * Finds closest match and invokes callback. + * + * @param {Element} element + * @param {String} selector + * @param {String} type + * @param {Function} callback + * @return {Function} + */ +function listener(element, selector, type, callback) { + return function(e) { + e.delegateTarget = closest(e.target, selector); + + if (e.delegateTarget) { + callback.call(element, e); + } + } +} + +module.exports = delegate; + + +/***/ }), +/* 5 */ +/***/ (function(module, exports) { + +var DOCUMENT_NODE_TYPE = 9; + +/** + * A polyfill for Element.matches() + */ +if (typeof Element !== 'undefined' && !Element.prototype.matches) { + var proto = Element.prototype; + + proto.matches = proto.matchesSelector || + proto.mozMatchesSelector || + proto.msMatchesSelector || + proto.oMatchesSelector || + proto.webkitMatchesSelector; +} + +/** + * Finds the closest parent that matches a selector. + * + * @param {Element} element + * @param {String} selector + * @return {Function} + */ +function closest (element, selector) { + while (element && element.nodeType !== DOCUMENT_NODE_TYPE) { + if (typeof element.matches === 'function' && + element.matches(selector)) { + return element; + } + element = element.parentNode; + } +} + +module.exports = closest; + + +/***/ }), +/* 6 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); + +// EXTERNAL MODULE: ./node_modules/select/src/select.js +var src_select = __webpack_require__(0); +var select_default = /*#__PURE__*/__webpack_require__.n(src_select); + +// CONCATENATED MODULE: ./src/clipboard-action.js +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + + +/** + * Inner class which performs selection from either `text` or `target` + * properties and then executes copy or cut operations. + */ + +var clipboard_action_ClipboardAction = function () { + /** + * @param {Object} options + */ + function ClipboardAction(options) { + _classCallCheck(this, ClipboardAction); + + this.resolveOptions(options); + this.initSelection(); + } + + /** + * Defines base properties passed from constructor. + * @param {Object} options + */ + + + _createClass(ClipboardAction, [{ + key: 'resolveOptions', + value: function resolveOptions() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.action = options.action; + this.container = options.container; + this.emitter = options.emitter; + this.target = options.target; + this.text = options.text; + this.trigger = options.trigger; + + this.selectedText = ''; + } + + /** + * Decides which selection strategy is going to be applied based + * on the existence of `text` and `target` properties. + */ + + }, { + key: 'initSelection', + value: function initSelection() { + if (this.text) { + this.selectFake(); + } else if (this.target) { + this.selectTarget(); + } + } + + /** + * Creates a fake textarea element, sets its value from `text` property, + * and makes a selection on it. + */ + + }, { + key: 'selectFake', + value: function selectFake() { + var _this = this; + + var isRTL = document.documentElement.getAttribute('dir') == 'rtl'; + + this.removeFake(); + + this.fakeHandlerCallback = function () { + return _this.removeFake(); + }; + this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true; + + this.fakeElem = document.createElement('textarea'); + // Prevent zooming on iOS + this.fakeElem.style.fontSize = '12pt'; + // Reset box model + this.fakeElem.style.border = '0'; + this.fakeElem.style.padding = '0'; + this.fakeElem.style.margin = '0'; + // Move element out of screen horizontally + this.fakeElem.style.position = 'absolute'; + this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; + // Move element to the same position vertically + var yPosition = window.pageYOffset || document.documentElement.scrollTop; + this.fakeElem.style.top = yPosition + 'px'; + + this.fakeElem.setAttribute('readonly', ''); + this.fakeElem.value = this.text; + + this.container.appendChild(this.fakeElem); + + this.selectedText = select_default()(this.fakeElem); + this.copyText(); + } + + /** + * Only removes the fake element after another click event, that way + * a user can hit `Ctrl+C` to copy because selection still exists. + */ + + }, { + key: 'removeFake', + value: function removeFake() { + if (this.fakeHandler) { + this.container.removeEventListener('click', this.fakeHandlerCallback); + this.fakeHandler = null; + this.fakeHandlerCallback = null; + } + + if (this.fakeElem) { + this.container.removeChild(this.fakeElem); + this.fakeElem = null; + } + } + + /** + * Selects the content from element passed on `target` property. + */ + + }, { + key: 'selectTarget', + value: function selectTarget() { + this.selectedText = select_default()(this.target); + this.copyText(); + } + + /** + * Executes the copy operation based on the current selection. + */ + + }, { + key: 'copyText', + value: function copyText() { + var succeeded = void 0; + + try { + succeeded = document.execCommand(this.action); + } catch (err) { + succeeded = false; + } + + this.handleResult(succeeded); + } + + /** + * Fires an event based on the copy operation result. + * @param {Boolean} succeeded + */ + + }, { + key: 'handleResult', + value: function handleResult(succeeded) { + this.emitter.emit(succeeded ? 'success' : 'error', { + action: this.action, + text: this.selectedText, + trigger: this.trigger, + clearSelection: this.clearSelection.bind(this) + }); + } + + /** + * Moves focus away from `target` and back to the trigger, removes current selection. + */ + + }, { + key: 'clearSelection', + value: function clearSelection() { + if (this.trigger) { + this.trigger.focus(); + } + document.activeElement.blur(); + window.getSelection().removeAllRanges(); + } + + /** + * Sets the `action` to be performed which can be either 'copy' or 'cut'. + * @param {String} action + */ + + }, { + key: 'destroy', + + + /** + * Destroy lifecycle. + */ + value: function destroy() { + this.removeFake(); + } + }, { + key: 'action', + set: function set() { + var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy'; + + this._action = action; + + if (this._action !== 'copy' && this._action !== 'cut') { + throw new Error('Invalid "action" value, use either "copy" or "cut"'); + } + } + + /** + * Gets the `action` property. + * @return {String} + */ + , + get: function get() { + return this._action; + } + + /** + * Sets the `target` property using an element + * that will be have its content copied. + * @param {Element} target + */ + + }, { + key: 'target', + set: function set(target) { + if (target !== undefined) { + if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) { + if (this.action === 'copy' && target.hasAttribute('disabled')) { + throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'); + } + + if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) { + throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes'); + } + + this._target = target; + } else { + throw new Error('Invalid "target" value, use a valid Element'); + } + } + } + + /** + * Gets the `target` property. + * @return {String|HTMLElement} + */ + , + get: function get() { + return this._target; + } + }]); + + return ClipboardAction; +}(); + +/* harmony default export */ var clipboard_action = (clipboard_action_ClipboardAction); +// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js +var tiny_emitter = __webpack_require__(1); +var tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter); + +// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js +var listen = __webpack_require__(2); +var listen_default = /*#__PURE__*/__webpack_require__.n(listen); + +// CONCATENATED MODULE: ./src/clipboard.js +var clipboard_typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var clipboard_createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function clipboard_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + + + + +/** + * Base class which takes one or more elements, adds event listeners to them, + * and instantiates a new `ClipboardAction` on each click. + */ + +var clipboard_Clipboard = function (_Emitter) { + _inherits(Clipboard, _Emitter); + + /** + * @param {String|HTMLElement|HTMLCollection|NodeList} trigger + * @param {Object} options + */ + function Clipboard(trigger, options) { + clipboard_classCallCheck(this, Clipboard); + + var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this)); + + _this.resolveOptions(options); + _this.listenClick(trigger); + return _this; + } + + /** + * Defines if attributes would be resolved using internal setter functions + * or custom functions that were passed in the constructor. + * @param {Object} options + */ + + + clipboard_createClass(Clipboard, [{ + key: 'resolveOptions', + value: function resolveOptions() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.action = typeof options.action === 'function' ? options.action : this.defaultAction; + this.target = typeof options.target === 'function' ? options.target : this.defaultTarget; + this.text = typeof options.text === 'function' ? options.text : this.defaultText; + this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body; + } + + /** + * Adds a click event listener to the passed trigger. + * @param {String|HTMLElement|HTMLCollection|NodeList} trigger + */ + + }, { + key: 'listenClick', + value: function listenClick(trigger) { + var _this2 = this; + + this.listener = listen_default()(trigger, 'click', function (e) { + return _this2.onClick(e); + }); + } + + /** + * Defines a new `ClipboardAction` on each click event. + * @param {Event} e + */ + + }, { + key: 'onClick', + value: function onClick(e) { + var trigger = e.delegateTarget || e.currentTarget; + + if (this.clipboardAction) { + this.clipboardAction = null; + } + + this.clipboardAction = new clipboard_action({ + action: this.action(trigger), + target: this.target(trigger), + text: this.text(trigger), + container: this.container, + trigger: trigger, + emitter: this + }); + } + + /** + * Default `action` lookup function. + * @param {Element} trigger + */ + + }, { + key: 'defaultAction', + value: function defaultAction(trigger) { + return getAttributeValue('action', trigger); + } + + /** + * Default `target` lookup function. + * @param {Element} trigger + */ + + }, { + key: 'defaultTarget', + value: function defaultTarget(trigger) { + var selector = getAttributeValue('target', trigger); + + if (selector) { + return document.querySelector(selector); + } + } + + /** + * Returns the support of the given action, or all actions if no action is + * given. + * @param {String} [action] + */ + + }, { + key: 'defaultText', + + + /** + * Default `text` lookup function. + * @param {Element} trigger + */ + value: function defaultText(trigger) { + return getAttributeValue('text', trigger); + } + + /** + * Destroy lifecycle. + */ + + }, { + key: 'destroy', + value: function destroy() { + this.listener.destroy(); + + if (this.clipboardAction) { + this.clipboardAction.destroy(); + this.clipboardAction = null; + } + } + }], [{ + key: 'isSupported', + value: function isSupported() { + var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut']; + + var actions = typeof action === 'string' ? [action] : action; + var support = !!document.queryCommandSupported; + + actions.forEach(function (action) { + support = support && !!document.queryCommandSupported(action); + }); + + return support; + } + }]); + + return Clipboard; +}(tiny_emitter_default.a); + +/** + * Helper function to retrieve attribute value. + * @param {String} suffix + * @param {Element} element + */ + + +function getAttributeValue(suffix, element) { + var attribute = 'data-clipboard-' + suffix; + + if (!element.hasAttribute(attribute)) { + return; + } + + return element.getAttribute(attribute); +} + +/* harmony default export */ var clipboard = __webpack_exports__["default"] = (clipboard_Clipboard); + +/***/ }) +/******/ ])["default"]; +}); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/clipboard/clipboard.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/clipboard/clipboard.min.js new file mode 100644 index 0000000000..28650f3cc9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/clipboard/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.6 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return o={},r.m=n=[function(t,e){t.exports=function(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var o=this;function r(){o.off(t,r),e.apply(n,arguments)}return r._=e,this.on(t,r,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;o