diff --git a/.gitignore b/.gitignore index 73d74ec7c6..eb4dbedfae 100644 --- a/.gitignore +++ b/.gitignore @@ -291,3 +291,5 @@ samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/Logs/logs.txt samples/MicroserviceDemo/microservices/BloggingService.Host/Logs/logs.txt samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt modules/docs/app/Volo.DocsTestApp/Logs/logs.txt +framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Logs/logs.txt +samples/MicroserviceDemo/microservices/TenantManagementService.Host/Logs/logs.txt diff --git a/README.md b/README.md index 8d872160ca..14b044f8b1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ See the documentation. #### Pre Requirements -- Visual Studio 2019 16.3.0+ +- Visual Studio 2019 16.4.0+ #### Framework diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json index f461fe8bbd..d4e51eeadf 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json @@ -1,7 +1,7 @@ { "culture": "en", "texts": { - "Account": "Account", + "Account": "ABP Account - Login & Register | ABP.IO", "Welcome": "Welcome", "UseOneOfTheFollowingLinksToContinue": "Use one of the following links to continue", "FrameworkHomePage": "Framework home page", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json new file mode 100644 index 0000000000..586cb24760 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json @@ -0,0 +1,13 @@ +{ + "culture": "sl", + "texts": { + "Account": "Račun", + "Welcome": "Dobrodošli", + "UseOneOfTheFollowingLinksToContinue": "Za nadaljevanje uporabite eno od naslednjih povezav", + "FrameworkHomePage": "Domača stran razvojnega okolja", + "FrameworkDocumentation": "Dokumentacija razvojnega okolja", + "OfficialBlog": "Uradni blog", + "CommercialHomePage": "Domača stran različice Commercial", + "CommercialSupportWebSite": "Spletna stran za komercialno podporo" + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 9c00e584ab..2a27cce735 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -53,6 +53,7 @@ "CreateANugetPackage": "Create a Nuget package", "AddNew": "Add new", "PackageAlreadyExist{0}": "\"{0}\" package is already added.", + "ModuleAlreadyExist{0}": "\"{0}\" module is already added.", "ClearCache": "Clear cache", "SuccessfullyCleared": "Successfully cleared", "Menu:NpmPackages": "NPM Packages", @@ -80,9 +81,16 @@ "LastSeenDate": "Last seen date", "{0}Computer{1}WillBeRemovedFromRecords": "Computer of {0} ({1}) will be removed from records", "OrganizationDeletionWarningMessage": "Organization will be deleted", + "DeletingLastOwnerWarningMessage": "An organization must have at least one owner! Therefore you cannot remove this owner", "This{0}AlreadyExistInThisOrganization": "This {0} already exist in this organization", "AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?", "DeleteAll": "Delete all", - "DoYouWantToCreateNewUser": "Do you want to create new user?" + "DoYouWantToCreateNewUser": "Do you want to create new user?", + "MasterModules": "Master Modules", + "OrganizationName": "Organization name", + "OrganizationNamePlaceholder": "Organization name...", + "UsernameOrEmail": "Username or email", + "UsernameOrEmailPlaceholder": "Username or email...", + "Member": "Member" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/sl.json new file mode 100644 index 0000000000..2e90120185 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/sl.json @@ -0,0 +1,90 @@ +{ + "culture": "sl", + "texts": { + "Permission:Organizations": "Organizacije", + "Permission:Manage": "Upravljaj organizacije", + "Permission:NpmPackages": "NPM paketi", + "Permission:NugetPackages": "Nuget paketi", + "Permission:Maintenance": "Vzdrževanje", + "Permission:Maintain": "Vzdrževanje", + "Permission:ClearCaches": "Počisti predpomnilnike", + "Permission:Modules": "Moduli", + "Permission:Packages": "Paketi", + "Permission:Edit": "Urejanje", + "Permission:Delete": "Brisanje", + "Permission:Create": "Ustvarjanje", + "Menu:Organizations": "Organizacije", + "Menu:Packages": "Paketi", + "NpmPackageDeletionWarningMessage": "Ta paket NPM bo izbrisan. Ali to potrjujete?", + "NugetPackageDeletionWarningMessage": "Ta Nuget paket bo izbrisan. Ali to potrjujete?", + "ModuleDeletionWarningMessage": "Ta modul bo izbrisan. Ali to potrjujete?", + "Name": "Naziv", + "DisplayName": "Naziv za prikaz", + "ShortDescription": "Kratek opis", + "NameFilter": "Naziv", + "CreationTime": "Čas nastanka", + "IsPro": "Je pro", + "EfCoreConfigureMethodName": "Konfiguriraj ime metode", + "IsProFilter": "Je pro", + "ApplicationType": "Tip aplikacije", + "Target": "Cilj", + "TargetFilter": "Cilj", + "ModuleClass": "Razred modula", + "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": "DeleteAllEntityFramework Core", + "NugetPackageTarget.MongoDB": "MongoDB", + "Edit": "Uredi", + "Delete": "Izbriši", + "Refresh": "Osveži", + "NpmPackages": "NPM paketi", + "NugetPackages": "Nuget paketi", + "NpmPackageCount": "Število NPM paketov", + "NugetPackageCount": "Število Nuget paketov", + "Module": "Moduli", + "ModuleInfo": "Informacije o modulu", + "CreateANpmPackage": "Ustvari NPM paket", + "CreateAModule": "Ustvari modul", + "CreateANugetPackage": "Ustvari Nuget paket", + "AddNew": "Dodaj novega", + "PackageAlreadyExist{0}": "Paket \"{0}\" je že dodan.", + "ModuleAlreadyExist{0}": "Modul \"{0}\" je že dodan.", + "ClearCache": "Počisti predpomnilnik", + "SuccessfullyCleared": "Uspešno izbrisano", + "Menu:NpmPackages": "NPM paketi", + "Menu:Modules": "Moduli", + "Menu:Maintenance": "Vzdrževanje", + "Menu:NugetPackages": "Nuget paketi", + "CreateAnOrganization": "Ustvari organizacijo", + "Organizations": "Organizacije", + "LongName": "Dolg naziv", + "LicenseType": "Tip licence", + "LicenseStartTime": "Čas začetka licence", + "LicenseEndTime": "Čas konca licence", + "AllowedDeveloperCount": "Dovoljeno število razvijalcev", + "UserNameOrEmailAddress": "Uporabniško ime ali e-poštni naslov", + "AddOwner": "Dodaj lastnika", + "UserName": "Uporabniško ime", + "Email": "E-poštni naslov", + "Developers": "Razvijalci", + "AddDeveloper": "Dodaj razvijalca", + "Create": "Ustvari", + "UserNotFound": "Uporabnika ni mogoče najti", + "{0}WillBeRemovedFromMembers": "{0} bo odstranjen iz članov", + "Computers": "Računalniki", + "UniqueComputerId": "Unikatni id računalnika", + "LastSeenDate": "Zadnjič viden", + "{0}Computer{1}WillBeRemovedFromRecords": "Računalnik {0} ({1}) bo odstranjen iz zapisov", + "OrganizationDeletionWarningMessage": "Organizacija bo izbrisana", + "This{0}AlreadyExistInThisOrganization": "{0} že obstaja v tej organizaciji", + "AreYouSureYouWantToDeleteAllComputers": "Ali ste prepričani, da želite izbrisati vse računalnike?", + "DeleteAll": "Izbriši vse", + "DoYouWantToCreateNewUser": "Ali želite ustvariti novega uporabnika?", + "MasterModules": "Glavni moduli" + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json new file mode 100644 index 0000000000..61a3fa4a8c --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/sl.json @@ -0,0 +1,31 @@ +{ + "culture": "sl", + "texts": { + "Volo.AbpIo.Domain:010004": "Doseženo je največje število članov!", + "Volo.AbpIo.Domain:010005": "Doseženo je največje število lastnikov!", + "Volo.AbpIo.Domain:010006": "Ta uporabnik je že lastnik te organizacije!", + "Volo.AbpIo.Domain:010007": "Ta uporabnik je že razvijalec v tej organizaciji!", + "Volo.AbpIo.Domain:010008": "Dovoljeno število razvijalcev ne sme biti manjše od trenutnega števila razvijalcev!", + "Volo.AbpIo.Domain:010009": "Dovoljeno število razvijalcev ne sme biti manjše od 0!", + "Volo.AbpIo.Domain:010010": "Največje število mac naslovov je preseženo!", + "Volo.AbpIo.Domain:010011": "Osebne licence ne sme imeti več kot 1 razvijalec!", + "Volo.AbpIo.Domain:010012": "Licence ni mogoče podaljšati en mesec po poteku licence!", + "Volo.AbpIo.Domain:020001": "Tega paketa NPM ni mogoče izbrisati, ker so \"{NugetPackages}\" Nuget paketi odvisni od tega paketa.", + "Volo.AbpIo.Domain:020002": "Tega paketa NPM ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket.", + "Volo.AbpIo.Domain:020003": "Tega paketa NPM ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket in \"{NugetPackages}\" Nuget paketi so odvisni od tega paketa.", + "Volo.AbpIo.Domain:020004": "Nuget paketa ni mogoče izbrisati, ker \"{Modules}\" moduli uporabljajo ta paket.", + "WantToLearn?": "Se želite naučiti?", + "ReadyToGetStarted?": "Pripravljeni, da bi začeli?", + "JoinOurCommunity": "Pridružite se naši skupnosti", + "GetStartedUpper": "ZAČNI", + "ForkMeOnGitHub": "Naredi vejitev na GitHub", + "Features": "Funkcionalnosti", + "GetStarted": "Začni", + "Documents": "Dokumenti", + "Community": "Skupnost", + "ContributionGuide": "Vodič za prispevke", + "Blog": "Blog", + "Commercial": "Commercial", + "SeeDocuments": "Poglej dokumente" + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Blog/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Blog/Localization/Resources/sl.json new file mode 100644 index 0000000000..103359cbc2 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Blog/Localization/Resources/sl.json @@ -0,0 +1,5 @@ +{ + "culture": "sl", + "texts": { + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 7987b9ab32..2a57c6b25d 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -5,8 +5,9 @@ "OrganizationList": "Organization list", "Volo.AbpIo.Commercial:010003": "You are not owner of this organization!", "OrganizationNotFoundMessage": "No organization found!", - "DeveloperCount": "Developer count", - "QuestionCount": "Question count", + "DeveloperCount": "Allocated / total developers", + "QuestionCount": "Remaining / total questions", + "Unlimited": "Unlimited", "Owners": "Owners", "AddMember": "Add member", "AddOwner": "Add owner", @@ -28,7 +29,7 @@ "ApiKey": "API key", "UserNameNotFound": "There is no user with username {0}", "SuccessfullyAddedToNewsletter": "Thanks you for subscribing to our newsletter!", - "ManageProfile": "Manage your profile", + "MyProfile": "My profile", "EmailNotValid": "Please enter a valid email address." - } + } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/sl.json new file mode 100644 index 0000000000..fc6022b556 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/sl.json @@ -0,0 +1,35 @@ +{ + "culture": "sl", + "texts": { + "OrganizationManagement": "Upravljanje organizacije", + "OrganizationList": "Seznam organizacij", + "Volo.AbpIo.Commercial:010003": "Niste lastnik te organizacije!", + "OrganizationNotFoundMessage": "Nobene organizacije ni bilo mogoče najti!", + "DeveloperCount": "Dodeljeni / skupno število razvijalcev", + "QuestionCount": "Preostala / skupno število vprašanj", + "Unlimited": "Neomejeno", + "Owners": "Lastniki", + "AddMember": "Dodaj člana", + "AddOwner": "Dodaj lastnika", + "AddDeveloper": "Dodaj razvijalce", + "UserName": "Uporabniško ime", + "Name": "Ime", + "EmailAddress": "E-poštni naslov", + "Developers": "Razvijalci", + "LicenseType": "Tip licence", + "Manage": "Upravljaj", + "StartDate": "Datum začetka", + "EndDate": "Datum konca", + "Modules": "Moduli", + "LicenseExtendMessage": "Končni datum veljavnosti licence je podaljšan na {0}", + "LicenseUpgradeMessage": "Vaša licenca je nadgrajena na {0}", + "LicenseAddDeveloperMessage": "{0} razvijalcev je bilo dodanih k vaši licenci", + "Volo.AbpIo.Commercial:010004": "Navedenega uporabnika ni mogoče najti! Uporabnik mora biti registriran.", + "MyOrganizations": "Moje organizacije", + "ApiKey": "Ključ API", + "UserNameNotFound": "Ni uporabnika z uporabniškim imenom {0}", + "SuccessfullyAddedToNewsletter": "Hvala, ker ste se naročili na naše novice!", + "ManageProfile": "Upravljaj svoj profil", + "EmailNotValid": "Vnesite veljaven e-poštni naslov." + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Docs/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Docs/Localization/Resources/sl.json new file mode 100644 index 0000000000..103359cbc2 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Docs/Localization/Resources/sl.json @@ -0,0 +1,5 @@ +{ + "culture": "sl", + "texts": { + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Support/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Support/Localization/Resources/sl.json new file mode 100644 index 0000000000..103359cbc2 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Support/Localization/Resources/sl.json @@ -0,0 +1,5 @@ +{ + "culture": "sl", + "texts": { + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index be193b7396..accf9a91b3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -1,7 +1,7 @@ { "culture": "en", "texts": { - "GetStarted": "Get Started", + "GetStarted": "Get Started - Startup Templates", "Create": "Create", "NewProject": "New Project", "DirectDownload": "Direct Download", @@ -86,7 +86,7 @@ "BasedOnFamiliarToolsExplanation": "Built on and integrated to popular tools you already know. Low learning curve, easy adaptation, comfortable development.", "ORMIndependent": "ORM Independent", "ORMIndependentExplanation": "The core framework is ORM/database independent and can work with any data source. Entity Framework Core and MongoDB providers are already available.", - "Features": "Features", + "Features": "Explore the ABP Framework Features", "ABPCLI": "ABP CLI", "Modularity": "Modularity", "BootstrapTagHelpers": "Bootstrap Tag Helpers", @@ -154,6 +154,8 @@ "SeeTheDocumentForMoreInformation": "See the {0} document for more information", "IndexPageHeroSection": "open sourceWeb Application
Framework
for asp.net core", "UiFramework": "UI Framework", - "EmailAddress": "Email address" + "EmailAddress": "Email address", + "Mobile": "Mobile", + "ReactNative": "React Native" } } \ No newline at end of file diff --git a/build/common.ps1 b/build/common.ps1 index e4ba6a8adc..f2e72a45cf 100644 --- a/build/common.ps1 +++ b/build/common.ps1 @@ -21,13 +21,12 @@ $solutionPaths = ( "../modules/client-simulation", "../templates/module/aspnet-core", "../templates/app/aspnet-core", - "../samples/BasicAspNetCoreApplication", - "../samples/BasicConsoleApplication", "../samples/BookStore", "../samples/BookStore-Angular-MongoDb/aspnet-core", "../samples/BookStore-Modular/modules/book-management", "../samples/BookStore-Modular/application", "../samples/DashboardDemo", + "../samples/EfCoreMigrationDemo", "../samples/MicroserviceDemo", "../samples/RabbitMqEventBus", "../abp_io/AbpIoLocalization" diff --git a/common.DotSettings b/common.DotSettings index cb0b2c919f..0eb4875d49 100644 --- a/common.DotSettings +++ b/common.DotSettings @@ -20,4 +20,5 @@ False False SQL + True \ No newline at end of file diff --git a/common.props b/common.props index 1cccebd015..8b0b8291c4 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 2.1.0 + 2.5.0 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io @@ -9,9 +9,7 @@ git https://github.com/abpframework/abp/ - - \ No newline at end of file diff --git a/docs/cs/AspNetCore/Widgets.md b/docs/cs/AspNetCore/Widgets.md index c885da9c62..6498f349df 100644 --- a/docs/cs/AspNetCore/Widgets.md +++ b/docs/cs/AspNetCore/Widgets.md @@ -478,19 +478,19 @@ namespace DashboardDemo.Web.Pages.Components.MySimpleWidget ## WidgetOptions -Jako alternativu k atributu `Widget` můžete ke konfiguraci widgetů použít `WidgetOptions`: +Jako alternativu k atributu `Widget` můžete ke konfiguraci widgetů použít `AbpWidgetOptions`: ```csharp -Configure(options => +Configure(options => { options.Widgets.Add(); }); ``` -Toto vepište do metody `ConfigureServices` vašeho [modulu](../Module-Development-Basics.md). Veškerá konfigurace udělaná přes atribut `Widget` je dostupná i za pomoci `WidgetOptions`. Příklad konfigurace, která přidává styl pro widget: +Toto vepište do metody `ConfigureServices` vašeho [modulu](../Module-Development-Basics.md). Veškerá konfigurace udělaná přes atribut `Widget` je dostupná i za pomoci `AbpWidgetOptions`. Příklad konfigurace, která přidává styl pro widget: ````csharp -Configure(options => +Configure(options => { options.Widgets .Add() @@ -498,7 +498,7 @@ Configure(options => }); ```` -> Tip: `WidgetOptions` lze také použít k získání existujícího widgetu a ke změně jeho konfigurace. To je obzvláště užitečné, pokud chcete změnit konfiguraci widgetu uvnitř modulu používaného vaší aplikací. Použíjte `options.Widgets.Find` k získání existujícího `WidgetDefinition`. +> Tip: `AbpWidgetOptions` lze také použít k získání existujícího widgetu a ke změně jeho konfigurace. To je obzvláště užitečné, pokud chcete změnit konfiguraci widgetu uvnitř modulu používaného vaší aplikací. Použíjte `options.Widgets.Find` k získání existujícího `WidgetDefinition`. ## Podívejte se také na diff --git a/docs/cs/CLI.md b/docs/cs/CLI.md index 7d2ab4c479..db6918384e 100644 --- a/docs/cs/CLI.md +++ b/docs/cs/CLI.md @@ -1,4 +1,4 @@ -# ABP CLI +# ABP CLI ABP CLI (Command Line Interface) je nástroj v příkazovém řádku k provádění některých běžných úkonů v řešeních založených na ABP. @@ -109,6 +109,7 @@ abp add-module Volo.Blogging * `--solution` nebo `-s`: Určuje cestu k řešení (.sln). Pokud není zadáno, CLI se pokusí najít soubor .sln v aktuálním adresáři. * `--skip-db-migrations`: Pro poskytovatele databáze EF Core automaticky přidá nový kód první migrace (`Add-Migration`) a v případě potřeby aktualizuje databázi (`Update-Database`). Tuto možnost určete k vynechání této operace. * `-sp` nebo `--startup-project`: Relativní cesta ke složce spouštěcího projektu. Výchozí hodnota je aktuální adresář. +* `--with-source-code`: Místo balíčků NuGet/NPM přidejte zdrojový kód modulu. ### update diff --git a/docs/cs/Getting-Started-Angular-Template.md b/docs/cs/Getting-Started-Angular-Template.md index ea5ef0a4bf..076fb45671 100644 --- a/docs/cs/Getting-Started-Angular-Template.md +++ b/docs/cs/Getting-Started-Angular-Template.md @@ -26,10 +26,10 @@ abp new Acme.BookStore -u angular Vytvořené řešení vyžaduje; -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4.0+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### Struktura řešení diff --git a/docs/cs/Getting-Started-AspNetCore-Application.md b/docs/cs/Getting-Started-AspNetCore-Application.md index 0537b01579..2e8ea1a343 100644 --- a/docs/cs/Getting-Started-AspNetCore-Application.md +++ b/docs/cs/Getting-Started-AspNetCore-Application.md @@ -4,7 +4,7 @@ Tento tutoriál vysvětluje jak začít s ABP z ničeho s minimem závislostí. ## Tvorba nového projektu -1. Vytvořte novou AspNet Core Web aplikaci ve Visual Studio 2019 (16.3.0+): +1. Vytvořte novou AspNet Core Web aplikaci ve Visual Studio 2019 (16.4.0+): ![](images/create-new-aspnet-core-application-v2.png) @@ -153,5 +153,5 @@ namespace BasicAspNetCoreApplication ## Zdrojový kód -Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp/tree/master/samples/BasicAspNetCoreApplication). +Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication). diff --git a/docs/cs/Getting-Started-AspNetCore-MVC-Template.md b/docs/cs/Getting-Started-AspNetCore-MVC-Template.md index 32edd59931..c053e1d76a 100644 --- a/docs/cs/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/cs/Getting-Started-AspNetCore-MVC-Template.md @@ -26,10 +26,10 @@ Příkaz `new` vytvoří **vrstvenou MVC aplikaci** s **Entity Framework Core** Vytvořené řešení vyžaduje; -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4.0+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### Struktura řešení diff --git a/docs/cs/Getting-Started-Console-Application.md b/docs/cs/Getting-Started-Console-Application.md index bac35149ec..dc822ec297 100644 --- a/docs/cs/Getting-Started-Console-Application.md +++ b/docs/cs/Getting-Started-Console-Application.md @@ -178,4 +178,4 @@ Stačí volat metodu `options.UseAutofac()` v možnostech `AbpApplicationFactory ## Zdrojový kód -Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp/tree/master/samples/BasicConsoleApplication). +Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication). diff --git a/docs/en/API/Auto-API-Controllers.md b/docs/en/API/Auto-API-Controllers.md new file mode 100644 index 0000000000..77819c48c9 --- /dev/null +++ b/docs/en/API/Auto-API-Controllers.md @@ -0,0 +1,140 @@ +# Auto API Controllers + +Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. + +ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. + +## Configuration + +Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: + +````csharp +[DependsOn(BookStoreApplicationModule)] +public class BookStoreWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly); + }); + } +} +```` + +This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). + +![bookstore-apis](../images/bookstore-apis.png) + +### Examples + +Some example method names and the corresponding routes calculated by convention: + +| Service Method Name | HTTP Method | Route | +| ----------------------------------------------------- | ----------- | -------------------------- | +| GetAsync(Guid id) | GET | /api/app/book/{id} | +| GetListAsync() | GET | /api/app/book | +| CreateAsync(CreateBookDto input) | POST | /api/app/book | +| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | +| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | +| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | +| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | + +### HTTP Method + +ABP uses a naming convention while determining the HTTP method for a service method (action): + +- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. +- **Put**: Used if the method name starts with 'Put' or 'Update'. +- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. +- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. +- **Patch**: Used if the method name starts with 'Patch'. +- Otherwise, **Post** is used **by default**. + +If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. + +### Route + +Route is calculated based on some conventions: + +* It always starts with '**/api**'. +* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: + +````csharp +Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.RootPath = "volosoft/book-store"; + }); +}); +```` + +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'. + * 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**. + * 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'). + +## Service Selection + +Creating conventional HTTP API controllers are not unique to application services actually. + +### IRemoteService Interface + +If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. + +### RemoteService Attribute + +`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: + +````csharp +[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +### TypePredicate Option + +You can further filter classes to become an API controller by providing the `TypePredicate` option: + +````csharp +services.Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.TypePredicate = type => { return true; }; + }); +}); +```` + +Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. + +## API Explorer + +API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. + +API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: + +````csharp +[RemoteService(IsMetadataEnabled = false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md new file mode 100644 index 0000000000..460623440f --- /dev/null +++ b/docs/en/API/Dynamic-CSharp-API-Clients.md @@ -0,0 +1,165 @@ +# Dynamic C# API Clients + +ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. + +## Service Interface + +Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: + +````csharp +public interface IBookAppService : IApplicationService +{ + Task> GetListAsync(); +} +```` + +Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. + +Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. + +## Client Proxy Generation + +First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: + +```` +Install-Package Volo.Abp.Http.Client +```` + +Then add `AbpHttpClientModule` dependency to your module: + +````csharp +[DependsOn(typeof(AbpHttpClientModule))] //add the dependency +public class MyClientAppModule : AbpModule +{ +} +```` + +Now, it's ready to create the client proxies. Example: + +````csharp +[DependsOn( + typeof(AbpHttpClientModule), //used to create client proxies + typeof(BookStoreApplicationModule) //contains the application service interfaces + )] +public class MyClientAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //Create dynamic client proxies + context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly + ); + } +} +```` + +`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. + +### Endpoint Configuration + +`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: + +```` +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + } + } +} +```` + +See the "AbpRemoteServiceOptions" section below for more detailed configuration. + +## Usage + +It's straightforward to use. Just inject the service interface in the client application code: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IBookAppService _bookService; + + public MyService(IBookAppService bookService) + { + _bookService = bookService; + } + + public async Task DoIt() + { + var books = await _bookService.GetListAsync(); + foreach (var book in books) + { + Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); + } + } +} +```` + +This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. + +### IHttpClientProxy Interface + +While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy` interface. + +## Configuration + +### AbpRemoteServiceOptions + +`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.Configure(options => + { + options.RemoteServices.Default = + new RemoteServiceConfiguration("http://localhost:53929/"); + }); + + //... +} +```` + +### Multiple Remote Service Endpoints + +The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: + +````json +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + }, + "BookStore": { + "BaseUrl": "http://localhost:48392/" + } + } +} +```` + +`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + remoteServiceName: "BookStore" +); +```` + +`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. + +### As Default Services + +When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + asDefaultServices: false +); +```` + +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 diff --git a/docs/en/API/JavaScript-API/Auth.md b/docs/en/API/JavaScript-API/Auth.md new file mode 100644 index 0000000000..60c9eb8866 --- /dev/null +++ b/docs/en/API/JavaScript-API/Auth.md @@ -0,0 +1,3 @@ +# abp.auth JavaScript API + +TODO \ No newline at end of file diff --git a/docs/en/API/JavaScript-API/Index.md b/docs/en/API/JavaScript-API/Index.md new file mode 100644 index 0000000000..ca9afca97f --- /dev/null +++ b/docs/en/API/JavaScript-API/Index.md @@ -0,0 +1,24 @@ +# 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. + +## APIs + +* abp.ajax +* [abp.auth](Auth.md) +* 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 + diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md index c860f0dd82..f7f7675f46 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -132,6 +132,8 @@ The `CreateAsync` method above manually creates a `Book` entity from given `Crea However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier. +Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default. + Let's create another method to get a book. First, define the method in the `IBookAppService` interface: ````csharp @@ -146,7 +148,6 @@ public interface IBookAppService : IApplicationService `BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below: ````csharp -[AbpAutoMapFrom(typeof(Book))] //Defines the mapping public class BookDto { public Guid Id { get; set; } @@ -159,21 +160,38 @@ public class BookDto } ```` -* `BookDto` defines `[AbpAutoMapFrom(typeof(Book))]` attribute to create the object mapping from `Book` to `BookDto`. +AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example: -Then you can implement the `GetAsync` method as shown below: +````csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap(); + } +} +```` + +You should then register profiles using the `AbpAutoMapperOptions`: ````csharp -public async Task GetAsync(Guid id) +[DependsOn(typeof(AbpAutoMapperModule))] +public class MyModule : AbpModule { - var book = await _bookRepository.GetAsync(id); - return book.MapTo(); + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + //Add all mappings defined in the assembly of the MyModule class + options.AddMaps(); + }); + } } ```` -`MapTo` extension method converts `Book` object to `BookDto` object by copying all properties with the same naming. +`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html). -An alternative to the `MapTo` is using the `IObjectMapper` service: +Then you can implement the `GetAsync` method as shown below: ````csharp public async Task GetAsync(Guid id) @@ -183,8 +201,6 @@ public async Task GetAsync(Guid id) } ```` -While the second syntax is a bit harder to write, it better works if you write unit tests. - See the [object to object mapping document](Object-To-Object-Mapping.md) for more. ## Validation @@ -201,7 +217,7 @@ See the [authorization document](Authorization.md) for more. ## CRUD Application Services -If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from `CrudAppService`. +If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from the `CrudAppService`. ### Example @@ -219,7 +235,9 @@ public interface IBookAppService : } ```` -* `ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface). +`ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface). + +> Creating interface for an application service is a good practice, but not required by the ABP Framework. You can skip the interface part. `ICrudAppService` declares the following methods: @@ -248,7 +266,6 @@ public interface ICrudAppService< DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`: ````csharp -[AbpAutoMapFrom(typeof(Book))] public class BookDto : AuditedEntityDto { public string Name { get; set; } @@ -258,7 +275,6 @@ public class BookDto : AuditedEntityDto public float Price { get; set; } } -[AbpAutoMapTo(typeof(Book))] public class CreateUpdateBookDto { [Required] @@ -273,6 +289,19 @@ public class CreateUpdateBookDto } ```` +[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class. + +```csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap(); + CreateMap(); + } +} +``` + * `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well. And finally, the `BookAppService` implementation is very simple: @@ -292,6 +321,52 @@ public class BookAppService : `CrudAppService` implements all methods declared in the `ICrudAppService` interface. You can then add your own custom methods or override and customize base methods. +> `CrudAppService` has different versions gets different number of generic arguments. Use the one suitable for you. + +### AbstractKeyCrudAppService + +`CrudAppService` requires to have an Id property as the primary key of your entity. If you are using composite keys then you can not utilize it. + +`AbstractKeyCrudAppService` implements the same `ICrudAppService` interface, but this time without making assumption about your primary key. + +#### Example + +Assume that you have a `District` entity with `CityId` and `Name` as a composite primary key. Using `AbstractKeyCrudAppService` requires to implement `DeleteByIdAsync` and `GetEntityByIdAsync` methods yourself: + +````csharp +public class DistrictAppService + : AbstractKeyCrudAppService +{ + public DistrictAppService(IRepository repository) + : base(repository) + { + } + + protected override async Task DeleteByIdAsync(DistrictKey id) + { + await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); + } + + protected override async Task GetEntityByIdAsync(DistrictKey id) + { + return await AsyncQueryableExecuter.FirstOrDefaultAsync( + Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) + ); + } +} +```` + +This implementation requires you to create a class represents your composite key: + +````csharp +public class DistrictKey +{ + public Guid CityId { get; set; } + + public string Name { get; set; } +} +```` + ## 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/AspNetCore/Auto-API-Controllers.md b/docs/en/AspNetCore/Auto-API-Controllers.md index 77819c48c9..52db2bde06 100644 --- a/docs/en/AspNetCore/Auto-API-Controllers.md +++ b/docs/en/AspNetCore/Auto-API-Controllers.md @@ -1,140 +1,3 @@ -# Auto API Controllers +This document has moved. -Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. - -ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. - -## Configuration - -Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: - -````csharp -[DependsOn(BookStoreApplicationModule)] -public class BookStoreWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly); - }); - } -} -```` - -This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). - -![bookstore-apis](../images/bookstore-apis.png) - -### Examples - -Some example method names and the corresponding routes calculated by convention: - -| Service Method Name | HTTP Method | Route | -| ----------------------------------------------------- | ----------- | -------------------------- | -| GetAsync(Guid id) | GET | /api/app/book/{id} | -| GetListAsync() | GET | /api/app/book | -| CreateAsync(CreateBookDto input) | POST | /api/app/book | -| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | -| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | -| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | -| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | - -### HTTP Method - -ABP uses a naming convention while determining the HTTP method for a service method (action): - -- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. -- **Put**: Used if the method name starts with 'Put' or 'Update'. -- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. -- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. -- **Patch**: Used if the method name starts with 'Patch'. -- Otherwise, **Post** is used **by default**. - -If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. - -### Route - -Route is calculated based on some conventions: - -* It always starts with '**/api**'. -* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: - -````csharp -Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.RootPath = "volosoft/book-store"; - }); -}); -```` - -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'. - * 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**. - * 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'). - -## Service Selection - -Creating conventional HTTP API controllers are not unique to application services actually. - -### IRemoteService Interface - -If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. - -### RemoteService Attribute - -`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: - -````csharp -[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -### TypePredicate Option - -You can further filter classes to become an API controller by providing the `TypePredicate` option: - -````csharp -services.Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.TypePredicate = type => { return true; }; - }); -}); -```` - -Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. - -## API Explorer - -API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. - -API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: - -````csharp -[RemoteService(IsMetadataEnabled = false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. +[Click to navigate to Auto API Controllers document](../API/Auto-API-Controllers.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Bundling-Minification.md b/docs/en/AspNetCore/Bundling-Minification.md index 8868d441e1..a67fbe429d 100644 --- a/docs/en/AspNetCore/Bundling-Minification.md +++ b/docs/en/AspNetCore/Bundling-Minification.md @@ -1,352 +1,4 @@ -# ASP.NET Core MVC Bundling & Minification +This document has moved. -There are many ways of bundling & minification of client side resources (JavaScript and CSS files). Most common ways are: - -* Using the [Bundler & Minifier](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier) Visual Studio extension or the [NuGet package](https://www.nuget.org/packages/BuildBundlerMinifier/). -* Using [Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/) task managers and their plugins. - -ABP offers a simple, dynamic, powerful, modular and built-in way. - -## Volo.Abp.AspNetCore.Mvc.UI.Bundling Package - -> This package is already installed by default with the startup templates. So, most of the time, you don't need to install it manually. - -Install the `Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget package to your project: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -Then you can add the `AbpAspNetCoreMvcUiBundlingModule` dependency to your module: - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -## Razor Bundling Tag Helpers - -The simplest way of creating a bundle is to use `abp-script-bundle` or `abp-style-bundle` tag helpers. Example: - -````html - - - - - - -```` - -This bundle defines a style bundle with a **unique name**: `MyGlobalBundle`. It's very easy to understand how to use it. Let's see how it *works*: - -* ABP creates the bundle as **lazy** from the provided files when it's **first requested**. For the subsequent calls, it's returned from the **cache**. That means if you conditionally add the files to the bundle, it's executed only once and any changes of the condition will not effect the bundle for the next requests. -* ABP adds bundle files **individually** to the page for the `development` environment. It automatically bundles & minifies for other environments (`staging`, `production`...). -* The bundle files may be **physical** files or [**virtual/embedded** files](../Virtual-File-System.md). -* ABP automatically adds **version query string** to the bundle file URL to prevent browsers from caching when the bundle is being updated. (like ?_v=67872834243042 - generated from last change date of the related files). The versioning works even if the bundle files are individually added to the page (on the development environment). - -### Importing The Bundling Tag Helpers - -> This is already imported by default with the startup templates. So, most of the time, you don't need to add it manually. - -In order to use bundle tag helpers, you need to add it into your `_ViewImports.cshtml` file or into your page: - -```` -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -### Unnamed Bundles - -The `name` is **optional** for the razor bundle tag helpers. If you don't define a name, it's automatically **calculated** based on the used bundle file names (they are **concatenated** and **hashed**). Example: - -````html - - - - - @if (ViewBag.IncludeCustomStyles != false) - { - - } - -```` - -This will potentially create **two different bundles** (one incudes the `my-global-style.css` and other does not). - -Advantages of **unnamed** bundles: - -* Can **conditionally add items** to the bundle. But this may lead to multiple variations of the bundle based on the conditions. - -Advantages of **named** bundles: - -* Other **modules can contribute** to the bundle by its name (see the sections below). - -### Single File - -If you need to just add a single file to the page, you can use the `abp-script` or `abp-style` tag without a wrapping in the `abp-script-bundle` or `abp-style-bundle` tag. Example: - -````xml - -```` - -The bundle name will be *scripts.my-scripts* for the example above ("/" is replaced by "."). All bundling features are work as expected for single file bundles too. - -## Bundling Options - -If you need to use same bundle in **multiple pages** or want to use some more **powerful features**, you can configure bundles **by code** in your [module](../Module-Development-Basics.md) class. - -### Creating A New Bundle - -Example usage: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] -public class MyWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Add("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/libs/jquery/jquery.js", - "/libs/bootstrap/js/bootstrap.js", - "/libs/toastr/toastr.min.js", - "/scripts/my-global-scripts.js" - ); - }); - }); - } -} -```` - -> You can use the same name (*MyGlobalBundle* here) for a script & style bundle since they are added to different collections (`ScriptBundles` and `StyleBundles`). - -After defining such a bundle, it can be included into a page using the same tag helpers defined above. Example: - -````html - -```` - -This time, no file defined in the tag helper definition because the bundle files are defined by the code. - -### Configuring An Existing Bundle - -ABP supports [modularity](../Module-Development-Basics.md) for bundling as well. A module can modify an existing bundle that is created by a dependant module. Example: - -````C# -[DependsOn(typeof(MyWebModule))] -public class MyWebExtensionModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/scripts/my-extension-script.js" - ); - }); - }); - } -} -```` - -> It's not possible to configure unnamed bundle tag helpers by code, because their name are not known at the development time. It's suggested to always use a name for a bundle tag helper. - -## Bundle Contributors - -Adding files to an existing bundle seems useful. What if you need to **replace** a file in the bundle or you want to **conditionally** add files? Defining a bundle contributor provides extra power for such cases. - -An example bundle contributor that replaces bootstrap.css with a customized version: - -````C# -public class MyExtensionGlobalStyleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.ReplaceOne( - "/libs/bootstrap/css/bootstrap.css", - "/styles/extensions/bootstrap-customized.css" - ); - } -} -```` - -Then you can use this contributor as like below: - -````C# -services.Configure(options => -{ - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); - }); -}); -```` - -> You can also add contributors while creating a new bundle. - -Contributors can also be used in the bundle tag helpers. Example: - -````xml - - - - - -```` - -`abp-style` and `abp-script` tags can get `type` attributes (instead of `src` attributes) as shown in this sample. When you add a bundle contributor, its dependencies are also automatically added to the bundle. - -### Contributor Dependencies - -A bundle contributor can have one or more dependencies to other contributors. -Example: - -````C# -[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -When a bundle contributor is added, its dependencies are **automatically and recursively** added. Dependencies added by the **dependency order** by preventing **duplicates**. Duplicates are prevented even if they are in separated bundles. ABP organizes all bundles in a page and eliminates duplications. - -Creating contributors and defining dependencies is a way of organizing bundle creation across different modules. - -### Contributor Extensions - -In some advanced scenarios, you may want to do some additional configuration whenever a bundle contributor is used. Contributor extensions works seamlessly when the extended contributor is used. - -The example below adds some styles for prism.js library: - -````csharp -public class MyPrismjsStyleExtension : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); - } -} -```` - -Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`. - -````csharp -Configure(options => -{ - options - .Extensions() - .Add(); -}); -```` - -Whenever `PrismjsStyleBundleContributor` is added into a bundle, `MyPrismjsStyleExtension` will also be automatically added. - -### Accessing to the IServiceProvider - -While it is rarely needed, `BundleConfigurationContext` has a `ServiceProvider` property that you can resolve service dependencies inside the `ConfigureBundle` method. - -### Standard Package Contributors - -Adding a specific NPM package resource (js, css files) into a bundle is pretty straight forward for that package. For example you always add the `bootstrap.css` file for the bootstrap NPM package. - -There are built-in contributors for all [standard NPM packages](Client-Side-Package-Management.md). For example, if your contributor depends on the bootstrap, you can just declare it, instead of adding the bootstrap.css yourself. - -````C# -[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -Using the built-in contributors for standard packages; - -* Prevents you typing **the invalid resource paths**. -* Prevents changing your contributor if the resource **path changes** (the dependant contributor will handle it). -* Prevents multiple modules adding the **duplicate files**. -* Manages **dependencies recursively** (adds dependencies of dependencies, if necessary). - -#### Volo.Abp.AspNetCore.Mvc.UI.Packages Package - -> This package is already installed by default in the startup templates. So, most of the time, you don't need to install it manually. - -Standard package contributors are defined in the `Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet package. -To install it to your project: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Packages -```` - -Then add the `AbpAspNetCoreMvcUiPackagesModule` module dependency to your own module; - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -### Bundle Inheritance - -In some specific cases, it may be needed to create a **new** bundle **inherited** from other bundle(s). Inheriting from a bundle (recursively) inherits all files/contributors of that bundle. Then the derived bundle can add or modify files/contributors **without modifying** the original bundle. -Example: - -````c# -services.Configure(options => -{ - options - .StyleBundles - .Add("MyTheme.MyGlobalBundle", bundle => { - bundle - .AddBaseBundles("MyGlobalBundle") //Can add multiple - .AddFiles( - "/styles/mytheme-global-styles.css" - ); - }); -}); -```` - -## Themes - -Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. - -## Best Practices & Suggestions - -It's suggested to define multiple bundles for an application, each one is used for different purposes. - -* **Global bundle**: Global style/script bundles are included to every page in the application. Themes already defines global style & script bundles. Your module can contribute to them. -* **Layout bundles**: This is a specific bundle to an individual layout. Only contains resources shared among all the pages use the layout. Use the bundling tag helpers to create the bundle as a good practice. -* **Module bundles**: For shared resources among the pages of an individual module. -* **Page bundles**: Specific bundles created for each page. Use the bundling tag helpers to create the bundle as a best practice. - -Establish a balance between performance, network bandwidth usage and count of many bundles. - -## See Also - -* [Client Side Package Management](Client-Side-Package-Management.md) -* [Theming](Theming.md) +[Click to navigate to ASP.NET Core MVC Bundling & Minification document](../UI/AspNetCore/Bundling-Minification.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Client-Side-Package-Management.md b/docs/en/AspNetCore/Client-Side-Package-Management.md index f6dea58ff3..780633de50 100644 --- a/docs/en/AspNetCore/Client-Side-Package-Management.md +++ b/docs/en/AspNetCore/Client-Side-Package-Management.md @@ -1,116 +1,4 @@ -## ASP.NET Core MVC Client Side Package Management +This document has moved. -ABP framework can work with any type of client side package management systems. You can even decide to use no package management system and manage your dependencies manually. - -However, ABP framework works best with **NPM/Yarn**. By default, built-in modules are configured to work with NPM/Yarn. - -Finally, we suggest the [**Yarn**](https://yarnpkg.com/) over the NPM since it's faster, stable and also compatible with the NPM. - -### @ABP NPM Packages - -ABP is a modular platform. Every developer can create modules and the modules should work together in a **compatible** and **stable** state. - -One challenge is the **versions of the dependant NPM packages**. What if two different modules use the same JavaScript library but its different (and potentially incompatible) versions. - -To solve the versioning problem, we created a **standard set of packages** those depends on some common third-party libraries. Some example packages are [@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap) and [@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). You can see the **list of packages** from the [Github repository](https://github.com/volosoft/abp/tree/master/npm/packs). - -The benefit of a **standard package** is: - -* It depends on a **standard version** of a package. Depending on this package is **safe** because all modules depend on the same version. -* It contains the gulp task to copy library resources (js, css, img... files) from the **node_modules** folder to **wwwroot/libs** folder. See the *Mapping The Library Resources* section for more. - -Depending on a standard package is easy. Just add it to your **package.json** file like you normally do. Example: - -```` -{ - ... - "dependencies": { - "@abp/bootstrap": "^1.0.0" - } -} -```` - -It's suggested to depend on a standard package instead of directly depending on a third-party package. - -#### Package Installation - -After depending on a NPM package, all you should do is to run the **yarn** command from the command line to install all the packages and their dependencies: - -```` -yarn -```` - -Alternatively, you can use `npm install` but [Yarn](https://yarnpkg.com/) is suggested as mentioned before. - -#### Package Contribution - -If you need a third-party NPM package that is not in the standard set of packages, you can create a Pull Request on the Github [repository](https://github.com/volosoft/abp). A pull request that follows these rules is accepted: - -* Package name should be named as `@abp/package-name` for a `package-name` on NPM (example: `@abp/bootstrap` for the `bootstrap` package). -* It should be the **latest stable** version of the package. -* It should only depend a **single** third-party package. It can depend on multiple `@abp/*` packages. -* The package should include a `abp.resourcemapping.js` file formatted as defined in the *Mapping The Library Resources* section. This file should only map resources for the depended package. -* You also need to create [bundle contributor(s)](Bundling-Minification.md) for the package you have created. - -See current standard packages for examples. - -### Mapping The Library Resources - -Using NPM packages and NPM/Yarn tool is the de facto standard for client side libraries. NPM/Yarn tool creates a **node_modules** folder in the root folder of your web project. - -Next challenge is copying needed resources (js, css, img... files) from the `node_modules` into a folder inside the **wwwroot** folder to make it accessible to the clients/browsers. - -ABP defines a [Gulp](https://gulpjs.com/) based task to **copy resources** from **node_modules** to **wwwroot/libs** folder. Each **standard package** (see the *@ABP NPM Packages* section) defines the mapping for its own files. So, most of the time, you only configure dependencies. - -The **startup templates** are already configured to work all these out of the box. This section will explain the configuration options. - -#### Resource Mapping Definition File - -A module should define a JavaScript file named `abp.resourcemapping.js` which is formatted as in the example below: - -````js -module.exports = { - aliases: { - "@node_modules": "./node_modules", - "@libs": "./wwwroot/libs" - }, - clean: [ - "@libs" - ], - mappings: { - - } -} -```` - -* **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. -* **clean** section is a list of folders to clean before copying the files. -* **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. - -An example mapping configuration is shown below: - -````js -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/" -} -```` - -#### Using The Gulp - -Once you properly configure the `abp.resourcemapping.js` file, you can run the gulp command from the command line: - -```` -gulp -```` - -When you run the `gulp`, all packages will copy their own resources into the **wwwroot/libs** folder. Running `yarn & gulp` is only necessary if you make a change in your dependencies in the **package.json** file. - -> When you run the Gulp command, dependencies of the application are resolved using the package.json file. The Gulp task automatically discovers and maps all resources from all dependencies (recursively). - -#### See Also - -* [Bundling & Minification](Bundling-Minification.md) -* [Theming](Theming.md) +[Click to navigate to ASP.NET Core MVC Client Side Package Management document](../UI/AspNetCore/Client-Side-Package-Management.md) diff --git a/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md b/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md index 0b0667dd0d..4ca26e8220 100644 --- a/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md +++ b/docs/en/AspNetCore/Dynamic-CSharp-API-Clients.md @@ -1,165 +1,3 @@ -# Dynamic C# API Clients +This document has moved. -ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. - -## Service Interface - -Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. - -Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. - -## Client Proxy Generation - -First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: - -```` -Install-Package Volo.Abp.Http.Client -```` - -Then add `AbpHttpClientModule` dependency to your module: - -````csharp -[DependsOn(typeof(AbpHttpClientModule))] //add the dependency -public class MyClientAppModule : AbpModule -{ -} -```` - -Now, it's ready to create the client proxies. Example: - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //used to create client proxies - typeof(BookStoreApplicationModule) //contains the application service interfaces - )] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //Create dynamic client proxies - context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly - ); - } -} -```` - -`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. - -### Endpoint Configuration - -`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: - -```` -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -```` - -See the "RemoteServiceOptions" section below for more detailed configuration. - -## Usage - -It's straightforward to use. Just inject the service interface in the client application code: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoIt() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. - -### IHttpClientProxy Interface - -While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy` interface. - -## Configuration - -### RemoteServiceOptions - -`RemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### Multiple Remote Service Endpoints - -The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - remoteServiceName: "BookStore" -); -```` - -`remoteServiceName` parameter matches the service endpoint configured via `RemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. - -### As Default Services - -When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - asDefaultServices: false -); -```` - -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 +[Click to navigate to Dynamic C# API Clients document](../API/Dynamic-CSharp-API-Clients.md) diff --git a/docs/en/AspNetCore/JavaScript-API/Auth.md b/docs/en/AspNetCore/JavaScript-API/Auth.md index 60c9eb8866..fb83bcfbff 100644 --- a/docs/en/AspNetCore/JavaScript-API/Auth.md +++ b/docs/en/AspNetCore/JavaScript-API/Auth.md @@ -1,3 +1,3 @@ -# abp.auth JavaScript API +This document has moved. -TODO \ No newline at end of file +[Click to navigate to JavaScript Auth document](../../API/JavaScript-API/Auth.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/JavaScript-API/Index.md b/docs/en/AspNetCore/JavaScript-API/Index.md index ca9afca97f..1ed7f0285c 100644 --- a/docs/en/AspNetCore/JavaScript-API/Index.md +++ b/docs/en/AspNetCore/JavaScript-API/Index.md @@ -1,24 +1,3 @@ -# 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. - -## APIs - -* abp.ajax -* [abp.auth](Auth.md) -* 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 +This document has moved. +[Click to navigate to JavaScript API document](../../API/JavaScript-API/Index.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md index 6d700f030d..35382845ce 100644 --- a/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/en/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,3 +1,3 @@ -## Dynamic Forms +This document has moved. -This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. \ No newline at end of file +[Click to navigate to Dynamic Forms document](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Tag-Helpers/Index.md b/docs/en/AspNetCore/Tag-Helpers/Index.md index 2d8b465c23..c15c645feb 100644 --- a/docs/en/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/AspNetCore/Tag-Helpers/Index.md @@ -1,3 +1,3 @@ -## ABP Tag Helpers +This document has moved. -"ABP tag helpers" is not documented yet. You can see a [demo of components](http://bootstrap-taghelpers.abp.io/) for now. \ No newline at end of file +[Click to navigate to ABP Tag Helpers document](../../UI/AspNetCore/Tag-Helpers/Index.md) diff --git a/docs/en/AspNetCore/Theming.md b/docs/en/AspNetCore/Theming.md index 470ef1a458..716cf5fa9e 100644 --- a/docs/en/AspNetCore/Theming.md +++ b/docs/en/AspNetCore/Theming.md @@ -1,3 +1,4 @@ -# Theming -TODO \ No newline at end of file +This document has moved. + +[Click to navigate to Theming document](../UI/AspNetCore/Theming.md) \ No newline at end of file diff --git a/docs/en/AspNetCore/Widgets.md b/docs/en/AspNetCore/Widgets.md index f217e24816..562678c1d8 100644 --- a/docs/en/AspNetCore/Widgets.md +++ b/docs/en/AspNetCore/Widgets.md @@ -1,505 +1,4 @@ -# Widgets -ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; +This document has moved. -* Have **scripts & styles** dependencies for your widget. -* Create **dashboards** with widgets used inside. -* Define widgets in reusable **[modules](../Module-Development-Basics.md)**. -* Co-operate widgets with **[authorization](../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. - -## Basic Widget Definition - -### Create a View Component - -As the first step, create a new regular ASP.NET Core View Component: - -![widget-basic-files](../images/widget-basic-files.png) - -**MySimpleWidgetViewComponent.cs**: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. - -You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. - -**Default.cshtml**: - -```xml -
-

My Simple Widget

-

This is a simple widget!

-
-``` - -### Define the Widget - -Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## Rendering a Widget - -Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: - -````xml -@await Component.InvokeAsync("MySimpleWidget") -@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) -```` - -First approach uses the widget name while second approach uses the view component type. - -### Widgets with Arguments - -ASP.NET Core's view component system allows you to accept arguments for view components. The sample view component below accepts `startDate` and `endDate` and uses these arguments to retrieve data from a service. - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget -{ - [Widget] - public class CountersWidgetViewComponent : AbpViewComponent - { - private readonly IDashboardAppService _dashboardAppService; - - public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) - { - _dashboardAppService = dashboardAppService; - } - - public async Task InvokeAsync( - DateTime startDate, DateTime endDate) - { - var result = await _dashboardAppService.GetCountersWidgetAsync( - new CountersWidgetInputDto - { - StartDate = startDate, - EndDate = endDate - } - ); - - return View(result); - } - } -} -```` - -Now, you need to pass an anonymous object to pass arguments as shown below: - -````xml -@await Component.InvokeAsync("CountersWidget", new -{ - startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), - endDate = DateTime.Now -}) -```` - -## Widget Name - -Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. - -To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - [ViewComponent(Name = "MyCustomNamedWidget")] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); - } - } -} -``` - -ABP will respect to the custom name by handling the widget. - -> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. - -### Display Name - -You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: - -````csharp -using DashboardDemo.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - DisplayName = "MySimpleWidgetDisplayName", //Localization key - DisplayNameResource = typeof(DashboardDemoResource) //localization resource - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -See [the localization document](../Localization.md) to learn about localization resources and keys. - -## Style & Script Dependencies - -There are some challenges when your widget has script and style files; - -* Any page uses the widget should also include the **its script & styles** files into the page. -* The page should also care about **depended libraries/files** of the widget. - -ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. - -### Defining as Simple File Paths - -The example widget below adds a style and a script file: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, - ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../Virtual-File-System.md). - -### Defining Bundle Contributors - -All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. - -The sample code below does the same with the code above, but defines and uses bundle contributors: - -````csharp -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, - ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } - - public class MySimpleWidgetStyleBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); - } - } - - public class MySimpleWidgetScriptBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); - } - } -} - -```` - -Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. - -See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. - -## RefreshUrl - -A widget may design a `RefreshUrl` that is used whenever the widget needs to be refreshed. If it is defined, the widget is re-rendered on the server side on every refresh (see the refresh `method` of the `WidgetManager` below). - -````csharp -[Widget(RefreshUrl = "Widgets/Counters")] -public class CountersWidgetViewComponent : AbpViewComponent -{ - -} -```` - -Once you define a `RefreshUrl` for your widget, you need to provide an endpoint to render and return it: - -````csharp -[Route("Widgets")] -public class CountersWidgetController : AbpController -{ - [HttpGet] - [Route("Counters")] - public IActionResult Counters(DateTime startDate, DateTime endDate) - { - return ViewComponent("CountersWidget", new {startDate, endDate}); - } -} -```` - -`Widgets/Counters` route matches to the `RefreshUrl` declared before. - -> 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). - -## 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. - -### WidgetManager - -`WidgetManager` is used to initialize and refresh one or more widgets. Create a new `WidgetManager` as shown below: - -````js -$(function() { - var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); -}) -```` - -`MyDashboardWidgetsArea` may contain one or more widgets inside. - -> Using the `WidgetManager` inside document.ready (like above) is a good practice since its functions use the DOM and need DOM to be ready. - -#### WidgetManager.init() - -`init` simply initializes the `WidgetManager` and calls `init` methods of the related widgets if they define (see Widget JavaScript API section below) - -```js -myWidgetManager.init(); -``` - -#### WidgetManager.refresh() - -`refresh` method refreshes all widgets related to this `WidgetManager`: - -```` -myWidgetManager.refresh(); -```` - -#### WidgetManager Options - -WidgetManager has some additional options. - -##### Filter Form - -If your widgets require parameters/filters then you will generally have a form to filter the widgets. In such cases, you can create a form that has some form elements and a dashboard area with some widgets inside. Example: - -````xml -
- ...form elements -
- -
- ...widgets -
-```` - -`data-widget-filter` attribute relates the form with the widgets. Whenever the form is submitted, all the widgets are automatically refreshed with the form fields as the filter. - -Instead of the `data-widget-filter` attribute, you can use the `filterForm` parameter of the `WidgetManager` constructor. Example: - -````js -var myWidgetManager = new abp.WidgetManager({ - wrapper: '#MyDashboardWidgetsArea', - filterForm: '#MyDashboardFilterForm' -}); -```` - -##### Filter Callback - -You may want to have a better control to provide filters while initializing and refreshing the widgets. In this case, you can use the `filterCallback` option: - -````js -var myWidgetManager = new abp.WidgetManager({ - wrapper: '#MyDashboardWidgetsArea', - filterCallback: function() { - return $('#MyDashboardFilterForm').serializeFormToObject(); - } -}); -```` - -This example shows the default implementation of the `filterCallback`. You can return any JavaScript object with fields. Example: - -````js -filterCallback: function() { - return { - 'startDate': $('#StartDateInput').val(), - 'endDate': $('#EndDateInput').val() - }; -} -```` - -The returning filters are passed to all widgets on `init` and `refresh`. - -### Widget JavaScript API - -A widget can define a JavaScript API that is invoked by the `WidgetManager` when needed. The code sample below can be used to start to define an API for a widget. - -````js -(function () { - abp.widgets.NewUserStatisticWidget = function ($wrapper) { - - var getFilters = function () { - return { - ... - }; - } - - var refresh = function (filters) { - ... - }; - - var init = function (filters) { - ... - }; - - return { - getFilters: getFilters, - init: init, - refresh: refresh - }; - }; -})(); -```` - -`NewUserStatisticWidget` is the name of the widget here. It should match the widget name defined in the server side. All of the functions are optional. - -#### getFilters - -If the widget has internal custom filters, this function should return the filter object. Example: - -````js -var getFilters = function() { - return { - frequency: $wrapper.find('.frequency-filter option:selected').val() - }; -} -```` - -This method is used by the `WidgetManager` while building filters. - -#### init - -Used to initialize the widget when needed. It has a filter argument that can be used while getting data from server. `init` method is used when `WidgetManager.init()` function is called. It is also called if your widget requires a full re-load on refresh. See the `RefreshUrl` widget option. - -#### refresh - -Used to refresh the widget when needed. It has a filter argument that can be used while getting data from server. `refresh` method is used whenever `WidgetManager.refresh()` function is called. - -## Authorization - -Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: - -* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). -* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../Authorization.md) for more info about policies. - -Example: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget(RequiredPolicies = new[] { "MyPolicyName" })] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## WidgetOptions - -As alternative to the `Widget` attribute, you can use the `WidgetOptions` to configure widgets: - -```csharp -Configure(options => -{ - options.Widgets.Add(); -}); -``` - -Write this into the `ConfigureServices` method of your [module](../Module-Development-Basics.md). All the configuration done with the `Widget` attribute is also possible with the `WidgetOptions`. Example configuration that adds a style for the widget: - -````csharp -Configure(options => -{ - options.Widgets - .Add() - .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); -}); -```` - -> Tip: `WidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. - -## See Also - -* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). \ No newline at end of file +[Click to navigate to Widgets document](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index 6cf6917ba6..1522fa586a 100644 --- a/docs/en/Authorization.md +++ b/docs/en/Authorization.md @@ -1,6 +1,6 @@ # Authorization -Authorization is used to check if a user is allowed to perform some specific operations in the application. +Authorization is used to check if a user is allowed to perform some specific operations in the application. ABP extends [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 authorization system to be usable in the **[application services](Application-Services.md)** too. @@ -12,7 +12,7 @@ ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet Example: -````csharp +```csharp using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -43,11 +43,11 @@ namespace Acme.BookStore } } -```` +``` -* `Authorize` attribute forces the user to login into the application in order to use the `AuthorAppService` methods. So, `GetListAsync` method is only available to the authenticated users. -* `AllowAnonymous` suppresses the authentication. So, `GetAsync` method is available to everyone including unauthorized users. -* `[Authorize("BookStore_Author_Create")]` defines a policy (see [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies)) that is checked to authorize the current user. +- `Authorize` attribute forces the user to login into the application in order to use the `AuthorAppService` methods. So, `GetListAsync` method is only available to the authenticated users. +- `AllowAnonymous` suppresses the authentication. So, `GetAsync` method is available to everyone including unauthorized users. +- `[Authorize("BookStore_Author_Create")]` defines a policy (see [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies)) that is checked to authorize the current user. "BookStore_Author_Create" is an arbitrary policy name. If you declare an attribute like that, ASP.NET Core authorization system expects a policy to be defined before. @@ -61,7 +61,7 @@ A permission is a simple policy that is granted or prohibited for a particular u To define permissions, create a class inheriting from the `PermissionDefinitionProvider` as shown below: -````csharp +```csharp using Volo.Abp.Authorization.Permissions; namespace Acme.BookStore.Permissions @@ -76,7 +76,7 @@ namespace Acme.BookStore.Permissions } } } -```` +``` > ABP automatically discovers this class. No additional configuration required! @@ -86,8 +86,8 @@ When you define a permission, it becomes usable in the ASP.NET Core authorizatio ![authorization-new-permission-ui](images/authorization-new-permission-ui.png) -* The "BookStore" group is shown as a new tab on the left side. -* "BookStore_Author_Create" on the right side is the permission name. You can grant or prohibit it for the role. +- The "BookStore" group is shown as a new tab on the left side. +- "BookStore_Author_Create" on the right side is the permission name. You can grant or prohibit it for the role. When you save the dialog, it is saved to the database and used in the authorization system. @@ -97,7 +97,7 @@ When you save the dialog, it is saved to the database and used in the authorizat "BookStore_Author_Create" is not a good permission name for the UI. Fortunately, `AddPermission` and `AddGroup` methods can take `LocalizableString` as second parameters: -````csharp +```csharp var myGroup = context.AddGroup( "BookStore", LocalizableString.Create("BookStore") @@ -107,14 +107,14 @@ myGroup.AddPermission( "BookStore_Author_Create", LocalizableString.Create("Permission:BookStore_Author_Create") ); -```` +``` Then you can define texts for "BookStore" and "Permission:BookStore_Author_Create" keys in the localization file: -````json +```json "BookStore": "Book Store", "Permission:BookStore_Author_Create": "Creating a new author" -```` +``` > For more information, see the [localization document](Localization.md) on the localization system. @@ -126,34 +126,48 @@ The localized UI will be as seen below: ABP supports [multi-tenancy](Multi-Tenancy.md) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below: -* **Host**: The permission is available only for the host side. -* **Tenant**: The permission is available only for the tenant side. -* **Both** (default): The permission is available both for tenant and host sides. +- **Host**: The permission is available only for the host side. +- **Tenant**: The permission is available only for the tenant side. +- **Both** (default): The permission is available both for tenant and host sides. > If your application is not multi-tenant, you can ignore this option. To set the multi-tenancy side option, pass to the third parameter of the `AddPermission` method: -````csharp +```csharp myGroup.AddPermission( "BookStore_Author_Create", LocalizableString.Create("Permission:BookStore_Author_Create"), multiTenancySide: MultiTenancySides.Tenant //set multi-tenancy side! ); +``` + +#### Enable/Disable Permissions + +A permission is enabled by default. It is possible to disable a permission. A disabled permission will be prohibited for everyone. You can still check for the permission, but it will always return prohibited. + +Example definition: + +````csharp +myGroup.AddPermission("Author_Management", isEnabled: false); ```` +You normally don't need to define a disabled permission (unless you temporary want disable a feature of your application). However, you may want to disable a permission defined in a depended module. In this way you can disable the related application functionality. See the "*Changing Permission Definitions of a Depended Module*" section below for an example usage. + +> Note: Checking an undefined permission will throw an exception while a disabled permission check simply returns prohibited (false). + #### Child Permissions A permission may have child permissions. It is especially useful when you want to create a hierarchical permission tree where a permission may have additional sub permissions which are available only if the parent permission has been granted. Example definition: -````csharp +```csharp var authorManagement = myGroup.AddPermission("Author_Management"); authorManagement.AddChild("Author_Management_Create_Books"); authorManagement.AddChild("Author_Management_Edit_Books"); authorManagement.AddChild("Author_Management_Delete_Books"); -```` +``` The result on the UI is shown below (you probably want to localize permissions for your application): @@ -161,7 +175,7 @@ The result on the UI is shown below (you probably want to localize permissions f For the example code, it is assumed that a role/user with "Author_Management" permission granted may have additional permissions. Then a typical application service that checks permissions can be defined as shown below: -````csharp +```csharp [Authorize("Author_Management")] public class AuthorAppService : ApplicationService, IAuthorAppService { @@ -193,10 +207,10 @@ public class AuthorAppService : ApplicationService, IAuthorAppService ... } } -```` +``` -* `GetListAsync` and `GetAsync` will be available to users if they have `Author_Management` permission is granted. -* Other methods require additional permissions. +- `GetListAsync` and `GetAsync` will be available to users if they have `Author_Management` permission is granted. +- Other methods require additional permissions. ### Overriding a Permission by a Custom Policy @@ -204,13 +218,29 @@ If you define and register a policy to the ASP.NET Core authorization system wit See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) document to learn how to define a custom policy. +### Changing Permission Definitions of a Depended Module + +A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions. + +Example: + +````csharp +context + .GetPermissionOrNull(IdentityPermissions.Roles.Delete) + .IsEnabled = false; +```` + +When you write this code inside your permission definition provider, it finds the "role deletion" permission of the [Identity Module](Modules/Identity.md) and disabled the permission, so no one can delete a role on the application. + +> Tip: It is better to check the value returned by the `GetPermissionOrNull` method since it may return null if the given permission was not defined. + ## IAuthorizationService ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization. Example: -````csharp +```csharp public async Task CreateAsync(CreateAuthorDto input) { var result = await AuthorizationService @@ -223,7 +253,7 @@ public async Task CreateAsync(CreateAuthorDto input) //continue to the normal flow... } -```` +``` > `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](Dependency-Injection.md) it into your class. @@ -231,14 +261,14 @@ Since this is a typical code block, ABP provides extension methods to simplify i Example: -````csharp +```csharp public async Task CreateAsync(CreateAuthorDto input) { await AuthorizationService.CheckAsync("Author_Management_Create_Books"); //continue to the normal flow... } -```` +``` `CheckAsync` extension method throws `AbpAuthorizationException` if the current user/client is not granted for the given permission. There is also `IsGrantedAsync` extension method that returns `true` or `false`. @@ -250,11 +280,11 @@ public async Task CreateAsync(CreateAuthorDto input) You may need to check a policy/permission on the client side. For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. Example: -````js +```js abp.auth.isGranted('MyPermissionName'); -```` +``` -See [abp.auth](AspNetCore/JavaScript-API/Auth.md) API documentation for details. +See [abp.auth](API/JavaScript-API/Auth.md) API documentation for details. ## Permission Management @@ -264,7 +294,7 @@ Permission management is normally done by an admin user using the permission man If you need to manage permissions by code, inject the `IPermissionManager` and use as shown below: -````csharp +```csharp public class MyService : ITransientDependency { private readonly IPermissionManager _permissionManager; @@ -284,7 +314,7 @@ public class MyService : ITransientDependency await _permissionManager.SetForUserAsync(userId, permissionName, false); } } -```` +``` `SetForUserAsync` sets the value (true/false) for a permission of a user. There are more extension methods like `SetForRoleAsync` and `SetForClientAsync`. @@ -296,15 +326,15 @@ public class MyService : ITransientDependency Permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers: -* `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property. -* `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property. -* `ClientPermissionValueProvider` checks if the current client is granted for the given permission. This is especially useful on a machine to machine interaction where there is no current user. It gets the client id from the current claims. Client claim name is defined with the `AbpClaimTypes.ClientId` static property. +- `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property. +- `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property. +- `ClientPermissionValueProvider` checks if the current client is granted for the given permission. This is especially useful on a machine to machine interaction where there is no current user. It gets the client id from the current claims. Client claim name is defined with the `AbpClaimTypes.ClientId` static property. You can extend the permission checking system by defining your own permission value provider. Example: -````csharp +```csharp public class SystemAdminPermissionValueProvider : PermissionValueProvider { public SystemAdminPermissionValueProvider(IPermissionStore permissionStore) @@ -314,7 +344,7 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider public override string Name => "SystemAdmin"; - public override async Task + public override async Task CheckAsync(PermissionValueCheckContext context) { if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") @@ -325,24 +355,24 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider return PermissionGrantResult.Undefined; } } -```` +``` This provider allows for all permissions to a user with a `User_Type` claim that has `SystemAdmin` value. It is common to use current claims and `IPermissionStore` in a permission value provider. A permission value provider should return one of the following values from the `CheckAsync` method: -* `PermissionGrantResult.Granted` is returned to grant the user for the permission. If any of the providers return `Granted`, the result will be `Granted`, if no other provider returns `Prohibited`. -* `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return. -* `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission. +- `PermissionGrantResult.Granted` is returned to grant the user for the permission. If any of the providers return `Granted`, the result will be `Granted`, if no other provider returns `Prohibited`. +- `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return. +- `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission. -Once a provider is defined, it should be added to the `PermissionOptions` as shown below: +Once a provider is defined, it should be added to the `AbpPermissionOptions` as shown below: -````csharp -Configure(options => +```csharp +Configure(options => { options.ValueProviders.Add(); }); -```` +``` ### Permission Store @@ -354,16 +384,17 @@ Configure(options => Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](Dependency-Injection.md) system: -````csharp +```csharp public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAlwaysAllowAuthorization(); } -```` +``` This is already done for the startup template integration tests. ## See Also * [Permission Management Module](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](AspNetCore/JavaScript-API/Auth.md) +* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](API/JavaScript-API/Auth.md) +* [Permission Management in Angular UI](UI/Angular/Permission-Management.md) diff --git a/docs/en/Background-Jobs-Hangfire.md b/docs/en/Background-Jobs-Hangfire.md index 588fcace9e..3a98fed66e 100644 --- a/docs/en/Background-Jobs-Hangfire.md +++ b/docs/en/Background-Jobs-Hangfire.md @@ -40,4 +40,45 @@ public class YourModule : AbpModule ## Configuration -TODO... \ No newline at end of file +You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package). + +After you have installed these NuGet packages, you need to configure your project to use Hangfire. + +1.First, we change the `Module` class (example: `HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method: + +````csharp + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + var hostingEnvironment = context.Services.GetHostingEnvironment(); + + //... other configarations. + + ConfigureHangfire(context, configuration); + } + + private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) + { + context.Services.AddHangfire(config => + { + config.UseSqlServerStorage(configuration.GetConnectionString("Default")); + }); + } +```` + +2. We need to add `UseHangfireServer` call in the `OnApplicationInitialization` method in `Module` class + +If you want to use hangfire's dashboard, you can add it, too: by `UseHangfireDashboard` + +````csharp + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + // ... others + + app.UseHangfireServer(); + app.UseHangfireDashboard(); + + } +```` diff --git a/docs/en/Background-Jobs-Quartz.md b/docs/en/Background-Jobs-Quartz.md new file mode 100644 index 0000000000..8039192708 --- /dev/null +++ b/docs/en/Background-Jobs-Quartz.md @@ -0,0 +1,73 @@ +# Quartz Background Job Manager + +[Quartz](https://www.quartz-scheduler.net/) is an advanced background job manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background job manager](Background-Jobs.md). In this way, you can use the same background job API for Quartz and your code will be independent of Quartz. If you like, you can directly use Quartz's API, too. + +> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the Quartz integration. + +## Installation + +It is suggested to use the [ABP CLI](CLI.md) to install this package. + +### Using the ABP CLI + +Open a command line window in the folder of the project (.csproj file) and type the following command: + +````bash +abp add-package Volo.Abp.BackgroundJobs.Quartz +```` + +### Manual Installation + +If you want to manually install; + +1. Add the [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet package to your project: + + ```` + Install-Package Volo.Abp.BackgroundJobs.Quartz + ```` + +2. Add the `AbpBackgroundJobsQuartzModule` to the dependency list of your module: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +## Configuration + +Quartz is a very configurable library,and the ABP framework provides `AbpQuartzPreOptions` for this. You can use the `PreConfigure` method in your module class to pre-configure this option. ABP will use it when initializing the Quartz module. For example: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + PreConfigure(options => + { + options.Properties = new NameValueCollection + { + ["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp", + ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", + ["quartz.jobStore.tablePrefix"] = "QRTZ_", + ["quartz.serializer.type"] = "json", + ["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"), + ["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer", + ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", + }; + }); + } +} +```` + +Quartz stores job and scheduling information **in memory by default**. In the example, we use the pre-configuration of [options pattern](Options.md) to change it to the database. For more configuration of Quartz, please refer to the Quartz's [documentation](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/index.html). \ No newline at end of file diff --git a/docs/en/Background-Jobs.md b/docs/en/Background-Jobs.md index 70f06bbadb..e5ef87bb2d 100644 --- a/docs/en/Background-Jobs.md +++ b/docs/en/Background-Jobs.md @@ -11,7 +11,7 @@ Background jobs are **persistent** that means they will be **re-tried** and **ex ## Abstraction Package -ABP provides an **abstraction** module and **several implementations** for background jobs. It has a built-in/default implementation as well as Hangfire and RabbitMQ integrations. +ABP provides an **abstraction** module and **several implementations** for background jobs. It has a built-in/default implementation as well as Hangfire, RabbitMQ and Quartz integrations. `Volo.Abp.BackgroundJobs.Abstractions` nuget package provides needed services to create background jobs and queue background job items. If your module only depend on this package, it can be independent from the actual implementation/integration. @@ -171,4 +171,8 @@ Background job system is extensible and you can change the default background jo See pre-built job manager alternatives: * [Hangfire Background Job Manager](Background-Jobs-Hangfire.md) -* [RabbitMQ Background Job Manager](Background-Jobs-RabbitMq.md) \ No newline at end of file +* [RabbitMQ Background Job Manager](Background-Jobs-RabbitMq.md) +* [Quartz Background Job Manager](Background-Jobs-Quartz.md) + +## See Also +* [Background Workers](Background-Workers.md) \ No newline at end of file diff --git a/docs/en/Background-Workers-Quartz.md b/docs/en/Background-Workers-Quartz.md new file mode 100644 index 0000000000..0e9c640ee2 --- /dev/null +++ b/docs/en/Background-Workers-Quartz.md @@ -0,0 +1,68 @@ +# Quartz Background Worker Manager + +[Quartz](https://www.quartz-scheduler.net/) is an advanced background worker manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background worker manager](Background-Worker.md). ABP simply integrates quartz. + +## Installation + +It is suggested to use the [ABP CLI](CLI.md) to install this package. + +### Using the ABP CLI + +Open a command line window in the folder of the project (.csproj file) and type the following command: + +````bash +abp add-package Volo.Abp.BackgroundWorkers.Quartz +```` + +### Manual Installation + +If you want to manually install; + +1. Add the [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet package to your project: + + ```` + Install-Package Volo.Abp.BackgroundWorkers.Quartz + ```` + +2. Add the `AbpBackgroundWorkersQuartzModule` to the dependency list of your module: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +### Configuration + +See [Configuration](Background-Jobs-Quartz#Configuration). + +### Create a Background Worker + +A background work is a class that derives from the `QuartzBackgroundWorkerBase` base class. for example. A simple worker class is shown below: + +```` csharp +public class MyLogWorker : QuartzBackgroundWorkerBase +{ + public MyLogWorker() + { + JobDetail = JobBuilder.Create().Build(); + Trigger = TriggerBuilder.Create().StartNow().Build(); + } + + public override Task Execute(IJobExecutionContext context) + { + Logger.LogInformation("Executed MyLogWorker..!"); + return Task.CompletedTask; + } +} +```` + +We simply implemented the Execute method to write a log. The background worker is a **singleton by default**. If you want, you can also implement a [dependency interface](Dependency-Injection#DependencyInterfaces) to register it as another life cycle. + +### More + +Please see Quartz's [documentation](https://www.quartz-scheduler.net/documentation/index.html) for more information. diff --git a/docs/en/Background-Workers.md b/docs/en/Background-Workers.md new file mode 100644 index 0000000000..c8a617efec --- /dev/null +++ b/docs/en/Background-Workers.md @@ -0,0 +1,140 @@ +# Background Workers + +## Introduction + +Background workers are simple independent threads in the application running in the background. Generally, they run periodically to perform some tasks. Examples; + +* A background worker can run periodically to **delete old logs**. +* A background worker can run periodically to **determine inactive users** and **send emails** to get users to return to your application. + + +## Create a Background Worker + +A background worker should directly or indirectly implement the `IBackgroundWorker` interface. + +> A background worker is inherently [singleton](Dependency-Injection.md). So, only a single instance of your worker class is instantiated and run. + +### BackgroundWorkerBase + +`BackgroundWorkerBase` is an easy way to create a background worker. + +````csharp +public class MyWorker : BackgroundWorkerBase +{ + public override Task StartAsync(CancellationToken cancellationToken = default) + { + //... + } + + public override Task StopAsync(CancellationToken cancellationToken = default) + { + //... + } +} +```` + +Start your worker in the `StartAsync` (which is called when the application begins) and stop in the `StopAsync` (which is called when the application shuts down). + +> You can directly implement the `IBackgroundWorker`, but `BackgroundWorkerBase` provides some useful properties like `Logger`. + +### AsyncPeriodicBackgroundWorkerBase + +Assume that we want to make a user passive, if the user has not logged in to the application in last 30 days. `AsyncPeriodicBackgroundWorkerBase` class simplifies to create periodic workers, so we will use it for the example below: + +````csharp +public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase +{ + public PassiveUserCheckerWorker( + AbpTimer timer, + IServiceScopeFactory serviceScopeFactory + ) : base( + timer, + serviceScopeFactory) + { + Timer.Period = 600000; //10 minutes + } + + protected override async Task DoWorkAsync( + PeriodicBackgroundWorkerContext workerContext) + { + Logger.LogInformation("Starting: Setting status of inactive users..."); + + //Resolve dependencies + var userRepository = workerContext + .ServiceProvider + .GetRequiredService(); + + //Do the work + await userRepository.UpdateInactiveUserStatusesAsync(); + + Logger.LogInformation("Completed: Setting status of inactive users..."); + } +} +```` + +* `AsyncPeriodicBackgroundWorkerBase` uses the `AbpTimer` (a thread-safe timer) object to determine **the period**. We can set its `Period` property in the constructor. +* It required to implement the `DoWorkAsync` method to **execute** the periodic work. +* It is a good practice to **resolve dependencies** from the `PeriodicBackgroundWorkerContext` instead of constructor injection. Because `AsyncPeriodicBackgroundWorkerBase` uses a `IServiceScope` that is **disposed** when your work finishes. +* `AsyncPeriodicBackgroundWorkerBase` **catches and logs exceptions** thrown by the `DoWorkAsync` method. + + +## Register Background Worker + +After creating a background worker class, you should to add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class: + +````csharp +[DependsOn(typeof(AbpBackgroundWorkersModule))] +public class MyModule : AbpModule +{ + public override void OnApplicationInitialization( + ApplicationInitializationContext context) + { + context.AddBackgroundWorker(); + } +} +```` + +`context.AddBackgroundWorker(...)` is a shortcut extension method for the expression below: + +````csharp +context.ServiceProvider + .GetRequiredService() + .Add( + context + .ServiceProvider + .GetRequiredService() + ); +```` + +So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`. + +While we generally add workers in `OnApplicationInitialization`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. + +## Options + +`AbpBackgroundWorkerOptions` class is used to [set options](Options.md) for the background workers. Currently, there is only one option: + +* `IsEnabled` (default: true): Used to **enable/disable** the background worker system for your application. + +> See the [Options](Options.md) document to learn how to set options. + +## Making Your Application Always Run + +Background workers only work if your application is running. If you host the background job execution in your web application (this is the default behavior), you should ensure that your web application is configured to always be running. Otherwise, background jobs only work while your application is in use. + +## Running On a Cluster + +Be careful if you run multiple instances of your application simultaneously in a clustered environment. In that case, every application runs the same worker which may create conflicts if your workers are running on the same resources (processing the same data, for example). + +If that's a problem for your workers, you have two options; + +* Disable the background worker system using the `AbpBackgroundWorkerOptions` described above, for all the application instances, except one of them. +* Disable the background worker system for all the application instances and create another special application that runs on a single server and execute the workers. + +## Quartz Integration + +ABP Framework's background worker system is good to implement periodic tasks. However, you may want to use an advanced task scheduler like [Quartz](https://www.quartz-scheduler.net/). See the community contributed [quartz integration](Background-Workers-Quartz.md) for the background workers. + +## See Also +* [Quartz Integration for the background workers](Background-Workers-Quartz.md) +* [Background Jobs](Background-Jobs.md) \ No newline at end of file diff --git a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md index 4f7e20dd70..f487f9d61d 100644 --- a/docs/en/Best-Practices/Entity-Framework-Core-Integration.md +++ b/docs/en/Best-Practices/Entity-Framework-Core-Integration.md @@ -89,13 +89,15 @@ public static class IdentityDbContextModelBuilderExtensions builder.Entity(b => { - b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ConfigureByConvention(); //code omitted for brevity }); builder.Entity(b => { b.ToTable(options.TablePrefix + "UserClaims", options.Schema); + b.ConfigureByConvention(); //code omitted for brevity }); @@ -104,10 +106,11 @@ public static class IdentityDbContextModelBuilderExtensions } ```` -* **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example: +* **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above). +* **Do** create a **configuration options** class by inheriting from the `AbpModelBuilderConfigurationOptions`. Example: ````C# -public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions +public class IdentityModelBuilderConfigurationOptions : AbpModelBuilderConfigurationOptions { public IdentityModelBuilderConfigurationOptions() : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) diff --git a/docs/en/Best-Practices/MongoDB-Integration.md b/docs/en/Best-Practices/MongoDB-Integration.md index 76b6e8561f..74a0e429f1 100644 --- a/docs/en/Best-Practices/MongoDB-Integration.md +++ b/docs/en/Best-Practices/MongoDB-Integration.md @@ -90,11 +90,11 @@ public static class AbpIdentityMongoDbContextExtensions } ``` -- **Do** create a **configuration options** class by inheriting from the `MongoModelBuilderConfigurationOptions`. Example: +- **Do** create a **configuration options** class by inheriting from the `AbpMongoModelBuilderConfigurationOptions`. Example: ```c# public class IdentityMongoModelBuilderConfigurationOptions - : MongoModelBuilderConfigurationOptions + : AbpMongoModelBuilderConfigurationOptions { public IdentityMongoModelBuilderConfigurationOptions() : base(AbpIdentityConsts.DefaultDbTablePrefix) diff --git a/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md b/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md index 437ee87387..dc6f5fa3e8 100644 --- a/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md +++ b/docs/en/Blog-Posts/2018-09-24-Announcement/Post.md @@ -120,7 +120,7 @@ Dynamic bundling & minification system works on the virtual file system and allo This code creates a new style bundle on the fly by including bootstrap (and its dependencies if there are) and two more css files. These files are bundled & minified on production environment, but will be added individually on the development environment. -See [the documentation](https://github.com/abpframework/abp/blob/master/docs/AspNetCore/Bundling-Minification.md) for more. +See [the documentation](https://github.com/abpframework/abp/blob/master/docs/UI/AspNetCore/Bundling-Minification.md) for more. #### Distributed Event Bus In current ABP, there is an IEventBus service to trigger and handle events inside the application. In addition to this local event bus, we are creating a distributed event bus abstraction (and RabbitMQ integration) to implement distributed messaging patterns. diff --git a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md index eec70c5566..feeb3c2bb6 100644 --- a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -12,11 +12,11 @@ Finally, ABP has a **SPA UI** option with the latest [Angular](https://angular.i * Created Angular UI packages for the modules like account, identity and tenant-management. * Created a minimal startup template that authenticates using IdentityServer and uses the ASP.NET Core backend. This template uses the packages mentioned above. * Worked on the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) and the [download page](https://abp.io/get-started) to be able to generate projects with the new UI option. -* Created a [tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Angular/Part-I) to jump start with the new UI option. +* Created a [tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) to jump start with the new UI option. We've created the template, document and infrastructure based on the latest Angular tools and trends: -* Uses [NgBootstrap](https://ng-bootstrap.github.io/) and [PrimeNG](https://www.primefaces.org/primeng/) as the UI component libraries. You can use your favorite library, no problem, but pre-built modules work with these libraries. +* Uses [NgBootstrap](https://ng-bootstrap.github.io/) as the UI component library. You can use your favorite library, but pre-built modules work with these libraries. * Uses [NGXS](https://ngxs.gitbook.io/ngxs/) as the state management library. Angular was the first SPA UI option, but it is not the last. After v1.0 release, we will start to work on a second UI option. Not decided yet, but candidates are Blazor, React and Vue.js. Waiting your feedback. You can thumb up using the following issues: @@ -27,7 +27,7 @@ Angular was the first SPA UI option, but it is not the last. After v1.0 release, ### Widget System -[Widget system](https://docs.abp.io/en/abp/latest/AspNetCore/Widgets) allows to **define and reuse** widgets for ASP.NET Core MVC applications. Widgets may have their own script and style resources and dependencies to 3rd-party libraries which are managed by the ABP framework. +[Widget system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Widgets) allows to **define and reuse** widgets for ASP.NET Core MVC applications. Widgets may have their own script and style resources and dependencies to 3rd-party libraries which are managed by the ABP framework. ### Others diff --git a/docs/en/Blog-Posts/2020-03-19 v2_3_Release/Post.md b/docs/en/Blog-Posts/2020-03-19 v2_3_Release/Post.md new file mode 100644 index 0000000000..dd081f38e5 --- /dev/null +++ b/docs/en/Blog-Posts/2020-03-19 v2_3_Release/Post.md @@ -0,0 +1,142 @@ +# ABP Framework v2.3.0 Has Been Released! + +In the days of **coronavirus**, we have released **ABP Framework v2.3** and this post will explain **what's new** with this release and **what we've done** in the last two weeks. + +## About the Coronavirus & Our Team + +**We are very sad** about the coronavirus case. As [Volosoft](https://volosoft.com/) team, we have **remote workers** working in their home in different countries. Beginning from the last week, we've **completely started to work remotely** from home including our main office employees. + +We believe in and pray for that the humanity will overcome this issue in a short time. + +## About the Release Cycle + +Beginning from the ABP v2.1.0, we have started to release feature versions once **in two weeks**, on Thursdays. This is the 3rd release after that decision and we see that it works fine for now and improved our agility. + +We will continue to release **feature versions** (like v2.4, v2.5) in every two weeks. In addition, we may release **hotfix versions** (like v2.3.1, v2.3.2) whenever needed. + +## What's New in ABP Framework v2.3.0 + +We've completed & merged **[104](https://github.com/abpframework/abp/milestone/30?closed=1) issues and pull requests** with **393 commits** in this two weeks development period. + +I will introduce some new features and enhancements introduced with this release. + +### React Native Mobile Application + +We have finally completed the **react native mobile application**. It currently allows you to **login**, manage your **users** and **tenants**. It utilizes the same setting, authorization and localization systems of the ABP Framework. + +A few screenshots from the application: + +![mobile-ui](react-native-ui.png) + +It doesn't have much functionality but it is a **perfect starting point** for your own mobile application since it is completely integrated to the backend and supports multi-tenancy. + +### Angular TypeScript Proxy Generator + +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 he server side) and **model objects** (matches to [DTOs](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) in the server side). + +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: + +* 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](https://docs.abp.io/en/abp/latest/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. + +So, we've decided to create an ABP CLI command to automatically generate the typescript client proxies ([#2222](https://github.com/abpframework/abp/issues/2222)) for your REST API developed with the ABP Framework. + +It is easy to use. Just run the following command in the **root folder** of the angular application: + +````bash +abp generate-proxy +```` + +It only creates proxies only for your own application's services. It doesn't create proxies for the services of the application modules you're using (by default). There are several options. See the [CLI documentation](https://docs.abp.io/en/abp/latest/CLI). + +### CRUD Application Services for Entities with Composite Keys + +` CrudAppService ` is a useful base class to create CRUD application services for your entities. But it doesn't support entities with **composite primary keys**. `AbstractKeyCrudAppService` is the new base class that is developed to support entities with composite primary keys. See [the documentation](https://docs.abp.io/en/abp/latest/Application-Services#abstractkeycrudappservice) for more. + +### Add Source Code of the Modules + +The application startup template comes with some [application modules](https://docs.abp.io/en/abp/latest/Modules/Index) **pre-installed** as **NuGet & NPM packages**. This have a few important advantages: + +* You can **easily [upgrade](https://docs.abp.io/en/abp/latest/CLI#update)** these modules when a new version is available. +* Your solution becomes **cleaner**, so you can focus on your own code. + +However, when you need to make **major customizations** for a depended module, it is not easy as its source code is in your applications. To solve this problem, we've introduces a new command to the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) that **replaces** NuGet packages with their **source code** in your solution. The usage is simple: + +````bash +abp add-module --with-source-code +```` + +This command adds a module with source code or replaces with its source code if it is already added as package references. + +> It is suggested to **save your changes** to your source control system before using this command since it makes a lot of changes in your source code. + +In addition, we've documented how to customize depended modules without changing their source code (see the section below). It is suggested to use modules as packages to easily upgrade them in the future. + +> Source code of the free modules are licensed under **MIT**, so you can freely change them and add into your solution. + +### Switch to Preview + +ABP Framework is rapidly evolving and we are frequently releasing new versions. However, if you want to follow it closer, you can use the **daily preview packages**. + +We've created an ABP CLI command to easily **update to the latest preview packages** for your solution. Run the following command in the root folder of your solution: + +````bash +abp switch-to-preview +```` + +It will change the versions of all ABP related NuGet and NPM packages. You can **switch back to the latest stable** when you want: + +````bash +abp switch-to-stable +```` + +See the [ABP CLI document](https://docs.abp.io/en/abp/latest/CLI#switch-to-preview) fore more. + +### Documentation Improvements + +#### Extending/Customizing Depended Application Modules + +We've created a huge documentation that explains how to customize a depended module without changing its source code. See [the documentation](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide). + +In addition to the documentation, we've revised all the modules ([#3166](https://github.com/abpframework/abp/issues/3166)) to make their services easily extensible & customizable. + +#### EF Core Migration Guide + +We've recently created a guide to explain the migration system that is used by the ABP startup templates. [This guide](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations) also explains how to customize the migration structure, split your modules across multiple databases, reusing a module's table and son on. + +#### Migration from the ASP.NET Boilerplate + +If you have a solution built on the ASP.NET Boilerplate, we've [created a guide](https://docs.abp.io/en/abp/latest/AspNet-Boilerplate-Migration-Guide) that tries to help you if you want to migrate your solution to the new ABP Framework. + +### Some Other Features + +#### The Framework + +* Add `IRepository.GetAsync` and `IRepository.FindAsync` methods ([#3184](https://github.com/abpframework/abp/issues/3148)). + +#### Modules + +* Get password & email address of the admin while creating a new tenant, for the tenant management module ([#3088](https://github.com/abpframework/abp/issues/3088)). +* Elastic search integrated full text search for the docs module ([#2901](https://github.com/abpframework/abp/pull/2901)). +* New Quartz background worker module ([#2762](https://github.com/abpframework/abp/issues/2762)) + +#### Samples + +* Add multi-tenancy support to the microservice demo ([#3032](https://github.com/abpframework/abp/pull/3032)). + +See [the release notes](https://github.com/abpframework/abp/releases/tag/2.3.0) for all feature, enhancement and bugfixes. + +## What's Next? + +We have the following goals for the next few months: + +* Complete the **documentation and samples**, write more tutorials. +* Make the framework and existing modules more **customizable and extensible**. +* Integrate to **gRPC** & implement gRPC endpoint for pre-built modules ([#2882](https://github.com/abpframework/abp/issues/2882)). +* Create a **Blazor UI** for the ABP Framework & implement it for all the modules and startup templates ([#394](https://github.com/abpframework/abp/issues/394)). +* Add **new features** to pre-built modules and create new modules for the [ABP Commercial](https://commercial.abp.io/). + +See [the GitHub milestones](https://github.com/abpframework/abp/milestones) for details. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png b/docs/en/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png new file mode 100644 index 0000000000..8c0a2bf241 Binary files /dev/null and b/docs/en/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png differ diff --git a/docs/en/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png b/docs/en/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png new file mode 100644 index 0000000000..47d889413c Binary files /dev/null and b/docs/en/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png differ diff --git a/docs/en/CLI.md b/docs/en/CLI.md index a1d3d51138..f0fd3ae7bd 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -22,7 +22,7 @@ dotnet tool update -g Volo.Abp.Cli Generates a new solution based on the ABP [startup templates](Startup-Templates/Index.md). -Basic usage: +Usage: ````bash abp new [options] @@ -48,13 +48,19 @@ abp new Acme.BookStore * `--separate-identity-server`: Separates the identity server application from the API host application. If not specified, you will have a single endpoint in the server side. * `none`: Without UI. There are some additional options for this template: * `--separate-identity-server`: Separates the identity server application from the API host application. If not specified, you will have a single endpoint in the server side. - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: + * `--mobile` or `-m`: Specifies the mobile application framework. Default framework is `react-native`. Available frameworks: + * `none`: no mobile application. + * `react-native`: React Native. + * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: * `ef`: Entity Framework Core. * `mongodb`: MongoDB. - * `module`: [Module template](Startup-Templates/Module.md). Additional options: - * `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI). + * `module`: [Module template](Startup-Templates/Module.md). Additional options: + * `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI). * `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. * `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version. +* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D\localTemplate` or `https://.zip`). +* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. +* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. You can set your own connection string if you don't want to use the default. Be aware that the default database provider is `SQL Server`, therefore you can only enter connection string for SQL Server! ### add-package @@ -89,7 +95,7 @@ Adds a [multi-package application module](Modules/Index) to a solution by findin > A business module generally consists of several packages (because of layering, different database provider options or other reasons). Using `add-module` command dramatically simplifies adding a module to a solution. However, each module may require some additional configurations which is generally indicated in the documentation of the related module. -Basic usage: +Usage ````bash abp add-module [options] @@ -108,6 +114,7 @@ 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. ### update @@ -128,9 +135,35 @@ abp update [options] * `--npm`: Only updates NPM packages. * `--nuget`: Only updates NuGet packages. +### switch-to-preview + +You can use this command to switch your project to latest preview version of the ABP framework packages. + +Usage: + +````bash +abp switch-to-preview [options] +```` +#### Options + +`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. + +### switch-to-stable + +If you're using the ABP Framework preview packages, you can switch back to stable version using this command. + +Usage: + +````bash +abp switch-to-stable [options] +```` +#### Options + +`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. + ### login -Some features of the CLI requires to be logged in to abp.io platform. To login with your username write +Some features of the CLI requires to be logged in to abp.io platform. To login with your username write: ```bash abp login @@ -146,6 +179,29 @@ Logs you out by removing the session token from your computer. abp logout ``` +### generate-proxy + +Generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running `generate-proxy` command, your host must be up and running. + +Usage: + +````bash +abp generate-proxy [options] +```` + +#### Options + +* `--apiUrl` or `-a`: Specifies the root URL of the HTTP API. The default value is being retrieved from the `environment.ts` file for the Angular application. Make sure your host is up and running before running `abp generate-proxy`. +* `--ui` or `-u`: Specifies the UI framework. Default value is `angular` and it is the only UI framework supported for now. Creates TypeScript code. +* `--module` or `-m`: Specifies the module name. Default module name is `app`, which indicates your own application (you typically want this since every module is responsible to maintain its own client proxies). Set `all` for to generate proxies for all the modules. + +Example usage with the options: + +````bash +abp generate-proxy --apiUrl https://localhost:44305 --ui angular --module all +```` + + ### help Writes basic usage information of the CLI. @@ -162,4 +218,3 @@ Examples: abp help # Shows a general help. abp help new # Shows help about the "new" command. ```` - diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md new file mode 100644 index 0000000000..be28465ff7 --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -0,0 +1,179 @@ +# Customizing the Application Modules: Extending Entities + +In some cases, you may want to add some additional properties (and database fields) for an entity defined in a depended module. This section will cover some different approaches to make this possible. + +## Extra Properties + +[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these objects. + +Example: + +````csharp +//SET AN EXTRA PROPERTY +var user = await _identityUserRepository.GetAsync(userId); +user.SetProperty("Title", "My custom title value!"); +await _identityUserRepository.UpdateAsync(user); + +//GET AN EXTRA PROPERTY +var user = await _identityUserRepository.GetAsync(userId); +return user.GetProperty("Title"); +```` + +This approach is very easy to use and available out of the box. No extra code needed. You can store more than one property at the same time by using different property names (like `Title` here). + +Extra properties are stored as a single `JSON` formatted string value in the database for the EF Core. For MongoDB, they are stored as separate fields of the document. + +See the [entities document](Entities.md) for more about the extra properties system. + +> It is possible to perform a **business logic** based on the value of an extra property. You can [override a service method](Customizing-Application-Modules-Overriding-Services.md), then get or set the value as shown above. + +## Entity Extensions (EF Core) + +As mentioned above, all extra properties of an entity are stored as a single JSON object in the database table. This is not so natural especially when you want to; + +* Create **indexes** and **foreign keys** for an extra property. +* Write **SQL** or **LINQ** using the extra property (search table by the property value, for example). +* Creating your **own entity** maps to the same table, but defines an extra property as a **regular property** in the entity (see the [EF Core migration document](Entity-Framework-Core-Migrations.md) for more). + +To overcome the difficulties described above, ABP Framework entity extension system for the Entity Framework Core that allows you to use the same extra properties API defined above, but store a desired property as a separate field in the database table. + +Assume that you want to add a `SocialSecurityNumber` to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). You can use the `ObjectExtensionManager`: + +````csharp +ObjectExtensionManager.Instance + .MapEfCoreProperty( + "SocialSecurityNumber", + b => { b.HasMaxLength(32); } + ); +```` + +* You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table). +* You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties). + +> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEntityExtensions`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself. + +Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database. + +You can then use the same extra properties system defined in the previous section to manipulate the property over the entity. + +## Creating a New Entity Maps to the Same Database Table/Collection + +While using the extra properties approach is **easy to use** and suitable for some scenarios, it has some drawbacks described in the [entities document](Entities.md). + +Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). + +`AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems. + +## Creating a New Entity with Its Own Database Table/Collection + +Mapping your entity to an **existing table** of a depended module has a few disadvantages; + +* You deal with the **database migration structure** for EF Core. While it is possible, you should extra care about the migration code especially when you want to add **relations** between entities. +* Your application database and the module database will be the **same physical database**. Normally, a module database can be separated if needed, but using the same table restricts it. + +If you want to **loose couple** your entity with the entity defined by the module, you can create your own database table/collection and map your entity to your own table in your own database. + +In this case, you need to deal with the **synchronization problems**, especially if you want to **duplicate** some properties/fields of the related entity. There are a few solutions; + +* If you are building a **monolithic** application (or managing your entity and the related module entity within the same process), you can use the [local event bus](Local-Event-Bus.md) to listen changes. +* If you are building a **distributed** system where the module entity is managed (created/updated/deleted) on a different process/service than your entity is managed, then you can subscribe to the [distributed event bus](Distributed-Event-Bus.md) for change events. + +Once you handle the event, you can update your own entity in your own database. + +### Subscribing to Local Events + +[Local Event Bus](Local-Event-Bus.md) system is a way to publish and subscribe to events occurring in the same application. + +Assume that you want to get informed when a `IdentityUser` entity changes (created, updated or deleted). You can create a class that implements the `ILocalEventHandler>` interface. + +````csharp +public class MyLocalIdentityUserChangeEventHandler : + ILocalEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityChangedEventData eventData) + { + var userId = eventData.Entity.Id; + var userName = eventData.Entity.UserName; + + //... + } +} +```` + +* `EntityChangedEventData` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). +* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional. + +> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). + +### Subscribing to Distributed Events + +[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server. + +Assume that you want to get informed when a `IdentityUser` entity created, updated or deleted. You can create a class like below: + +````csharp +public class MyDistributedIdentityUserChangeEventHandler : + IDistributedEventHandler>, + IDistributedEventHandler>, + IDistributedEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityCreatedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "created" event + } + } + + public async Task HandleEventAsync(EntityUpdatedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "updated" event + } + } + + public async Task HandleEventAsync(EntityDeletedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "deleted" event + } + } +} +```` + +* It implements multiple `IDistributedEventHandler` interfaces: **Created**, **Updated** and **Deleted**. Because, the distributed event bus system publishes events individually. There is no "Changed" event like the local event bus. +* It subscribes to `EntityEto`, which is a generic event class that is **automatically published** for all type of entities by the ABP framework. This is why it checks the **entity type** (checking the entity type as string since we assume that there is no type safe reference to the `IdentityUser` entity). + +Pre-built application modules do not define specialized event types yet (like `IdentityUserEto` - "ETO" means "Event Transfer Object"). This feature is on the road map and will be available in a short term ([follow this issue](https://github.com/abpframework/abp/issues/3033)). Once it is implemented, you will be able to subscribe to individual entity types. Example: + +````csharp +public class MyDistributedIdentityUserCreatedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityCreatedEto eventData) + { + var userId = eventData.Entity.Id; + var userName = eventData.Entity.UserName; + //...handle the "created" event + } + + //... +} +```` + +* This handler is executed only when a new user has been created. + +> The only pre-defined specialized event class is the `UserEto`. For example, you can subscribe to the `EntityCreatedEto` to get notified when a user has created. This event also works for the Identity module. + +## See Also + +* [Migration System for the EF Core](Entity-Framework-Core-Migrations.md) +* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) \ No newline at end of file diff --git a/docs/en/Customizing-Application-Modules-Guide.md b/docs/en/Customizing-Application-Modules-Guide.md new file mode 100644 index 0000000000..25f9daf0b3 --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Guide.md @@ -0,0 +1,62 @@ +# Customizing the Existing Modules + +ABP Framework provides was designed to support to build fully [modular applications](Module-Development-Basics.md) and systems. It also provides some [pre-built application modules](Modules/Index.md) those are **ready to use** in any kind of application. + +For example, you can **re-use** the [Identity Management Module](Modules/Identity.md) to add user, role and permission management to your application. The [application startup template](Startup-Templates/Application.md) already comes with Identity and some other modules **pre-installed**. + +## Re-Using an Application Module + +You have two options to re-use an application module. + +### As Package References + +You can add **NuGet** & **NPM** package references of the related module to your application and configure the module (based on its documentation) to integrate to your application. + +As mentioned before, the [application startup template](Startup-Templates/Application.md) already comes with some **fundamental modules pre-installed**. It uses the modules as NuGet & NPM package references. + +This approach has the following benefits: + +* Your solution will be **clean** and only contains your **own application code**. +* You can **easily upgrade** a module when a new version is available. `abp update` [CLI](CLI.md) command makes it even easier. In this way, you can continue to get **new features and bug fixes**. + +However, there is a drawback: + +* You may not able to **customize** the module because the module source is not in your solution. + +This document explains **how to customize or extend** a depended module without need to change its source code. While it is limited compared to a full source code change opportunity, there are still some good ways to make some customizations. + +If you don't think to make huge changes on the pre-built modules, re-using them as package reference is the recommended way. + +### Including the Source Code + +If you want to make **huge changes** or add **major features** on a pre-built module, but the available extension points are not enough, you can consider to directly work the source code of the depended module. + +In this case, you typically **add the source code** of the module to your solution and **replace package references** by local project references. **[ABP CLI](CLI.md)** automates this process for you. + +#### Separating the Module Solution + +You may prefer to not include the module source code **directly into your solution**. Every module consists of 10+ project files and adding **multiple modules** may impact on the **size** of your solution **load & development time.** Also, you may have different development teams working on different modules, so you don't want to make the module code available to the application development team. + +In any case, you can create a **separate solution** for the desired module and depend on the module as project references out of the solution. We do it like that for the [abp repository](https://github.com/abpframework/abp/). + +> One problem we see is Visual Studio doesn't play nice with this kind of approach (it doesn't support well to have references to local projects out of the solution directory). If you get error while building the application (depends on an external module), run `dotnet restore` in the command line after opening the application's solution in the Visual Studio. + +#### Publishing the Customized Module as Packages + +One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company. + +## Module Customization / Extending Approaches + +This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways: + +* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) +* [Overriding Services](Customizing-Application-Modules-Overriding-Services.md) +* [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) + +### See Also + +Also, see the following documents: + +* See [the localization document](Localization.md) to learn how to extend existing localization resources. +* See [the settings document](Settings.md) to learn how to change setting definitions of a depended module. +* See [the authorization document](Authorization.md) to learn how to change permission definitions of a depended module. \ No newline at end of file diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md new file mode 100644 index 0000000000..b735d51f6f --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -0,0 +1,166 @@ +# Customizing the Application Modules: Overriding Services + +You may need to **change behavior (business logic)** of a depended module for your application. In this case, you can use the power of the [dependency injection system](Dependency-Injection.md) to replace a service, controller or even a page model of the depended module by your own implementation. + +**Replacing a service** is possible for any type of class registered to the dependency injection, including services of the ABP Framework. + +You have different options can be used based on your requirement those will be explained in the next sections. + +> Notice that some service methods may not be virtual, so you may not be able to override. We make all virtual by design. If you find any method that is not overridable, please [create an issue](https://github.com/abpframework/abp/issues/new) or do it yourself and send a **pull request** on GitHub. + +## Replacing an Interface + +If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityUserAppService`, you can re-implement the same interface and replace the current implementation by your class. Example: + +````csharp +public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +`MyIdentityUserAppService` replaces the `IIdentityUserAppService` by naming convention (since both ends with `IdentityUserAppService`). If your class name doesn't match, you need to manually expose the service interface: + +````csharp +[ExposeServices(typeof(IIdentityUserAppService))] +public class TestAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. + +Example: + +````csharp +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IIdentityUserAppService))] +public class TestAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +In this way, there will be a single implementation of the `IIdentityUserAppService` interface, while it doesn't change the result for this case. Replacing a service is also possible by code: + +````csharp +context.Services.Replace( + ServiceDescriptor.Transient() +); +```` + +You can write this inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). + +## Overriding a Service Class + +In most cases, you will want to change one or a few methods of the current implementation for a service. Re-implementing the complete interface would not be efficient in this case. As a better approach, inherit from the original class and override the desired method. + +### Example: Overriding an Application Service + +````csharp +[Dependency(ReplaceServices = true)] +public class MyIdentityUserAppService : IdentityUserAppService +{ + //... + public MyIdentityUserAppService( + IdentityUserManager userManager, + IIdentityUserRepository userRepository, + IGuidGenerator guidGenerator + ) : base( + userManager, + userRepository, + guidGenerator) + { + } + + public override async Task CreateAsync(IdentityUserCreateDto input) + { + if (input.PhoneNumber.IsNullOrWhiteSpace()) + { + throw new AbpValidationException( + "Phone number is required for new users!", + new List + { + new ValidationResult( + "Phone number can not be empty!", + new []{"PhoneNumber"} + ) + } + ); } + + return await base.CreateAsync(input); + } +} +```` + +This class **overrides** the `CreateAsync` method of the `IdentityUserAppService` [application service](Application-Services.md) to check the phone number. Then calls the base method to continue to the **underlying business logic**. In this way, you can perform additional business logic **before** and **after** the base logic. + +You could completely **re-write** the entire business logic for a user creation without calling the base method. + +### Example: Overriding a Domain Service + +````csharp +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IdentityUserManager))] +public class MyIdentityUserManager : IdentityUserManager +{ + public MyIdentityUserManager( + IdentityUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider + ) : base( + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger, + cancellationTokenProvider) + { + } + + public override async Task CreateAsync(IdentityUser user) + { + if (user.PhoneNumber.IsNullOrWhiteSpace()) + { + throw new AbpValidationException( + "Phone number is required for new users!", + new List + { + new ValidationResult( + "Phone number can not be empty!", + new []{"PhoneNumber"} + ) + } + ); + } + + return await base.CreateAsync(user); + } +} +```` + +This example class inherits from the `IdentityUserManager` [domain service](Domain-Services.md) and overrides the `CreateAsync` method to perform the same phone number check implemented above. The result is same, but this time we've implemented it inside the domain service assuming that this is a **core domain logic** for our system. + +> `[ExposeServices(typeof(IdentityUserManager))]` attribute is **required** here since `IdentityUserManager` does not define an interface (like `IIdentityUserManager`) and dependency injection system doesn't expose services for inherited classes (like it does for the implemented interfaces) by convention. + +Check the [localization system](Localization.md) to learn how to localize the error messages. + +### 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. + +## How to Find the Services? + +[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. \ No newline at end of file diff --git a/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md b/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md new file mode 100644 index 0000000000..321ed2befd --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md @@ -0,0 +1,6 @@ +# Overriding the User Interface + +You may want to override a page, a component, a JavaScript, CSS or an image file of your depended module. Overriding the UI completely depends on the UI framework you're using. Select the UI framework to continue: + +* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) +* [Angular](UI/Angular/Customization-User-Interface.md) \ No newline at end of file diff --git a/docs/en/Entities.md b/docs/en/Entities.md index b5d196cccb..8a234b94a5 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -295,6 +295,102 @@ All these base classes also have non-generic versions to take `AuditedEntity` an All these base classes also have `...WithUser` pairs, like `FullAuditedAggregateRootWithUser` and`FullAuditedAggregateRootWithUser`. This makes possible to add a navigation property to your user entity. However, it is not a good practice to add navigation properties between aggregate roots, so this usage is not suggested (unless you are using an ORM, like EF Core, that well supports this scenario and you really need it - otherwise remember that this approach doesn't work for NoSQL databases like MongoDB where you must truly implement the aggregate pattern). +## Extra Properties + +ABP defines the `IHasExtraProperties` interface that can be implemented by an entity to be able to dynamically set and get properties for the entity. `AggregateRoot` base class already implements the `IHasExtraProperties` interface. If you've derived from this class (or one of the related audit class defined above), you can directly use the API. + +### GetProperty & SetProperty Extension Methods + +These extension methods are the recommended way to get and set data for an entity. Example: + +````csharp +public class ExtraPropertiesDemoService : ITransientDependency +{ + private readonly IIdentityUserRepository _identityUserRepository; + + public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository) + { + _identityUserRepository = identityUserRepository; + } + + public async Task SetTitle(Guid userId, string title) + { + var user = await _identityUserRepository.GetAsync(userId); + + //SET A PROPERTY + user.SetProperty("Title", title); + + await _identityUserRepository.UpdateAsync(user); + } + + public async Task GetTitle(Guid userId) + { + var user = await _identityUserRepository.GetAsync(userId); + + //GET A PROPERTY + return user.GetProperty("Title"); + } +} +```` + +* Property's **value is object** and can be any type of object (string, int, bool... etc). +* `GetProperty` returns `null` if given property was not set before. +* You can store more than one property at the same time by using different **property names** (like `Title` here). + +It would be a good practice to **define a constant** for the property name to prevent typo errors. It would be even a better practice to **define extension methods** to take the advantage of the intellisense. Example: + +````csharp +public static class IdentityUserExtensions +{ + private const string TitlePropertyName = "Title"; + + public static void SetTitle(this IdentityUser user, string title) + { + user.SetProperty(TitlePropertyName, title); + } + + public static string GetTitle(this IdentityUser user) + { + return user.GetProperty(TitlePropertyName); + } +} +```` + +Then you can directly use `user.SetTitle("...")` and `user.GetTitle()` for an `IdentityUser` object. + +### HasProperty & RemoveProperty Extension Methods + +* `HasProperty` is used to check if the object has a property set before. +* `RemoveProperty` is used to remove a property from the object. You can use this instead of setting a `null` value. + +### How it is Implemented? + +`IHasExtraProperties` interface requires to define a `Dictionary` property, named `ExtraProperties`, for the implemented class. + +So, you can directly use the `ExtraProperties` property to use the dictionary API, if you like. However, `SetProperty` and `GetProperty` methods are the recommended ways since they also check for `null`s. + +#### How is it Stored? + +The way to store this dictionary in the database depends on the database provider you're using. + +* For [Entity Framework Core](Entity-Framework-Core.md), here are two type of configurations; + * By default, it is stored in a single `ExtraProperties` field as a `JSON` string (that means all extra properties stored in a single database table field). Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core. + * If you want, you can use the `ObjectExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `ObjectExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). See the [EF Core integration document](Entity-Framework-Core.md) to learn how to use the `ObjectExtensionManager`. +* For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system. + +### Discussion for the Extra Properties + +Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way. + +You normally **don't need** to this system for your own entities, because it has the following drawbacks: + +* It is **not fully type safe** since it works with strings as property names. +* It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects. + +### Extra Properties Behind Entities + +`IHasExtraProperties` is not restricted to be used with entities. You can implement this interface for any kind of class and use the `GetProperty`, `SetProperty` and other related methods. + ## See Also -* [Best practice guide to design the entities](Best-Practices/Entities.md) \ No newline at end of file +* [Best practice guide to design the entities](Best-Practices/Entities.md) diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md new file mode 100644 index 0000000000..fbe0f59d70 --- /dev/null +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -0,0 +1,885 @@ +# EF Core Database Migrations + +This document begins by **introducing the default structure** provided by [the application startup template](Startup-Templates/Application.md) and **discusses various scenarios** you may want to implement for your own application. + +> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Index.md). + +### Source Code + +You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. + +## About the EF Core Code First Migrations + +Entity Framework Core provides an easy to use and powerful [database migration system](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). ABP Framework [startup templates](Startup-Templates/Index.md) take the advantage of this system to allow you to develop your application in a standard way. + +However, EF Core migration system is **not so good in a modular environment** where each module maintains its **own database schema** while two or more modules may **share a single database** in practical. + +Since ABP Framework cares about modularity in all aspects, it provides a **solution** to this problem. It is important to understand this solution if you need to **customize your database structure**. + +> See [EF Core's own documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to fully learn the EF Core Code First Migrations and why you need to such a system. + +## The Default Solution & Database Configuration + +When you [create a new web application](https://abp.io/get-started) (with EF Core, which is the default database provider), your solution structure will be similar to the picture below: + +![bookstore-visual-studio-solution-v3](images/bookstore-visual-studio-solution-v3.png) + +Actual solution structure may be a bit different based on your preferences, but the database part will be same. + +> This document will use the `Acme.BookStore` example project name to refer the projects and classes. You need to find the corresponding class/project in your solution. + +### The Database Structure + +The startup template has some [application modules](Modules/Index.md) pre-installed. Each layer of the solution has corresponding module **package references**. So, the `.EntityFrameworkCore` project has the NuGet references for the `.EntityFrameworkCore` packages of the used modules: + +![bookstore-efcore-dependencies](images/bookstore-efcore-dependencies.png) + +In this way, you collect all the **EF Core dependencies** under the `.EntityFrameworkCore` project. + +> In addition to the module references, it references to the `Volo.Abp.EntityFrameworkCore.SqlServer` package since the startup template is pre-configured for the **SQL Server**. See the documentation if you want to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). + +While every module has its own `DbContext` class by design and can use its **own physical database**, the solution is configured to use a **single shared database** as shown in the figure below: + +![single-database-usage](images/single-database-usage.png) + +This is **the simplest configuration** and suitable for most of the applications. `appsettings.json` file has a **single connection string**, named `Default`: + +````json +"ConnectionStrings": { + "Default": "..." +} +```` + +So, you have a **single database schema** which contains all the tables of the modules **sharing** this database. + +ABP Framework's [connection string](Connection-Strings.md) system allows you to easily **set a different connection string** for a desired module: + +````json +"ConnectionStrings": { + "Default": "...", + "AbpAuditLogging": "..." +} +```` + +The example configuration about tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md). + +**However, this is just the beginning**. You also need to create the second database, create audit log tables inside it and maintain the database tables using the code first migrations approach. One of the main purposes of this document is to guide you on such **database separation** scenarios. + +#### Module Tables + +Every module uses its **own databases tables**. For example, the [Identity Module](Modules/Identity.md) has some tables to manage the users and roles in the system. + +##### Table Prefixes + +Since it is allowed to share a single database by all modules (it is the default configuration), a module typically uses a **table name prefix** to group its own tables. + +The fundamental modules, like [Identity](Modules/Identity.md), [Tenant Management](Modules/Tenant-Management.md) and [Audit Logs](Modules/Audit-Logging.md), use the `Abp` prefix, while some other modules use their own prefixes. [Identity Server](Modules/IdentityServer.md) module uses the `IdentityServer` prefix for example. + +If you want, you can **change the database table name prefix** for a module for your application. Example: + +````csharp +Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; +```` + +This code changes the prefix of the [Identity Server](Modules/IdentityServer.md) module. Write this code **at the very beginning** in your application. + +> Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage. + +### The Projects + +From the database point of view, there are three important projects those will be explained in the next sections. + +#### .EntityFrameworkCore Project + +This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application. + +**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity: + +````csharp +[ConnectionStringName("Default")] +public class BookStoreDbContext : AbpDbContext +{ + public DbSet Users { get; set; } + + /* Add DbSet properties for your Aggregate Roots / Entities here. */ + + public BookStoreDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Configure the shared tables (with included modules) here */ + + builder.Entity(b => + { + //Sharing the same Users table with the IdentityUser + b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); + + b.ConfigureByConvention(); + b.ConfigureAbpUser(); + + /* Configure mappings for your additional properties + * Also see the MyProjectNameEntityExtensions class + */ + }); + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + builder.ConfigureBookStore(); + } +} +```` + +This simple `DbContext` class still needs some explanations: + +* It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`. +* It inherits from the `AbpDbContext` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext` base class implements some conventions of the ABP Framework to automate some common tasks for you. +* It declares a `DbSet` property for the `AppUser` entity. `AppUser` shares the same table (named `AbpUsers` by default) with the `IdentityUser` entity of the [Identity module](Modules/Identity.md). The startup template provides this entity inside the application since we think that the User entity is generally needs to be customized in your application. +* The constructor takes a `DbContextOptions` instance. +* It overrides the `OnModelCreating` method to define the EF Core mappings. + * It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us. + * It then configures the mapping for the `AppUser` entity. There is a special case for this entity (it shares a table with the Identity module), which will be explained in the next sections. + * It finally calls the `builder.ConfigureBookStore()` extension method to configure other entities of your application. + +This design will be explained in more details after introducing the other database related projects. + +#### .EntityFrameworkCore.DbMigrations Project + +As mentioned in the previous section, every module (and your application) have **their own** separate `DbContext` classes. Each `DbContext` class only defines the entity to table mappings related to its own module and each module (and your application) use the related `DbContext` class **on runtime**. + +As you know, EF Core Code First migration system relies on a `DbContext` class **to track and generate** the code first migrations. So, which `DbContext` we should use for the migrations? The answer is *none of them*. There is another `DbContext` defined in the `.EntityFrameworkCore.DbMigrations` project (which is the `BookStoreMigrationsDbContext` for this example solution). + +##### The MigrationsDbContext + +The `MigrationsDbContext` is only used to create and apply the database migrations. It is **not used on runtime**. It **merges** all the entity to table mappings of all the used modules plus the application's mappings. + +In this way, you create and maintain a **single database migration path**. However, there are some difficulties of this approach and the next sections explains how ABP Framework overcomes these difficulties. But first, see the `BookStoreMigrationsDbContext` class as an example: + +````csharp +/* This DbContext is only used for database migrations. + * It is not used on runtime. See BookStoreDbContext for the runtime DbContext. + * It is a unified model that includes configuration for + * all used modules and your application. + */ +public class BookStoreMigrationsDbContext : AbpDbContext +{ + public BookStoreMigrationsDbContext( + DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentity(); + builder.ConfigureIdentityServer(); + builder.ConfigureFeatureManagement(); + builder.ConfigureTenantManagement(); + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + builder.ConfigureBookStore(); + } +} +```` + +##### Sharing the Mapping Code + +First problem is that: A module uses its own `DbContext` which needs to the database mappings. The `MigrationsDbContext` also needs to the same mapping in order to create the database tables for this module. We definitely **don't want to duplicate** the mapping code. + +The solution is to define an **extension method** (on the `ModelBuilder`) that can be called by both `DbContext` classes. So, all modules define such extension methods. + +For example, the `builder.ConfigureBackgroundJobs()` method call configures the database tables for the [Background Jobs module](Modules/Background-Jobs.md). The definition of this extension method is something like that: + +````csharp +public static class BackgroundJobsDbContextModelCreatingExtensions +{ + public static void ConfigureBackgroundJobs( + this ModelBuilder builder, + Action optionsAction = null) + { + var options = new BackgroundJobsModelBuilderConfigurationOptions( + BackgroundJobsDbProperties.DbTablePrefix, + BackgroundJobsDbProperties.DbSchema + ); + + optionsAction?.Invoke(options); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); + + b.ConfigureCreationTime(); + b.ConfigureExtraProperties(); + + b.Property(x => x.JobName) + .IsRequired() + .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); + + //... + }); + } +} +```` + +This extension method also gets options to change the database table prefix and schema for this module, but it is not important here. + +The final application calls the extension methods inside the `MigrationsDbContext` class, so it can decide which modules are included in the database maintained by this `MigrationsDbContext`. If you want to create a second database and move some module tables to the second database, then you need to have a second `MigrationsDbContext` class which only calls the extension methods of the related modules. This topic will be detailed in the next sections. + +The same `ConfigureBackgroundJobs` method is also called in the `DbContext` of the Background Jobs module: + +````csharp +[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)] +public class BackgroundJobsDbContext + : AbpDbContext, IBackgroundJobsDbContext +{ + public DbSet BackgroundJobs { get; set; } + + public BackgroundJobsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + //Reuse the same extension method! + builder.ConfigureBackgroundJobs(); + } +} +```` + +In this way, the mapping configuration of a module can be shared between `DbContext` classes. The code above is inside the related module NuGet package, so you don't care about it. + +##### Reusing a Table of a Module + +You may want to **reuse a table** of a depended module in your application. In this case, you have two options: + +1. You can **directly use the entity** defined by the module (you can still [extend the entity](Customizing-Application-Modules-Extending-Entities.md) in some level). +2. You can **create a new entity** mapping to the same database table. + +###### Use the Entity Defined by a Module + +Using an entity defined a module is pretty easy and standard. For example, Identity module defines the `IdentityUser` entity. You can inject the [repository](Repositories.md) for the `IdentityUser` and perform the standard repository operations for this entity. Example: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Identity; + +namespace Acme.BookStore +{ + public class MyService : ITransientDependency + { + private readonly IRepository _identityUserRepository; + + public MyService(IRepository identityUserRepository) + { + _identityUserRepository = identityUserRepository; + } + + public async Task DoItAsync() + { + //Get all users + var users = await _identityUserRepository.GetListAsync(); + } + } +} +```` + +This example injects the `IRepository` (default repository) which defines the standard repository methods and implements the `IQueryable` interface. + +> In addition, Identity module defines the `IIdentityUserRepository` (custom repository) that can also be injected and used by your application. `IIdentityUserRepository` provides additional custom methods for the `IdentityUser` entity while it does not implement the `IQueryable` interface. + +###### Create a New Entity + +Working with an entity of a module is easy if you want to use the entity as is. However, you may want to define your own entity class and map to the same database table in the following cases; + +* You want to **add a new field** to the table and map it to a property in the entity. You can't use the module's entity since it doesn't have the related property. +* You want to **use a subset of the table fields**. You don't want to access to all properties of the entity and hide the unrelated properties (from a security perspective or just by design). +* You don't want to directly **depend on** a module entity class. + +In any case, the progress is same. Assume that you want to create an entity, named `AppRole`, mapped to the same table of the `IdentityRole` entity of the [Identity module](Modules/Identity.md). + +Here, we will show the implementation, then **will discuss the limitations** of this approach. + +First, create a new `AppRole` class in your `.Domain` project: + +````csharp +using System; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace Acme.BookStore.Roles +{ + public class AppRole : AggregateRoot, IMultiTenant + { + // Properties shared with the IdentityRole class + + public Guid? TenantId { get; private set; } + public string Name { get; private set; } + + //Additional properties + + public string Title { get; set; } + + private AppRole() + { + + } + } +} +```` + +* It's inherited from [the `AggregateRoot` class](Entities.md) and implements [the `IMultiTenant` interface](Multi-Tenancy.md) because the `IdentityRole` also does the same. +* You can add any properties defined by the `IdentityRole` entity. This examples add only the `TenantId` and `Name` properties since we only need them here. You can make the setters private (like in this example) to prevent changing Identity module's properties accidently. +* You can add custom (additional) properties. This example adds the `Title` property. +* The **constructor is private**, so it is not allowed to directly create a new `AppRole` entity. Creating a role is a responsibility of the Identity module. You can query roles, set/update your custom properties, but you should not create or delete a role in your code, as a best practice (while there is nothing restricts you). + +Now, it is time to define the EF Core mappings. Open the `DbContext` of your application (`BookStoreDbContext` in this sample) and add the following property: + +````csharp +public DbSet Roles { get; set; } +```` + +Then configure the mapping inside the `OnModelCreating` method (after calling the `base.OnModelCreating(builder)`): + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Configure the shared tables (with included modules) here */ + + //CONFIGURE THE AppRole ENTITY + builder.Entity(b => + { + b.ToTable("AbpRoles"); + b.ConfigureByConvention(); + b.Property(x => x.Title).HasMaxLength(128); + }); + + ... + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + + builder.ConfigureBookStore(); +} +```` + +We added the following lines: + +````csharp +builder.Entity(b => +{ + b.ToTable("AbpRoles"); + b.ConfigureByConvention(); + b.Property(x => x.Title).HasMaxLength(128); +}); +```` + +* It maps to the same `AbpRoles` table shared with the `IdentityRole` entity. +* `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it. + +You've configured the custom property for your `DbContext` that is used by your application on the runtime. We also need to configure the `MigrationsDbContext`. + +Instead of directly changing the `MigrationsDbContext`, we should use the entity extension system of the ABP Framework. Find the `YourProjectNameEntityExtensions` class in the `.EntityFrameworkCore` project of your solution (`BookStoreEntityExtensions` for this example) and change it as shown below: + +````csharp +public static class MyProjectNameEntityExtensions +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance + .MapEfCoreProperty( + "Title", + builder => { builder.HasMaxLength(64); } + ); + }); + } +} +```` + +> Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)` or use a constant string. + +`ObjectExtensionManager` is used to add properties to existing entities. Since `ObjectExtensionManager.Instance` is a static instance (singleton), we should call it once. `OneTimeRunner` is a simple utility class defined by the ABP Framework. + +See the [EF Core integration documentation](Entity-Framework-Core.md) for more about the entity extension system. + +> We've repeated a similar database mapping code, like `HasMaxLength(128)`, in both classes. + +Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project): + +![pmc-add-migration-role-title](images/pmc-add-migration-role-title.png) + +This command will create a new code first migration class as shown below: + +````csharp +public partial class Added_Title_To_Roles : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Title", + table: "AbpRoles", + maxLength: 128, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Title", + table: "AbpRoles"); + } +} +```` + +All done! Just run the `Update-Database` command in the PMC or run the `.DbMigrator` project in your solution to apply changes to database. + +Now, you can work with the `AppRole` entity just like any other entity of your application. An example [application service](Application-Services.md) that queries and updates roles: + +````csharp +public class AppRoleAppService : ApplicationService, IAppRoleAppService +{ + private readonly IRepository _appRoleRepository; + + public AppRoleAppService(IRepository appRoleRepository) + { + _appRoleRepository = appRoleRepository; + } + + public async Task> GetListAsync() + { + var roles = await _appRoleRepository.GetListAsync(); + + return roles + .Select(r => new AppRoleDto + { + Id = r.Id, + Name = r.Name, + Title = r.Title + }) + .ToList(); + } + + public async Task UpdateTitleAsync(Guid id, string title) + { + var role = await _appRoleRepository.GetAsync(id); + + role.Title = title; + + await _appRoleRepository.UpdateAsync(role); + } +} +```` + +There are some **limitations** of creating a new entity and mapping it to a table of a depended module: + +* Your **custom properties must be nullable**. For example, `AppRole.Title` was nullable here. Otherwise, Identity module throws exception because it doesn't know and can not fill the Title when it inserts a new role to the database. +* As a good practice, you should not update the **properties defined by the module**, especially if it requires a business logic. You typically want to manage your own properties. + +##### Alternative Approaches + +Instead of creating a new entity class to add a custom property, you can use the following approaches. + +###### Using the ExtraProperties + +All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property (because they implement the `IHasExtraProperties` interface), which is a `Dictionary` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity. + +For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example: + +````csharp +public class IdentityRoleExtendingService : ITransientDependency +{ + private readonly IIdentityRoleRepository _identityRoleRepository; + + public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) + { + _identityRoleRepository = identityRoleRepository; + } + + public async Task GetTitleAsync(Guid id) + { + var role = await _identityRoleRepository.GetAsync(id); + return role.GetProperty("Title"); + } + + public async Task SetTitleAsync(Guid id, string newTitle) + { + var role = await _identityRoleRepository.GetAsync(id); + role.SetProperty("Title", newTitle); + await _identityRoleRepository.UpdateAsync(role); + } +} +```` + +* `GetProperty` and `SetProperty` methods are shortcuts to get and set a value in the `role.ExtraProperties` dictionary and they are the recommended way to work with the extra properties. + +In this way, you can easily attach any type of value to an entity of a depended module. However, there are some drawbacks of this usage: + +* All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields. +* Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors. + +###### Using the Entity Extensions System + +Entity extension system solves the main problem of the extra properties: It can store an extra property in a **standard table field** in the database. + +All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database. + +###### Creating a New Table + +Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table. + +In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here). + +> See the "[extending entities](Customizing-Application-Modules-Extending-Entities.md)" guide for more details on extending entities, including data duplication and synchronization tips. + +#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path + +As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path. + +An alternative approach would be to allow each module to have its own migrations to maintain its database tables. While it seems more module in the beginning, it has some important drawbacks: + +* **EF Core migration system depends on the DBMS provider**. For example, if a module has created migrations for SQL Server, then you can not use this migration code for MySQL. It is not practical for a module to maintain migrations for all available DBMS providers. Leaving the migration to the application code (as explained in this document) allows you to **choose the DBMS in the application** code. +* It would be harder or impossible to **share a table** between modules or **re-use a table** of a module in your application. Because EF Core migration system can not handle it and will throw exceptions like "Table XXX is already exists in the database". +* It would be harder to **customize/enhance** the mapping and the resulting migration code. +* It would be harder to track and **apply changes** to database when you use multiple modules. + +## Using Multiple Databases + +The default startup template is organized to use a single database used by all the modules and by your application. However, the ABP Framework and all the pre-built modules are designed so that **they can use multiple databases**. Each module can use its own database or you can group modules into a few databases. + +This section will explain how to move Audit Logging, Setting Management and Permission Management module tables to a **second database** while the remaining modules continue to use the main ("Default") database. + +The resulting structure will be like the figure below: + +![single-database-usage](images/multiple-database-usage.png) + +### Change the Connection Strings Section + +First step is to change the connection string section inside all the `appsettings.json` files. Initially, it is like that: + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" +} +```` + +Change it as shown below: + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", + "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", + "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", + "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" +} +```` + +Added **three more connection strings** for the related module to target the `BookStore_SecondDb` database (they are all same). For example, `AbpPermissionManagement` is the connection string for the permission management module. + +The `AbpPermissionManagement` is a constant [defined](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11) by the permission management module. ABP Framework [connection string selection system](Connection-Strings.md) selects this connection string for the permission management module if you define. If you don't define, it fallbacks to the `Default` connection string. + +### Create a Second Migration Project + +Defining the connection strings as explained above is enough **on runtime**. However, `BookStore_SecondDb` database doesn't exist yet. You need to create the database and the tables for the related modules. + +Just like the main database, we want to use the EF Core Code First migration system to create and maintain the second database. + +An easy way is to create a second project (`.csproj`) for the second migration `DbContext`. + +So, create a new **class library project** in your solution named `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` (or name it better if you didn't like it). + +The `.csproj` content should be something like that: + +````xml + + + + + + netcoreapp3.1 + Acme.BookStore.DbMigrationsForSecondDb + + + + + + + + + + + +```` + +You can just copy & modify the content of the original `.DbMigrations` project. This project references to the `.EntityFrameworkCore` project. **Only difference** is the `RootNamespace` value. + +**Add a reference** to this project from the `.Web` project (otherwise, EF Core tooling doesn't allow to use the `Add-Migration` command). + +### Create the Second DbMigrationDbContext + +Create a new `DbContext` for the migrations and call the **extension methods** of the modules to configure the database tables for the related modules: + +````csharp +[ConnectionStringName("AbpPermissionManagement")] +public class BookStoreSecondMigrationsDbContext : + AbpDbContext +{ + public BookStoreSecondMigrationsDbContext( + DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureAuditLogging(); + } +} +```` + +> `[ConnectionStringName(...)]` attribute is important here and tells to the ABP Framework which connection string should be used for this `DbContext`. We've used `AbpPermissionManagement`, but all are the same. + +Create a **Design Time Db Factory** class, that is used by the EF Core tooling (by `Add-Migration` and `Update-Database` PCM commands for example): + +````csharp +/* This class is needed for EF Core console commands + * (like Add-Migration and Update-Database commands) */ +public class BookStoreSecondMigrationsDbContextFactory + : IDesignTimeDbContextFactory +{ + public BookStoreSecondMigrationsDbContext CreateDbContext(string[] args) + { + var configuration = BuildConfiguration(); + + var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement")); + + return new BookStoreSecondMigrationsDbContext(builder.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} +```` + +This is similar to the class inside the `.EntityFrameworCore.DbMigrations` project, except this one uses the `AbpPermissionManagement` connection string. + +Now, you can open the Package Manager Console, select the `.EntityFrameworkCore.DbMigrationsForSecondDb` project as the default project (make sure the `.Web` project is still the startup project) and run the `Add-Migration "Initial"` and `Update-Database` commands as shown below: + +![pmc-add-migration-initial-update-database](images/pmc-add-migration-initial-update-database.png) + +Now, you should have a new database contains only the tables needed by the related modules: + +![bookstore-second-database](images/bookstore-second-database.png) + +### Remove Modules from the Main Database + +We've **created a second database** contains tables for the Audit Logging, Permission Management and Setting Management modules. So, we should **delete these tables from the main database**. It is pretty easy. + +First, remove the following lines from the `MigrationsDbContext` class (`BookStoreMigrationsDbContext` for this example): + +````csharp +builder.ConfigurePermissionManagement(); +builder.ConfigureSettingManagement(); +builder.ConfigureAuditLogging(); +```` + +Open the Package Manager Console, select the `.EntityFrameworkCore.DbMigrations` as the Default project (make sure that the `.Web` project is still the startup project) and run the following command: + +```` +Add-Migration "Removed_Audit_Setting_Permission_Modules" +```` + +This command will create a new migration class as shown below: + +````csharp +public partial class Removed_Audit_Setting_Permission_Modules : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpAuditLogActions"); + + migrationBuilder.DropTable( + name: "AbpEntityPropertyChanges"); + + migrationBuilder.DropTable( + name: "AbpPermissionGrants"); + + migrationBuilder.DropTable( + name: "AbpSettings"); + + migrationBuilder.DropTable( + name: "AbpEntityChanges"); + + migrationBuilder.DropTable( + name: "AbpAuditLogs"); + } + + ... +} +```` + +Be careful in this step: + +* If you have a **live system**, then you should care about the **data loss**. You need to move the table contents to the second database before deleting the tables. +* If you **haven't started** your project yet, you can consider to **remove all the migrations** and re-create the initial one to have a cleaner migration history. + +Run the `Update-Database` command to delete the tables from your main database. + +Notice that you've also **deleted some initial seed data** (for example, permission grants for the admin role) if you haven't copied it to the new database. If you run the application, you may not login anymore. The solution is simple: **Re-run the `.DbMigrator` console application** in your solution, it will seed the new database. + +### Automate the Second Database Schema Migration + +`.DbMigrator` console application can run the database seed code across multiple databases, without any additional configuration. However, it can not run the EF Core Code First Migrations inside the second database migration project. Now, you will see how to configure the console migration application to handle both databases. + +#### Implementing the IBookStoreDbSchemaMigrator + +`EntityFrameworkCoreBookStoreDbSchemaMigrator` class inside the `Acme.BookStore.EntityFrameworkCore.DbMigrations` project is responsible to migrate the database schema for the `BookStoreMigrationsDbContext`. It should be like that: + +````csharp +[Dependency(ReplaceServices = true)] +public class EntityFrameworkCoreBookStoreDbSchemaMigrator + : IBookStoreDbSchemaMigrator, ITransientDependency +{ + private readonly IServiceProvider _serviceProvider; + + public EntityFrameworkCoreBookStoreDbSchemaMigrator( + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task MigrateAsync() + { + /* We are intentionally resolving the BookStoreMigrationsDbContext + * from IServiceProvider (instead of directly injecting it) + * to properly get the connection string of the current tenant in the + * current scope. + */ + + await _serviceProvider + .GetRequiredService() + .Database + .MigrateAsync(); + } +} +```` + +It implements the `IBookStoreDbSchemaMigrator` and **replaces existing services** (see the first line). + +Remove the `[Dependency(ReplaceServices = true)]` line, because we will have two implementations of this interface and we want to use both. We don't want to replace one of them. + +Create a copy of this class inside the new migration project (`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`), but use the `BookStoreSecondMigrationsDbContext`. Example implementation: + +````csharp +public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator + : IBookStoreDbSchemaMigrator, ITransientDependency +{ + private readonly IServiceProvider _serviceProvider; + + public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator( + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task MigrateAsync() + { + /* We are intentionally resolving the BookStoreSecondMigrationsDbContext + * from IServiceProvider (instead of directly injecting it) + * to properly get the connection string of the current tenant in the + * current scope. + */ + + await _serviceProvider + .GetRequiredService() + .Database + .MigrateAsync(); + } +} +```` + +> Name of this class is important for [dependency injection](Dependency-Injection.md). It should end with `BookStoreDbSchemaMigrator` to be injectable by `IBookStoreDbSchemaMigrator` reference. + +We, now, have two implementations of the `IBookStoreDbSchemaMigrator` interface, each one responsible to migrate the related database schema. + +#### Define a Module Class for the Second Migration Project + +It is time to define the [module](Module-Development-Basics.md) class for this second migrations (`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`) project: + +````csharp +[DependsOn( + typeof(BookStoreEntityFrameworkCoreModule) + )] +public class BookStoreEntityFrameworkCoreSecondDbMigrationsModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(); + } +} +```` + +Now, reference `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` project from the `Acme.BookStore.DbMigrator` project and `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` to the dependency list of the `BookStoreDbMigratorModule`. `BookStoreDbMigratorModule` class should be something like that: + +````csharp +[DependsOn( + typeof(AbpAutofacModule), + typeof(BookStoreEntityFrameworkCoreDbMigrationsModule), + typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule), // ADDED THIS! + typeof(BookStoreApplicationContractsModule) + )] +public class BookStoreDbMigratorModule : AbpModule +{ + ... +} +```` + +We had a reference to the `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` project from the `Acme.BookStore.Web` project, but hadn't added module dependency since we hadn't created it before. But, now we have it and we need to add `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` to the dependency list of the `BookStoreWebModule` class. + +#### Run the Database Migrator! + +You can run the `.DbMigrator` application to migrate & seed the databases. To test, you can delete both databases and run the `.DbMigrator` application again to see if it creates both of the databases. + +## Conclusion + +This document explains how to split your databases and manage your database migrations of your solution for Entity Framework Core. In brief, you need to have a separate migration project per different databases. + +## Source Code + +You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. \ No newline at end of file diff --git a/docs/en/Entity-Framework-Core-Other-DBMS.md b/docs/en/Entity-Framework-Core-Other-DBMS.md index 854309d070..d902a8d93e 100644 --- a/docs/en/Entity-Framework-Core-Other-DBMS.md +++ b/docs/en/Entity-Framework-Core-Other-DBMS.md @@ -1,12 +1,16 @@ -# Switch to another DBMS for Entity Framework Core +# Switch to Another DBMS for Entity Framework Core -**[The application startup template](Startup-Templates/Application.md)** comes with SQL Server provider pre-configured for the Entity Framework Core. Entity Framework Core supports [many other DBMSs](https://docs.microsoft.com/en-us/ef/core/providers/) and you can use any of them with your ABP based applications. +**[The application startup template](Startup-Templates/Application.md)** comes with **SQL Server provider pre-configured** for the Entity Framework Core. However, EF Core supports [many other DBMSs](https://docs.microsoft.com/en-us/ef/core/providers/) and you can use any of them within your ABP based applications. -ABP framework provides integration packages for some common DBMSs to make the configuration a bit easier (see the [entity framework core document](Entity-Framework-Core.md) for a list of available integration packages). However, you can configure your DBMS provider without these integration packages. +ABP framework provides **integration packages** for some common DBMSs to make the configuration a bit easier. You can use the following documents to learn how to **switch to your favorite DBMS**: -While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it manually if there is no integration package for your DBMS provider. +* [MySQL](Entity-Framework-Core-MySQL.md) +* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md) +* [SQLite](Entity-Framework-Core-SQLite.md) -This document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md). +However, you can configure your DBMS provider **without** these integration packages. While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it manually if there is no integration package for your DBMS provider. + +For an example, this document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md). ## Replace the SQL Server Dependency @@ -87,4 +91,18 @@ 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. + +## DBMS restrictions + +Different DBMS may have some restrictions, such as the maximum length of field names, index length, etc. +The module may provide some built-in solutions. You can configure it via `ModelBuilder`. eg: `Identity Server` module. + +```csharp +builder.ConfigureIdentityServer(options => +{ + options.DatabaseProvider = EfCoreDatabaseProvider.MySql; +}); +``` + +Related discussions: https://github.com/abpframework/abp/issues/1920 \ No newline at end of file diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 586c8d4a01..66f9d3c2f9 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -32,14 +32,9 @@ namespace MyCompany.MyProject Entity Framework Core supports various database management systems ([see all](https://docs.microsoft.com/en-us/ef/core/providers/)). ABP framework and this document doesn't depend on any specific DBMS. -If you are creating a reusable library, avoid to depend on a specific DBMS package. However, in a final application you eventually will select a DBMS. +If you are creating a [reusable application module](Modules/Index.md), avoid to depend on a specific DBMS package. However, in a final application you eventually will select a DBMS. -ABP framework provides integration packages for some common DBMSs to make the configuration a bit easier. [The startup templates](Startup-Templates/Index.md) come with **SQL Server (localdb) pre-configured**. See the following documents to learn how to configure for the other DBMS providers: - -* [MySQL](Entity-Framework-Core-MySQL.md) -* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md) -* [SQLite](Entity-Framework-Core-SQLite.md) -* [Others](Entity-Framework-Core-Other-DBMS.md) +See [Switch to Another DBMS for Entity Framework Core](Entity-Framework-Core-Other-DBMS.md) document to learn how to switch the DBMS. ## Creating DbContext @@ -63,6 +58,53 @@ namespace MyCompany.MyProject } ```` +### About the EF Core Fluent Mapping + +The [application startup template](Startup-Templates/Application.md) has been configured to use the [EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/) to map your entities to your database tables. + +You can still use the **data annotation attributes** (like `[Required]`) on the properties of your entity while the ABP documentation generally follows the **fluent mapping API** approach. It is up to you. + +ABP Framework has some **base entity classes** and **conventions** (see the [entities document](Entities.md)) and it provides some useful **extension methods** to configure the properties inherited from the base entity classes. + +#### ConfigureByConvention Method + +`ConfigureByConvention()` is the main extension method that **configures all the base properties** and conventions for your entities. So, it is a **best practice** to call this method for all your entities, in your fluent mapping code. + +**Example**: Assume that you've a `Book` entity derived from `AggregateRoot` base class: + +````csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } +} +```` + +You can override the `OnModelCreating` method in your `DbContext` and configure the mapping as shown below: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + //Always call the base method + base.OnModelCreating(builder); + + builder.Entity(b => + { + b.ToTable("Books"); + + //Configure the base properties + b.ConfigureByConvention(); + + //Configure other properties (if you are using the fluent API) + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + }); +} +```` + +* Calling `b.ConfigureByConvention()` is important here to properly **configure the base properties**. +* You can configure the `Name` property here or you can use the **data annotation attributes** (see the [EF Core document](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)). + +> While there are many extension methods to configure your base properties, `ConfigureByConvention()` internally calls them if necessary. So, it is enough to call it. + ### Configure the Connection String Selection If you have multiple databases in your application, you can configure the connection string name for your DbContext using the `[ConnectionStringName]` attribute. Example: @@ -230,7 +272,7 @@ public override async Task DeleteAsync( } ```` -### Access to the EF Core API +## Access to the EF Core API In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: @@ -256,9 +298,60 @@ public class BookService > Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case. -### Advanced Topics +## Extra Properties & Object Extension Manager + +Extra Properties system allows you to set/get dynamic properties to entities those implement the `IHasExtraProperties` interface. It is especially useful when you want to add custom properties to the entities defined in an [application module](Modules/Index.md), when you use the module as package reference. + +By default, all the extra properties of an entity are stored as a single `JSON` object in the database. -#### Set Default Repository Classes +Entity extension system allows you to to store desired extra properties in separate fields in the related database table. For more information about the extra properties & the entity extension system, see the following documents: + +* [Customizing the Application Modules: Extending Entities](Customizing-Application-Modules-Extending-Entities.md) +* [Entities](Entities.md) + +This section only explains the EF Core related usage of the `ObjectExtensionManager`. + +### ObjectExtensionManager.Instance + +`ObjectExtensionManager` implements the singleton pattern, so you need to use the static `ObjectExtensionManager.Instance` to perform all the operations. + +### MapEfCoreProperty + +`MapEfCoreProperty` is a shortcut extension method to define an extension property for an entity and map to the database. + +**Example**: Add `Title` property (database field) to the `IdentityRole` entity: + +````csharp +ObjectExtensionManager.Instance + .MapEfCoreProperty( + "Title", + builder => { builder.HasMaxLength(64); } + ); +```` + +If the related module has implemented this feature (by using the `ConfigureEfCoreEntity` explained below), then the new property is added to the model. Then you need to run the standard `Add-Migration` and `Update-Database` commands to update your database to add the new field. + +>`MapEfCoreProperty` method must be called before using the related `DbContext`. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a `YourProjectNameEntityExtensions` class that is safe to use this method inside. + +### ConfigureEfCoreEntity + +If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the `ConfigureEfCoreEntity` extension method in your entity mapping. However, there is a shortcut extension method `ConfigureObjectExtensions` that can be used while configuring the entity mapping: + +````csharp +builder.Entity(b => +{ + b.ConfigureObjectExtensions(); + //... +}); +```` + +> If you call `ConfigureByConvention()` extension method (like `b.ConfigureByConvention()` for this example), ABP Framework internally calls the `ConfigureObjectExtensions` method. It is a **best practice** to use the `ConfigureByConvention()` method since it also configures database mapping for base properties by convention. + +See the "*ConfigureByConvention Method*" section above for more information. + +## Advanced Topics + +### Set Default Repository Classes Default generic repositories are implemented by `EfCoreRepository` class by default. You can create your own implementation and use it for all the default repository implementations. @@ -304,7 +397,7 @@ context.Services.AddAbpDbContext(options => }); ``` -#### Set Base DbContext Class or Interface for Default Repositories +### Set Base DbContext Class or Interface for Default Repositories If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example: @@ -336,7 +429,7 @@ public class BookRepository : EfCoreRepository, One advantage of using an interface for a DbContext is then it will be replaceable by another implementation. -#### Replace Other DbContextes +### Replace Other DbContextes Once you properly define and use an interface for DbContext, then any other implementation can replace it using the `ReplaceDbContext` option: @@ -349,3 +442,7 @@ context.Services.AddAbpDbContext(options => ```` In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. + +## See Also + +* [Entities](Entities.md) \ No newline at end of file diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md index 93bad2b6ed..6dc4ccbd9f 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -1,4 +1,4 @@ -## Exception Handling +# Exception Handling ABP provides a built-in infrastructure and offers a standard model for handling exceptions in a web application. @@ -7,7 +7,7 @@ ABP provides a built-in infrastructure and offers a standard model for handling * Provides a configurable way to **localize** exception messages. * Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map these to custom exceptions. -### Automatic Exception Handling +## Automatic Exception Handling `AbpExceptionFilter` handles an exception if **any of the following conditions** are met: @@ -17,7 +17,7 @@ ABP provides a built-in infrastructure and offers a standard model for handling If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client. -#### Error Message Format +### Error Message Format Error Message is an instance of the `RemoteServiceErrorResponse` class. The simplest error JSON has a **message** property as shown below: @@ -83,11 +83,11 @@ Error **details** in an optional field of the JSON error message. Thrown `Except `AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have higly customised validation logic. -#### Logging +### Logging Caught exceptions are automatically logged. -##### Log Level +#### Log Level Exceptions are logged with the `Error` level by default. The Log level can be determined by the exception if it implements the `IHasLogLevel` interface. Example: @@ -100,7 +100,7 @@ public class MyException : Exception, IHasLogLevel } ```` -##### Self Logging Exceptions +#### Self Logging Exceptions Some exception types may need to write additional logs. They can implement the `IExceptionWithSelfLogging` if needed. Example: @@ -116,7 +116,7 @@ public class MyException : Exception, IExceptionWithSelfLogging > `ILogger.LogException` extension methods is used to write exception logs. You can use the same extension method when needed. -### Business Exceptions +## Business Exceptions Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception. @@ -145,11 +145,11 @@ Volo.Qa:010002 * You can **directly throw** a `BusinessException` or **derive** your own exception types from it when needed. * All properties are optional for the `BusinessException` class. But you generally set either `ErrorCode` or `Message` property. -### Exception Localization +## Exception Localization One problem with throwing exceptions is how to localize error messages while sending it to the client. ABP offers two models and their variants. -#### User Friendly Exception +### User Friendly Exception If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client. @@ -192,7 +192,7 @@ Then the localization text can be: * The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class. -#### Using Error Codes +### Using Error Codes `UserFriendlyException` is fine, but it has a few problems in advanced usages: @@ -204,7 +204,7 @@ Instead of localizing the message while throwing the exception, you can separate First, define the **code-namespace** to **localization resource** mapping in the module configuration: ````C# -services.Configure(options => +services.Configure(options => { options.MapCodeNamespace("Volo.Qa", typeof(QaResource)); }); @@ -230,7 +230,7 @@ throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer); * Throwing any exception implementing the `IHasErrorCode` interface behaves the same. So, the error code localization approach is not unique to the `BusinessException` class. * Defining localized string is not required for an error message. If it's not defined, ABP sends the default error message to the client. It does not use the `Message` property of the exception! if you want that, use the `UserFriendlyException` (or use an exception type that implements the `IUserFriendlyException` interface). -##### Using Message Parameters +#### Using Message Parameters If you have a parameterized error message, then you can set it with the exception's `Data` property. For example: @@ -265,7 +265,7 @@ Then the localized text can contain the `UserName` parameter: * `WithData` can be chained with more than one parameter (like `.WithData(...).WithData(...)`). -### HTTP Status Code Mapping +## HTTP Status Code Mapping ABP tries to automatically determine the most suitable HTTP status code for common exception types by following these rules: @@ -280,18 +280,38 @@ ABP tries to automatically determine the most suitable HTTP status code for comm The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP status code. The default implementation is the `DefaultHttpExceptionStatusCodeFinder` class. It can be replaced or extended as needed. -#### Custom Mappings +### Custom Mappings Automatic HTTP status code determination can be overrided by custom mappings. For example: ````C# -services.Configure(options => +services.Configure(options => { options.Map("Volo.Qa:010002", HttpStatusCode.Conflict); }); ```` -### Built-In Exceptions +## Subscribing to the Exceptions + +It is possible to be informed when the ABP Framework **handles an exception**. It automatically **logs** all the exceptions to the standard [logger](Logging.md), but you may want to do more. + +In this case, create a class derived from the `ExceptionSubscriber` class in your application: + +````csharp +public class MyExceptionSubscriber : ExceptionSubscriber +{ + public override async Task HandleAsync(ExceptionNotificationContext context) + { + //TODO... + } +} +```` + +The `context` object contains necessary information about the exception occurred. + +> You can have multiple subscribers, each gets a copy of the exception. Exceptions thrown by your subscriber is ignored (but still logged). + +## Built-In Exceptions Some exception types are automatically thrown by the framework: diff --git a/docs/en/Getting-Started-Angular-Template.md b/docs/en/Getting-Started-Angular-Template.md index d6d3b84be8..22d672d97a 100644 --- a/docs/en/Getting-Started-Angular-Template.md +++ b/docs/en/Getting-Started-Angular-Template.md @@ -1,6 +1,6 @@ ## Getting Started With the Angular Application Template -This tutorial explain how to create a new Angular application using the startup template, configure and run it. +This tutorial explains how to create a new Angular application using the startup template, configure and run it. ### Creating a New Project @@ -26,10 +26,10 @@ abp new Acme.BookStore -u angular The created solution requires; -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### The Solution Structure @@ -103,7 +103,7 @@ Most of the application APIs require authentication & authorization. If you want #### Run 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 in most cases): +Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest the [yarn](https://yarnpkg.com) package manager while `npm install` will also work in most cases) ````bash yarn @@ -123,4 +123,4 @@ The startup template includes the **identity management** and **tenant managemen ### What's Next? -* [Application development tutorial](Tutorials/Angular/Part-I.md) +* [Application development tutorial](Tutorials/Part-1) diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index 4abcd039c9..b78ce1b9cf 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -4,7 +4,7 @@ This tutorial explains how to start ABP from scratch with minimal dependencies. ## Create A New Project -1. Create a new AspNet Core Web Application from Visual Studio 2019 (16.3.0+): +1. Create a new AspNet Core Web Application from Visual Studio 2019 (16.4.0+): ![](images/create-new-aspnet-core-application-v2.png) @@ -153,5 +153,5 @@ namespace BasicAspNetCoreApplication ## Source Code -Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp/tree/master/samples/BasicAspNetCoreApplication). +Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication). diff --git a/docs/en/Getting-Started-AspNetCore-MVC-Template.md b/docs/en/Getting-Started-AspNetCore-MVC-Template.md index 563ce0182d..d074e8aaef 100644 --- a/docs/en/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/en/Getting-Started-AspNetCore-MVC-Template.md @@ -26,10 +26,10 @@ abp new Acme.BookStore The created solution requires; -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### The Solution Structure @@ -101,4 +101,4 @@ The startup template includes the **identity management** and **tenant managemen ### What's Next? -* [Application development tutorial](Tutorials/AspNetCore-Mvc/Part-I.md) +* [Application development tutorial](Tutorials/Part-1.md) diff --git a/docs/en/Getting-Started-Console-Application.md b/docs/en/Getting-Started-Console-Application.md index faaa8cd376..d5f3c8519b 100644 --- a/docs/en/Getting-Started-Console-Application.md +++ b/docs/en/Getting-Started-Console-Application.md @@ -178,4 +178,4 @@ Just called `options.UseAutofac()` method in the `AbpApplicationFactory.Create` ## Source Code -Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp/tree/master/samples/BasicConsoleApplication). +Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication). diff --git a/docs/en/Getting-Started-With-Startup-Templates.md b/docs/en/Getting-Started-With-Startup-Templates.md new file mode 100644 index 0000000000..19442ec81e --- /dev/null +++ b/docs/en/Getting-Started-With-Startup-Templates.md @@ -0,0 +1,6 @@ +# Getting Started with the Startup Templates + +See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: + +* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started-AspNetCore-MVC-Template.md) +* [Getting Started with the Angular UI](Getting-Started-Angular-Template.md) \ No newline at end of file diff --git a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md new file mode 100644 index 0000000000..10b1e05a4a --- /dev/null +++ b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -0,0 +1,201 @@ +# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications + +This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. + +Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. + +Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. + +1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. + +2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. + +> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. +> +> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below. + +## 1. AddAzureAD + +This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/). + +If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project. + +#### **Updating `appsettings.json`** + +You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`: + +````json + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Domain": "domain.onmicrosoft.com", + "CallbackPath": "/signin-azuread-oidc" + } +```` + +> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri. + +Then, you need to configure the `OpenIdConnectOptions` to complete the integration. + +#### Configuring OpenIdConnectOptions + +In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: + +````csharp +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "Acme.BookStore"; + }) + .AddAzureAD(options => configuration.Bind("AzureAd", options)); + + context.Services.Configure(AzureADDefaults.OpenIdScheme, options => + { + options.Authority = options.Authority + "/v2.0/"; + options.ClientId = configuration["AzureAd:ClientId"]; + options.CallbackPath = configuration["AzureAd:CallbackPath"]; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.RequireHttpsMetadata = false; + + options.TokenValidationParameters.ValidateIssuer = false; + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + options.SignInScheme = IdentityConstants.ExternalScheme; + + options.Scope.Add("email"); + }); + } +```` + +> **Don't forget to:** +> +> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out. +> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. +> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. +> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). +> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). + +You are done and integration is completed. + +## 2. Alternative Approach: AddOpenIdConnect + +If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication. + +You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`. + +To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project: + +````json + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Domain": "domain.onmicrosoft.com", + "CallbackPath": "/signin-azuread-oidc" + } +```` + +Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following: + +````csharp +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "BookStore"; + }) + .AddOpenIdConnect("AzureOpenId", "Azure Active Directory 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.RequireHttpsMetadata = false; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + + options.Scope.Add("email"); + }); + } +```` + +And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. + +## The Source Code + +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). + +# FAQ + +* Help! `GetExternalLoginInfoAsync` returns `null`! + + * There can be 2 reasons for this; + + 1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: + + ````csharp + options.SignInScheme = IdentityConstants.ExternalScheme; + ```` + + 2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: + + ````csharp + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + ```` + + +* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! + + * If you set your **CallbackPath** in appsettings as: + + ````csharp + "AzureAd": { + ... + "CallbackPath": "/signin-azuread-oidc" + } + ```` + + your **Redirect URI** of your application in azure portal must be with domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. + +* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! + + + * This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add + + ````csharp + options.Scope.Add("email"); + ```` + + to your openid configuration. + +* How can I **debug/watch** which claims I get before they get mapped? + + * You can add a simple event under openid configuration to debug before mapping like: + + ````csharp + options.Events.OnTokenValidated = (async context => + { + var claimsFromOidcProvider = context.Principal.Claims.ToList(); + await Task.CompletedTask; + }); + ```` + + +## See Also + +* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). +* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md). \ No newline at end of file diff --git a/docs/en/How-To/Customize-Login-Page-MVC.md b/docs/en/How-To/Customize-Login-Page-MVC.md new file mode 100644 index 0000000000..5678828bfb --- /dev/null +++ b/docs/en/How-To/Customize-Login-Page-MVC.md @@ -0,0 +1,113 @@ +# How to Customize the Login Page for MVC / Razor Page Applications + +When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. + +This document explains how to customize the login page for your own application. + +## Create a Login PageModel + +Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. + +````csharp +public class CustomLoginModel : LoginModel +{ + public CustomLoginModel( + Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, + Microsoft.Extensions.Options.IOptions accountOptions) + : base(schemeProvider, accountOptions) + { + } +} +```` + +> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. + +Then you can override any method you need and add new methods and properties needed by the UI. + +## Overriding the Login Page UI + +Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md). + +A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: + +````xml +@page +@using Volo.Abp.Account.Settings +@using Volo.Abp.Settings +@model Acme.BookStore.Web.Pages.Account.CustomLoginModel +@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage +@inject Volo.Abp.Settings.ISettingProvider SettingProvider +@if (Model.EnableLocalLogin) +{ +
+
+

@L["Login"]

+ @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) + { + + @L["AreYouANewUser"] + @L["Register"] + + } +
+ + +
+ + + +
+
+ + + +
+
+ +
+ @L["Login"] +
+
+ + +
+} + +@if (Model.VisibleExternalProviders.Any()) +{ +
+

@L["UseAnotherServiceToLogIn"]

+
+ + + @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
+
+} + +@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) +{ +
+ @L["InvalidLoginRequest"] + @L["ThereAreNoLoginSchemesConfiguredForThisClient"] +
+} +```` + +Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. + +## The Source Code + +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). + +## See Also + +* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md). \ No newline at end of file diff --git a/docs/en/How-To/Customize-SignIn-Manager.md b/docs/en/How-To/Customize-SignIn-Manager.md new file mode 100644 index 0000000000..61b6c3b7b5 --- /dev/null +++ b/docs/en/How-To/Customize-SignIn-Manager.md @@ -0,0 +1,101 @@ +# How to Customize the SignIn Manager for ABP Applications + +After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). + +To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. + +This document explains how to customize the SignIn Manager for your own application. + +## Create a CustomSignInManager + +Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package. + +````csharp +public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager +{ + public CustomSignInManager( + Microsoft.AspNetCore.Identity.UserManager userManager, + Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, + Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory claimsFactory, + Microsoft.Extensions.Options.IOptions optionsAccessor, + Microsoft.Extensions.Logging.ILogger> logger, + Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, + Microsoft.AspNetCore.Identity.IUserConfirmation confirmation) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } +} +```` + +> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. + +Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. + +## Overriding the GetExternalLoginInfoAsync Method + +In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented. + +A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. + +````csharp +public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) +{ + var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); + var items = auth?.Properties?.Items; + if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) + { + return null; + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey("XsrfKey")) + { + return null; + } + var userId = items[XsrfKey] as string; + if (userId != expectedXsrf) + { + return null; + } + } + + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + var provider = items[LoginProviderKey] as string; + if (providerKey == null || provider == null) + { + return null; + } + + var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName + ?? provider; + return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties.GetTokens() + }; +} +```` + +To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md). + +## Register to Dependency Injection + +Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs). + +Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one: + +````csharp +PreConfigure(identityBuilder => +{ + identityBuilder.AddSignInManager(); +}); +```` + +## The Source Code + +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). + +## See Also + +* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). +* [Identity Management Module](../Modules/Identity.md). \ No newline at end of file diff --git a/docs/en/How-To/Index.md b/docs/en/How-To/Index.md new file mode 100644 index 0000000000..da209d4bcb --- /dev/null +++ b/docs/en/How-To/Index.md @@ -0,0 +1,9 @@ +# "How To" Guides + +This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. + +## Authentication + +* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) +* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) +* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) \ No newline at end of file diff --git a/docs/en/IdentityServer-Integration.md b/docs/en/IdentityServer-Integration.md new file mode 100644 index 0000000000..da82d4bc6a --- /dev/null +++ b/docs/en/IdentityServer-Integration.md @@ -0,0 +1,3 @@ +# IdentityServer Integration + +TODO \ No newline at end of file diff --git a/docs/en/Localization.md b/docs/en/Localization.md index a9608482cb..11b838cf7f 100644 --- a/docs/en/Localization.md +++ b/docs/en/Localization.md @@ -46,7 +46,7 @@ public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { // "YourRootNameSpace" is the root namespace of your project. It can be empty if your root namespace is empty. options.FileSets.AddEmbedded("YourRootNameSpace"); @@ -67,7 +67,7 @@ In this example; * Added a new localization resource with "en" (English) as the default culture. * Used JSON files to store the localization strings. -* JSON files are embedded into the assembly using `VirtualFileSystemOptions` (see [virtual file system](Virtual-File-System.md)). +* JSON files are embedded into the assembly using `AbpVirtualFileSystemOptions` (see [virtual file system](Virtual-File-System.md)). JSON files are located under "/Localization/Resources/Test" project folder as shown below: @@ -190,4 +190,8 @@ Localize a string: ````js var str = testResource('HelloWorld'); -```` \ No newline at end of file +```` + +## See Also + +* [Localization in Angular UI](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/en/Microservice-Architecture.md b/docs/en/Microservice-Architecture.md index ff85873ee7..64a47784bb 100644 --- a/docs/en/Microservice-Architecture.md +++ b/docs/en/Microservice-Architecture.md @@ -12,8 +12,8 @@ One of the major goals of the ABP framework is to provide a convenient infrastru * Offers an [architectural model](Best-Practices/Module-Architecture.md) to develop your modules to be compatible to microservice development and deployment. * Provides [best practices guide](Best-Practices/Index.md) to develop your module standards-compliance. * Provides base infrastructure to implement [Domain Driven Design](Domain-Driven-Design.md) in your microservices. -* Provide services to [automatically create REST-style APIs](AspNetCore/Auto-API-Controllers.md) from your application services. -* Provide services to [automatically create C# API clients](AspNetCore/Dynamic-CSharp-API-Clients.md) that makes easy to consume your services from another service/application. +* Provide services to [automatically create REST-style APIs](API/Auto-API-Controllers.md) from your application services. +* Provide services to [automatically create C# API clients](API/Dynamic-CSharp-API-Clients.md) that makes easy to consume your services from another service/application. * Provides a [distributed event bus](Event-Bus.md) to communicate your services. * Provides many other services to make your daily development easier. diff --git a/docs/en/Module-Development-Basics.md b/docs/en/Module-Development-Basics.md index 8149ec2cb1..6aac90c294 100644 --- a/docs/en/Module-Development-Basics.md +++ b/docs/en/Module-Development-Basics.md @@ -2,7 +2,14 @@ ## Introduction -ABP is a **modular application framework** which consists of dozens of **nuget packages**. It also provides a complete infrastructure to build your own application modules which may have entities, services, database integration, APIs, UI components and so on. +ABP Framework was designed to support to build fully modular applications and systems where every module may have entities, services, database integration, APIs, UI components and so on; + +* This document introduces the basics of the module system. +* [Module development best practice guide](Best-Practices/Index.md) explains some **best practices** to develop **re-usable application modules** based on **DDD** principles and layers. A module designed based on this guide will be **database independent** and can be deployed as a **microservice** if needed. +* [Pre-built application modules](Modules/Index.md) are **ready to use** in any kind of application. +* [Module startup template](Startup-Templates/Module.md) is a jump start way to **create a new module**. +* [ABP CLI](CLI.md) has commands to support modular development. +* All other framework features are compatible to the modularity system. ## Module Class @@ -42,7 +49,7 @@ public class BlogModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { //Configure default connection string for the application - Configure(options => + Configure(options => { options.ConnectionStrings.Default = "......"; }); diff --git a/docs/en/Modules/Account.md b/docs/en/Modules/Account.md new file mode 100644 index 0000000000..6901b38f59 --- /dev/null +++ b/docs/en/Modules/Account.md @@ -0,0 +1,3 @@ +# Account Module + +TODO \ No newline at end of file diff --git a/docs/en/Modules/Blogging.md b/docs/en/Modules/Blogging.md index 0d2ab35c21..32d2e023ea 100644 --- a/docs/en/Modules/Blogging.md +++ b/docs/en/Modules/Blogging.md @@ -1,3 +1,3 @@ -# IdentityServer Module +# Blogging Module TODO \ No newline at end of file diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index afcc1a9dd7..bbed28e8fe 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -20,23 +20,25 @@ When you use GitHub to store your docs, Docs Module supports versioning. If you > Docs module follows the [module architecture best practices](../Best-Practices/Module-Architecture.md) guide. +## Installation +### 1- Download -## Installation +If you do not have an existing ABP project, this step shows you how to create a new project from [abp.io](https://abp.io) to add the Docs Module. If you already have an ABP project, you can skip this step. -### 1- Download +It is recommended to use ABP CLI to create new projects. Use the following command: -If you do not have an existing ABP project, this step shows you how to create a new project from [abp.io](https://abp.io) to add the Docs Module. If you already have an ABP project, you can skip this step. +`abp new Acme.MyProject` -Navigate to https://abp.io/Templates. Enter your project name as `Acme.MyProject`, select `ASP.NET Core Mvc Application` and select `Entity Framework Core` for the database provider. +You can also navigate to https://abp.io/get-started. Enter your project name as `Acme.MyProject`, other use default options. -Note that this document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider. +Note that this document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider. ![Create new project](../images/docs-module_download-new-abp-project.png) ### 2- Running The Empty Application -After you download the project, extract the ZIP file and open `Acme.MyProject.sln`. You will see that the solution consists of `Application`, `Domain `, `EntityFrameworkCore` and `Web` projects. Right click on `Acme.MyProject.Web` project and **Set as StartUp Project**. +After you download the project, extract the ZIP file and open `Acme.MyProject.sln`. You will see that the solution consists of `Application`, `Application.Contracts`, `DbMigrator`, `Domain`, `Domain.Shared`, `EntityFrameworkCore`, `EntityFrameworkCore.DbMigations`, `HttpApi`, `HttpApi.Client` and `Web` projects. Right click on `Acme.MyProject.Web` project and **Set as StartUp Project**. ![Create a new project](../images/docs-module_solution-explorer.png) @@ -45,16 +47,14 @@ The database connection string is located in `appsettings.json` of your `Acme.My ```json { "ConnectionStrings": { - "Default": "Server=localhost;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" } } ``` +Run `Acme.MyProject.DbMigrator` project, it will be responsible for applying database migration and seed data. The database `MyProject` will be created in your database server. - -Open `Package Manager Console` in the Visual Studio and choose `src\Acme.MyProject.EntityFrameworkCore` as the default project. Run `Update-Database` command to create your new database. The database `MyProject` will be created in your database server. - -Now an empty ABP project has been created! You can now run your project and see the empty website. +Now an empty ABP project has been created! You can now run your project and see the empty website. To login your website enter `admin` as the username and `1q2w3E*` as the password. @@ -62,36 +62,27 @@ To login your website enter `admin` as the username and `1q2w3E*` as the passwor Docs module packages are hosted on NuGet. There are 4 packages that needs be to installed to your application. Each package has to be installed to the relevant project. -* [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) needs to be referenced to `Acme.MyProject.Domain` project. +It is recommended to use the ABP CLI to install the module, open the CMD window in the solution file (`.sln`) directory, and run the following command: - * Edit `Acme.MyProject.Domain.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +`abp add-module Volo.Docs` - ```csharp - - ``` -* [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) needs to be referenced to `Acme.MyProject.EntityFrameworkCore` project. +Or you can also manually install nuget package to each project: - - Edit `Acme.MyProject.EntityFrameworkCore.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +* Install [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Domain` project. - ```csharp - - ``` -* [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) needs to be referenced to `Acme.MyProject.Application` project. + `Install-Package Volo.Docs.Domain` - * Edit `Acme.MyProject.Application.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +* Install [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget package to `Acme.MyProject.EntityFrameworkCore` project. - ```csharp - - ``` -* [Volo.Docs.Web ](https://www.nuget.org/packages/Volo.Docs.Web/)needs to be referenced to `Acme.MyProject.Web` project. + `Install-Package Volo.Docs.EntityFrameworkCore` - - Edit `Acme.MyProject.Web.csproj`file and add the below line to as a reference. Note that you need to change version (v0.9.0) to the latest. +* Install [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget package to `Acme.MyProject.Application` project. - ```csharp - - ``` + `Install-Package Volo.Docs.Application` +* Install [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Web` project. + `Install-Package Volo.Docs.Web` ### 3- Adding Module Dependencies @@ -132,7 +123,6 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a } ``` - * Open `MyProjectApplicationModule.cs`and add `typeof(DocsApplicationModule)` as shown below; ```csharp @@ -144,7 +134,7 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { options.DefinitionProviders.Add(); }); @@ -157,7 +147,6 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a } ``` - * Open `MyProjectWebModule.cs`and add `typeof(DocsWebModule)` as shown below; ```csharp @@ -176,43 +165,62 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a } ``` - - ### 4- Database Integration #### 4.1- Entity Framework Integration -If you choose Entity Framework as your database provider, you need to configure the Docs Module in your DbContext. To do this; +If you choose Entity Framework as your database provider, you need to configure the Docs Module. To do this; -- Open `MyProjectDbContext.cs` and add `modelBuilder.ConfigureDocs()` to the `OnModelCreating()` +- Open `MyProjectMigrationsDbContext.cs` and add `builder.ConfigureDocs()` to the `OnModelCreating()`. ```csharp - [ConnectionStringName("Default")] - public class MyProjectDbContext : AbpDbContext - { - public MyProjectDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - //... - modelBuilder.ConfigureDocs(); - } - } + public class MyProjectMigrationsDbContext : AbpDbContext + { + public MyProjectMigrationsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentity(); + builder.ConfigureIdentityServer(); + builder.ConfigureFeatureManagement(); + builder.ConfigureTenantManagement(); + builder.ConfigureDocs(); //Add this line to configure the Docs Module + + /* Configure customizations for entities from the modules included */ + + builder.Entity(b => + { + b.ConfigureCustomUserProperties(); + }); + + /* Configure your own tables/entities inside the ConfigureQaDoc method */ + + builder.ConfigureMyProject(); + } + } ``` -* Open `Package Manager Console` in `Visual Studio` and choose `Acme.MyProject.EntityFrameworkCore` as default project. Then write the below command to add the migration for Docs Module. +* Open `Package Manager Console` in `Visual Studio` and choose `Acme.MyProject.EntityFrameworkCore.DbMigrations` as default project. Then write the below command to add the migration for Docs Module. ```csharp add-migration Added_Docs_Module ``` - When the command successfully executes , you will see a new migration file named as `20181221111621_Added_Docs_Module` in the folder `Acme.MyProject.EntityFrameworkCore\Migrations`. + When the command successfully executes , you will see a new migration file named as `20181221111621_Added_Docs_Module` in the folder `Acme.MyProject.EntityFrameworkCore.DbMigrations\Migrations`. - Now, update the database for Docs module database changes. To do this run the below code on `Package Manager Console` in `Visual Studio`. Be sure `Acme.MyProject.EntityFrameworkCore` is still default project. + Now, update the database for Docs module database changes. To do this run the below code on `Package Manager Console` in `Visual Studio`. Be sure `Acme.MyProject.EntityFrameworkCore.DbMigrations` is still default project. ```csharp update-database @@ -220,7 +228,6 @@ If you choose Entity Framework as your database provider, you need to configure Finally, you can check your database to see the newly created tables. For example you can see `DocsProjects` table must be added to your database. - ### 5- Linking Docs Module The default route for Docs module is; @@ -250,7 +257,7 @@ To add Docs module link to your application menu; } ``` -The `Menu:Docs` keyword is a localization key. To localize the menu text, open `Localization\MyProject\en.json` in the project `Acme.MyProject.Domain`. And add the below line +The `Menu:Docs` keyword is a localization key. To localize the menu text, open `Localization\MyProject\en.json` in the project `Acme.MyProject.Domain`. And add the below line ```json "Menu:Docs": "Documents" @@ -270,7 +277,7 @@ Final look of **en.json** } ``` -The new menu item for Docs Module is added to the menu. Run your web application and browse to `http://localhost:YOUR_PORT_NUMBER/documents` URL. +The new menu item for Docs Module is added to the menu. Run your web application and browse to `http://localhost:YOUR_PORT_NUMBER/documents` URL. You will see a warning says; @@ -316,23 +323,25 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen - ExtraProperties: ```json - {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"} + {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""} ``` Note that `GitHubAccessToken` is masked with `***`. It's a private token that you must get it from GitHub. See https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ -- MainWebsiteUrl: `/` +- MainWebsiteUrl: `/` - LatestVersionBranchName: `master` For `SQL` databases, you can use the below `T-SQL` command to insert the specified sample into your `DocsProjects` table: ```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"}', N'/', N'master', N'') +INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'master', N'') ``` Be aware that `GitHubAccessToken` is masked. It's a private token and you must get your own token and replace the `***` string. +Now you can run the application and navigate to `/Documents`. + #### Sample Project Record for "FileSystem" You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documents to configure your GitHub document store. @@ -561,11 +570,34 @@ The upper sample `JSON` file renders the below navigation menu as `HTML`. ![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) +Finally a new Docs Module is added to your project which is feeded with GitHub. -Finally a new Docs Module is added to your project which is feeded with GitHub. +## Full-Text Search(Elastic Search) -## Next +The Docs module supports full-text search using Elastic Search. It is not enabled by default. You can configure `DocsElasticSearchOptions` to enable it. + +``` +Configure(options => +{ + options.Enable = true; + options.IndexName = "your_index_name"; //default IndexName is abp_documents +}); +``` + +The `Index` is automatically created after the application starts if the `Index` does not exist. -Docs Module is also available as a standalone application. Check out [VoloDocs](../Apps/VoloDocs). +`DefaultElasticClientProvider` is responsible for creating `IElasticClient`. By default, it reads Elastic Search's `Url` from `IConfiguration`. +If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](Dependency-Injection.md) system. + +``` +{ + "ElasticSearch": { + "Url": "http://localhost:9200" + } +} +``` + +## Next +Docs Module is also available as a standalone application. Check out [VoloDocs](../Apps/VoloDocs). \ No newline at end of file diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md new file mode 100644 index 0000000000..028144c624 --- /dev/null +++ b/docs/en/Modules/Identity.md @@ -0,0 +1,4 @@ +# Identity Management Module + +See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... + diff --git a/docs/en/Modules/Tenant-Management.md b/docs/en/Modules/Tenant-Management.md new file mode 100644 index 0000000000..73e16aee8e --- /dev/null +++ b/docs/en/Modules/Tenant-Management.md @@ -0,0 +1,3 @@ +# Tenant Management Module + +TODO \ No newline at end of file diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 26b28b0ad7..27fe70086c 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -4,7 +4,7 @@ ABP Multi-tenancy module provides base functionality to create multi tenant appl Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that: -> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of a 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. +> 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 @@ -168,7 +168,7 @@ namespace MyCompany.MyProject { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { options.Tenants = new[] { @@ -214,7 +214,7 @@ namespace MyCompany.MyProject { var configuration = BuildConfiguration(); - Configure(configuration); + Configure(configuration); } private static IConfigurationRoot BuildConfiguration() @@ -302,6 +302,7 @@ TODO:... 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**. * **QueryStringTenantResolver**: Tries to find current tenant id from query string parameter. Parameter name is "__tenant" by default. * **RouteTenantResolver**: 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. * **HeaderTenantResolver**: Tries to find current tenant id from HTTP header. Header name is "__tenant" by default. @@ -342,8 +343,10 @@ namespace MyCompany.MyProject { Configure(options => { - //Subdomain format: {0}.mydomain.com (adding as the highest priority resolver) - options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com")); + //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 DomainTenantResolver("{0}.mydomain.com")); }); //... @@ -354,7 +357,7 @@ namespace MyCompany.MyProject {0} is the the placeholder to determine current tenant's unique name. -Instead of ``options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));`` you can use this shortcut: +Instead of ``options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));`` you can use this shortcut: ````C# options.AddDomainTenantResolver("{0}.mydomain.com"); diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md new file mode 100644 index 0000000000..fad3ff2b0c --- /dev/null +++ b/docs/en/Object-Extensions.md @@ -0,0 +1,3 @@ +# Object Extensions + +TODO \ No newline at end of file diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index ba32878ea6..567d043ef9 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -24,82 +24,15 @@ This sample aims to demonstrate a simple yet complete microservice solution; The diagram below shows the system: -![microservice-sample-diagram-2](../images/microservice-sample-diagram-2.png) +![microservice-sample-diagram-2](../images/microservice-sample-diagram-3.png) ### Source Code You can get the source code from [the GitHub repository](https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo). -### Status - -Initial version of this sample has been completed. Additional improvement are still in development. - ## Running the Solution -You can either run from the **source code** or from the pre-configured **docker-compose** file. - -### Using the Docker Containers - -#### Pre Requirements - -Running as docker containers is easier since all dependencies are pre-configured. You only need to install the [latest docker](https://docs.docker.com/compose/install/). - -#### Running Containers - -- Clone or download the [ABP repository](https://github.com/abpframework/abp). - -- Open a command line in the `samples/MicroserviceDemo` folder of the repository. - -- Pull images from Docker Hub: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml pull - ``` - -- If you want to build images locally you may skip the above step and instead use build command: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml build - ``` - - Building images may take a **long time** depending on your machine. - -- Restore SQL Server databases: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml run restore-database - ``` - -- Start the containers: - - ``` - docker-compose up -d - ``` - -- Add this line to the end of your `hosts` file: - - ``` - 127.0.0.1 auth-server - ``` - - hosts file is located inside the `C:\Windows\System32\Drivers\etc\hosts` folder on Windows and `/etc/hosts` for Linux/MacOS. - -#### Run the Applications - -There are a few applications running in the containers you may want to explore: - -* Backend Admin Application (BackendAdminApp.Host): `http://localhost:51512` - *(Used to manage users & products in the system)* -* Public Web Site (PublicWebsite.Host): `http://localhost:51513` - *(Used to list products and run/manage the blog module)* -* Authentication Server (AuthServer.Host): `http://auth-server:51511/` - *(Used as a single sign on and authentication server built with IdentityServer4)* -* Kibana UI: `http://localhost:51510` - *(Use to show/trace logs written by all services/applications/gateways)* - -### Running From the Source Code - -#### Pre Requirements +### Pre Requirements To be able to run the solution from source code, following tools should be installed and running on your computer: @@ -110,19 +43,19 @@ To be able to run the solution from source code, following tools should be insta * [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ * [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (optional, recommended to show logs) -#### Open & Build the Visual Studio Solution +### Open & Build the Visual Studio Solution * Open the `samples\MicroserviceDemo\MicroserviceDemo.sln` in Visual Studio 2017 (15.9.0+). * Run `dotnet restore` from the command line inside the `samples\MicroserviceDemo` folder. * Build the solution in Visual Studio. -#### Create Databases +### Create Databases MongoDB database is created dynamically, however you need to create database schemas for SQL server databases. The solution is configured to use Entity Core Code First migrations, so you can easily create databases. There are two SQL server databases in this solution. -##### MsDemo_Identity Database +#### MsDemo_Identity Database * Right click to the `AuthServer.Host` project and click to the `Set as startup project`. * Open the **Package Manager Console** (Tools -> Nuget Package Manager -> Package Manager Console) @@ -131,7 +64,7 @@ There are two SQL server databases in this solution. ![microservice-sample-update-database-authserver](../images/microservice-sample-update-database-authserver.png) -##### MsDemo_ProductManagement +#### MsDemo_ProductManagement - Right click to the `ProductService.Host` project and click to the `Set as startup project`. - Open the **Package Manager Console** (Tools -> Nuget Package Manager -> Package Manager Console) @@ -140,12 +73,13 @@ There are two SQL server databases in this solution. ![microservice-sample-update-database-products](../images/microservice-sample-update-database-products.png) -#### Run Projects +### Run Projects Run the projects with the following order (right click to each project, set as startup project an press Ctrl+F5 to run without debug): * AuthServer.Host * IdentityService.Host +* TenantManagementService.Host * BloggingService.Host * ProductService.Host * InternalGateway.Host @@ -160,7 +94,7 @@ When you run projects, they will add some initial demo data to their databases. The Visual Studio solution consists of multiple projects each have different roles in the system: -![microservice-sample-solution](../images/microservice-sample-solution.png) +![microservice-sample-solution](../images/microservice-sample-solution-2.png) ### Applications @@ -183,8 +117,9 @@ Gateways are used to provide a single entry point to the applications. It can al Microservices have no UI, but exposes some REST APIs. -- **IdentityService.Host**: Host the ABP Identity module which is used to manage users & roles. It has no additional service, but only hosts the Identity module's API. -- **BloggingService.Host**: Host the ABP Blogging module which is used to manage blog & posts (a typical blog application). It has no additional service, but only hosts the Blogging module's API. +- **IdentityService.Host**: Hosts the ABP Identity module which is used to manage users & roles. It has no additional service, but only hosts the Identity module's API. +- **TenantManagementService.Host**: Hosts the ABP Tenant Management module which is used to manage roles. It has no additional service, but only hosts the Tenant Management module's API. +- **BloggingService.Host**: Hosts the ABP Blogging module which is used to manage blog & posts (a typical blog application). It has no additional service, but only hosts the Blogging module's API. - **ProductService.Host**: Hosts the Product module (that is inside the solution) which is used to manage products. It also contains the EF Core migrations to create/update the Product Management database schema. ### Modules @@ -195,7 +130,7 @@ Microservices have no UI, but exposes some REST APIs. This solution is using multiple databases: -* **MsDemo_Identity**: An SQL database. Used **SQL Server** by default, but can be any DBMS supported by the EF Core. Shared by AuthServer and IdentityService. Also audit logs, permissions and settings are stored in this database (while they could easily have their own databases, shared the same database to keep it simple). +* **MsDemo_Identity**: An SQL database. Used **SQL Server** by default, but can be any DBMS supported by the EF Core. Shared by AuthServer, IdentityService and the TenantManagementService. Also audit logs, permissions and settings are stored in this database (while they could have their own databases, shared the same database to keep it simple). * **MsDemo_ProductManagement**: An SQL database. Again, used **SQL Server** by default, but can be any DBMS supported by the EF Core. Used by the ProductService as a dedicated database. * **MsDemo_Blogging**: A **MongoDB** database. Used by the BloggingService. * **Elasticsearch**: Used to write logs over Serilog. @@ -341,7 +276,7 @@ Backend admin application uses the Identity and Product microservices for all op ##### HTTP Clients -ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. +ABP application modules generally provides C# client libraries to consume services (APIs) easily (they generally uses the [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature of the ABP framework). That means if you need to consume Identity service API, you can reference to its client package and easily use the APIs by provided interfaces. For that purpose, `BackendAdminAppHostModule` class declares dependencies for `AbpIdentityHttpApiClientModule` and `ProductManagementHttpApiClientModule`. @@ -419,7 +354,7 @@ A screenshot from the Products page: #### Using Microservices -Publc web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host). +Public web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host). ##### Remote End Point @@ -1103,7 +1038,7 @@ Product Management is a module that consists of several layers and packages/proj * `ProductManagement.Application` contains the implementation of application services. * `ProductManagement.EntityFrameworkCore` contains DbConext and other EF Core related classes and configuration. * `ProductManagement.HttpApi` contains API Controllers. -* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. +* `ProductManagement.HttpApi.Client` contains C# proxies to directly use the HTTP API remotely. Uses [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature of the ABP framework. * `ProductManagement.Web` contains the UI elements (pages, scripts, styles... etc). By the help of this layering, it is possible to use the same module as a package reference in a monolithic application or use as a service that runs in another server. It is possible to separate UI (Web) and API layers, so they run in different servers. @@ -1472,3 +1407,7 @@ ABP provides automatic audit logging which saves every request in detail (who is All of the services and applications are configured to write audit logs. Audit logs are saved to the MsDemo_Identity SQL database. So, you can query all audit logs of all applications from a single point. An Audit Log record has a `CorrelationId` property that can be used to track a request. When a service calls another service in a single web request, they both save audit logs with the same `CorrelationId`. See the `AbpAuditLogs` table in the database. + +### Multi-Tenancy + +The solution has been configured to provide a [multi-tenant](../Multi-Tenancy.md) system, where each tenant can have their isolated users, roles, permissions and other data. \ No newline at end of file diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 1fbf1be8e2..af76bb14ff 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -2,11 +2,12 @@ ## Introduction -This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. This document explains the solution structure and projects in details. If you want to start quickly, follow the guides below: +This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. -* See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template (uses MVC as the UI framework and Entity Framework Core as the database provider). -* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider). -* See the [Angular Application Development Tutorial](../Tutorials/Angular/Part-I.md) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider). +This document explains **the solution structure** and projects in details. If you want to start quickly, follow the guides below: + +* [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes. +* [The application development tutorial](../Tutorials/Part-1) explains step by step application development. ## How to Start With? @@ -123,6 +124,8 @@ Notice that the migration `DbContext` is only used for database migrations and * * Depends on the `.EntityFrameworkCore` project since it re-uses the configuration defined for the `DbContext` of the application. > This project is available only if you are using EF Core as the database provider. +> +> See the [Entity Framework Core Migrations Guide](../Entity-Framework-Core-Migrations.md) to understand this project in details. #### .DbMigrator Project @@ -145,7 +148,7 @@ While creating database & applying migrations seems only necessary for relationa This project is used to define your API Controllers. -Most of time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../AspNetCore/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. +Most of time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../API/Auto-API-Controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. * Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. @@ -153,7 +156,7 @@ Most of time you don't need to manually define API Controllers since ABP's [Auto This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other type of applications, they can still use your APIs, either manually or using a tool in their own platform) -Most of time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../AspNetCore/Dynamic-CSharp-API-Clients.md) feature. +Most of time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../API/Dynamic-CSharp-API-Clients.md) feature. `.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. @@ -258,16 +261,183 @@ You should run the application with the given order: ### Angular UI -If you choose Angular as the UI framework (using the `-u angular` option), the solution is separated into two folders: +If you choose `Angular` as the UI framework (using the `-u angular` option), the solution is being separated into three folders: + +* `angular` folder contains the Angular UI application, the client-side code. +* `aspnet-core` folder contains the ASP.NET Core solution, the server-side code. +* `react-native` folder contains the React Native UI application, the client-side code for mobile. + +The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the `Angular` application consumes it. + +Angular application folder structure looks like below: + +![angular-folder-structure](../images/angular-folder-structure.png) + + +Each of ABP Commercial modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: + +```bash +yarn list --pattern abp +``` + +Angular application module structure: + +![Angular template structure diagram](../images/angular-template-structure-diagram.png) + +#### AppModule + +`AppModule` is the root module of the application. Some of ABP modules and some essential modules imported to the `AppModule`. + +ABP Config modules also have imported to `AppModule`  for initially requirements of lazy-loadable ABP modules. + +#### AppRoutingModule + +There are lazy-loadable ABP modules in the `AppRoutingModule` as routes. + +> Paths of ABP Modules should not be changed. + +You should add `routes` property in the `data` object to add a link on the menu to redirect to your custom pages. + +```js +{ + path: 'dashboard', + loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), + canActivate: [AuthGuard, PermissionGuard], + data: { + routes: { + name: 'ProjectName::Menu:Dashboard', + order: 2, + iconClass: 'fa fa-dashboard', + requiredPolicy: 'ProjectName.Dashboard.Host' + } as ABP.Route + } +} +``` +In the above example; +* If the user is not logged in, AuthGuard blocks access and redirects to the login page. +* PermissionGuard checks the user's permission with `requiredPolicy` property of the `rotues` object. If the user is not authorized to access the page, the 403 page appears. +* `name` property of `routes` is the menu link label. A localization key can be defined . +* `iconClass` property of `routes` object is the menu link icon class. +* `requiredPolicy` property of `routes` object is the required policy key to access the page. + +After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu. + +#### Shared Module + +The modules that may be required for all modules have imported to the `SharedModule`. You should import the `SharedModule` to all modules. + +See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document. + +#### Environments + +The files under the `src/environments` folder has the essential configuration of the application. + +#### Home Module + +Home module is an example lazy-loadable module that loads on the root address of the application. + +#### Styles + +The required style files added to `styles` array in the `angular.json`. `AppComponent` loads some style files lazily via `LazyLoadService` after the main bundle is loaded to shorten the first rendering time. + +#### Testing + +You should create your tests in the same folder as the file file you want to test. + +See the [testing document](https://angular.io/guide/testing). + +#### Depended Packages + +* [NG Bootstrap](https://ng-bootstrap.github.io/) is used as UI component library. +* [NGXS](https://www.ngxs.io/) is used as state management library. +* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) is used to support for OAuth 2 and OpenId Connect (OIDC). +* [Chart.js](https://www.chartjs.org/) is used to create widgets. +* [ngx-validate](https://github.com/ng-turkey/ngx-validate) is used for dynamic validation of reactive forms. + +### React Native + +The solution includes the [React Native](https://reactnative.dev/) application in the `react-native` folder as default. + +The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the React Native application consumes it. + +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. + +React Native application folder structure as like below: + +![react-native-folder-structure](../images/react-native-folder-structure.png) + +* `App.js` is bootstrap component of the application. +* `Environment.js` file has the essential configuration of the application. `prod` and `dev` configurations defined in this file. +* [Contexts](https://reactjs.org/docs/context.html) are created in the `src/contexts` folder. +* [Higher order components](https://reactjs.org/docs/higher-order-components.html) are created in the`src/hocs` folder. +* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) are created in the`src/hooks`. +* [Axios interceptors](https://github.com/axios/axios#interceptors) are created in the `src/interceptors` folder. +* Utility functions are exported from `src/utils` folder. + +#### Components + +Components that can be used on all screens are created in the `src/components` folder. All components have created as a function that able to use [hooks](https://reactjs.org/docs/hooks-intro.html). + +#### Screens + +![react-native-navigation-structure](../images/react-native-navigation-structure.png) + +Screens are created by creating folders that separate their names in the `src/screens` folder. Certain parts of some screens can be split into components. + +Each screen is used in a navigator in the `src/navigators` folder. + +#### Navigation + +[React Navigation](https://reactnavigation.org/) is used as a navigation library. Navigators are created in the `src/navigators`. A [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) navigator and several [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) navigators have created in this folder. See the [above diagram](#screens) for navigation structure. + +#### State Management + +[Redux](https://redux.js.org/) is used as state management library. [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. + +Actions, reducers, sagas, selectors are created in the `src/store` folder. Store folder as like below: + +![react-native-store-folder](../images/react-native-store-folder.png) + +* [**Store**](https://redux.js.org/basics/store) is defined in the `src/store/index.js` file. +* [**Actions**](https://redux.js.org/basics/actions/) are payloads of information that send data from your application to your store. +* [**Reducers**](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store. +* [**Redux-Saga**](https://redux-saga.js.org/) is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage. Sagas are created in the `src/store/sagas` folder. +* [**Reselect**](https://github.com/reduxjs/reselect) library is used to create memoized selectors. Selectors are created in the `src/store/selectors` folder. + +#### APIs + +[Axios](https://github.com/axios/axios) is used as an HTTP client library. An Axios instance has exported from `src/api/API.js` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls. + +#### Theming + +[Native Base](https://nativebase.io/) is used as UI components library. Native Base components can customize easily. See the [Native Base customize](https://docs.nativebase.io/Customize.html#Customize) documentation. We followed the same way. + +* Native Base theme variables are in the `src/theme/variables` folder. +* Native Base component styles are in the `src/theme/components` folder. These files have been generated with Native Base's `ejectTheme` script. +* Styles of components override with the files under the `src/theme/overrides` folder. + +#### Testing + +Unit tests will be created. -* `angular` folder contains the Angular UI solution, the client side. -* `aspnet-core` folder contains the ASP.NET Core solution, the server side. +See the [Testing Overview](https://reactjs.org/docs/testing.html) document. -Server side is very similar to the solution described above. `.HttpApi.Host` project serves the API, so the Angular application can consume it. +#### Depended Libraries -The files under the `angular/src/environments` folder has the essential configuration of the application. +* [Native Base](https://nativebase.io/) is used as UI components library. +* [React Navigation](https://reactnavigation.org/) is used as navigation library. +* [Axios](https://github.com/axios/axios) is used as HTTP client library. +* [Redux](https://redux.js.org/) is used as state management library. +* [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. +* [Redux-Saga](https://redux-saga.js.org/) is used to manage asynchronous processes. +* [Redux Persist](https://github.com/rt2zz/redux-persist) is used as state persistance. +* [Reselect](https://github.com/reduxjs/reselect) is used to create memoized selectors. +* [i18n-js](https://github.com/fnando/i18n-js) is used as i18n library. +* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) library allows loading fonts easily. +* [Formik](https://github.com/jaredpalmer/formik) is used to build forms. +* [Yup](https://github.com/jquense/yup) is used for form validations. ## What's Next? -- See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. -- See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template. +- [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes. +- [The application development tutorial](../Tutorials/Part-1) explains step by step application development. diff --git a/docs/en/Startup-Templates/Module.md b/docs/en/Startup-Templates/Module.md index 2b95e49a56..45e123971b 100644 --- a/docs/en/Startup-Templates/Module.md +++ b/docs/en/Startup-Templates/Module.md @@ -133,7 +133,7 @@ For the `.Web.Unified` application, there is a single database, named `YourProje ##### How to Run? -Set it as the startup project, run `Update-Database` command for the EF Core from Package Manager Console and run your application. Default username is `admin` and password is `1q2w3E*`. +Set `host/YourProjectName.Web.Unified` as the startup project, run `Update-Database` command for the EF Core from Package Manager Console and run your application. Default username is `admin` and password is `1q2w3E*`. #### Separated Deployment & Databases Scenario diff --git a/docs/en/Themes/Basic.md b/docs/en/Themes/Basic.md new file mode 100644 index 0000000000..a0165c803a --- /dev/null +++ b/docs/en/Themes/Basic.md @@ -0,0 +1,3 @@ +## Basic Theme + +TODO \ No newline at end of file diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md index 6d5f91bce7..65a7dc5714 100644 --- a/docs/en/Tutorials/Angular/Part-I.md +++ b/docs/en/Tutorials/Angular/Part-I.md @@ -1,659 +1,6 @@ -## Angular Tutorial - Part I +# Tutorials -### About this Tutorial +## Application Development -In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Angular** will be used as the UI framework and **MongoDB** will be used as the database provider. - -This is the first part of the Angular tutorial series. See all parts: - -- **Part I: Create the project and a book list page (this tutorial)** -- [Part II: Create, Update and Delete books](Part-II.md) -- [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). - -### Creating the Project - -Create a new project named `Acme.BookStore` by selecting the Angular as the UI framework and MongoDB as the database provider, create the database and run the application by following the [Getting Started document](../../Getting-Started-Angular-Template.md). - -### Solution Structure (Backend) - -This is how the layered solution structure looks after it's created: - -![bookstore-backend-solution](images/bookstore-backend-solution-v2.png) - -> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial. - -### Create the Book Entity - -Domain layer in the startup template is separated into two projects: - -- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects. -- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients. - -Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: - -```C# -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices. -- `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. -- `Guid` is the **primary key type** of the `Book` entity. - -#### BookType Enum - -Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: - -```C# -namespace Acme.BookStore -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -``` - -#### Add Book Entity to Your DbContext - -Add a `IMongoCollection` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project: - -```csharp -public class BookStoreMongoDbContext : AbpMongoDbContext -{ - public IMongoCollection Books => Collection(); - ... -} -``` - -#### Add Seed (Sample) Data - -This section is optional, but it would be good to have an initial data in the database in the first run. ABP provides a [data seed system](../../Data-Seeding.md). Create a class deriving from the `IDataSeedContributor` in the `.Domain` project: - -```csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - } - ); - } - } -} - -``` - -`BookStoreDataSeederContributor` simply inserts two books into database if there is no book added before. ABP automatically discovers and executes this class when you seed the database by running the `Acme.BookStore.DbMigrator` project. - -### Create the Application Service - -The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects: - -- `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces. -- `Acme.BookStore.Application` contains the implementations of your application services. - -#### BookDto - -Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: - -```C# -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- **DTO** classes are used to **transfer data** between the _presentation layer_ and the _application layer_. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details. -- `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. -- `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above. - -It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: - -```csharp -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -``` - -#### CreateUpdateBookDto - -Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: - -```c# -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public DateTime PublishDate { get; set; } - - [Required] - public float Price { get; set; } - } -} -``` - -- This DTO class is used to get book information from the user interface while creating or updating a book. -- It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework. - -Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity: - -```csharp -CreateMap(); -``` - -#### IBookAppService - -Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: - -```C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book - { - - } -} -``` - -- Defining interfaces for application services is not required by the framework. However, it's suggested as a best practice. -- `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. -- There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. - -#### BookAppService - -Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: - -```C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookAppService : - CrudAppService, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -``` - -- `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above. -- `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md). -- `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected. - -### Auto API Controllers - -You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention. - -#### 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 `Acme.BookStore.HttpApi.Host` application and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser. - -You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: - -![bookstore-swagger](images/bookstore-swagger-api.png) - -Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. - -### Create the Books Page - -In this tutorial; - -- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services -- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library -- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. -- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). - -#### Install NPM Packages - -Open a terminal window and go to `angular` folder and then run `yarn` command for installing NPM packages: - -``` -yarn -``` - -#### BooksModule - -Run the following command line to create a new module, named `BooksModule`: - -```bash -yarn ng generate module books --route books --module app.module -``` - -![creating-books-module.terminal](images/bookstore-creating-books-module-terminal.png) - -Run `yarn start`, wait Angular to run the application and open `http://localhost:4200/books` on a browser: - -![initial-books-page](images/bookstore-initial-books-page.png) - -#### Routing - -Open the `app-routing.module.ts` and replace `books` as shown below: - -```js -import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; - -//... -{ - path: 'books', - component: ApplicationLayoutComponent, - loadChildren: () => import('./books/books.module').then(m => m.BooksModule), - data: { - routes: { - name: 'Books', - } as ABP.Route, - }, -}, -``` - -`ApplicationLayoutComponent` configuration sets the application layout to the new page. If you would like to see your route on the navigation bar (main menu) you must also add the `data` object with `name` property in your route. - -![initial-books-page](images/bookstore-initial-books-page-with-layout.png) - -#### Book List Component - -First, replace the `books.component.html` to the following line to place the router-outlet: - -```html - -``` - -Then run the command below on the terminal in the root folder to generate a new component, named book-list: - -```bash -yarn ng generate component books/book-list -``` - -![creating-books-list-terminal](images/bookstore-creating-book-list-terminal.png) - -Import the `SharedModule` to the `BooksModule` to reuse some components and services defined in: - -```js -import { SharedModule } from '../shared/shared.module'; - -@NgModule({ - //... - imports: [ - //... - SharedModule, - ], -}) -export class BooksModule {} -``` - -Then, update the `routes` in the `books-routing.module.ts` to add the new book-list component: - -```js -import { BookListComponent } from './book-list/book-list.component'; - -const routes: Routes = [ - { - path: '', - component: BooksComponent, - children: [{ path: '', component: BookListComponent }], - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class BooksRoutingModule {} -``` - -![initial-book-list-page](images/bookstore-initial-book-list-page.png) - -#### Create BooksState - -Run the following command in the terminal to create a new state, named `BooksState`: - -```shell -yarn ng generate ngxs-schematic:state books -``` - -This command creates several new files and edits `app.modules.ts` to import the `NgxsModule` with the new state: - -```js -// app.module.ts - -import { BooksState } from './store/states/books.state'; - -@NgModule({ - imports: [ - //... - NgxsModule.forRoot([BooksState]), - ], - //... -}) -export class AppModule {} -``` - -#### Get Books Data from Backend - -First, create data types to map data returning from the backend (you can check swagger UI or your backend API to know the data format). - -Modify the `books.ts` as shown below: - -```js -export namespace Books { - export interface State { - books: Response; - } - - export interface Response { - items: Book[]; - totalCount: number; - } - - export interface Book { - name: string; - type: BookType; - publishDate: string; - price: number; - lastModificationTime: string; - lastModifierId: string; - creationTime: string; - creatorId: string; - id: string; - } - - export enum BookType { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry, - } -} -``` - -Added `Book` interface that represents a book object and `BookType` enum represents a book category. - -#### BooksService - -Now, create a new service, named `BooksService` to perform HTTP calls to the server: - -```bash -yarn ng generate service books/shared/books -``` - -![service-terminal-output](images/bookstore-service-terminal-output.png) - -Modify `books.service.ts` as shown below: - -```js -import { Injectable } from '@angular/core'; -import { RestService } from '@abp/ng.core'; -import { Books } from '../../store/models'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class BooksService { - constructor(private restService: RestService) {} - - get(): Observable { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } -} -``` - -Added the `get` method to get the list of books by performing an HTTP request to the related endpoint. - -Replace `books.actions.ts` content as shown below: - -```js -export class GetBooks { - static readonly type = '[Books] Get'; -} -``` - -#### Implement the BooksState - -Open the `books.state.ts` and change the file as shown below: - -```js -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks } from '../actions/books.actions'; -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; -import { tap } from 'rxjs/operators'; - -@State({ - name: 'BooksState', - defaults: { books: {} } as Books.State, -}) -export class BooksState { - @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; - } - - constructor(private booksService: BooksService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { - ctx.patchState({ - books: booksResponse, - }); - }), - ); - } -} -``` - -Added the `GetBooks` action that uses the `BookService` defined above to get the books and patch the state. - -> NGXS requires to return the observable without subscribing it, as done in this sample (in the get function). - -#### BookListComponent - -Modify the `book-list.component.ts` as shown below: - -```js -import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; -import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], -}) -export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) - books$: Observable; - - booksType = Books.BookType; - - loading = false; - - constructor(private store: Store) {} - - ngOnInit() { - this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); - } -} -``` - -> See the [Dispatching Actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the NGXS documentation for more information on these NGXS features. - -Replace `book-list.component.html` content as shown below: - -```html -
-
-
-
-
- Books -
-
-
-
-
- - - - Book name - Book type - Publish date - Price - - - - - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - - -
-
-``` - -> We've used [PrimeNG table](https://www.primefaces.org/primeng/#/table) in this component. - -The resulting books page is shown below: - -![bookstore-book-list](images/bookstore-book-list.png) - -And this is the folder & file structure by the end of this tutorial: - - - -> This tutorial follows the [Angular Style Guide](https://angular.io/guide/styleguide#file-tree). - -### Next Part - -See the [next part](Part-II.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md index 6d1c563600..65a7dc5714 100644 --- a/docs/en/Tutorials/Angular/Part-II.md +++ b/docs/en/Tutorials/Angular/Part-II.md @@ -1,587 +1,6 @@ -## Angular Tutorial - Part II +# Tutorials -### About this Tutorial +## Application Development -This is the second part of the Angular tutorial series. See all parts: - -- [Part I: Create the project and a book list page](Part-I.md) -- **Part II: Create, Update and Delete books (this tutorial)** -- [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). - -### Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. - -#### Type Definition - -Create an interface, named `CreateUpdateBookInput` in the `books.ts` as shown below: - -```js -export namespace Books { - //... - export interface CreateUpdateBookInput { - name: string; - type: BookType; - publishDate: string; - price: number; - } -} -``` - -`CreateUpdateBookInput` interface matches the `CreateUpdateBookDto` in the backend. - -#### Service Method - -Open the `books.service.ts` and add a new method, named `create` to perform an HTTP POST request to the server: - -```js -create(createBookInput: Books.CreateUpdateBookInput): Observable { - return this.restService.request({ - method: 'POST', - url: '/api/app/book', - body: createBookInput - }); -} -``` - -- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). - -#### State Definitions - -Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below: - -```js -import { Books } from '../models'; - -export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput) {} -} -``` - -Open `books.state.ts` and define the `save` method that will listen to a `CreateUpdateBook` action to create a book: - -```js -import { ... , CreateUpdateBook } from '../actions/books.actions'; -import { ... , switchMap } from 'rxjs/operators'; -//... -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - return this.booksService - .create(action.payload) - .pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -When the `SaveBook` action dispatched, the save method is executed. It call `create` method of the `BooksService` defined before. After the service call, `BooksState` dispatches the `GetBooks` action to get books again from the server to refresh the page. - -#### Add a Modal to BookListComponent - -Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book. - -```html - - -

New Book

-
- - - - - - -
-``` - -`abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. - -Add a button, labeled `New book` to show the modal: - -```html -
-
-
- Books -
-
-
- -
-
-``` - -Open the `book-list.component.ts` and add `isModalOpen` variable and `createBook` method to show/hide the modal. - -```js -isModalOpen = false; - -//... - -createBook() { - this.isModalOpen = true; -} -``` - -![empty-modal](images/bookstore-empty-new-book-modal.png) - -#### Create a Reactive Form - -> [Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. - -Add a `form` variable and inject a `FormBuilder` service to the `book-list.component.ts` as shown below (remember add the import statement). - -```js -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -form: FormGroup; - -constructor( - //... - private fb: FormBuilder -) {} -``` - -> The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. - -Add the `buildForm` method to create book form. - -```js -buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); -} -``` - -- The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`. -- Added `Validators.required` static method that validates the related form element. - -Modify the `createBook` method as shown below: - -```js -createBook() { - this.buildForm(); - this.isModalOpen = true; -} -``` - -#### Create the DOM Elements of the Form - -Open `book-list.component.html` and add the form in the body template of the modal. - -```html - -
-
- * - -
- -
- * - -
- -
- * - -
- -
- * - -
-
-
-``` - -- This template creates a form with Name, Price, Type and Publish date fields. - -> We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. - -#### Datepicker Requirements - -You need to import `NgbDatepickerModule` to the `books.module.ts`: - -```js -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; - -@NgModule({ - imports: [ - // ... - NgbDatepickerModule, - ], -}) -export class BooksModule {} -``` - -Then open the `book-list.component.ts` and add `providers` as shown below: - -```js -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - // ... - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookListComponent implements OnInit { -// ... -``` - -> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. - -#### Create the Book Type Array - -Open the `book-list.component.ts` and then create an array, named `bookTypeArr`: - -```js -//... -booksType = Books.BookType; - -bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' -); -``` - -The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: - -```js -['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] -``` - -This array was used in the previous form template (in the `ngFor` loop). - - -![new-book-form](images/bookstore-new-book-form.png) - -#### Saving the Book - -Open the `book-list.component.html` and add an `abp-button` to save the form. - -```html - - - - -``` - -This adds a save button to the bottom area of the modal: - -![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) - -Then define a `save` method in the `BookListComponent`: - -```js -//... -import { ..., CreateUpdateBook } from '../../store/actions'; -//... -save() { - if (this.form.invalid) { - return; - } - - this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - }); -} -``` - -### Updating An Existing Book - -#### BooksService - -Open the `books.service.ts` and then add the `getById` and `update` methods. - -```js -getById(id: string): Observable { - return this.restService.request({ - method: 'GET', - url: `/api/app/book/${id}` - }); -} - -update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { - return this.restService.request({ - method: 'PUT', - url: `/api/app/book/${id}`, - body: updateBookInput - }); -} -``` - -#### CreateUpdateBook Action - -Open the `books.actions.ts` and add `id` parameter to the `CreateUpdateBook` action: - -```js -export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} -} -``` - -Open `books.state.ts` and modify the `save` method as show below: - -```js -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - let request; - - if (action.id) { - request = this.booksService.update(action.payload, action.id); - } else { - request = this.booksService.create(action.payload); - } - - return request.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -#### BookListComponent - -Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`. - -```js -import { BooksService } from '../shared/books.service'; -//... -selectedBook = {} as Books.Book; - -constructor( - //... - private booksService: BooksService -) -``` - -`booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book. - -```js -buildForm() { - this.form = this.fb.group({ - name: [this.selectedBook.name || '', Validators.required], - type: this.selectedBook.type || null, - publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - price: this.selectedBook.price || null, - }); -} -``` - -Add the `editBook` method as shown below: - -```js - editBook(id: string) { - this.booksService.getById(id).subscribe(book => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } -``` - -Added `editBook` method to get the editing book, build the form and show the modal. - -Now, add the `selectedBook` definition to `createBook` method to reuse the same form while creating a new book: - -```js - createBook() { - this.selectedBook = {} as Books.Book; - //... - } -``` - -Modify the `save` method to pass the id of the selected book as shown below: - -```js -save() { - if (this.form.invalid) { - return; - } - - this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) - .subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - }); -} -``` - -#### Add "Actions" Dropdown to the Table - -Open the `book-list.component.html` and add modify the `p-table` as shown below: - -```html - - - - Actions - Book name - Book type - Publish date - Price - - - - - -
- -
- -
-
- - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - -
-
-``` - -- Added a `th` for the "Actions" column. -- Added `button` with `ngbDropdownToggle` to open actions when clicked the button. - -> We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. - -The final UI looks like: - -![actions-buttons](images/bookstore-actions-buttons.png) - -Update the modal header to change the title based on the current operation: - -```html - -

{%{{{ selectedBook.id ? 'Edit' : 'New Book' }}}%}

-
-``` - -![actions-buttons](images/bookstore-edit-modal.png) - -### Deleting an Existing Book - -#### BooksService - -Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint: - -```js -delete(id: string): Observable { - return this.restService.request({ - method: 'DELETE', - url: `/api/app/book/${id}` - }); -} -``` - -#### DeleteBook Action - -Add an action named `DeleteBook` to `books.actions.ts`: - -```js -export class DeleteBook { - static readonly type = '[Books] Delete'; - constructor(public id: string) {} -} -``` - -Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book: - -```js -import { ... , DeleteBook } from '../actions/books.actions'; -//... -@Action(DeleteBook) -delete(ctx: StateContext, action: DeleteBook) { - return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -- Added `DeleteBook` to the import list. -- Uses `bookService` to delete the book. - -#### Add a Delete Button - -Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: - -```html -
- ... - -
-``` - -The final actions dropdown UI looks like below: - -![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) - -#### Delete Confirmation Dialog - -Open `book-list.component.ts` and inject the `ConfirmationService`. - -```js -import { ConfirmationService } from '@abp/ng.theme.shared'; -//... -constructor( - //... - private confirmationService: ConfirmationService -) -``` - -> `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG. - -Add a delete method to the `BookListComponent`: - -```js -import { ... , DeleteBook } from '../../store/actions'; -import { ... , Toaster } from '@abp/ng.theme.shared'; -//... -delete(id: string, name: string) { - this.confirmationService - .error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') - .subscribe(status => { - if (status === Toaster.Status.confirm) { - this.store.dispatch(new DeleteBook(id)); - } - }); -} -``` - -The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: - -![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) - -### Next Part - -See the [next part](Part-III.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md index 6601bfb938..65a7dc5714 100644 --- a/docs/en/Tutorials/Angular/Part-III.md +++ b/docs/en/Tutorials/Angular/Part-III.md @@ -1,178 +1,6 @@ -## Angular Tutorial - Part III +# Tutorials -### About this Tutorial +## Application Development -This is the third part of the Angular tutorial series. See all parts: - -- [Part I: Create the project and a book list page](Part-I.md) -- [Part II: Create, Update and Delete books](Part-II.md) -- **Part III: Integration Tests (this tutorial)** - -This part covers the **server side** tests. You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). - -### Test Projects in the Solution - -There are multiple test projects in the solution: - -![bookstore-test-projects](images/bookstore-test-projects-v3.png) - -Each project is used to test the related application project. Test projects use the following libraries for testing: - -* [xunit](https://xunit.github.io/) as the main test framework. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. -* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. - -### Adding Test Data - -Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. - -Change the `BookStoreTestDataSeedContributor` class as show below: - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 1", - Type = BookType.Fantastic, - PublishDate = new DateTime(2015, 05, 24), - Price = 21 - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 2", - Type = BookType.Science, - PublishDate = new DateTime(2014, 02, 11), - Price = 15 - } - ); - } - } -} -```` - -* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data. -* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). - -### Testing the BookAppService - -Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: - -````C# -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -```` - -* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. - -Add a new test that creates a valid new book: - -````C# -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -Add a new test that tries to create an invalid book and fails: - -````C# -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -```` - -* Since the `Name` is empty, ABP throws an `AbpValidationException`. - -Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: - -![bookstore-appservice-tests](images/bookstore-test-explorer.png) - -Congratulations, green icons show that tests have been successfully passed! \ No newline at end of file +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png b/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png deleted file mode 100644 index aecf31c1ad..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png deleted file mode 100644 index be05ad3e4a..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png b/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png deleted file mode 100644 index 79bcecb561..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-book-list.png b/docs/en/Tutorials/Angular/images/bookstore-book-list.png deleted file mode 100644 index 3f9717df6e..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png deleted file mode 100644 index 9f01e94121..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png deleted file mode 100644 index c74b37b1b5..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png b/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png deleted file mode 100644 index c911403792..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png b/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png deleted file mode 100644 index 6b0be415c4..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png b/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png deleted file mode 100644 index 0b345ac61d..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png b/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png deleted file mode 100644 index 484837f78a..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png b/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png deleted file mode 100644 index 4af86c50fa..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png b/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png deleted file mode 100644 index 69aaccba31..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png b/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png deleted file mode 100644 index 437c772503..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png b/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png deleted file mode 100644 index 06e9e7d331..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png b/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png deleted file mode 100644 index 32ea91b325..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md index a1fbd1de54..65a7dc5714 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md @@ -1,476 +1,6 @@ -## ASP.NET Core MVC Tutorial - Part I +# Tutorials -### About this Tutorial +## Application Development -In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Entity Framework Core** (EF Core) will be used as the ORM provider as it is the default database provider. - -This is the first part of the ASP.NET Core MVC tutorial series. See all parts: - -- **Part I: Create the project and a book list page (this tutorial)** -- [Part II: Create, Update and Delete books](Part-II.md) -- [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from [the GitHub repository](https://github.com/abpframework/abp/tree/master/samples/BookStore). - -> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. - -### Creating the Project - -Create a new project named `Acme.BookStore`, create the database and run the application by following the [Getting Started document](../../Getting-Started-AspNetCore-MVC-Template.md). - -### Solution Structure - -This is how the layered solution structure looks after it's created: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) - -> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial. - -### Create the Book Entity - -Domain layer in the startup template is separated into two projects: - -- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects. -- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients. - -Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: - -````C# -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - - protected Book() - { - - } - - public Book(Guid id, string name, BookType type, DateTime publishDate, float price) - :base(id) - { - Name = name; - Type = type; - PublishDate = publishDate; - Price = price; - } - } -} -```` - -* ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices. -* `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. -* `Guid` is the **primary key type** of the `Book` entity. - -#### BookType Enum - -Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: - -````C# -namespace Acme.BookStore -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -```` - -#### Add Book Entity to Your DbContext - -EF Core requires you to relate entities with your DbContext. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below: - -````C# - public class BookStoreDbContext : AbpDbContext - { - public DbSet Books { get; set; } - ... - } -```` - -#### Configure Your Book Entity - -Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity: - -````C# -builder.Entity(b => -{ - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); -}); -```` - -#### Add New Migration & Update the Database - -The Startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** (under the *Tools/Nuget Package Manager* menu), select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command: - -![bookstore-pmc-add-book-migration](images/bookstore-pmc-add-book-migration-v2.png) - -This will create a new migration class inside the `Migrations` folder. Then execute the `Update-Database` command to update the database schema: - -```` -PM> Update-Database -```` - -#### Add Sample Data - -`Update-Database` command created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the page: - -![bookstore-books-table](images/bookstore-books-table.png) - -### Create the Application Service - -The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects: - -* `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces. -* `Acme.BookStore.Application` contains the implementations of your application services. - -#### BookDto - -Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: - -````C# -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -```` - -* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details. -* `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. -* `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above. - -It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: - -````csharp -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -#### CreateUpdateBookDto - -Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: - -````c# -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public DateTime PublishDate { get; set; } - - [Required] - public float Price { get; set; } - } -} -```` - -* This DTO class is used to get book information from the user interface while creating or updating a book. -* It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework. - -Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity: - -````csharp -CreateMap(); -```` - -#### IBookAppService - -Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: - -````C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book - { - - } -} -```` - -* Defining interfaces for application services is not required by the framework. However, it's suggested as a best practice. -* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. -* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. - -#### BookAppService - -Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: - -````C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookAppService : - CrudAppService, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -```` - -* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above. -* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md). -* `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected. - -### Auto API Controllers - -You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention. - -#### 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 and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser. - -You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: - -![bookstore-swagger](images/bookstore-swagger.png) - -Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. - -### Dynamic JavaScript Proxies - -It's common to call HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. - -ABP **dynamically** creates JavaScript **proxies** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. - -#### Testing in the Browser Developer Console - -You can easily test the JavaScript proxies using your favorite browser's **Developer Console** now. Run the application, open your browser's **developer tools** (shortcut: F12), switch to the **Console** tab, type the following code and press enter: - -````js -acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); -```` - -* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case). -* `book` is the conventional name for the `BookAppService` (removed AppService postfix and converted to camelCase). -* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed Async postfix and converted to camelCase). -* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object). -* `getList` function returns a `promise`. So, you can pass a callback to the `done` (or `then`) function to get the result from the server. - -Running this code produces the following output: - -![bookstore-test-js-proxy-getlist](images/bookstore-test-js-proxy-getlist.png) - -You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication: - -![bookstore-test-js-proxy-getlist-network](images/bookstore-test-js-proxy-getlist-network.png) - -Let's **create a new book** using the `create` function: - -````js -acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); -```` - -You should see a message in the console something like that: - -```` -successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 -```` - -Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself. - -### Create the Books Page - -It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft. - -Create a new `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project and add a new Razor Page named `Index.cshtml`: - -![bookstore-add-index-page](images/bookstore-add-index-page-v2.png) - -Open the `Index.cshtml` and change the content as shown below: - -````html -@page -@using Acme.BookStore.Web.Pages.Books -@inherits Acme.BookStore.Web.Pages.BookStorePage -@model IndexModel - -

Books

-```` - -* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template and provides some shared properties/methods used by all pages. -* Ensure that the `IndexModel` (*Index.cshtml.cs)* has the `Acme.BookStore.Web.Pages.Books` namespace, or update it in the `Index.cshtml`. - -#### Add Books Page to the Main Menu - -Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method: - -````c# -context.Menu.AddItem( - new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) - .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) -); -```` - -#### Localizing the Menu Items - -Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: - -![bookstore-localization-files](images/bookstore-localization-files-v2.png) - -Open the `en.json` file and add localization texts for `Menu:BookStore` and `Menu:Books` keys to the end of the file: - -````json -{ - "culture": "en", - "texts": { - "Menu:BookStore": "Book Store", - "Menu:Books": "Books" - } -} -```` - -* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](../../Localization.md) for details. -* Localization key names are arbitrary. You can set any name. We prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (ASP.NET Core's standard behavior). - -Run the application and see the new menu item has been added to the top bar: - -![bookstore-menu-items](images/bookstore-menu-items.png) - -When you click to the Books menu item, you are redirected to the new Books page. - -#### Book List - -We will use the [Datatables.net](https://datatables.net/) JQuery plugin to show list of tables on the page. Datatables can completely work via AJAX, it is fast and provides a good user experience. Datatables plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page. - -##### Index.cshtml - -Change the `Pages/Books/Index.cshtml` as following: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@model Acme.BookStore.Web.Pages.Books.IndexModel -@section scripts -{ - -} - - -

@L["Books"]

-
- - - - - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - - -
-```` - -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning** for example. See the [bundling & minification document](../../AspNetCore/Bundling-Minification.md) for details. -* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are many tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help of the intellisense and compile time type checking. See the [tag helpers document](../../AspNetCore/Tag-Helpers/Index.md). -* You can **localize** the column names in the localization file as you did for the menu items above. - -##### Add a Script File - -Create `index.js` JavaScript file under the `Pages/Books/` folder: - -![bookstore-index-js-file](images/bookstore-index-js-file-v2.png) - -`index.js` content is shown below: - -````js -$(function () { - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); -}); -```` - -* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to Datatable's format. -* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the datatables configuration by providing conventional values for missing options. -* `acme.bookStore.book.getList` is the function to get list of books (you have seen it before). -* See [Datatable's documentation](https://datatables.net/manual/) for more configuration options. - -The final UI is shown below: - -![bookstore-book-list](images/bookstore-book-list-2.png) - -### Next Part - -See the [next part](Part-II.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md index 505de55adc..65a7dc5714 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md @@ -1,432 +1,6 @@ -## ASP.NET Core MVC Tutorial - Part II +# Tutorials -### About this Tutorial +## Application Development -This is the second part of the ASP.NET Core MVC tutorial series. See all parts: - -* [Part I: Create the project and a book list page](Part-I.md) -* **Part II: Create, Update and Delete books (this tutorial)** -* [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore). - -> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. - -### Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. The result dialog will be like that: - -![bookstore-create-dialog](images/bookstore-create-dialog-2.png) - -#### Create the Modal Form - -Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: - -![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png) - -##### CreateModal.cshtml.cs - -Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code: - -````C# -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -```` - -* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties/methods those can be used by your page model classes. -* `[BindProperty]` attribute on the `Book` property binds post request data to this property. -* This class simply injects the `IBookAppService` in its constructor and calls the `CreateAsync` method in the `OnPostAsync` handler. - -##### CreateModal.cshtml - -Open the `CreateModal.cshtml` file and paste the code below: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model Acme.BookStore.Web.Pages.Books.CreateModalModel -@{ - Layout = null; -} - - - - - - - - - -```` - -* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the `CreateBookViewModel` class. - * `abp-model` attribute indicates the model object, the `Book` property in this case. - * `data-ajaxForm` attribute makes the form submitting via AJAX, instead of a classic page post. - * `abp-form-content` tag helper is a placeholder to render the form controls (this is optional and needed only if you added some other content in the `abp-dynamic-form` tag, just like in this page). - -#### Add the "New book" Button - -Open the `Pages/Books/Index.cshtml` and change the `abp-card-header` tag as shown below: - -````html - - - -

@L["Books"]

-
- - - -
-
-```` - -Just added a **New book** button to the **top right** of the table: - -![bookstore-new-book-button](images/bookstore-new-book-button.png) - -Open the `pages/books/index.js` and add the following code just after the datatable configuration: - -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` - -* `abp.ModalManager` is a helper class to open and manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API. - -Now, you can **run the application** and add new books using the new modal form. - -### Updating An Existing Book - -Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: - -![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png) - -#### EditModal.cshtml.cs - -Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -```` - -* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. Used `SupportsGet` to be able to get Id value from query string parameter of the request. -* Mapped `BookDto` (received from the `BookAppService.GetAsync`) to `CreateUpdateBookDto` in the `GetAsync` method. -* The `OnPostAsync` simply uses `BookAppService.UpdateAsync` to update the entity. - -#### BookDto to CreateUpdateBookDto Mapping - -In order to perform `BookDto` to `CreateUpdateBookDto` object mapping, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below: - -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -* Just added `CreateMap();` as the mapping definition. - -#### EditModal.cshtml - -Replace `EditModal.cshtml` content with the following content: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Acme.BookStore.Web.Pages.Books -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@{ - Layout = null; -} - - - - - - - - - - -```` - -This page is very similar to the `CreateModal.cshtml` except; - -* It includes an `abp-input` for the `Id` property to store id of the editing book (which is a hidden input). -* It uses `Books/EditModal` as the post URL and *Update* text as the modal header. - -#### Add "Actions" Dropdown to the Table - -We will add a dropdown button ("Actions") for each row of the table. The final UI looks like this: - -![bookstore-books-table-actions](images/bookstore-books-table-actions.png) - -Open the `Pages/Books/Index.cshtml` page and change the table section as shown below: - -````html - - - - @L["Actions"] - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - -```` - -* Just added a new `th` tag for the "Actions". - -Open the `pages/books/index.js` and replace the content as below: - -````js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server side. -* Added a new `ModalManager` named `createModal` to open the create modal dialog. -* Added a new `ModalManager` named `editModal` to open the edit modal dialog. -* Added a new column at the beginning of the `columnDefs` section. This column is used for the "Actions" dropdown button. -* "New Book" action simply calls `createModal.open` to open the create dialog. -* "Edit" action simply calls `editModal.open` to open the edit dialog. -` -You can run the application and edit any book by selecting the edit action. - -### Deleting an Existing Book - -Open the `pages/books/index.js` and add a new item to the `rowAction` `items`: - -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* `confirmMessage` option is used to ask a confirmation question before executing the `action`. -* Used `acme.bookStore.book.delete` javascript proxy function to perform an AJAX request to delete a book. -* `abp.notify.info` is used to show a toastr notification just after the deletion. - -The final `index.js` content is shown below: - -````js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following line: - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", -"SuccessfullyDeleted": "Successfully deleted" -```` - -Run the application and try to delete a book. - -### Next Part - -See the [next part](Part-III.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md index f207d9df0c..65a7dc5714 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md @@ -1,166 +1,6 @@ -## ASP.NET Core MVC Tutorial - Part III +# Tutorials -### About this Tutorial +## Application Development -This is the third part of the ASP.NET Core MVC tutorial series. See all parts: - -- [Part I: Create the project and a book list page](Part-I.md) -- [Part II: Create, Update and Delete books](Part-II.md) -- **Part III: Integration Tests (this tutorial)** - -You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore). - -> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. - -### Test Projects in the Solution - -There are multiple test projects in the solution: - -![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png) - -Each project is used to test the related application project. Test projects use the following libraries for testing: - -* [xunit](https://xunit.github.io/) as the main test framework. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. -* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. - -### Adding Test Data - -Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. - -Change the `BookStoreTestDataSeedContributor` class as show below: - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _bookRepository.InsertAsync( - new Book(_guidGenerator.Create(), "Test book 1", BookType.Fantastic, new DateTime(2015, 05, 24), 21) - ); - - await _bookRepository.InsertAsync( - new Book(_guidGenerator.Create(), "Test book 2", BookType.Science, new DateTime(2014, 02, 11), 15) - ); - } - } -} -```` - -* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data. -* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). - -### Testing the BookAppService - -Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: - -````C# -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -```` - -* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. - -Add a new test that creates a valid new book: - -````C# -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -Add a new test that tries to create an invalid book and fails: - -````C# -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -```` - -* Since the `Name` is empty, ABP throws an `AbpValidationException`. - -Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: - -![bookstore-appservice-tests](images/bookstore-appservice-tests.png) - -Congratulations, green icons show that tests have been successfully passed! \ No newline at end of file +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png deleted file mode 100644 index a7d49a661b..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png deleted file mode 100644 index f531e6f457..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png deleted file mode 100644 index 7cc96c8c94..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png deleted file mode 100644 index 79314dd2dc..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png deleted file mode 100644 index dfd4b5d8aa..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png deleted file mode 100644 index edf2826361..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png deleted file mode 100644 index 437c772503..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png and /dev/null differ diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md new file mode 100644 index 0000000000..20b64f3189 --- /dev/null +++ b/docs/en/Tutorials/Part-1.md @@ -0,0 +1,1117 @@ +## ASP.NET Core {{UI_Value}} Tutorial - Part 1 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### About this tutorial: + +In this tutorial series, you will build an ABP Commercial application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used. + +The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts: + +- **Part-1: Creating the project and book list page (this tutorial)** +- [Part-2: Creating, updating and deleting books](part-2.md) +- [Part-3: Integration tests](part-3.md) + +*You can also check out [the video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by the community, based on this tutorial.* + +### Creating the project + +Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI. But first of all, we need to login to the ABP Platform to create a commercial project. + +#### Create the project + +By running the below command, it creates a new ABP Commercial project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. + +```bash +abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} +``` +![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) + +### Apply migrations + +After creating the project, you need to apply the initial migrations and create the database. To apply migrations, right click on the `Acme.BookStore.DbMigrator` and click **Debug** > **Start New Instance**. This will run the application and apply all migrations. You will see the below result when it successfully completes the process. The application database is ready! + +![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png) + +> Alternatively, you can run `Update-Database` command in the Visual Studio > Package Manager Console to apply migrations. + +#### Initial database tables + +![Initial database tables](./images/bookstore-database-tables-{{DB}}.png) + +### Run the application + +To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}} + +Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application + +![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) + +{{if UI == "NG"}} + +To start Angular project, go to the `angular` folder, open a command line terminal, execute the `yarn` command: + +```bash +yarn +``` + +Once all node modules are loaded, execute the `yarn start` command: + +```bash +yarn start +``` + +The website will be accessible from the following default URL: + +http://localhost:4200/ + +If you see the website's landing page successfully, you can exit Angular hosting by pressing `ctrl-c`. (We'll later start it again.) + +> Be aware that, Firefox does not use the Windows Certificate Store, so you'll need to add the self-signed developer certificate to Firefox manually. To do this, open Firefox and navigate to the below URL: +> +> https://localhost:44322/api/abp/application-configuration +> +> If you see the below screen, click the **Accept the Risk and Continue** button to bypass this warning. +> +> ![Set as startup project](./images/mozilla-self-signed-cert-error.png) + +{{end}} + +The default login credentials are; + +* **Username**: admin +* **Password**: 1q2w3E* + +### Solution structure + +This is how the layered solution structure looks like: + +![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png) + +Check out the [solution structure](../startup-templates/application#solution-structure) section to understand the structure in details. + +### Create the book entity + +Domain layer in the startup template is separated into two projects: + +- `Acme.BookStore.Domain` contains your [entities](https://docs.abp.io/en/abp/latest/Entities), [domain services](https://docs.abp.io/en/abp/latest/Domain-Services) and other core domain objects. +- `Acme.BookStore.Domain.Shared` contains `constants`, `enums` or other domain related objects those can be shared with clients. + +Define [entities](https://docs.abp.io/en/abp/latest/Entities) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: + +````csharp +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Acme.BookStore +{ + public class Book : AuditedAggregateRoot + { + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + + protected Book() + { + + } + + public Book(Guid id, string name, BookType type, DateTime publishDate, float price) : + base(id) + { + Name = name; + Type = type; + PublishDate = publishDate; + Price = price; + } + } +} +```` + +* ABP has 2 fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](https://docs.abp.io/en/abp/latest/Entities) for details and best practices. +* `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. +* `Guid` is the **primary key type** of the `Book` entity. + +#### BookType enum + +Create the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: + +````csharp +namespace Acme.BookStore +{ + public enum BookType + { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry + } +} +```` + +#### Add book entity to the DbContext + +{{if DB == "ef"}} + +EF Core requires to relate entities with your `DbContext`. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below: + +````csharp + public class BookStoreDbContext : AbpDbContext + { + public DbSet Users { get; set; } + public DbSet Books { get; set; } //<--added this line--> + //... + } +```` + +{{end}} + +{{if DB == "mongodb"}} + +Add a `IMongoCollection Books` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project: + +```csharp +public class BookStoreMongoDbContext : AbpMongoDbContext +{ + public IMongoCollection Users => Collection(); + public IMongoCollection Books => Collection();//<--added this line--> + //... +} +``` + +{{end}} + +{{if DB == "ef"}} + +#### Configure the book entity + +Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity: + +````csharp +builder.Entity(b => +{ + b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); + b.ConfigureByConvention(); //auto configure for the base class props + b.Property(x => x.Name).IsRequired().HasMaxLength(128); +}); +```` + +Add the `using Volo.Abp.EntityFrameworkCore.Modeling;` statement to resolve `ConfigureByConvention` extension method. + +{{end}} + +{{if DB == "mongodb"}} + +#### Add seed (sample) data + +Adding sample data is optional, but it's good to have initial data in the database for the first run. ABP provides a [data seed system](https://docs.abp.io/en/abp/latest/Data-Seeding). Create a class deriving from the `IDataSeedContributor` in the `*.Domain` project: + +```csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book( + id: _guidGenerator.Create(), + name: "1984", + type: BookType.Dystopia, + publishDate: new DateTime(1949, 6, 8), + price: 19.84f + ) + ); + + await _bookRepository.InsertAsync( + new Book( + id: _guidGenerator.Create(), + name: "The Hitchhiker's Guide to the Galaxy", + type: BookType.ScienceFiction, + publishDate: new DateTime(1995, 9, 27), + price: 42.0f + ) + ); + } + } +} +``` + +{{end}} + +{{if DB == "ef"}} + +#### Add new migration & update the database + +The startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** under the menu *Tools > NuGet Package Manager*. + +![Open Package Manager Console](./images/bookstore-open-package-manager-console.png) + +Select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command: + +```bash +Add-Migration "Created_Book_Entity" +``` + +![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) + +This will create a new migration class inside the `Migrations` folder of the `Acme.BookStore.EntityFrameworkCore.DbMigrations` project. Then execute the `Update-Database` command to update the database schema: + +````bash +Update-Database +```` + +![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png) + +#### Add initial (sample) data + +`Update-Database` command has created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the listing page. + +```mssql +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84') + +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42') + +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7') +``` + +![bookstore-books-table](./images/bookstore-books-table.png) + +{{end}} + +### Create the application service + +The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects: + +* `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces. +* `Acme.BookStore.Application` contains the implementations of your application services. + +#### BookDto + +Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore +{ + public class BookDto : AuditedEntityDto + { + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + } +} +```` + +* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) for more details. +* `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. +* `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above. + +It will be needed to map `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. The startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: + +````csharp +using AutoMapper; + +namespace Acme.BookStore +{ + public class BookStoreApplicationAutoMapperProfile : Profile + { + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +#### CreateUpdateBookDto + +Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using System.ComponentModel.DataAnnotations; + +namespace Acme.BookStore +{ + public class CreateUpdateBookDto + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + [Required] + public BookType Type { get; set; } = BookType.Undefined; + + [Required] + public DateTime PublishDate { get; set; } + + [Required] + public float Price { get; set; } + } +} +```` + +* This `DTO` class is used to get book information from the user interface while creating or updating a book. +* It defines data annotation attributes (like `[Required]`) to define validations for the properties. `DTO`s are [automatically validated](https://docs.abp.io/en/abp/latest/Validation) by the ABP framework. + +Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity with the `CreateMap();` command: + +````csharp +using AutoMapper; + +namespace Acme.BookStore +{ + public class BookStoreApplicationAutoMapperProfile : Profile + { + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + CreateMap(); //<--added this line--> + } + } +} +```` + +#### IBookAppService + +Create an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore +{ + public interface IBookAppService : + ICrudAppService< //Defines CRUD methods + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books + CreateUpdateBookDto, //Used to create a new book + CreateUpdateBookDto> //Used to update a book + { + + } +} +```` + +* Defining interfaces for the application services **are not required** by the framework. However, it's suggested as a best practice. +* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. +* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. + +#### BookAppService + +Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class BookAppService : + CrudAppService, + IBookAppService + { + public BookAppService(IRepository repository) + : base(repository) + { + + } + } +} +```` + +* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD (create, read, update, delete) methods defined above. +* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](https://docs.abp.io/en/abp/latest/Repositories). +* `BookAppService` uses `IObjectMapper` to map `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. We have defined the mappings before, so it will work as expected. + +### Auto API Controllers + +We normally create **Controllers** to expose application services as **HTTP API** endpoints. This allows browsers or 3rd-party clients to call them via AJAX. ABP can [**automagically**](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) configures your application services as MVC API Controllers by convention. + +#### 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.) + +You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: + +![bookstore-swagger](./images/bookstore-swagger.png) + +Swagger has a nice interface to test the APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. + +{{if UI == "MVC"}} + +### Dynamic JavaScript proxies + +It's common to call HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. + +ABP **dynamically** creates JavaScript **proxies** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. + +#### Testing in developer console of the browser + +You can easily test the JavaScript proxies using your favorite browser's **Developer Console**. Run the application, open your browser's **developer tools** (*shortcut is F12 for Chrome*), switch to the **Console** tab, type the following code and press enter: + +````js +acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); +```` + +* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case). +* `book` is the conventional name for the `BookAppService` (removed `AppService` postfix and converted to camelCase). +* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed `Async` postfix and converted to camelCase). +* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object). +* `getList` function returns a `promise`. You can pass a callback to the `done` (or `then`) function to get the result from the server. + +Running this code produces the following output: + +![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png) + +You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication: + +![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png) + +Let's **create a new book** using the `create` function: + +````js +acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); +```` + +You should see a message in the console something like that: + +````text +successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 +```` + +Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself. + +### Create the books page + +It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft. + +Create `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project. Add a new Razor Page by right clicking the Books folder then selecting **Add > Razor Page** menu item. Name it as `Index`: + +![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) + +Open the `Index.cshtml` and change the whole content as shown below: + +**Index.cshtml:** + +````html +@page +@using Acme.BookStore.Web.Pages.Books +@inherits Acme.BookStore.Web.Pages.BookStorePage +@model IndexModel + +

Books

+```` + +* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template, provides some shared properties/methods used by all pages. + +* Set the `IndexModel`'s namespace to `Acme.BookStore.Pages.Books` in `Index.cshtml.cs`. + + + +**Index.cshtml.cs:** + +```csharp +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} +``` + +#### Add books page to the main menu + +Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method: + +````csharp +//... +namespace Acme.BookStore.Web.Menus +{ + public class BookStoreMenuContributor : IMenuContributor + { + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + //<-- added the below code + context.Menu.AddItem( + new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) + .AddItem( + new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books") + ) + ); + //--> + } + } +} +```` + +{{end}} + +#### Localize the menu items + +Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: + +![bookstore-localization-files](./images/bookstore-localization-files-v2.png) + +Open the `en.json` (*English translations*) file and add the below localization texts to the end of the file: + +````json +{ + "Culture": "en", + "Texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", + + "Menu:BookStore": "Book Store", + "Menu:Books": "Books", + "Actions": "Actions", + "Edit": "Edit", + "PublishDate": "Publish date", + "NewBook": "New book", + "Name": "Name", + "Type": "Type", + "Price": "Price", + "CreationTime": "Creation time", + "AreYouSureToDelete": "Are you sure you want to delete this item?" + } +} +```` + +* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](https://docs.abp.io/en/abp/latest/Localization) for details. +* Localization key names are arbitrary. You can set any name. As a best practice, we prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (as ASP.NET Core's standard behavior). + +{{if UI == "MVC"}} + +Run the project, login to the application with the username `admin` and password `1q2w3E*` and see the new menu item has been added to the menu. + +![bookstore-menu-items](./images/bookstore-new-menu-item.png) + +When you click to the Books menu item under the Book Store parent, you are being redirected to the new Books page. + +#### Book list + +We will use the [Datatables.net](https://datatables.net/) jQuery plugin to show the book list. [Datatables](https://datatables.net/) can completely work via AJAX, it is fast, popular and provides a good user experience. [Datatables](https://datatables.net/) plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page. + +##### Index.cshtml + +Change the `Pages/Books/Index.cshtml` as following: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@model Acme.BookStore.Web.Pages.Books.IndexModel +@section scripts +{ + +} + + +

@L["Books"]

+
+ + + + + @L["Name"] + @L["Type"] + @L["PublishDate"] + @L["Price"] + @L["CreationTime"] + + + + +
+```` + +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning**. See the [bundling & minification document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification) for details. +* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are other useful tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help the of IntelliSense and compile time type checking. Further information, see the [tag helpers](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Index) document. +* You can **localize** the column names in the localization file as you did for the menu items above. + +##### Add a Script File + +Create `index.js` JavaScript file under the `Pages/Books/` folder: + +![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png) + +`index.js` content is shown below: + +````js +$(function () { + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); +}); +```` + +* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to [Datatable](https://datatables.net/)'s format. +* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the [Datatables](https://datatables.net/) configuration by providing conventional values for missing options. +* `acme.bookStore.book.getList` is the function to get list of books (as described in [dynamic JavaScript proxies](#dynamic-javascript-proxies)). +* See [Datatables documentation](https://datatables.net/manual/) for all configuration options. + +It's end of this part. The final UI of this work is shown as below: + +![Book list](./images/bookstore-book-list-2.png) + +{{end}} + +{{if UI == "NG"}} + +### Angular development +#### Create the books page + +It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application: + +- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services. +- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library. +- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. +- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). + +#### Install NPM packages + +Open a new command line interface (terminal window) and go to your `angular` folder and then run `yarn` command to install NPM packages: + +```bash +yarn +``` + +#### BooksModule + +Run the following command line to create a new module, named `BooksModule`: + +```bash +yarn ng generate module books --route books --module app.module +``` + +![Generating books module](./images/bookstore-creating-books-module-terminal.png) + +#### Routing + +Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and replace `books` path as shown below + +```js +import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== + +//...replaced original books path with the below +{ + path: 'books', + component: ApplicationLayoutComponent, + loadChildren: () => import('./books/books.module').then(m => m.BooksModule), + data: { + routes: { + name: '::Menu:Books', + iconClass: 'fas fa-book' + } as ABP.Route + }, +} +``` + +* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item. + +Run `yarn start` and wait for Angular to serve the application: + +```bash +yarn start +``` + +Open the browser and navigate to http://localhost:4200/books. You'll see a blank page saying "*books works!*". + +![initial-books-page](./images/bookstore-initial-books-page-with-layout.png) + +#### Book list component + +Replace the `books.component.html` in the `app\books` folder with the following content: + +```html + +``` + +Then run the command below on the terminal in the root folder to generate a new component, named book-list: + +```bash +yarn ng generate component books/book-list +``` + +![Creating books list](./images/bookstore-creating-book-list-terminal.png) + +Open `books.module.ts` file in the `app\books` folder and replace the content as below: + +```js +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BooksRoutingModule } from './books-routing.module'; +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; +import { SharedModule } from '../shared/shared.module'; //<== added this line ==> + +@NgModule({ + declarations: [BooksComponent, BookListComponent], + imports: [ + CommonModule, + BooksRoutingModule, + SharedModule, //<== added this line ==> + ] +}) +export class BooksModule { } +``` + +* We imported `SharedModule` and added to `imports` array. + +Open `books-routing.module.ts` file in the `app\books` folder and replace the content as below: + +```js +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==> + +//<== replaced routes ==> +const routes: Routes = [ + { + path: '', + component: BooksComponent, + children: [{ path: '', component: BookListComponent }], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class BooksRoutingModule { } +``` + +* We imported `BookListComponent` and replaced `routes` const. + +We'll see **book-list works!** text on the books page: + +![Initial book list page](./images/bookstore-initial-book-list-page.png) + +#### Create BooksState + +Run the following command in the terminal to create a new state, named `BooksState`: + +![Initial book list page](./images/bookstore-generate-state-books.png) + +```bash +yarn ng generate ngxs-schematic:state books +``` + +* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state. + +#### Get books data from backend + +Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format). + +![BookDto properties](./images/bookstore-swagger-book-dto-properties.png) + +Open the `books.ts` file in the `app\store\models` folder and replace the content as below: + +```js +export namespace Books { + export interface State { + books: Response; + } + + export interface Response { + items: Book[]; + totalCount: number; + } + + export interface Book { + name: string; + type: BookType; + publishDate: string; + price: number; + lastModificationTime: string; + lastModifierId: string; + creationTime: string; + creatorId: string; + id: string; + } + + export enum BookType { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry, + } +} +``` + +* Added `Book` interface that represents a book object and `BookType` enum which represents a book category. + +#### BooksService + +Create a new service, named `BooksService` to perform `HTTP` calls to the server: + +```bash +yarn ng generate service books/shared/books +``` + +![service-terminal-output](./images/bookstore-service-terminal-output.png) + +Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: + +```js +import { Injectable } from '@angular/core'; +import { RestService } from '@abp/ng.core'; +import { Books } from '../../store/models'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class BooksService { + constructor(private restService: RestService) {} + + get(): Observable { + return this.restService.request({ + method: 'GET', + url: '/api/app/book' + }); + } +} +``` + +* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint. + +Open the`books.actions.ts` file in `app\store\actions` folder and replace the content below: + +```js +export class GetBooks { + static readonly type = '[Books] Get'; +} +``` + +#### Implement BooksState + +Open the `books.state.ts` file in `app\store\states` folder and replace the content below: + +```js +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks } from '../actions/books.actions'; +import { Books } from '../models/books'; +import { BooksService } from '../../books/shared/books.service'; +import { tap } from 'rxjs/operators'; + +@State({ + name: 'BooksState', + defaults: { books: {} } as Books.State, +}) +export class BooksState { + @Selector() + static getBooks(state: Books.State) { + return state.books.items || []; + } + + constructor(private booksService: BooksService) {} + + @Action(GetBooks) + get(ctx: StateContext) { + return this.booksService.get().pipe( + tap(booksResponse => { + ctx.patchState({ + books: booksResponse, + }); + }), + ); + } +} +``` + +* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state. +* `NGXS` requires to return the observable without subscribing it in the get function. + +#### BookListComponent + +Open the `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + loading = false; + + constructor(private store: Store) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } +} +``` + +* We added the `get` function that updates store to get the books. +* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features. + +Open the `book-list.component.html` file in `app\books\book-list` folder and replace the content as below: + +```html +
+
+
+
+
+ {%{{{ "::Menu:Books" | abpLocalization }}}%} +
+
+
+
+
+
+ + + + + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + + +
+
+``` + +* We added HTML code of book list page. + +Now you can see the final result on your browser: + +![Book list final result](./images/bookstore-book-list.png) + +The file system structure of the project: + +![Book list final result](./images/bookstore-angular-file-tree.png) + +In this tutorial we have applied the rules of official [Angular Style Guide](https://angular.io/guide/styleguide#file-tree). + +{{end}} + +### Next Part + +See the [part 2](part-2.md) for creating, updating and deleting books. diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md new file mode 100644 index 0000000000..08e17e0e3f --- /dev/null +++ b/docs/en/Tutorials/Part-2.md @@ -0,0 +1,1446 @@ +## ASP.NET Core {{UI_Value}} Tutorial - Part 2 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` + +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### About this tutorial + +This is the second part of the ASP.NET Core {{UI_Value}} tutorial series. All parts: + +* [Part I: Creating the project and book list page](part-1.md) +* **Part II: Creating, updating and deleting books (this tutorial)** +* [Part III: Integration tests](part-3.md) + +*You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial.* + +{{if UI == "MVC"}} + +### Creating a new book + +In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like in the below image: + +![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) + +#### Create the modal form + +Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project. + +![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) + +##### CreateModal.cshtml.cs + +Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code: + +````C# +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class CreateModalModel : BookStorePageModel + { + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } + } +} +```` + +* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties & methods that can be used in your page model classes. +* `[BindProperty]` attribute on the `Book` property binds post request data to this property. +* This class simply injects the `IBookAppService` in the constructor and calls the `CreateAsync` method in the `OnPostAsync` handler. + +##### CreateModal.cshtml + +Open the `CreateModal.cshtml` file and paste the code below: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Acme.BookStore.Web.Pages.Books.CreateModalModel +@{ + Layout = null; +} + + + + + + + + + +```` + +* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the model `CreateBookViewModel`. + * `abp-model` attribute indicates the model object where it's the `Book` property in this case. + * `data-ajaxForm` attribute sets the form to submit via AJAX, instead of a classic page post. + * `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page). + +#### Add the "New book" button + +Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag as below: + +````html + + + +

@L["Books"]

+
+ + + +
+
+```` + +This adds a new button called **New book** to the **top-right** of the table: + +![bookstore-new-book-button](./images/bookstore-new-book-button.png) + +Open the `pages/books/index.js` and add the following code just after the `Datatable` configuration: + +````js +var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + +createModal.onResult(function () { + dataTable.ajax.reload(); +}); + +$('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); +}); +```` + +* `abp.ModalManager` is a helper class to manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API. + +Now, you can **run the application** and add new books using the new modal form. + +### Updating a book + +Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: + +![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) + +#### EditModal.cshtml.cs + +Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code: + +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class EditModalModel : BookStorePageModel + { + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public EditModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var bookDto = await _bookAppService.GetAsync(Id); + Book = ObjectMapper.Map(bookDto); + } + + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync(Id, Book); + return NoContent(); + } + } +} +```` + +* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get `Id` value from query string parameter of the request. +* In the `GetAsync` method, we get `BookDto `from `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto`. +* The `OnPostAsync` uses `BookAppService.UpdateAsync()` to update the entity. + +#### Mapping from BookDto to CreateUpdateBookDto + +To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below: + +````csharp +using AutoMapper; + +namespace Acme.BookStore.Web +{ + public class BookStoreWebAutoMapperProfile : Profile + { + public BookStoreWebAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +* We have just added `CreateMap();` to define this mapping. + +#### EditModal.cshtml + +Replace `EditModal.cshtml` content with the following content: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@using Acme.BookStore.Web.Pages.Books +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@{ + Layout = null; +} + + + + + + + + + + +```` + +This page is very similar to the `CreateModal.cshtml`, except: + +* It includes an `abp-input` for the `Id` property to store `Id` of the editing book (which is a hidden input). +* It uses `Books/EditModal` as the post URL and *Update* text as the modal header. + +#### Add "Actions" dropdown to the table + +We will add a dropdown button to the table named *Actions*. + +Open the `Pages/Books/Index.cshtml` page and change the `` section as shown below: + +````html + + + + @L["Actions"] + @L["Name"] + @L["Type"] + @L["PublishDate"] + @L["Price"] + @L["CreationTime"] + + + +```` + +* We just added a new `th` tag for the "*Actions*" button. + +Open the `pages/books/index.js` and replace the content as below: + +````js +$(function () { + + var l = abp.localization.getResource('BookStore'); + + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + searching: false, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + } + ] + } + }, + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server-side. +* Added a new `ModalManager` named `createModal` to open the create modal dialog. +* Added a new `ModalManager` named `editModal` to open the edit modal dialog. +* Added a new column at the beginning of the `columnDefs` section. This column is used for the "*Actions*" dropdown button. +* "*New Book*" action simply calls `createModal.open()` to open the create dialog. +* "*Edit*" action simply calls `editModal.open()` to open the edit dialog. + +You can run the application and edit any book by selecting the edit action. The final UI looks as below: + +![bookstore-books-table-actions](./images/bookstore-edit-button.png) + +### Deleting a book + +Open the `pages/books/index.js` and add a new item to the `rowAction` `items`: + +````js +{ + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } +} +```` + +* `confirmMessage` option is used to ask a confirmation question before executing the `action`. +* `acme.bookStore.book.delete()` method makes an AJAX request to JavaScript proxy function to delete a book. +* `abp.notify.info()` shows a notification after the delete operation. + +The final `index.js` content is shown below: + +````js +$(function () { + + var l = abp.localization.getResource('BookStore'); + + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + processing: true, + serverSide: true, + paging: true, + searching: false, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following translations: + +````json +"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", +"SuccessfullyDeleted": "Successfully deleted" +```` + +Run the application and try to delete a book. + +{{end}} + +{{if UI == "NG"}} + +### Creating a new book + +In this section, you will learn how to create a new modal dialog form to create a new book. + +#### Type definition + +Open `books.ts` file in `app\store\models` folder and replace the content as below: + +```js +export namespace Books { + export interface State { + books: Response; + } + + export interface Response { + items: Book[]; + totalCount: number; + } + + export interface Book { + name: string; + type: BookType; + publishDate: string; + price: number; + lastModificationTime: string; + lastModifierId: string; + creationTime: string; + creatorId: string; + id: string; + } + + export enum BookType { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry, + } + + //<== added CreateUpdateBookInput interface ==> + export interface CreateUpdateBookInput { + name: string; + type: BookType; + publishDate: string; + price: number; + } +} +``` + +* We added `CreateUpdateBookInput` interface. +* You can see the properties of this interface from Swagger UI. +* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend. + +#### Service method + +Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: + +```js +import { Injectable } from '@angular/core'; +import { RestService } from '@abp/ng.core'; +import { Books } from '../../store/models'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class BooksService { + constructor(private restService: RestService) {} + + get(): Observable { + return this.restService.request({ + method: 'GET', + url: '/api/app/book' + }); + } + + //<== added create method ==> + create(createBookInput: Books.CreateUpdateBookInput): Observable { + return this.restService.request({ + method: 'POST', + url: '/api/app/book', + body: createBookInput + }); + } +} +``` + +- We added the `create` method to perform an HTTP Post request to the server. +- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). + +#### State definitions + +Open `books.action.ts` in `app\store\actions` folder and replace the content as below: + +```js +import { Books } from '../models'; //<== added this line ==> + +export class GetBooks { + static readonly type = '[Books] Get'; +} + +//added CreateUpdateBook class +export class CreateUpdateBook { + static readonly type = '[Books] Create Update Book'; + constructor(public payload: Books.CreateUpdateBookInput) { } +} +``` + +* We imported the Books namespace and created the `CreateUpdateBook` action. + +Open `books.state.ts` file in `app\store\states` and replace the content as below: + +```js +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== added CreateUpdateBook==> +import { Books } from '../models/books'; +import { BooksService } from '../../books/shared/books.service'; +import { tap } from 'rxjs/operators'; + +@State({ + name: 'BooksState', + defaults: { books: {} } as Books.State, +}) +export class BooksState { + @Selector() + static getBooks(state: Books.State) { + return state.books.items || []; + } + + constructor(private booksService: BooksService) { } + + @Action(GetBooks) + get(ctx: StateContext) { + return this.booksService.get().pipe( + tap(booksResponse => { + ctx.patchState({ + books: booksResponse, + }); + }), + ); + } + + //added CreateUpdateBook action listener + @Action(CreateUpdateBook) + save(ctx: StateContext, action: CreateUpdateBook) { + return this.booksService.create(action.payload); + } +} +``` + +* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book. + +When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BooksService`. + +#### Add a modal to BookListComponent + +Open `book-list.component.html` file in `books\book-list` folder and replace the content as below: + +```html +
+
+
+
+
+ {%{{{ '::Menu:Books' | abpLocalization }}}%} +
+
+ +
+
+ +
+
+
+
+
+ + + + + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + + +
+
+ + + + +

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

+
+ + + + + + +
+``` + +* We added the `abp-modal` which renders a modal to allow user to create a new book. +* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. +* We added `New book` button to the `AbpContentToolbar`. + +Open `book-list.component.` file in `books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + loading = false; + + isModalOpen = false; //<== added this line ==> + + constructor(private store: Store) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + //added createBook method + createBook() { + this.isModalOpen = true; + } +} +``` + +* We added `isModalOpen = false` and `createBook` method. + +You can open your browser and click **New book** button to see the new modal. + +![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) + +#### Create a reactive form + +[Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. + +Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==> + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + createBook() { + this.buildForm(); //<== added this line ==> + this.isModalOpen = true; + } + + //added buildForm method + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } +} +``` + +* We imported `FormGroup, FormBuilder and Validators`. +* We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. +* We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book. + * The `group` method of `FormBuilder`, `fb` creates a `FormGroup`. + * Added `Validators.required` static method which validates the relevant form element. + +#### Create the DOM elements of the form + +Open `book-list.component.html` in `app\books\book-list` folder and replace ` ` with the following code part: + +```html + +
+
+ * + +
+ +
+ * + +
+ +
+ * + +
+ +
+ * + +
+
+
+``` + +- This template creates a form with `Name`, `Price`, `Type` and `Publish` date fields. +- We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. + +#### Datepicker requirements + +Open `books.module.ts` file in `app\books` folder and replace the content as below: + +```js +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BooksRoutingModule } from './books-routing.module'; +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; +import { SharedModule } from '../shared/shared.module'; +import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> + +@NgModule({ + declarations: [BooksComponent, BookListComponent], + imports: [ + CommonModule, + BooksRoutingModule, + SharedModule, + NgbDatepickerModule //<== added this line ==> + ] +}) +export class BooksModule { } +``` + +* We imported `NgbDatepickerModule` to be able to use the date picker. + + + +Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==> +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + //added bookTypeArr array + bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } +} +``` + +* We imported ` NgbDateNativeAdapter, NgbDateAdapter` + +* We added a new provider `NgbDateAdapter` that converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. + +* We added `bookTypeArr` array to be able to use it in the combobox values. The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: + + ```js + ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] + ``` + + This array was used in the previous form template in the `ngFor` loop. + +Now, you can open your browser to see the changes: + + +![New book modal](./images/bookstore-new-book-form.png) + +#### Saving the book + +Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book. + +```html + + + + + + +``` + +* This adds a save button to the bottom area of the modal: + +![Save button to the modal](./images/bookstore-new-book-form-v2.png) + +Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==> +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + //<== added save ==> + save() { + if (this.form.invalid) { + return; + } + + this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.get(); + }); + } +} +``` + +* We imported `CreateUpdateBook`. +* We added `save` method + +### Updating an existing book + +#### BooksService + +Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods. + +```js +getById(id: string): Observable { + return this.restService.request({ + method: 'GET', + url: `/api/app/book/${id}` + }); +} + +update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { + return this.restService.request({ + method: 'PUT', + url: `/api/app/book/${id}`, + body: updateBookInput + }); +} +``` + +#### CreateUpdateBook action + +Open the `books.actions.ts` in `app\store\actions` folder and replace the content as below: + +```js +import { Books } from '../models'; + +export class GetBooks { + static readonly type = '[Books] Get'; +} + +export class CreateUpdateBook { + static readonly type = '[Books] Create Update Book'; + constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==> +} +``` + +* We added `id` parameter to the `CreateUpdateBook` action's constructor. + +Open the `books.state.ts` in `app\store\states` folder and replace the `save` method as below: + +```js +@Action(CreateUpdateBook) +save(ctx: StateContext, action: CreateUpdateBook) { + if (action.id) { + return this.booksService.update(action.payload, action.id); + } else { + return this.booksService.create(action.payload); + } +} +``` + +#### BookListComponent + +Open `book-list.component.ts` in `app\books\book-list` folder and inject `BooksService` dependency by adding it to the constructor and add a variable named `selectedBook`. + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks, CreateUpdateBook } from '../../store/actions'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { BooksService } from '../shared/books.service'; //<== imported BooksService ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + selectedBook = {} as Books.Book; //<== declared selectedBook ==> + + constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + //<== this method is replaced ==> + createBook() { + this.selectedBook = {} as Books.Book; //<== added ==> + this.buildForm(); + this.isModalOpen = true; + } + + //<== added editBook method ==> + editBook(id: string) { + this.booksService.getById(id).subscribe(book => { + this.selectedBook = book; + this.buildForm(); + this.isModalOpen = true; + }); + } + + //<== this method is replaced ==> + buildForm() { + this.form = this.fb.group({ + name: [this.selectedBook.name || "", Validators.required], + type: [this.selectedBook.type || null, Validators.required], + publishDate: [ + this.selectedBook.publishDate + ? new Date(this.selectedBook.publishDate) + : null, + Validators.required + ], + price: [this.selectedBook.price || null, Validators.required] + }); + } + + save() { + if (this.form.invalid) { + return; + } + + //<== added this.selectedBook.id ==> + this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) + .subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.get(); + }); + } +} +``` + +* We imported `BooksService`. +* We declared a variable named `selectedBook` as `Books.Book`. +* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited. +* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object. +* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data. +* We replaced the `createBook` method so it sets `selectedBook` to an empty object. +* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook`. + +#### Add "Actions" dropdown to the table + +Open the `book-list.component.html` in `app\books\book-list` folder and replace the `
` tag as below: + +```html +
+ + + + + {%{{{ "::Actions" | abpLocalization }}}%} + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + +
+ +
+ +
+
+ + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + +
+
+``` + +- We added a `th` for the "Actions" column. +- We added `button` with `ngbDropdownToggle` to open actions when clicked the button. +- We have used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. + +The final UI looks like as below: + +![Action buttons](./images/bookstore-actions-buttons.png) + +Open `book-list.component.html` in `app\books\book-list` folder and find the `` tag and replace the content as below. + +```html + +

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

+
+``` + +* This template will show **Edit** text for edit record operation, **New Book** for new record operation in the title. + +### Deleting a book + +#### BooksService + +Open `books.service.ts` in `app\books\shared` folder and add the below `delete` method to delete a book. + +```js +delete(id: string): Observable { + return this.restService.request({ + method: 'DELETE', + url: `/api/app/book/${id}` + }); +} +``` + +* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint. + +#### DeleteBook action + +Open `books.actions.ts` in `app\store\actions `folder and add an action named `DeleteBook`. + +```js +export class DeleteBook { + static readonly type = '[Books] Delete'; + constructor(public id: string) {} +} +``` + +Open the `books.state.ts` in `app\store\states` folder and replace the content as below: + +```js +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; //<== added DeleteBook==> +import { Books } from '../models/books'; +import { BooksService } from '../../books/shared/books.service'; +import { tap } from 'rxjs/operators'; + +@State({ + name: 'BooksState', + defaults: { books: {} } as Books.State, +}) +export class BooksState { + @Selector() + static getBooks(state: Books.State) { + return state.books.items || []; + } + + constructor(private booksService: BooksService) { } + + @Action(GetBooks) + get(ctx: StateContext) { + return this.booksService.get().pipe( + tap(booksResponse => { + ctx.patchState({ + books: booksResponse, + }); + }), + ); + } + + @Action(CreateUpdateBook) + save(ctx: StateContext, action: CreateUpdateBook) { + if (action.id) { + return this.booksService.update(action.payload, action.id); + } else { + return this.booksService.create(action.payload); + } + } + + //<== added DeleteBook ==> + @Action(DeleteBook) + delete(ctx: StateContext, action: DeleteBook) { + return this.booksService.delete(action.id); + } +} +``` + +- We imported `DeleteBook` . + +- We added `DeleteBook` action listener to the end of the file. + + + +#### Add a delete button + + +Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below: + +```html +
+ + +
+``` + +The final actions dropdown UI looks like below: + +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) + +#### Delete confirmation dialog + +Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`. + +Replace the constructor as below: + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; +//... + +constructor( + private store: Store, private fb: FormBuilder, + private booksService: BooksService, + private confirmationService: ConfirmationService // <== added this line ==> +) { } +``` + +* We imported `ConfirmationService`. +* We injected `ConfirmationService` to the constructor. + +In the `book-list.component.ts` add a delete method : + +```js +import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==> + +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==> + +//... + +delete(id: string, name: string) { + this.confirmationService + .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); + } + }); +} +``` + +The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: + +![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) + +{{end}} + +### Next Part + +See the [next part](part-3.md) of this tutorial. diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md new file mode 100644 index 0000000000..4cbefde69e --- /dev/null +++ b/docs/en/Tutorials/Part-3.md @@ -0,0 +1,198 @@ +## ASP.NET Core {{UI_Value}} Tutorial - Part 3 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` + +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### About this tutorial + +This is the third part of the ASP.NET Core {{UI_Value}} tutorial series. See all parts: + +- [Part I: Creating the project and book list page](part-1.md) +- [Part II: Creating, updating and deleting books](part-2.md) +- **Part III: Integration tests (this tutorial)** + +*You can also check out [the video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by the community, based on this tutorial.* + +### Test projects in the solution + +This part covers the **server side** tests. There are several test projects in the solution: + +![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png) + +Each project is used to test the related project. Test projects use the following libraries for testing: + +* [Xunit](https://xunit.github.io/) as the main test framework. +* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as the assertion library. +* [NSubstitute](http://nsubstitute.github.io/) as the mocking library. + +### Adding test data + +Startup template contains the `BookStoreTestDataBuilder` class in the `Acme.BookStore.TestBase` project which creates initial data to run tests. Change the content of `BookStoreTestDataSeedContributor` class as show below: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreTestDataSeedContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(id: _guidGenerator.Create(), + name: "Test book 1", + type: BookType.Fantastic, + publishDate: new DateTime(2015, 05, 24), + price: 21 + ) + ); + + await _bookRepository.InsertAsync( + new Book(id: _guidGenerator.Create(), + name: "Test book 2", + type: BookType.Science, + publishDate: new DateTime(2014, 02, 11), + price: 15 + ) + ); + } + } +} +```` + +* `IRepository` is injected and used it in the `SeedAsync` to create two book entities as the test data. + +### Testing the application service BookAppService +* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](https://docs.abp.io/{{Document_Language_Code}}/abp/{{Document_Version}}/Guid-Generation). + +Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Acme.BookStore +{ + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test book 1"); + } + } +} +```` + +* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. + +Add a new test that creates a valid new book: + +````csharp +[Fact] +public async Task Should_Create_A_Valid_Book() +{ + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); +} +```` + +Add a new test that tries to create an invalid book and fails: + +````csharp +[Fact] +public async Task Should_Not_Create_A_Book_Without_Name() +{ + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); +} +```` + +* Since the `Name` is empty, ABP will throw an `AbpValidationException`. + +Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: + +![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) + +Congratulations, the green icons show, the tests have been successfully passed! + diff --git a/docs/en/Tutorials/images/bookstore-actions-buttons.png b/docs/en/Tutorials/images/bookstore-actions-buttons.png new file mode 100644 index 0000000000..e8243fedc7 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-actions-buttons.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png b/docs/en/Tutorials/images/bookstore-add-create-dialog-v2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png rename to docs/en/Tutorials/images/bookstore-add-create-dialog-v2.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png b/docs/en/Tutorials/images/bookstore-add-edit-dialog.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png rename to docs/en/Tutorials/images/bookstore-add-edit-dialog.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png b/docs/en/Tutorials/images/bookstore-add-index-page-v2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png rename to docs/en/Tutorials/images/bookstore-add-index-page-v2.png diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png new file mode 100644 index 0000000000..28e570f604 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png b/docs/en/Tutorials/images/bookstore-appservice-tests.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png rename to docs/en/Tutorials/images/bookstore-appservice-tests.png diff --git a/docs/en/Tutorials/images/bookstore-book-list-2.png b/docs/en/Tutorials/images/bookstore-book-list-2.png new file mode 100644 index 0000000000..a460d4241b Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-book-list.png b/docs/en/Tutorials/images/bookstore-book-list.png new file mode 100644 index 0000000000..9e6cc9e010 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png b/docs/en/Tutorials/images/bookstore-books-table-actions.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png rename to docs/en/Tutorials/images/bookstore-books-table-actions.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png b/docs/en/Tutorials/images/bookstore-books-table.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png rename to docs/en/Tutorials/images/bookstore-books-table.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-confirmation-popup.png b/docs/en/Tutorials/images/bookstore-confirmation-popup.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-confirmation-popup.png rename to docs/en/Tutorials/images/bookstore-confirmation-popup.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png b/docs/en/Tutorials/images/bookstore-create-dialog-2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png rename to docs/en/Tutorials/images/bookstore-create-dialog-2.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png b/docs/en/Tutorials/images/bookstore-create-dialog.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png rename to docs/en/Tutorials/images/bookstore-create-dialog.png diff --git a/docs/en/Tutorials/images/bookstore-create-project-angular.png b/docs/en/Tutorials/images/bookstore-create-project-angular.png new file mode 100644 index 0000000000..b9eb38b8b7 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-project-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-create-project-mvc.png b/docs/en/Tutorials/images/bookstore-create-project-mvc.png new file mode 100644 index 0000000000..f453b20279 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-project-mvc.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png new file mode 100644 index 0000000000..6f19dcc7bf Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png new file mode 100644 index 0000000000..ec9ef4c42f Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-database-tables-ef.png b/docs/en/Tutorials/images/bookstore-database-tables-ef.png new file mode 100644 index 0000000000..857b10de5b Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-database-tables-ef.png differ diff --git a/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png b/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png new file mode 100644 index 0000000000..8d78bd9a54 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png differ diff --git a/docs/en/Tutorials/images/bookstore-edit-button.png b/docs/en/Tutorials/images/bookstore-edit-button.png new file mode 100644 index 0000000000..bfc1c64797 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-edit-button.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-empty-new-book-modal.png b/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-empty-new-book-modal.png rename to docs/en/Tutorials/images/bookstore-empty-new-book-modal.png diff --git a/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png b/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png new file mode 100644 index 0000000000..4f41829f0d Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png differ diff --git a/docs/en/Tutorials/images/bookstore-generate-state-books.png b/docs/en/Tutorials/images/bookstore-generate-state-books.png new file mode 100644 index 0000000000..be7a919017 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-generate-state-books.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png b/docs/en/Tutorials/images/bookstore-homepage.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png rename to docs/en/Tutorials/images/bookstore-homepage.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png b/docs/en/Tutorials/images/bookstore-index-js-file-v2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png rename to docs/en/Tutorials/images/bookstore-index-js-file-v2.png diff --git a/docs/en/Tutorials/images/bookstore-initial-book-list-page.png b/docs/en/Tutorials/images/bookstore-initial-book-list-page.png new file mode 100644 index 0000000000..591cffb121 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-initial-book-list-page.png differ diff --git a/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png b/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png new file mode 100644 index 0000000000..629ad46444 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png differ diff --git a/docs/en/Tutorials/images/bookstore-localization-files-v2.png b/docs/en/Tutorials/images/bookstore-localization-files-v2.png new file mode 100644 index 0000000000..542cda209c Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-localization-files-v2.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png b/docs/en/Tutorials/images/bookstore-menu-items.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png rename to docs/en/Tutorials/images/bookstore-menu-items.png diff --git a/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png b/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png new file mode 100644 index 0000000000..0724e4ae8f Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png b/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png new file mode 100644 index 0000000000..d59c0ce1d3 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-book-button.png b/docs/en/Tutorials/images/bookstore-new-book-button.png new file mode 100644 index 0000000000..8112fe1352 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-book-button.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-new-book-form-v2.png b/docs/en/Tutorials/images/bookstore-new-book-form-v2.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-new-book-form-v2.png rename to docs/en/Tutorials/images/bookstore-new-book-form-v2.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-new-book-form.png b/docs/en/Tutorials/images/bookstore-new-book-form.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-new-book-form.png rename to docs/en/Tutorials/images/bookstore-new-book-form.png diff --git a/docs/en/Tutorials/images/bookstore-new-menu-item.png b/docs/en/Tutorials/images/bookstore-new-menu-item.png new file mode 100644 index 0000000000..97bf7fc7c1 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-menu-item.png differ diff --git a/docs/en/Tutorials/images/bookstore-open-package-manager-console.png b/docs/en/Tutorials/images/bookstore-open-package-manager-console.png new file mode 100644 index 0000000000..a640eb2681 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-open-package-manager-console.png differ diff --git a/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png new file mode 100644 index 0000000000..2baea20236 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png rename to docs/en/Tutorials/images/bookstore-pmc-add-book-migration.png diff --git a/docs/en/Tutorials/images/bookstore-service-terminal-output.png b/docs/en/Tutorials/images/bookstore-service-terminal-output.png new file mode 100644 index 0000000000..cf6145e03f Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-service-terminal-output.png differ diff --git a/docs/en/Tutorials/images/bookstore-solution-structure-angular.png b/docs/en/Tutorials/images/bookstore-solution-structure-angular.png new file mode 100644 index 0000000000..07d064a880 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-solution-structure-angular.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-solution-structure-mvc.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png rename to docs/en/Tutorials/images/bookstore-solution-structure-mvc.png diff --git a/docs/en/Tutorials/images/bookstore-start-project-angular.png b/docs/en/Tutorials/images/bookstore-start-project-angular.png new file mode 100644 index 0000000000..08abf845a8 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-start-project-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-start-project-mvc.png b/docs/en/Tutorials/images/bookstore-start-project-mvc.png new file mode 100644 index 0000000000..133dc6f131 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-start-project-mvc.png differ diff --git a/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png b/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png new file mode 100644 index 0000000000..66d630bb56 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png differ diff --git a/docs/en/Tutorials/images/bookstore-swagger.png b/docs/en/Tutorials/images/bookstore-swagger.png new file mode 100644 index 0000000000..3ce36a11bc Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-swagger.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png b/docs/en/Tutorials/images/bookstore-test-js-proxy-getlist-network.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png rename to docs/en/Tutorials/images/bookstore-test-js-proxy-getlist-network.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png b/docs/en/Tutorials/images/bookstore-test-js-proxy-getlist.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png rename to docs/en/Tutorials/images/bookstore-test-js-proxy-getlist.png diff --git a/docs/en/Tutorials/images/bookstore-test-projects-angular.png b/docs/en/Tutorials/images/bookstore-test-projects-angular.png new file mode 100644 index 0000000000..6a8947238e Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-test-projects-angular.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/images/bookstore-test-projects-mvc.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png rename to docs/en/Tutorials/images/bookstore-test-projects-mvc.png diff --git a/docs/en/Tutorials/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/images/bookstore-test-projects-v2.png new file mode 100644 index 0000000000..8701164d75 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-test-projects-v2.png differ diff --git a/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png b/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png new file mode 100644 index 0000000000..4889f4f757 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png b/docs/en/Tutorials/images/bookstore-user-management.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png rename to docs/en/Tutorials/images/bookstore-user-management.png diff --git a/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png new file mode 100644 index 0000000000..307e3516a5 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png differ diff --git a/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png b/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png new file mode 100644 index 0000000000..c9e2fc0e65 Binary files /dev/null and b/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png differ diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md new file mode 100644 index 0000000000..d718b46117 --- /dev/null +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -0,0 +1,99 @@ +## Component Replacement + +You can replace some ABP components with your custom components. + +The reason that you **can replace** but **cannot customize** default ABP components is disabling or changing a part of that component can cause problems. So we named those components as _Replaceable Components_. + +### How to Replace a Component + +Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. + +Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; +export class AppComponent { + constructor(..., private store: Store) {} + + ngOnInit() { + this.store.dispatch( + new AddReplaceableComponent({ + component: YourNewRoleComponent, + key: 'Identity.RolesComponent', + }), + ); + //... + } +} +``` + +![Example Usage](./images/component-replacement.gif) + + +### How to Replace a Layout + +Each ABP theme module has 3 layouts named `ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. These layouts can be replaced with the same way. + +> A layout component template should contain `` element. + +The below example describes how to replace the `ApplicationLayoutComponent`: + +Run the following command to generate a layout in `angular` folder: + +```bash +yarn ng generate component shared/my-application-layout --export --entryComponent + +# You don't need the --entryComponent option in Angular 9 +``` + +Add the following code in your layout template (`my-layout.component.html`) where you want the page to be loaded. + +```html + +``` + +Open the `app.component.ts` and add the below content: + +```js +import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent +import { Store } from '@ngxs/store'; // imported Store +//... +export class AppComponent { + constructor(..., private store: Store) {} // injected Store + + ngOnInit() { + // added below content + this.store.dispatch( + new AddReplaceableComponent({ + component: MyApplicationLayoutComponent, + key: 'Theme.ApplicationLayoutComponent', + }), + ); + + //... + } +} +``` + +### Available Replaceable Components + +| Component key | Description | +| -------------------------------------------------- | --------------------------------------------- | +| Account.LoginComponent | Login page | +| Account.RegisterComponent | Register page | +| Account.ManageProfileComponent | Manage Profile page | +| Account.AuthWrapperComponent | This component wraps register and login pages | +| Account.ChangePasswordComponent | Change password form | +| Account.PersonalSettingsComponent | Personal settings form | +| Account.TenantBoxComponentInputs | Tenant changing box | +| FeatureManagement.FeatureManagementComponent | Features modal | +| Identity.UsersComponent | Users page | +| Identity.RolesComponent | Roles page | +| PermissionManagement.PermissionManagementComponent | Permissions modal | +| SettingManagement.SettingManagementComponent | Setting Management page | +| TenantManagement.TenantsComponent | Tenants page | + +## What's Next? + +- [Custom Setting Page](./Custom-Setting-Page.md) diff --git a/docs/en/UI/Angular/Config-State.md b/docs/en/UI/Angular/Config-State.md new file mode 100644 index 0000000000..7fc202982c --- /dev/null +++ b/docs/en/UI/Angular/Config-State.md @@ -0,0 +1,294 @@ +# Config State + +`ConfigStateService` is a singleton service, i.e. provided in root level of your application, and is actually a façade for interacting with application configuration state in the `Store`. + +## Before Use + +In order to use the `ConfigStateService` you must inject it in your class as a dependency. + +```js +import { ConfigStateService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private config: ConfigStateService) {} +} +``` + +You do not have to provide the `ConfigStateService` at module or component/directive level, because it is already **provided in root**. + +## Selector Methods + +`ConfigStateService` has numerous selector methods which allow you to get a specific configuration or all configurations from the `Store`. + +### How to Get All Configurations From the Store + +You can use the `getAll` method of `ConfigStateService` to get all of the configuration object from the store. It is used as follows: + +```js +// this.config is instance of ConfigStateService + +const config = this.config.getAll(); +``` + +### How to Get a Specific Configuration From the Store + +You can use the `getOne` method of `ConfigStateService` to get a specific configuration property from the store. For that, the property name should be passed to the method as parameter. + +```js +// this.config is instance of ConfigStateService + +const currentUser = this.config.getOne("currentUser"); +``` + +On occasion, you will probably want to be more specific than getting just the current user. For example, here is how you can get the `tenantId`: + +```js +const tenantId = this.config.getDeep("currentUser.tenantId"); +``` + +or by giving an array of keys as parameter: + +```js +const tenantId = this.config.getDeep(["currentUser", "tenantId"]); +``` + +FYI, `getDeep` is able to do everything `getOne` does. Just keep in mind that `getOne` is slightly faster. + +#### Config State Properties + +Please refer to `Config.State` type for all the properties you can get with `getOne` and `getDeep`. 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#L7). + +### How to Get the Application Information From the Store + +The `getApplicationInfo` method is used to get the application information from the environment variables stored as the config state. This is how you can use it: + +```js +// this.config is instance of ConfigStateService + +const appInfo = this.config.getApplicationInfo(); +``` + +This method never returns `undefined` or `null` and returns an empty object literal (`{}`) instead. In other words, you will never get an error when referring to the properties of `appInfo` above. + +#### Application Information Properties + +Please refer to `Config.Application` type for all the properties you can get with `getApplicationInfo`. 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#L21). + +### How to Get API URL From the Store + +The `getApplicationInfo` method is used to get a specific API URL from the environment variables stored as the config state. This is how you can use it: + +```js +// this.config is instance of ConfigStateService + +const apiUrl = this.config.getApiUrl(); +// environment.apis.default.url + +const searchUrl = this.config.getApiUrl("search"); +// environment.apis.search.url +``` + +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 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. + +```js +// this.config is instance of ConfigStateService + +const hasIdentityPermission = this.config.getGrantedPolicy("Abp.Identity"); +// true +``` + +You may also **combine policy keys** to fine tune your selection: + +```js +// this.config is instance of ConfigStateService + +const hasIdentityAndAccountPermission = this.config.getGrantedPolicy( + "Abp.Identity && Abp.Account" +); +// false + +const hasIdentityOrAccountPermission = this.config.getGrantedPolicy( + "Abp.Identity || Abp.Account" +); +// true +``` + +Please consider the following **rules** when creating your permission selectors: + +- Maximum 2 keys can be combined. +- `&&` operator looks for both keys. +- `||` operator looks for either key. +- Empty string `''` as key will return `true` +- Using an operator without a second key will return `false` + +### How to Get Translations From the Store + +The `getLocalization` method of `ConfigStateService` is used for translations. Here are some examples: + +```js +// this.config is instance of ConfigStateService + +const identity = this.config.getLocalization("AbpIdentity::Identity"); +// 'identity' + +const notFound = this.config.getLocalization("AbpIdentity::IDENTITY"); +// 'AbpIdentity::IDENTITY' + +const defaultValue = this.config.getLocalization({ + key: "AbpIdentity::IDENTITY", + defaultValue: "IDENTITY" +}); +// 'IDENTITY' +``` + +Please check out the [localization documentation](./Localization.md) for details. + +## Dispatch Methods + +`ConfigStateService` has several dispatch methods which allow you to conveniently dispatch predefined actions to the `Store`. + +### How to Get Application Configuration From Server + +The `dispatchGetAppConfiguration` triggers a request to an endpoint that responds with the application state and then places this response to the `Store` as configuration state. + +```js +// this.config is instance of ConfigStateService + +this.config.dispatchGetAppConfiguration(); +// returns a state stream which emits after dispatch action is complete +``` + +Note that **you do not have to call this method at application initiation**, because the application configuration is already being received from the server at start. + +### How to Patch Route Configuration + +The `dispatchPatchRouteByName` finds a route by its name and replaces its configuration in the `Store` with the new configuration passed as the second parameter. + +```js +// this.config is instance of ConfigStateService + +const newRouteConfig: Partial = { + name: "Home", + path: "home", + children: [ + { + name: "Dashboard", + path: "dashboard" + } + ] +}; + +this.config.dispatchPatchRouteByName("::Menu:Home", newRouteConfig); +// returns a state stream which emits after dispatch action is complete +``` + +### How to Add a New Route Configuration + +The `dispatchAddRoute` adds a new route to the configuration state in the `Store`. For this, the route config should be passed as the parameter of the method. + +```js +// this.config is instance of ConfigStateService + +const newRoute: ABP.Route = { + name: "My New Page", + iconClass: "fa fa-dashboard", + path: "page", + invisible: false, + order: 2, + requiredPolicy: "MyProjectName::MyNewPage" +}; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +The `newRoute` will be placed as at root level, i.e. without any parent routes and its url will be stored as `'/path'`. + +If you want **to add a child route, you can do this:** + +```js +// this.config is instance of ConfigStateService + +const newRoute: ABP.Route = { + parentName: "AbpAccount::Login", + name: "My New Page", + iconClass: "fa fa-dashboard", + path: "page", + invisible: false, + order: 2, + requiredPolicy: "MyProjectName::MyNewPage" +}; + +this.config.dispatchAddRoute(newRoute); +// returns a state stream which emits after dispatch action is complete +``` + +The `newRoute` will then be placed as a child of the parent route named `'AbpAccount::Login'` and its url will be set as `'/account/login/page'`. + +#### Route Configuration Properties + +Please refer to `ABP.Route` type for all the properties you can pass to `dispatchSetEnvironment` in its parameter. It can be found in the [common.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L27). + +### How to Set the Environment + +The `dispatchSetEnvironment` places environment variables passed to it in the `Store` under the configuration state. Here is how it is used: + +```js +// this.config is instance of ConfigStateService + +this.config.dispatchSetEnvironment({ + /* environment properties here */ +}); +// returns a state stream which emits after dispatch action is complete +``` + +Note that **you do not have to call this method at application initiation**, because the environment variables are already being stored at start. + +#### Environment Properties + +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). + +## What's Next? + +* [Component Replacement](./Component-Replacement.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Content-Security-Strategy.md b/docs/en/UI/Angular/Content-Security-Strategy.md new file mode 100644 index 0000000000..f32317c9ca --- /dev/null +++ b/docs/en/UI/Angular/Content-Security-Strategy.md @@ -0,0 +1,74 @@ +# ContentSecurityStrategy + +`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). + + + + +## API + + +### constructor + +```js +constructor(public nonce?: string) +``` + +- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. + + +### applyCSP + +```js +applyCSP(element: HTMLScriptElement | HTMLStyleElement): void +``` + +This method maps the aforementioned properties to the given `element`. + + + + +## LooseContentSecurityPolicy + +`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `` element will place at the **end** of ``. + +Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. + + +### How to Insert Styles + +If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `` element will place at the **end** of ``. + +Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. + + +## API + +### insertContent + +```js +insertContent(strategy: ContentStrategy): void +``` + +`strategy` parameter is the primary focus here and is explained above. + + +## What's Next? + +- [TrackByService](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Dom-Strategy.md b/docs/en/UI/Angular/Dom-Strategy.md new file mode 100644 index 0000000000..2318e13205 --- /dev/null +++ b/docs/en/UI/Angular/Dom-Strategy.md @@ -0,0 +1,89 @@ +# DomStrategy + +`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`. + + +## API + + +### constructor + +```js +constructor( + public target?: HTMLElement, + public position?: InsertPosition +) +``` + +- `target` is an HTMLElement (_default: document.head_). +- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_). + + +### insertElement + +```js +insertElement(element: HTMLElement): void +``` + +This method inserts given `element` to `target` based on the `position`. + + + +## Predefined Dom Strategies + +Predefined dom strategies are accessible via `DOM_STRATEGY` constant. + + +### AppendToBody + +```js +DOM_STRATEGY.AppendToBody() +``` + +`insertElement` will place the given `element` at the end of ``. + + +### AppendToHead + +```js +DOM_STRATEGY.AppendToHead() +``` + +`insertElement` will place the given `element` at the end of ``. + + +### PrependToHead + +```js +DOM_STRATEGY.PrependToHead() +``` + +`insertElement` will place the given `element` at the beginning of ``. + + +### AfterElement + +```js +DOM_STRATEGY.AfterElement(target: HTMLElement) +``` + +`insertElement` will place the given `element` after (as a sibling to) the `target`. + + +### BeforeElement + +```js +DOM_STRATEGY.BeforeElement(target: HTMLElement) +``` + +`insertElement` will place the given `element` before (as a sibling to) the `target`. + + + + +## See Also + +- [DomInsertionService](./Dom-Insertion-Service.md) +- [LazyLoadService](./Lazy-Load-Service.md) +- [LoadingStrategy](./Loading-Strategy.md) +- [ContentStrategy](./Content-Strategy.md) diff --git a/docs/en/UI/Angular/Http-Requests.md b/docs/en/UI/Angular/Http-Requests.md new file mode 100644 index 0000000000..71878c8045 --- /dev/null +++ b/docs/en/UI/Angular/Http-Requests.md @@ -0,0 +1,209 @@ +# How to Make HTTP Requests + + + +## About HttpClient + +Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project. + +However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned. + +```js +getConfig() { + this.http.get(this.configUrl).subscribe( + config => this.updateConfig(config), + error => { + // Handle error here + }, + ); +} +``` + +Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable. + +An `HttpInterceptor` is able to catch `HttpErrorResponse`  and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details. + + + +## RestService + +ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult. + + + +### Getting Started with RestService + +In order to use the `RestService`, you must inject it in your class as a dependency. + +```js +import { RestService } from '@abp/ng.core'; + +@Injectable({ + /* class metadata here */ +}) +class DemoService { + constructor(private rest: RestService) {} +} +``` + +You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**. + + + +### How to Make a Request with RestService + +You can use the `request` method of the `RestService` is for HTTP requests. Here is an example: + +```js +getFoo(id: number) { + const request: Rest.Request = { + method: 'GET', + url: '/api/some/path/to/foo/' + id, + }; + + return this.rest.request(request); +} +``` + + + +The `request` method always returns an `Observable`. Therefore you can do the following wherever you use `getFoo` method: + +```js +doSomethingWithFoo(id: number) { + this.demoService.getFoo(id).subscribe( + foo => { + // Do something with foo. + } + ) +} +``` + + + +**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error. + + + +As you see, `request` method gets a request options object with `Rest.Request` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one: + +```js +postFoo(body: Foo) { + const request: Rest.Request = { + method: 'POST', + url: '/api/some/path/to/foo', + body + }; + + return this.rest.request(request); +} +``` + + + +You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request` type, which has only a few chages compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular. + + + +### How to Disable Default Error Handler of RestService + +The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself: + +```js +deleteFoo(id: number) { + const request: Rest.Request = { + method: 'DELETE', + url: '/api/some/path/to/foo/' + id, + }; + + return this.rest.request(request, { skipHandleError: true }); +} +``` + + + +`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription. + +```js +removeFooFromList(id: number) { + this.demoService.deleteFoo(id).subscribe( + foo => { + // Do something with foo. + }, + error => { + // Do something with error. + } + ) +} +``` + + + +### How to Get a Specific API Endpoint From Application Config + +Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration. + + + +```js +putFoo(body: Foo, id: string) { + const request: Rest.Request = { + method: 'PUT', + url: '/' + id, + body + }; + + return this.rest.request(request, {apiName: 'foo'}); +} +``` + + + +`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows: + +```js +// environment.ts + +export const environment = { + apis: { + default: { + url: 'https://localhost:44305', + }, + foo: { + url: 'https://localhost:44305/api/some/path/to/foo', + }, + }, + + /* rest of the environment variables here */ +} +``` + + + +### How to Observe Response Object or HTTP Events Instead of Body + +`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object. + +```js +getSomeCustomHeaderValue() { + const request: Rest.Request = { + method: 'GET', + url: '/api/some/path/that/sends/some-custom-header', + }; + + return this.rest.request>( + request, + {observe: Rest.Observe.Response}, + ).pipe( + map(response => response.headers.get('Some-Custom-Header')) + ); +} +``` + + + +You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). + +## What's Next? + +* [Localization](./Localization.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Lazy-Load-Service.md b/docs/en/UI/Angular/Lazy-Load-Service.md new file mode 100644 index 0000000000..a03381869e --- /dev/null +++ b/docs/en/UI/Angular/Lazy-Load-Service.md @@ -0,0 +1,213 @@ +# How to Lazy Load Scripts and 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. + + + + +## Getting Started + +You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. + +```js +import { LazyLoadService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private lazyLoadService: LazyLoadService) {} +} +``` + + + + +## Usage + +You can use the `load` method of `LazyLoadService` to create a ` +```` + +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. You (or the modules you are using) can add multiple items to the same hook. All of them will be added to 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. + +### 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 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. + +#### ITheme + +ABP Framework uses the `ITheme` service to get the layout location by the layout name. You can replace this service to dynamically select the layout location. + +#### IThemeManager + +`IThemeManager` is used to obtain the current theme and get the layout path. Any page can determine the layout of its own. Example: + +````html +@using Volo.Abp.AspNetCore.Mvc.UI.Theming +@inject IThemeManager ThemeManager +@{ + Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty); +} +```` + +This page will use the empty layout. You use `ThemeManager.CurrentTheme.GetEmptyLayout();` extension method as a shortcut. + +If you want to set the layout for all the pages under a specific folder, then write the code above in a `_ViewStart.cshtml` file under that folder. diff --git a/docs/en/UI/AspNetCore/Layout-Hooks.md b/docs/en/UI/AspNetCore/Layout-Hooks.md new file mode 100644 index 0000000000..49b334c6c0 --- /dev/null +++ b/docs/en/UI/AspNetCore/Layout-Hooks.md @@ -0,0 +1,3 @@ +# Layout Hooks + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md b/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md new file mode 100644 index 0000000000..a5a66e9e19 --- /dev/null +++ b/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md @@ -0,0 +1,3 @@ +# ABP Datatables.Net Integration for ASP.NET Core UI + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Navigation-Menu.md b/docs/en/UI/AspNetCore/Navigation-Menu.md new file mode 100644 index 0000000000..4e0164ec72 --- /dev/null +++ b/docs/en/UI/AspNetCore/Navigation-Menu.md @@ -0,0 +1,3 @@ +# Navigation Menu + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Alerts.md b/docs/en/UI/AspNetCore/Tag-Helpers/Alerts.md new file mode 100644 index 0000000000..cf19e31e29 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Alerts.md @@ -0,0 +1,82 @@ +# Alerts + +## Introduction + +`abp-alert` is a main element to create an alert. + +Basic usage: + +````xml + + A simple primary alert—check it out! + +```` + + + +## Demo + +See the [alerts demo page](https://bootstrap-taghelpers.abp.io/Components/Alerts) to see it in action. + +## Attributes + +### alert-type + +A value indicates the type of the alert. Should be one of the following values: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +Example: + +````xml + + A simple warning alert—check it out! + +```` + +### alert-link + +A value provides matching colored links within any alert. + +Example: + +````xml + + A simple danger alert with an example link. Give it a click if you like. + +```` + +### dismissible + +A value to make the alert dismissible. + +Example: + +````xml + + Holy guacamole! You should check in on some of those fields below. + +```` + +### Additional content + +`abp-alert` can also contain additional HTML elements like headings, paragraphs and dividers. + +Example: + +````xml + +

Well done!

+

Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.

+
+

Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

+
+```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Buttons.md b/docs/en/UI/AspNetCore/Tag-Helpers/Buttons.md new file mode 100644 index 0000000000..ae62c7f2b7 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Buttons.md @@ -0,0 +1,86 @@ +# Buttons + +## Introduction + +`abp-button` is the main element to create buttons. + +Basic usage: + +````xml +Click Me +```` + +## Demo + +See the [buttons demo page](https://bootstrap-taghelpers.abp.io/Components/Buttons) to see it in action. + +## Attributes + +### button-type + +A value indicates the main style/type of the button. Should be one of the following values: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Outline_Primary` +* `Outline_Secondary` +* `Outline_Success` +* `Outline_Danger` +* `Outline_Warning` +* `Outline_Info` +* `Outline_Light` +* `Outline_Dark` +* `Link` + +### size + +A value indicates the size of the button. Should be one of the following values: + +* `Default` (default value) +* `Small` +* `Medium` +* `Large` +* `Block` +* `Block_Small` +* `Block_Medium` +* `Block_Large` + +### busy-text + +A text that is shown when the button is busy. + +### text + +The text of the button. This is a shortcut if you simply want to set a text to the button. Example: + +````xml + +```` + +In this case, you can use a self-closing tag to make it shorter. + +### icon + +Used to set an icon for the button. It works with the [Font Awesome](https://fontawesome.com/) icon classes by default. Example: + +````xml + +```` + +##### icon-type + +If you don't want to use font-awesome, you have two options: + +1. Set `icon-type` to `Other` and write the CSS class of the font icon you're using. +2. If you don't use a font icon use the opening and closing tags manually and write any code inside the tags. + +### disabled + +Set `true` to make the button initially disabled. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Cards.md b/docs/en/UI/AspNetCore/Tag-Helpers/Cards.md new file mode 100644 index 0000000000..d7f3896cdd --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Cards.md @@ -0,0 +1,188 @@ +# Cards + +## Introduction + +`abp-card` is a content container derived from bootstrap card element. + +Basic usage: + +````xml + + + + Card Title + Some quick example text to build on the card title and make up the bulk of the card's content. + Go somewhere + + +```` + + + +##### Using Titles, Text and Links: + +Following tags can be used under main `abp-card` tag + +* `abp-card-title` +* `abp-card-subtitle` +* `a abp-card-link` + +Sample: + +````xml + + + Card title + Card subtitle + Some quick example text to build on the card title and make up the bulk of the card's content. + Card link + Another link + + +```` + + + +##### Using List Groups: + +* `abp-list-group flush="true"` : `flush` attribute renders into bootstrap `list-group-flush` class which is used for removing borders and rounded corners to render list group items edge to edge in a parent container. +* `abp-list-group-item` + +Kitchen Sink Sample: + +````xml + + + + Card Title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + Cras justo odio + Dapibus ac facilisis in + Vestibulum at eros + + + Card link + Another link + + +```` + + + +##### Using Header, Footer and Blockquote: + +* `abp-card-header` +* `abp-card-footer` +* `abp-blockquote` + +Sample: + +```xml + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + +``` + +Quote Sample: + +```xml + + Quote + + +

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

+
Someone famous in Source Title
+
+
+
+``` + +Footer Sample: + +```xml + + Featured + + +

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

+
Someone famous in Source Title
+
+
+ 2 days ago +
+``` + + + +## Demo + +See the [cards demo page](https://bootstrap-taghelpers.abp.io/Components/Cards) to see it in action. + +## abp-card Attributes + +- **background:** A value indicates the background color of the card. +- **text-color**: A value indicates the color of the text inside the card. +- **border:** A value indicates the color of the border inside the card. + +Should be one of the following values: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +Example: + +````xml + +```` + +### sizing + +Cards has default 100% with and can be changed with custom CSS, grid classes, grid Sass mixins or [utilities](https://getbootstrap.com/docs/4.0/utilities/sizing/). + +````xml + +```` + +### card-deck and card-columns + +`abp-card` can be used inside `card-deck` or `card-columns` aswell. + +````xml +
+ + First Deck + + Ace + Here is the content for Ace. + + + + Second Deck + + Beta + Beta content. + + + + Third Deck + + Epsilon + Content for Epsilon. + + +
+```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md b/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md new file mode 100644 index 0000000000..38fab4d44b --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Collapse.md @@ -0,0 +1,92 @@ +# Collapse + +## Introduction + +`abp-collapse-body` is the main container for showing and hiding content. `abp-collapse-id` is used to show and hide the content container. Can be triggered with both `abp-button` and `a` tags. + +Basic usage: + +````xml + + Link with href + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + +```` + + + +## Demo + +See the [collapse demo page](https://bootstrap-taghelpers.abp.io/Components/Collapse) to see it in action. + +## Attributes + +### show + +A value indicates if the collapse body will be initialized visible or hidden. Should be one of the following values: + +* `false` (default value) +* `true` + +### multi + +A value indicates if an `abp-collapse-body` can be shown or hidden by an element that can show/hide multiple collapse bodies. Basically, this attribute adds "multi-collapse" class to `abp-collapse-body`. Should be one of the following values: + +* `false` (default value) +* `true` + +Sample: + +````xml + Toggle first element + + + + + + + Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit + + + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. + + + +```` + +## Accordion example + +`abp-accordion` is the main container for the accordion items. + +Basic usage: + +````xml + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry rtat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + +```` + +## Attributes + +### active + +A value indicates if the accordion item will be initialized visible or hidden. Should be one of the following values: + +* `false` (default value) +* `true` + +### title + +A value indicates the visible title of the accordion item. Should be a string value. diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dropdowns.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dropdowns.md new file mode 100644 index 0000000000..1c062b5f73 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dropdowns.md @@ -0,0 +1,97 @@ +# Dropdowns + +## Introduction + +`abp-dropdown` is the main container for dropdown content. + +Basic usage: + +````xml + + + + Action + Another action + Something else here + + +```` + + + +## Demo + +See the [dropdown demo page](https://bootstrap-taghelpers.abp.io/Components/Dropdowns) to see it in action. + +## Attributes + +### direction + +A value indicates which direction the dropdown buttons will be displayed to. Should be one of the following values: + +* `Down` (default value) +* `Up` +* `Right` +* `Left` + +### dropdown-style + +A value indicates if an `abp-dropdown-button` will have split icon for dropdown. Should be one of the following values: + +* `Single` (default value) +* `Split` + + + +## Menu items + +`abp-dropdown-menu` is the main container for dropdown menu items. + +Basic usage: + +````xml + + + + Dropdown Header + Action + Active action + Disabled action + + Dropdown Item Text + Something else here + + +```` + +## Attributes + +### align + +A value indicates which direction `abp-dropdown-menu` items will be aligned to. Should be one of the following values: + +* `Left` (default value) +* `Right` + +### Additional content + +`abp-dropdown-menu` can also contain additional HTML elements like headings, paragraphs, dividers or form element. + +Example: + +````xml + + + +
+ + + + + + + New around here? Sign up + Forgot password? +
+
+```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md new file mode 100644 index 0000000000..6d700f030d --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -0,0 +1,3 @@ +## Dynamic Forms + +This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Grids.md b/docs/en/UI/AspNetCore/Tag-Helpers/Grids.md new file mode 100644 index 0000000000..b33fa03c46 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Grids.md @@ -0,0 +1,286 @@ +# Grids + +## Introduction + +Abp tag helpers for bootstrap based grid system. + + + +## Demo + +See the [grids demo page](https://bootstrap-taghelpers.abp.io/Components/Grids) to see it in action. + + + +### Sizing + +**Equal Width:** Creates columns with equal width. + +Sample: + +````xml + + + 1 of 2 + 2 of 2 + + + 1 of 3 + 2 of 3 + 3 of 3 + + +```` + +**Column Breaker:** `abp-column-breaker` is used for breaking the automatic width of placement of the current row and starting in a new row afterwards. + +Sample: + +````xml + + + column + column + + column + column + + +```` + +**Setting one column width:** size attribute is used for setting the width for a specific column. + +Sample: + +```xml + + + 1 of 3 + 2 of 3 (wider) + 3 of 3 + + + 1 of 3 + 2 of 3 (wider) + 3 of 3 + + +``` + +**Variable width content:** Auto resizing column based on content. + +```xml + + + 1 of 3 + Contrary to popular belief, Lorem Ipsum is not simply random text. + 3 of 3 + + + 1 of 3 + Variable width content + 3 of 3 + + +``` + + + +### Responsive Classes + +Responsive classes can be used strongly typed within abp tags. + +```xml + + col-sm-8 + col-sm-4 + + + col-sm + col-sm + col-sm + col-sm + + + + .col-12 .col-md-8 + .col-6 .col-md-4 + + + + + .col-6 .col-md-4 + .col-6 .col-md-4 + .col-6 .col-md-4 + + + + + .col-6 + .col-6 + +``` + + + +### Alignment + +Column alignments can be done strongly typed in abp tags with both vertically and horizontally. + +**Vertical-alignment**: `v-align` attribute value is used to align the columns vertically. + +Sample: + +```xml + + + column + column + column + + + column + column + column + + + column + column + column + + +``` + +**Horizontal-alignment**: `h-align` attribute value is used to align the columns horizontally. + +Sample: + +```xml + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + +``` + +**No gutters**: The gutters between columns in predefined grid classes can be removed with `gutters="false"`. This removes the negative `margin`s from `abp-row` and the horizontal `padding` from all immediate children columns. + +Sample: + +```xml + + One of two columns + One of two columns + +``` + +**Column wrapping**: If more than 12 columns are placed within a single row, each group of extra columns will, as one unit, wrap onto a new line. + +Sample: + +```xml + + .col-9 + .col-4
Since 9 + 4 = 13 > 12, this 4-column-wide div gets wrapped onto a new line as one contiguous unit.
+ .col-6
Subsequent columns continue along the new line.s
+
+``` + + + +### Reordering + +**Order Classes**: `order` attribute is used for controlling the visual order of the content. + +Sample: + +```xml + + + First, but Last + Second, but unordered + Third, but Second + + +``` + +**Offsetting columns**: `offset` attribute is used for setting the offset of the grid columns. + +Sample: + +```xml + + + .col-md-4 + .col-md-4 .offset-md-4 + + + .col-md-3 .offset-md-3 + .col-md-3 .offset-md-3 + + + .col-md-6 .offset-md-3 + + + .col-sm-5 .col-md-6 + .col-sm-5 .offset-sm-2 .col-md-6 .offset-md-0 + + + col-sm-6 .col-md-5 .col-lg-6 + .col-sm-6 .col-md-5 .offset-md-2 .col-lg-6 .offset-lg-0 + + +``` + + + +## abp-row Attributes + +- **v-align:** A value indicates the vertical positioning of the containing columns. Should be one of the following values: + * `Default` (default value) + * `Start` + * `Center` + * `End` + +- **h-align**: A value indicates the horizontal positioning of the containing columns. Should be one of the following values: + * `Default` (default value) + * `Start` + * `Center` + * `Around` + * `Between` + * `End` +- **gutter**: A value indicates if the negative `margin` and horizontal `padding` will be removed from all children columns. Will act as `true` value if this attribute is not set. Should be one of the following values: + * `true` + * `false` + +## abp-column Attributes + +- **size:** A value indicates the width of the column from `_`, `Undefined`, `_1`..`_12`, `Auto`. Or can be used with predefined values like: + - `size-sm` + - `size-md` + - `size-lg` + - `size-xl` +- **order**: A value indicates the order of column from `Undefined`, `_1`..`_12`, `First` and `Last`. +- **offset:** A value indicates offset of the column from `_`, `Undefined`, `_1`..`_12`, `Auto`. Or can be used with predefined values like: + - `offset-sm` + - `offset-md` + - `offset-lg` + - `offset-xl` + diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md new file mode 100644 index 0000000000..a7aa0b65d2 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -0,0 +1,38 @@ +# ABP Tag Helpers + +ABP Framework defines a set of **tag helper components** to simply the user interface development for ASP.NET Core (MVC / Razor Pages) applications. + +## Bootstrap Component Wrappers + +Most of the tag helpers are [Bootstrap](https://getbootstrap.com/) (v4+) wrappers. Coding bootstrap is not so easy, not so type-safe and contains too much repetitive HTML tags. ABP Tag Helpers makes it **easier** and **type safe**. + +We don't aim to wrap bootstrap components 100%. Writing **native bootstrap style code** is still possible (actually, tag helpers generates native bootstrap code in the end), but we suggest to use the tag helpers wherever possible. + +ABP Framework also adds some **useful features** to the standard bootstrap components. + +Here, the list of components those are wrapped by the ABP Framework: + +* [Buttons](Buttons.md) +* [Cards](Cards.md) +* [Alerts](Alerts.md) +* [Tabs](Tabs.md) +* [Grids](Grids.md) +* [Modals](Modals.md) +* [Collapse](Collapse.md) +* [Dropdowns](Dropdowns.md) +* [List Groups](List-Groups.md) +* [Paginator](Paginator.md) +* [Popovers](Popovers.md) +* [Progress Bars](Progress-Bars.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. + +## Form Elements + +See [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). + +## Dynamic Inputs + +See [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md b/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md new file mode 100644 index 0000000000..b1e6e7f499 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md @@ -0,0 +1,78 @@ +# List Groups + +## Introduction + +`abp-list-group` is the main container for list group content. + +Basic usage: + +````xml + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + +```` + + + +## Demo + +See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroups) to see it in action. + +## Attributes + +### flush + +A value indicates `abp-list-group` items to remove some borders and rounded corners to render list group items edge-to-edge in a parent container. Should be one of the following values: + +* `false` (default value) +* `true` + +### active + +A value indicates if an `abp-list-group-item` to be active. Should be one of the following values: + +* `false` (default value) +* `true` + +### disabled + +A value indicates if an `abp-list-group-item` to be disabled. Should be one of the following values: + +* `false` (default value) +* `true` + +### href + +A value indicates if an `abp-list-group-item` has a link. Should be a string link value. + +### type + +A value indicates an `abp-list-group-item` style class with a stateful background and color. Should be one of the following values: + +* `Default` (default value) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Link` + +### Additional content + +`abp-list-group-item` can also contain additional HTML elements like spans. + +Example: + +````xml + + Cras justo odio 14 + Dapibus ac facilisis in 2 + Morbi leo risus 1 + +```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md new file mode 100644 index 0000000000..9cf1258373 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md @@ -0,0 +1,81 @@ +# Modals + +## Introduction + +`abp-modal` is a main element to create a modal. + +Basic usage: + +````xml +Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + +```` + + + +## Demo + +See the [modals demo page](https://bootstrap-taghelpers.abp.io/Components/Modals) to see it in action. + +## Attributes + +### centered + +A value indicates the positioning of the modal. Should be one of the following values: + +* `false` (default value) +* `true` + +### size + +A value indicates the size of the modal. Should be one of the following values: + +* `Default` (default value) +* `Small` +* `Large` +* `ExtraLarge` + +### static + +A value indicates if the modal will be static. Should be one of the following values: + +* `false` (default value) +* `true` + +### Additional content + +`abp-modal-footer` can have multiple buttons with alignment option. + +Add `@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal` to your page. + +Example: + +````xml +Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + +```` + +### button-alignment + +A value indicates the positioning of your modal footer buttons. Should be one of the following values: + +* `Default` (default value) +* `Start` +* `Center` +* `Around` +* `Between` +* `End` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md new file mode 100644 index 0000000000..0cc63d49e8 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -0,0 +1,57 @@ +# Paginator + +## Introduction + +`abp-paginator` is the abp tag for pagination. Requires `Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` type of model. + +Basic usage: + +````xml + +```` + +Model: + +````xml +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components +{ + public class PaginatorModel : PageModel + { + public PagerModel PagerModel { get; set; } + + public void OnGet(int currentPage, string sort) + { + PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + } + } +} +```` + + + +## Demo + +See the [paginator demo page](https://bootstrap-taghelpers.abp.io/Components/Paginator) to see it in action. + +## Attributes + +### model + +`Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` type of model can be initialized with the following data: + +* `totalCount` +* `shownItemsCount` +* `currentPage` +* `pageSize` +* `pageUrl` +* `sort` (default null) + +### show-info + +A value indicates if an extra information about start, end and total records will be displayed. Should be one of the following values: + +* `false` (default value) +* `true` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md b/docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md new file mode 100644 index 0000000000..55bb9fb0f6 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md @@ -0,0 +1,42 @@ +# Popovers + +## Introduction + +`abp-popover` is the abp tag for popover messages. + +Basic usage: + +````xml + + Popover Default + +```` + + + +## Demo + +See the [popovers demo page](https://bootstrap-taghelpers.abp.io/Components/Popovers) to see it in action. + +## Attributes + +### disabled + +A value indicates if the element should be disabled for interaction. If this value is set to `true`, `dismissable` attribute will be ignored. Should be one of the following values: + +* `false` (default value) +* `true` + +### dismissable + +A value indicates to dismiss the popovers on the user's next click of a different element than the toggle element. Should be one of the following values: + +* `false` (default value) +* `true` + +### hoverable + +A value indicates if the popover content will be displayed on mouse hover. Should be one of the following values: + +* `false` (default value) +* `true` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md b/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md new file mode 100644 index 0000000000..dc69f7e47e --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md @@ -0,0 +1,70 @@ +# Progress Bars + +## Introduction + +`abp-progress-bar` is the abp tag for progress bar status. + +Basic usage: + +````xml + + + %25 + + + + %50 + + + + %10 + + +```` + + + +## Demo + +See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progress-Bars) to see it in action. + +## Attributes + +### value + +A value indicates the current progress of the bar. + +### type + +A value indicates the background color of the progress bar. Should be one of the following values: + +* `Default` (default value) +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +### min-value + +Minimum value of the progress bar. Default is 0. + +### max-value + +Maximum value of the progress bar. Default is 100. + +### strip + +A value indicates if the background style of the progress bar is stripped. Should be one of the following values: + +* `false` (default value) +* `true` + +### animation + +A value indicates if the stripped background style of the progress bar is animated. Should be one of the following values: + +* `false` (default value) +* `true` \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Tabs.md b/docs/en/UI/AspNetCore/Tag-Helpers/Tabs.md new file mode 100644 index 0000000000..4b8763dc4e --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Tabs.md @@ -0,0 +1,93 @@ +# Tabs + +## Introduction + +`abp-tab` is the basic tab navigation content container derived from bootstrap tab element. + +Basic usage: + +````xml + + + Content_Home + + + + Content_Profile + + + + Content_1_Content + + + Content_2_Content + + + +```` + + + +## Demo + +See the [tabs demo page](https://bootstrap-taghelpers.abp.io/Components/Tabs) to see it in action. + +## abp-tab Attributes + +- **title**: Sets the text of the tab menu. +- **name:** Sets "id" attribute of generated elements. Default value is a Guid. Not needed unless tabs are changed or modified with Jquery. +- **active**: Sets the active tab. + +Example: + +````xml + + + Content_Home + + + Content_Profile + + + Content_Contact + + +```` + +### Pills + +Example: + +````xml + + + Content_Home + + + Content_Profile + + + Content_Contact + + +```` + +### Vertical + +**vertical-header-size**: Sets the column width of tab headers. + +Example: + +````xml + + + Content_Home + + + Content_Profile + + + Content_Contact + + +```` diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Tooltips.md b/docs/en/UI/AspNetCore/Tag-Helpers/Tooltips.md new file mode 100644 index 0000000000..55b6e574e8 --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Tooltips.md @@ -0,0 +1,35 @@ +# Tooltips + +## Introduction + +`abp-tooltip` is the abp tag for tooltips. + +Basic usage: + +````xml + + Tooltip Default + + + + Tooltip on top + + + + Tooltip on right + + + + Tooltip on bottom + + + + Disabled button Tooltip + +```` + + + +## Demo + +See the [tooltips demo page](https://bootstrap-taghelpers.abp.io/Components/Tooltips) to see it in action. diff --git a/docs/en/UI/AspNetCore/Theming.md b/docs/en/UI/AspNetCore/Theming.md new file mode 100644 index 0000000000..e41545635c --- /dev/null +++ b/docs/en/UI/AspNetCore/Theming.md @@ -0,0 +1,3 @@ +# ASP.NET Core MVC / Razor Pages Theming + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Toolbars.md b/docs/en/UI/AspNetCore/Toolbars.md new file mode 100644 index 0000000000..cc3bcd95be --- /dev/null +++ b/docs/en/UI/AspNetCore/Toolbars.md @@ -0,0 +1,3 @@ +# Toolbars + +TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Widgets.md b/docs/en/UI/AspNetCore/Widgets.md new file mode 100644 index 0000000000..062fefb078 --- /dev/null +++ b/docs/en/UI/AspNetCore/Widgets.md @@ -0,0 +1,505 @@ +# Widgets + +ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; + +* Have **scripts & styles** dependencies for your widget. +* Create **dashboards** with widgets used inside. +* Define widgets in reusable **[modules](../../Module-Development-Basics.md)**. +* Co-operate widgets with **[authorization](../../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. + +## Basic Widget Definition + +### Create a View Component + +As the first step, create a new regular ASP.NET Core View Component: + +![widget-basic-files](../../images/widget-basic-files.png) + +**MySimpleWidgetViewComponent.cs**: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. + +You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. + +**Default.cshtml**: + +```xml +
+

My Simple Widget

+

This is a simple widget!

+
+``` + +### Define the Widget + +Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## Rendering a Widget + +Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: + +````xml +@await Component.InvokeAsync("MySimpleWidget") +@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) +```` + +First approach uses the widget name while second approach uses the view component type. + +### Widgets with Arguments + +ASP.NET Core's view component system allows you to accept arguments for view components. The sample view component below accepts `startDate` and `endDate` and uses these arguments to retrieve data from a service. + +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget +{ + [Widget] + public class CountersWidgetViewComponent : AbpViewComponent + { + private readonly IDashboardAppService _dashboardAppService; + + public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) + { + _dashboardAppService = dashboardAppService; + } + + public async Task InvokeAsync( + DateTime startDate, DateTime endDate) + { + var result = await _dashboardAppService.GetCountersWidgetAsync( + new CountersWidgetInputDto + { + StartDate = startDate, + EndDate = endDate + } + ); + + return View(result); + } + } +} +```` + +Now, you need to pass an anonymous object to pass arguments as shown below: + +````xml +@await Component.InvokeAsync("CountersWidget", new +{ + startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), + endDate = DateTime.Now +}) +```` + +## Widget Name + +Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. + +To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: + +```csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + [ViewComponent(Name = "MyCustomNamedWidget")] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); + } + } +} +``` + +ABP will respect to the custom name by handling the widget. + +> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. + +### Display Name + +You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: + +````csharp +using DashboardDemo.Localization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + DisplayName = "MySimpleWidgetDisplayName", //Localization key + DisplayNameResource = typeof(DashboardDemoResource) //localization resource + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +See [the localization document](../../Localization.md) to learn about localization resources and keys. + +## Style & Script Dependencies + +There are some challenges when your widget has script and style files; + +* Any page uses the widget should also include the **its script & styles** files into the page. +* The page should also care about **depended libraries/files** of the widget. + +ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. + +### Defining as Simple File Paths + +The example widget below adds a style and a script file: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, + ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../../Virtual-File-System.md). + +### Defining Bundle Contributors + +All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. + +The sample code below does the same with the code above, but defines and uses bundle contributors: + +````csharp +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, + ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } + + public class MySimpleWidgetStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); + } + } + + public class MySimpleWidgetScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); + } + } +} + +```` + +Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. + +See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. + +## RefreshUrl + +A widget may design a `RefreshUrl` that is used whenever the widget needs to be refreshed. If it is defined, the widget is re-rendered on the server side on every refresh (see the refresh `method` of the `WidgetManager` below). + +````csharp +[Widget(RefreshUrl = "Widgets/Counters")] +public class CountersWidgetViewComponent : AbpViewComponent +{ + +} +```` + +Once you define a `RefreshUrl` for your widget, you need to provide an endpoint to render and return it: + +````csharp +[Route("Widgets")] +public class CountersWidgetController : AbpController +{ + [HttpGet] + [Route("Counters")] + public IActionResult Counters(DateTime startDate, DateTime endDate) + { + return ViewComponent("CountersWidget", new {startDate, endDate}); + } +} +```` + +`Widgets/Counters` route matches to the `RefreshUrl` declared before. + +> 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). + +## 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. + +### WidgetManager + +`WidgetManager` is used to initialize and refresh one or more widgets. Create a new `WidgetManager` as shown below: + +````js +$(function() { + var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); +}) +```` + +`MyDashboardWidgetsArea` may contain one or more widgets inside. + +> Using the `WidgetManager` inside document.ready (like above) is a good practice since its functions use the DOM and need DOM to be ready. + +#### WidgetManager.init() + +`init` simply initializes the `WidgetManager` and calls `init` methods of the related widgets if they define (see Widget JavaScript API section below) + +```js +myWidgetManager.init(); +``` + +#### WidgetManager.refresh() + +`refresh` method refreshes all widgets related to this `WidgetManager`: + +```` +myWidgetManager.refresh(); +```` + +#### WidgetManager Options + +WidgetManager has some additional options. + +##### Filter Form + +If your widgets require parameters/filters then you will generally have a form to filter the widgets. In such cases, you can create a form that has some form elements and a dashboard area with some widgets inside. Example: + +````xml +
+ ...form elements +
+ +
+ ...widgets +
+```` + +`data-widget-filter` attribute relates the form with the widgets. Whenever the form is submitted, all the widgets are automatically refreshed with the form fields as the filter. + +Instead of the `data-widget-filter` attribute, you can use the `filterForm` parameter of the `WidgetManager` constructor. Example: + +````js +var myWidgetManager = new abp.WidgetManager({ + wrapper: '#MyDashboardWidgetsArea', + filterForm: '#MyDashboardFilterForm' +}); +```` + +##### Filter Callback + +You may want to have a better control to provide filters while initializing and refreshing the widgets. In this case, you can use the `filterCallback` option: + +````js +var myWidgetManager = new abp.WidgetManager({ + wrapper: '#MyDashboardWidgetsArea', + filterCallback: function() { + return $('#MyDashboardFilterForm').serializeFormToObject(); + } +}); +```` + +This example shows the default implementation of the `filterCallback`. You can return any JavaScript object with fields. Example: + +````js +filterCallback: function() { + return { + 'startDate': $('#StartDateInput').val(), + 'endDate': $('#EndDateInput').val() + }; +} +```` + +The returning filters are passed to all widgets on `init` and `refresh`. + +### Widget JavaScript API + +A widget can define a JavaScript API that is invoked by the `WidgetManager` when needed. The code sample below can be used to start to define an API for a widget. + +````js +(function () { + abp.widgets.NewUserStatisticWidget = function ($wrapper) { + + var getFilters = function () { + return { + ... + }; + } + + var refresh = function (filters) { + ... + }; + + var init = function (filters) { + ... + }; + + return { + getFilters: getFilters, + init: init, + refresh: refresh + }; + }; +})(); +```` + +`NewUserStatisticWidget` is the name of the widget here. It should match the widget name defined in the server side. All of the functions are optional. + +#### getFilters + +If the widget has internal custom filters, this function should return the filter object. Example: + +````js +var getFilters = function() { + return { + frequency: $wrapper.find('.frequency-filter option:selected').val() + }; +} +```` + +This method is used by the `WidgetManager` while building filters. + +#### init + +Used to initialize the widget when needed. It has a filter argument that can be used while getting data from server. `init` method is used when `WidgetManager.init()` function is called. It is also called if your widget requires a full re-load on refresh. See the `RefreshUrl` widget option. + +#### refresh + +Used to refresh the widget when needed. It has a filter argument that can be used while getting data from server. `refresh` method is used whenever `WidgetManager.refresh()` function is called. + +## Authorization + +Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: + +* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). +* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../../Authorization.md) for more info about policies. + +Example: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget(RequiredPolicies = new[] { "MyPolicyName" })] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## WidgetOptions + +As alternative to the `Widget` attribute, you can use the `AbpWidgetOptions` to configure widgets: + +```csharp +Configure(options => +{ + options.Widgets.Add(); +}); +``` + +Write this into the `ConfigureServices` method of your [module](../../Module-Development-Basics.md). All the configuration done with the `Widget` attribute is also possible with the `AbpWidgetOptions`. Example configuration that adds a style for the widget: + +````csharp +Configure(options => +{ + options.Widgets + .Add() + .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); +}); +```` + +> Tip: `AbpWidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. + +## See Also + +* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). \ No newline at end of file diff --git a/docs/en/UI/Common/Utils/Linked-List.md b/docs/en/UI/Common/Utils/Linked-List.md new file mode 100644 index 0000000000..5da9cc10e9 --- /dev/null +++ b/docs/en/UI/Common/Utils/Linked-List.md @@ -0,0 +1,1488 @@ +# Linked List (Doubly) + + + +The core module provides a useful data structure known as a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list). Briefly, a doubly linked list is a series of records (a.k.a. nodes) which has information on the previous node, the next node, and its own value (or data). + + + +## Getting Started + +To create a doubly linked list, all you have to do is to import and create a new instance of it: + +```js +import { LinkedList } from '@abp/ng.core'; + +const list = new LinkedList(); +``` + + + +The constructor does not get any parameters. + + + +## Usage + +### How to Add New Nodes + +There are several methods to create new nodes in a linked list and all of them are separately available as well as revealed by `add` and `addMany` methods. + + + +#### addHead(value) + +```js +addHead(value: T): ListNode\ +``` + +Adds a node with given value as the first node in list: + +```js +list.addHead('a'); + +// "a" + +list.addHead('b'); + +// "b" <-> "a" + +list.addHead('c'); + +// "c" <-> "b" <-> "a" +``` + + + +#### addManyHead(values) + +```js +addManyHead(values: T\[\]): ListNode\\[\] +``` + +Adds multiple nodes with given values as the first nodes in list: + +```js +list.addManyHead(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +list.addManyHead(['x', 'y', 'z']); + +// "x" <-> "y" <-> "z" <-> "a" <-> "b" <-> "c" +``` + + + +#### addTail(value) + +```js +addTail(value: T): ListNode\ +``` + +Adds a node with given value as the last node in list: + +```js +list.addTail('a'); + +// "a" + +list.addTail('b'); + +// "a" <-> "b" + +list.addTail('c'); + +// "a" <-> "b" <-> "c" +``` + + + +#### addManyTail(values) + +```js +addManyTail(values: T\[\]): ListNode\\[\] +``` + +Adds multiple nodes with given values as the last nodes in list: + +```js +list.addManyTail(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +list.addManyTail(['x', 'y', 'z']); + +// "a" <-> "b" <-> "c" <-> "x" <-> "y" <-> "z" +``` + + + +#### addAfter(value, previousValue, compareFn) + +```js +addAfter(value: T, previousValue: T, compareFn = compare): ListNode\ +``` + +Adds a node with given value after the first node that has the previous value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addAfter('x', 'b'); + +// "a" <-> "b" <-> "x" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 2 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addAfter( + { x: 0 }, + 2, + (value, searchedValue) => value.x === searchedValue +); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addManyAfter(values, previousValue, compareFn) + +```js +addManyAfter(values: T\[\], previousValue: T, compareFn = compare): ListNode\\[\] +``` + +Adds multiple nodes with given values after the first node that has the previous value: + +```js +list.addManyTail(['a', 'b', 'b', 'c']); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addManyAfter(['x', 'y'], 'b'); + +// "a" <-> "b" <-> "x" <-> "y" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addManyAfter( + [{ x: 4 }, { x: 5 }], + 2, + (value, searchedValue) => value.x === searchedValue +); + +// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addBefore(value, nextValue, compareFn) + +```js +addBefore(value: T, nextValue: T, compareFn = compare): ListNode\ +``` + +Adds a node with given value before the first node that has the next value: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addBefore('x', 'b'); + +// "a" <-> "x" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTail({ x: 1 }); +list.addTail({ x: 2 }); +list.addTail({ x: 3 }); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addBefore( + { x: 0 }, + 2, + (value, searchedValue) => value.x === searchedValue +); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addManyBefore(values, nextValue, compareFn) + +```js +addManyBefore(values: T\[\], nextValue: T, compareFn = compare): ListNode\\[\] +``` + +Adds multiple nodes with given values before the first node that has the next value: + +```js +list.addManyTail(['a', 'b', 'b', 'c']); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addManyBefore(['x', 'y'], 'b'); + +// "a" <-> "x" <-> "y" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list.addManyBefore( + [{ x: 4 }, { x: 5 }], + 2, + (value, searchedValue) => value.x === searchedValue +); + +// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addByIndex(value, position) + +```js +addByIndex(value: T, position: number): ListNode\ +``` + +Adds a node with given value at the specified position in the list: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.addByIndex('x', 2); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +It works with negative index too: + +```js +list.addTail('a'); +list.addTail('b'); +list.addTail('c'); + +// "a" <-> "b" <-> "c" + +list.addByIndex('x', -1); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +#### addManyByIndex(values, position) + +```js +addManyByIndex(values: T\[\], position: number): ListNode\\[\] +``` + +Adds multiple nodes with given values at the specified position in the list: + +```js +list.addManyTail(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +list.addManyByIndex(['x', 'y'], 2); + +// "a" <-> "b" <-> "x" <-> "y" <-> "c" +``` + + + +It works with negative index too: + +```js +list.addManyTail(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +list.addManyByIndex(['x', 'y'], -1); + +// "a" <-> "b" <-> "x" <-> "y" <-> "c" +``` + + + +#### add(value).head() + +```js +add(value: T).head(): ListNode\ +``` + +Adds a node with given value as the first node in list: + +```js +list.add('a').head(); + +// "a" + +list.add('b').head(); + +// "b" <-> "a" + +list.add('c').head(); + +// "c" <-> "b" <-> "a" +``` + + + +> This is an alternative API for `addHead`. + + + +#### add(value).tail() + +```js +add(value: T).tail(): ListNode\ +``` + +Adds a node with given value as the last node in list: + +```js +list.add('a').tail(); + +// "a" + +list.add('b').tail(); + +// "a" <-> "b" + +list.add('c').tail(); + +// "a" <-> "b" <-> "c" +``` + + + +> This is an alternative API for `addTail`. + + + +#### add(value).after(previousValue, compareFn) + +```js +add(value: T).after(previousValue: T, compareFn = compare): ListNode\ +``` + +Adds a node with given value after the first node that has the previous value: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.add('x').after('b'); + +// "a" <-> "b" <-> "x" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list + .add({ x: 0 }) + .after(2, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> This is an alternative API for `addAfter`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### add(value).before(nextValue, compareFn) + +```js +add(value: T).before(nextValue: T, compareFn = compare): ListNode\ +``` + +Adds a node with given value before the first node that has the next value: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.add('x').before('b'); + +// "a" <-> "x" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.add({ x: 1 }).tail(); +list.add({ x: 2 }).tail(); +list.add({ x: 3 }).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list + .add({ x: 0 }) + .before(2, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `addBefore`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### add(value).byIndex(position) + +```js +add(value: T).byIndex(position: number): ListNode\ +``` + +Adds a node with given value at the specified position in the list: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.add('x').byIndex(2); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +It works with negative index too: + +```js +list.add('a').tail(); +list.add('b').tail(); +list.add('c').tail(); + +// "a" <-> "b" <-> "c" + +list.add('x').byIndex(-1); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +> This is an alternative API for `addByIndex`. + + + +#### addMany(values).head() + +```js +addMany(values: T\[\]).head(): ListNode\\[\] +``` + +Adds multiple nodes with given values as the first nodes in list: + +```js +list.addMany(['a', 'b', 'c']).head(); + +// "a" <-> "b" <-> "c" + +list.addMany(['x', 'y', 'z']).head(); + +// "x" <-> "y" <-> "z" <-> "a" <-> "b" <-> "c" +``` + + + +> This is an alternative API for `addManyHead`. + + + +#### addMany(values).tail() + +```js +addMany(values: T\[\]).tail(): ListNode\\[\] +``` + +Adds multiple nodes with given values as the last nodes in list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.addMany(['x', 'y', 'z']).tail(); + +// "a" <-> "b" <-> "c" <-> "x" <-> "y" <-> "z" +``` + + + +> This is an alternative API for `addManyTail`. + + + +#### addMany(values).after(previousValue, compareFn) + +```js +addMany(values: T\[\]).after(previousValue: T, compareFn = compare): ListNode\\[\] +``` + +Adds multiple nodes with given values after the first node that has the previous value: + +```js +list.addMany(['a', 'b', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addMany(['x', 'y']).after('b'); + +// "a" <-> "b" <-> "x" <-> "y" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list + .addMany([{ x: 4 }, { x: 5 }]) + .after(2, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3} +``` + + + +> This is an alternative API for `addManyAfter`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addMany(values).before(nextValue, compareFn) + +```js +addMany(values: T\[\]).before(nextValue: T, compareFn = compare): ListNode\\[\] +``` + +Adds multiple nodes with given values before the first node that has the next value: + +```js +list.addMany(['a', 'b', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "b" <-> "c" + +list.addMany(['x', 'y']).before('b'); + +// "a" <-> "x" <-> "y" <-> "b" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} + +list + .addMany([{ x: 4 }, { x: 5 }]) + .before(2, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `addManyBefore`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### addMany(values).byIndex(position) + +```js +addMany(values: T\[\]).byIndex(position: number): ListNode\\[\] +``` + +Adds multiple nodes with given values at the specified position in the list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.addMany(['x', 'y']).byIndex(2); + +// "a" <-> "b" <-> "x" <-> "y" <-> "c" +``` + + + +It works with negative index too: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.addMany(['x', 'y']).byIndex(-1); + +// "a" <-> "b" <-> "x" <-> "y" <-> "c" +``` + + + +> This is an alternative API for `addManyByIndex`. + + + +### How to Remove Nodes + +There are a few methods to remove nodes from a linked list and all of them are separately available as well as revealed from a `drop` method. + + + +#### dropHead() + +```js +dropHead(): ListNode\ | undefined +``` + +Removes the first node from the list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropHead(); + +// "b" <-> "c" +``` + + + +#### dropManyHead(count) + +```js +dropManyHead(count: number): ListNode\\[\] +``` + +Removes the first nodes from the list based on given count: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropManyHead(2); + +// "c" +``` + + + +#### dropTail() + +```js +dropTail(): ListNode\ | undefined +``` + +Removes the last node from the list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropTail(); + +// "a" <-> "b" +``` + + + +#### dropManyTail(count) + +```js +dropManyTail(count: number): ListNode\\[\] +``` + +Removes the last nodes from the list based on given count: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropManyTail(2); + +// "a" +``` + + + +#### dropByIndex(position) + +```js +dropByIndex(position: number): ListNode\ | undefined +``` + +Removes the node with the specified position from the list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropByIndex(1); + +// "a" <-> "c" +``` + + + +It works with negative index too: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropByIndex(-2); + +// "a" <-> "c" +``` + + + +#### dropManyByIndex(count, position) + +```js +dropManyByIndex(count: number, position: number): ListNode\\[\] +``` + +Removes the nodes starting from the specified position from the list based on given count: + +```js +list.addMany(['a', 'b', 'c', 'd']).tail(); + +// "a" <-> "b" <-> "c" <-> "d + +list.dropManyByIndex(2, 1); + +// "a" <-> "d" +``` + + + +It works with negative index too: + +```js +list.addMany(['a', 'b', 'c', 'd']).tail(); + +// "a" <-> "b" <-> "c" <-> "d + +list.dropManyByIndex(2, -2); + +// "a" <-> "d" +``` + + + +#### dropByValue(value, compareFn) + +```js +dropByValue(value: T, compareFn = compare): ListNode\ | undefined +``` + +Removes the first node with given value from the list: + +```js +list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.dropByValue('x'); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.dropByValue(0, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### dropByValueAll(value, compareFn) + +```js +dropByValueAll(value: T, compareFn = compare): ListNode\\[\] +``` + +Removes all nodes with given value from the list: + +```js +list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.dropByValueAll('x'); + +// "a" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list.dropByValue(0, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":2} <-> {"x":3} +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### drop().head() + +```js +drop().head(): ListNode\ | undefined +``` + +Removes the first node in list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.drop().head(); + +// "b" <-> "c" +``` + + + +> This is an alternative API for `dropHead`. + + + +#### drop().tail() + +```js +drop().tail(): ListNode\ | undefined +``` + +Removes the last node in list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.drop().tail(); + +// "a" <-> "b" +``` + + + +> This is an alternative API for `dropTail`. + + + +#### drop().byIndex(position) + +```js +drop().byIndex(position: number): ListNode\ | undefined +``` + +Removes the node with the specified position from the list: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.drop().byIndex(1); + +// "a" <-> "c" +``` + + + +It works with negative index too: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.drop().byIndex(-2); + +// "a" <-> "c" +``` + + + +> This is an alternative API for `dropByIndex`. + + + +#### drop().byValue(value, compareFn) + +```js +drop().byValue(value: T, compareFn = compare): ListNode\ | undefined +``` + +Removes the first node with given value from the list: + +```js +list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.drop().byValue('x'); + +// "a" <-> "b" <-> "x" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list + .drop() + .byValue(0, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3} +``` + + + +> This is an alternative API for `dropByValue`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### drop().byValueAll(value, compareFn) + +```js +drop().byValueAll(value: T, compareFn = compare): ListNode\\[\] +``` + +Removes all nodes with given value from the list: + +```js +list.addMany(['a', 'x', 'b', 'x', 'c']).tail(); + +// "a" <-> "x" <-> "b" <-> "x" <-> "c" + +list.drop().byValueAll('x'); + +// "a" <-> "b" <-> "c" +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail(); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +list + .drop() + .byValueAll(0, (value, searchedValue) => value.x === searchedValue); + +// {"x":1} <-> {"x":2} <-> {"x":3} +``` + + + +> This is an alternative API for `dropByValueAll`. +> +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +#### dropMany(count).head() + +```js +dropMany(count: number).head(): ListNode\\[\] +``` + +Removes the first nodes from the list based on given count: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropMany(2).head(); + +// "c" +``` + + + +> This is an alternative API for `dropManyHead`. + + + +#### dropMany(count).tail() + +```js +dropMany(count: number).tail(): ListNode\\[\] +``` + +Removes the last nodes from the list based on given count: + +```js +list.addMany(['a', 'b', 'c']).tail(); + +// "a" <-> "b" <-> "c" + +list.dropMany(2).tail(); + +// "a" +``` + + + +> This is an alternative API for `dropManyTail`. + + + +#### dropMany(count).byIndex(position) + +```js +dropMany(count: number).byIndex(position: number): ListNode\\[\] +``` + +Removes the nodes starting from the specified position from the list based on given count: + +```js +list.addMany(['a', 'b', 'c', 'd']).tail(); + +// "a" <-> "b" <-> "c" <-> "d + +list.dropMany(2).byIndex(1); + +// "a" <-> "d" +``` + + + +It works with negative index too: + +```js +list.addMany(['a', 'b', 'c', 'd']).tail(); + +// "a" <-> "b" <-> "c" <-> "d + +list.dropMany(2).byIndex(-2); + +// "a" <-> "d" +``` + + + +> This is an alternative API for `dropManyByIndex`. + + + +### How to Find Nodes + +There are a few methods to find specific nodes in a linked list. + + + +#### find(predicate) + +```js +find(predicate: ListIteratorFunction\): ListNode\ | undefined +``` + +Finds the first node from the list that matches the given predicate: + +```js +list.addTailMany(['a', 'b', 'b', 'c']); + +// "a" <-> "b" <-> "b" <-> "c" + +const found = list.find(node => node.value === 'b'); + +/* +found.value === "b" +found.previous.value === "a" +found.next.value === "b" +*/ +``` + + + +#### findIndex(predicate) + +```js +findIndex(predicate: ListIteratorFunction\): number +``` + +Finds the position of the first node from the list that matches the given predicate: + +```js +list.addTailMany(['a', 'b', 'b', 'c']); + +// "a" <-> "b" <-> "b" <-> "c" + +const i0 = list.findIndex(node => node.next && node.next.value === 'b'); +const i1 = list.findIndex(node => node.value === 'b'); +const i2 = list.findIndex(node => node.previous && node.previous.value === 'b'); +const i3 = list.findIndex(node => node.value === 'x'); + +/* +i0 === 0 +i1 === 1 +i2 === 2 +i3 === -1 +*/ +``` + + + +#### get(position) + +```js +get(position: number): ListNode\ | undefined +``` + +Finds and returns the node with specific position in the list: + +```js +list.addTailMany(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +const found = list.get(1); + +/* +found.value === "b" +found.previous.value === "a" +found.next.value === "c" +*/ +``` + + + +#### indexOf(value, compareFn) + +```js +indexOf(value: T, compareFn = compare): number +``` + +Finds the position of the first node from the list that has the given value: + +```js +list.addTailMany(['a', 'b', 'b', 'c']); + +// "a" <-> "b" <-> "b" <-> "c" + +const i0 = list.indexOf('a'); +const i1 = list.indexOf('b'); +const i2 = list.indexOf('c'); +const i3 = list.indexOf('x'); + +/* +i0 === 0 +i1 === 1 +i2 === 3 +i3 === -1 +*/ +``` + + + +You may pass a custom compare function to detect the searched value: + +```js +list.addTailMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]); + +// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3} + +const i0 = indexOf(1, (value, searchedValue) => value.x === searchedValue); +const i1 = indexOf(2, (value, searchedValue) => value.x === searchedValue); +const i2 = indexOf(3, (value, searchedValue) => value.x === searchedValue); +const i3 = indexOf(0, (value, searchedValue) => value.x === searchedValue); +const i4 = indexOf(4, (value, searchedValue) => value.x === searchedValue); + +/* +i0 === 0 +i1 === 2 +i2 === 4 +i3 === 1 +i4 === -1 +*/ +``` + + + +> The default compare function checks deep equality, so you will rarely need to pass that parameter. + + + +### How to Check All Nodes + +There are a few ways to iterate over or display a linked list. + + + +#### forEach(callback) + +```js +forEach(callback: ListIteratorFunction\): void +``` + +Runs a callback function on all nodes in a linked list from head to tail: + +```js +list.addTailMany(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +list.forEach((node, index) => console.log(node.value + index)); + +// 'a0' +// 'b1' +// 'c2' +``` + + + +#### \*\[Symbol.iterator\]\(\) + +A linked list is iterable. In other words, you may use methods like `for...of` on it. + +```js +list.addTailMany(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +for(const node of list) { + console.log(node.value); +} + +// 'a' +// 'b' +// 'c' +``` + + + +#### toArray() + +```js +toArray(): T\[\] +``` + +Converts a linked list to an array of values: + +```js +list.addTailMany(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +const arr = list.toArray(); + +/* +arr === ['a', 'b', 'c'] +*/ +``` + + + +#### toNodeArray() + +```js +toNodeArray(): T\[\] +``` + +Converts a linked list to an array of nodes: + +```js +list.addTailMany(['a', 'b', 'c']); + +// "a" <-> "b" <-> "c" + +const arr = list.toNodeArray(); + +/* +arr[0].value === 'a' +arr[1].value === 'a' +arr[2].value === 'a' +*/ +``` + + + +#### toString() + +```js +toString(): string +``` + +Converts a linked list to a string representation of nodes and their relations: + +```js +list.addTailMany(['a', 2, 'c', { k: 4, v: 'd' }]); + +// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"} + +const str = list.toString(); + +/* +str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}' +*/ +``` + + + +You may pass a custom mapper function to map values before stringifying them: + +```js +list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }]).tail(); + +// {"x":1} <-> {"x":2} <-> {"x":3} <-> {"x":4} <-> {"x":5} + +const str = list.toString(value => value.x); + +/* +str === '1 <-> 2 <-> 3 <-> 4 <-> 5' +*/ +``` + + + diff --git a/docs/en/_resources/Diagrams.docx b/docs/en/_resources/Diagrams.docx new file mode 100644 index 0000000000..424e281470 Binary files /dev/null and b/docs/en/_resources/Diagrams.docx differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index cbb4c80e72..471e215e3a 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -5,6 +5,7 @@ "items": [ { "text": "From Startup Templates", + "path": "Getting-Started-With-Startup-Templates.md", "items": [ { "text": "Application with MVC (Razor Pages) UI", @@ -38,20 +39,51 @@ "text": "Application Development", "items": [ { - "text": "With ASP.NET Core MVC UI", - "path": "Tutorials/AspNetCore-Mvc/Part-I.md" + "text": "Part-1: Creating a new solution and listing items", + "path": "Tutorials/Part-1.md" }, { - "text": "With Angular UI", - "path": "Tutorials/Angular/Part-I.md" + "text": "Part-2: CRUD operations", + "path": "Tutorials/Part-2.md" + }, + { + "text": "Part-3: Integration tests", + "path": "Tutorials/Part-3.md" } ] } ] }, { - "text": "ASP.NET Boilerplate Migration Guide", - "path": "AspNet-Boilerplate-Migration-Guide.md" + "text": "Guides", + "items": [ + { + "text": "Customizing the Application Modules", + "path": "Customizing-Application-Modules-Guide.md", + "items": [ + { + "text": "Extending Entities", + "path": "Customizing-Application-Modules-Extending-Entities.md" + }, + { + "text": "Overriding Services", + "path": "Customizing-Application-Modules-Overriding-Services.md" + }, + { + "text": "Overriding the User Interface", + "path": "Customizing-Application-Modules-Overriding-User-Interface.md" + } + ] + }, + { + "text": "\"How to\" Guides", + "path": "How-To/Index.md" + }, + { + "text": "Migrating from the ASP.NET Boilerplate", + "path": "AspNet-Boilerplate-Migration-Guide.md" + } + ] }, { "text": "CLI", @@ -215,7 +247,8 @@ "text": "Domain Services" }, { - "text": "Specifications" + "text": "Specifications", + "path": "Specifications.md" } ] }, @@ -237,43 +270,101 @@ ] }, { - "text": "ASP.NET Core", + "text": "API", "items": [ { - "text": "API", - "items": [ - { - "text": "Auto API Controllers", - "path": "AspNetCore/Auto-API-Controllers.md" - }, - { - "text": "Dynamic C# API Clients", - "path": "AspNetCore/Dynamic-CSharp-API-Clients.md" - } - ] + "text": "Auto API Controllers", + "path": "API/Auto-API-Controllers.md" }, { - "text": "User Interface", + "text": "Dynamic C# API Clients", + "path": "API/Dynamic-CSharp-API-Clients.md" + } + ] + }, + { + "text": "User Interface", + "items": [ + { + "text": "ASP.NET Core", "items": [ { "text": "Client Side Package Management", - "path": "AspNetCore/Client-Side-Package-Management.md" + "path": "UI/AspNetCore/Client-Side-Package-Management.md" }, { "text": "Bundling & Minification", - "path": "AspNetCore/Bundling-Minification.md" + "path": "UI/AspNetCore/Bundling-Minification.md" }, { "text": "Tag Helpers", - "path": "AspNetCore/Tag-Helpers/Index.md" + "path": "UI/AspNetCore/Tag-Helpers/Index.md" }, { "text": "Widgets", - "path": "AspNetCore/Widgets.md" + "path": "UI/AspNetCore/Widgets.md" }, { "text": "Theming", - "path": "AspNetCore/Theming.md" + "path": "UI/AspNetCore/Theming.md" + }, + { + "text": "Customize/Extend the UI", + "path": "UI/AspNetCore/Customization-User-Interface.md" + } + ] + }, + { + "text": "Angular", + "items": [ + { + "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": "Config State", + "path": "UI/Angular/Config-State.md" + }, + { + "text": "Component Replacement", + "path": "UI/Angular/Component-Replacement.md" + }, + { + "text": "Custom Setting Page", + "path": "UI/Angular/Custom-Setting-Page.md" + }, + { + "text": "Lazy Loading Scripts & Styles", + "path": "UI/Angular/Lazy-Load-Service.md" + }, + { + "text": "DomInsertionService", + "path": "UI/Angular/Dom-Insertion-Service.md" + }, + { + "text": "TrackByService", + "path": "UI/Angular/Track-By-Service.md" + } + ] + }, + { + "text": "Common", + "items": [ + { + "text": "Linked List (Doubly)", + "path": "UI/Common/Utils/Linked-List.md" } ] } @@ -295,20 +386,26 @@ "path": "Entity-Framework-Core.md", "items": [ { - "text": "Switch to MySQL", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "Switch to PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - }, - { - "text": "Switch to SQLite", - "path": "Entity-Framework-Core-SQLite.md" + "text": "Database Migrations", + "path": "Entity-Framework-Core-Migrations.md" }, { - "text": "Switch to another DBMS", - "path": "Entity-Framework-Core-Other-DBMS.md" + "text": "Switch DBMS", + "path": "Entity-Framework-Core-Other-DBMS.md", + "items": [ + { + "text": "To MySQL", + "path": "Entity-Framework-Core-MySQL.md" + }, + { + "text": "To PostgreSQL", + "path": "Entity-Framework-Core-PostgreSQL.md" + }, + { + "text": "To SQLite", + "path": "Entity-Framework-Core-SQLite.md" + } + ] } ] }, @@ -338,6 +435,20 @@ { "text": "RabbitMQ Integration", "path": "Background-Jobs-RabbitMq.md" + }, + { + "text": "Quartz Integration", + "path": "Background-Jobs-Quartz.md" + } + ] + }, + { + "text": "Background Workers", + "path": "Background-Workers.md", + "items": [ + { + "text": "Quartz Integration", + "path": "Background-Workers-Quartz.md" } ] } @@ -386,4 +497,4 @@ "path": "Contribution/Index.md" } ] -} \ No newline at end of file +} diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json new file mode 100644 index 0000000000..23d079f9bb --- /dev/null +++ b/docs/en/docs-params.json @@ -0,0 +1,28 @@ +{ + "parameters": [ + { + "name": "UI", + "displayName": "UI", + "values": { + "MVC": "MVC / Razor Pages", + "NG": "Angular" + } + }, + { + "name": "DB", + "displayName": "Database", + "values": { + "EF": "Entity Framework Core", + "Mongo": "MongoDB" + } + }, + { + "name": "Tiered", + "displayName": "Tiered", + "values": { + "No": "Not Tiered", + "Yes": "Tiered" + } + } + ] +} \ No newline at end of file diff --git a/docs/en/images/angular-folder-structure.png b/docs/en/images/angular-folder-structure.png new file mode 100644 index 0000000000..e17eb135ce Binary files /dev/null and b/docs/en/images/angular-folder-structure.png differ diff --git a/docs/en/images/angular-template-structure-diagram.png b/docs/en/images/angular-template-structure-diagram.png new file mode 100644 index 0000000000..d27d75bf7f Binary files /dev/null and b/docs/en/images/angular-template-structure-diagram.png differ diff --git a/docs/en/images/bookstore-added-brand-files.png b/docs/en/images/bookstore-added-brand-files.png new file mode 100644 index 0000000000..15ef283a2a Binary files /dev/null and b/docs/en/images/bookstore-added-brand-files.png differ diff --git a/docs/en/images/bookstore-added-logo.png b/docs/en/images/bookstore-added-logo.png new file mode 100644 index 0000000000..55af77a5f4 Binary files /dev/null and b/docs/en/images/bookstore-added-logo.png differ diff --git a/docs/en/images/bookstore-added-role-js-file.png b/docs/en/images/bookstore-added-role-js-file.png new file mode 100644 index 0000000000..00881bf808 Binary files /dev/null and b/docs/en/images/bookstore-added-role-js-file.png differ diff --git a/docs/en/images/bookstore-brand-area-highlighted.png b/docs/en/images/bookstore-brand-area-highlighted.png new file mode 100644 index 0000000000..7ab1b8fb7f Binary files /dev/null and b/docs/en/images/bookstore-brand-area-highlighted.png differ diff --git a/docs/en/images/bookstore-efcore-dependencies.png b/docs/en/images/bookstore-efcore-dependencies.png new file mode 100644 index 0000000000..3de5a679ef Binary files /dev/null and b/docs/en/images/bookstore-efcore-dependencies.png differ diff --git a/docs/en/images/bookstore-global-css-file.png b/docs/en/images/bookstore-global-css-file.png new file mode 100644 index 0000000000..e764fef8ee Binary files /dev/null and b/docs/en/images/bookstore-global-css-file.png differ diff --git a/docs/en/images/bookstore-google-analytics-view-component.png b/docs/en/images/bookstore-google-analytics-view-component.png new file mode 100644 index 0000000000..e548a00935 Binary files /dev/null and b/docs/en/images/bookstore-google-analytics-view-component.png differ diff --git a/docs/en/images/bookstore-menus-highlighted.png b/docs/en/images/bookstore-menus-highlighted.png new file mode 100644 index 0000000000..c4f3237b00 Binary files /dev/null and b/docs/en/images/bookstore-menus-highlighted.png differ diff --git a/docs/en/images/bookstore-notification-icon-on-toolbar.png b/docs/en/images/bookstore-notification-icon-on-toolbar.png new file mode 100644 index 0000000000..817491e9eb Binary files /dev/null and b/docs/en/images/bookstore-notification-icon-on-toolbar.png differ diff --git a/docs/en/images/bookstore-notification-view-component.png b/docs/en/images/bookstore-notification-view-component.png new file mode 100644 index 0000000000..3fc300efe1 Binary files /dev/null and b/docs/en/images/bookstore-notification-view-component.png differ diff --git a/docs/en/images/bookstore-second-database.png b/docs/en/images/bookstore-second-database.png new file mode 100644 index 0000000000..3faf19496c Binary files /dev/null and b/docs/en/images/bookstore-second-database.png differ diff --git a/docs/en/images/bookstore-toolbar-highlighted.png b/docs/en/images/bookstore-toolbar-highlighted.png new file mode 100644 index 0000000000..f2b7c3f57e Binary files /dev/null and b/docs/en/images/bookstore-toolbar-highlighted.png differ diff --git a/docs/en/images/docs-module_download-new-abp-project.png b/docs/en/images/docs-module_download-new-abp-project.png index 0da3b7a67a..6424ad7622 100644 Binary files a/docs/en/images/docs-module_download-new-abp-project.png and b/docs/en/images/docs-module_download-new-abp-project.png differ diff --git a/docs/en/images/docs-module_solution-explorer.png b/docs/en/images/docs-module_solution-explorer.png index cafc38f0b0..2988ec4134 100644 Binary files a/docs/en/images/docs-module_solution-explorer.png and b/docs/en/images/docs-module_solution-explorer.png differ diff --git a/docs/en/images/microservice-sample-diagram-3.png b/docs/en/images/microservice-sample-diagram-3.png new file mode 100644 index 0000000000..b8066ef6be Binary files /dev/null and b/docs/en/images/microservice-sample-diagram-3.png differ diff --git a/docs/en/images/microservice-sample-solution-2.png b/docs/en/images/microservice-sample-solution-2.png new file mode 100644 index 0000000000..3da5d160f8 Binary files /dev/null and b/docs/en/images/microservice-sample-solution-2.png differ diff --git a/docs/en/images/multiple-database-usage.png b/docs/en/images/multiple-database-usage.png new file mode 100644 index 0000000000..ada6177629 Binary files /dev/null and b/docs/en/images/multiple-database-usage.png differ diff --git a/docs/en/images/overriding-login-cshtml.png b/docs/en/images/overriding-login-cshtml.png new file mode 100644 index 0000000000..d6daa4a141 Binary files /dev/null and b/docs/en/images/overriding-login-cshtml.png differ diff --git a/docs/en/images/pmc-add-migration-initial-update-database.png b/docs/en/images/pmc-add-migration-initial-update-database.png new file mode 100644 index 0000000000..3cc45e5ddf Binary files /dev/null and b/docs/en/images/pmc-add-migration-initial-update-database.png differ diff --git a/docs/en/images/pmc-add-migration-role-title.png b/docs/en/images/pmc-add-migration-role-title.png new file mode 100644 index 0000000000..69fc5b7bc5 Binary files /dev/null and b/docs/en/images/pmc-add-migration-role-title.png differ diff --git a/docs/en/images/react-native-folder-structure.png b/docs/en/images/react-native-folder-structure.png new file mode 100644 index 0000000000..cf703ea061 Binary files /dev/null and b/docs/en/images/react-native-folder-structure.png differ diff --git a/docs/en/images/react-native-navigation-structure.png b/docs/en/images/react-native-navigation-structure.png new file mode 100644 index 0000000000..9f17375b68 Binary files /dev/null and b/docs/en/images/react-native-navigation-structure.png differ diff --git a/docs/en/images/react-native-store-folder.png b/docs/en/images/react-native-store-folder.png new file mode 100644 index 0000000000..e90ae9debb Binary files /dev/null and b/docs/en/images/react-native-store-folder.png differ diff --git a/docs/en/images/single-database-usage.png b/docs/en/images/single-database-usage.png new file mode 100644 index 0000000000..108173fd86 Binary files /dev/null and b/docs/en/images/single-database-usage.png differ diff --git a/docs/pt-BR/Getting-Started-Angular-Template.md b/docs/pt-BR/Getting-Started-Angular-Template.md index 707e06cef3..f1b3abe790 100644 --- a/docs/pt-BR/Getting-Started-Angular-Template.md +++ b/docs/pt-BR/Getting-Started-Angular-Template.md @@ -26,10 +26,10 @@ abp new Acme.BookStore -u angular A solução criada requer; -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### A Estrutura da Solução @@ -103,7 +103,7 @@ A maioria das APIs de aplicativos requer autenticação e autorização. Se voc #### Execute o aplicativo angular (lado do cliente) -Vá para a `angular`pasta, abra um terminal de linha de comando, digite o `yarn`comando (sugerimos ao gerenciador de pacotes do [yarn](https://yarnpkg.com/) enquanto o npm install também funcionará na maioria dos casos): +Vá para a `angular`pasta, abra um terminal de linha de comando, digite o `yarn`comando (sugerimos ao gerenciador de pacotes do [yarn](https://classic.yarnpkg.com/) enquanto o npm install também funcionará na maioria dos casos): ```bash yarn diff --git a/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md b/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md index ac82ff0279..808c0bd94d 100644 --- a/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/pt-BR/Getting-Started-AspNetCore-MVC-Template.md @@ -26,10 +26,10 @@ abp new Acme.BookStore A solução criada requer; -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### A Estrutura da Solução diff --git a/docs/pt-BR/Localization.md b/docs/pt-BR/Localization.md index f665b68073..35b400b233 100644 --- a/docs/pt-BR/Localization.md +++ b/docs/pt-BR/Localization.md @@ -46,7 +46,7 @@ public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { options.FileSets.AddEmbedded(); }); @@ -66,7 +66,7 @@ Neste exemplo; - Adicionado um novo recurso de localização com "en" (inglês) como a cultura padrão. - Arquivos JSON usados para armazenar as sequências de localização. -- Os arquivos JSON são incorporados ao assembly usando `VirtualFileSystemOptions`(consulte [sistema de arquivos virtual](Virtual-File-System.md) ). +- Os arquivos JSON são incorporados ao assembly usando `AbpVirtualFileSystemOptions`(consulte [sistema de arquivos virtual](Virtual-File-System.md) ). Os arquivos JSON estão localizados na pasta do projeto "/ Localização / Recursos / Teste", como mostrado abaixo: diff --git a/docs/zh-Hans/API/Auto-API-Controllers.md b/docs/zh-Hans/API/Auto-API-Controllers.md new file mode 100644 index 0000000000..93ac124913 --- /dev/null +++ b/docs/zh-Hans/API/Auto-API-Controllers.md @@ -0,0 +1,140 @@ +# 自动API控制器 + +创建[应用程序服务](Application-Services.md)后, 通常需要创建API控制器以将此服务公开为HTTP(REST)API端点. 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做. + +ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制器. 大多数时候你不关心它的详细配置,但它可以完全被自定义. + +## 配置 + +基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: + +````csharp +[DependsOn(BookStoreApplicationModule)] +public class BookStoreWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly); + }); + } +} +```` + +此示例代码配置包含类`BookStoreApplicationModule`的程序集中的所有应用程序服务.下图显示了[Swagger UI](https://swagger.io/tools/swagger-ui/)上的API内容. + +![bookstore-apis](../images/bookstore-apis.png) + +### 例子 + +一些示例方法名称和按约定生成的相应路由: + +| 服务方法名称 | HTTP Method | 路由 | +| ----------------------------------------------------- | ----------- | -------------------------- | +| GetAsync(Guid id) | GET | /api/app/book/{id} | +| GetListAsync() | GET | /api/app/book | +| CreateAsync(CreateBookDto input) | POST | /api/app/book | +| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | +| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | +| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | +| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | + +### HTTP Method + +ABP在确定服务方法的HTTP Method时使用命名约定: + +- **Get**: 如果方法名称以`GetList`,`GetAll`或`Get`开头. +- **Put**: 如果方法名称以`Put`或`Update`开头. +- **Delete**: 如果方法名称以`Delete`或`Remove`开头. +- **Post**: 如果方法名称以`Create`,`Add`,`Insert`或`Post`开头. +- **Patch**: 如果方法名称以`Patch`开头. +- 其他情况, **Post** 为 **默认方式**. + +如果需要为特定方法自定义HTTP Method, 则可以使用标准ASP.NET Core的属性([HttpPost], [HttpGet], [HttpPut]... 等等.). 这需要添加[Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core)的Nuget包. + +### 路由 + +路由根据一些惯例生成: + +* 它始终以 **/api**开头. +* 接着是**路由路径**. 默认值为"**/app**", 可以进行如下配置: + +````csharp +Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.RootPath = "volosoft/book-store"; + }); +}); +```` + +然后获得一本书的路由将是'**/api/volosoft/book-store/book/{id}**'. 此示例使用两级根路径,但通常使用单个级别的深度. + +* 接着 **标准化控制器/服务名称**. 会删除`AppService`,`ApplicationService`和`Service`的后缀并将其转换为 **camelCase**. 如果你的应用程序服务类名称为`BookAppService`.那么它将变为`/book`. + * 如果要自定义命名, 则设置`UrlControllerNameNormalizer`选项. 它是一个委托允许你自定义每个控制器/服务的名称. +* 如果该方法具有 '**id**'参数, 则会在路由中添加'**/{id}**'. +* 如有必要,它会添加操作名称. 操作名称从服务上的方法名称获取并标准化; + * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. + * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. + * 将结果转换为**camelCase**. + * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. + * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. +* 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). + +## 服务选择 + +创建的HTTP API控制器并不是应用服务所独有的功能. + +### IRemoteService 接口 + +如果一个类实现了`IRemoteService`接口, 那么它会被自动选择为API控制器. 由于应用程序服务本身实现了`IRemoteService`接口, 因此它自然就成为API控制器. + +### RemoteService Attribute + +`RemoteService`可用于将实现`IRemoteService`接口的类标记为远程服务或禁用它. 例如: + +````csharp +[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +### TypePredicate 选项 + +你可以通过提供`TypePedicate`选项进一步过滤类以成为API控制器: + +````csharp +services.Configure(options => +{ + options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.TypePredicate = type => { return true; }; + }); +}); +```` + +如果你不想将此类型公开为API控制器, 则可以在类型检查时返回`false`. + +## API Explorer + +API Explorer是可以由客户端获取API结构的服务. Swagger使用它为endpoint创建文档和test UI. + +默认情况下, HTTP API控制器会自动启用API Explorer, 可以使用`RemoteService`按类或方法的级别控制它. 例如: + +````csharp +[RemoteService(IsMetadataEnabled = false)] +public class PersonAppService : ApplicationService +{ + +} +```` + +禁用`IsMetadataEnabled`从而从API Explorer中隐藏此服务, 并且无法被发现. 但是它仍然可以被知道确切API路径/路由的客户端使用. \ No newline at end of file diff --git a/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md new file mode 100644 index 0000000000..eae86d6117 --- /dev/null +++ b/docs/zh-Hans/API/Dynamic-CSharp-API-Clients.md @@ -0,0 +1,165 @@ +# 动态 C# API 客户端 + +ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS).通过这种方式,你不需要通过 `HttpClient` 或者其他低级的HTTP功能调用远程服务并获取数据. + +## 服务接口 + +你的service或controller需要实现一个在服务端和客户端共享的接口.因此,首先需要在一个共享的类库项目中定义一个服务接口.例如: + +````csharp +public interface IBookAppService : IApplicationService +{ + Task> GetListAsync(); +} +```` + +为了能自动被发现,你的接口需要实现`IRemoteService`接口.由于`IApplicationService`继承自`IRemoteService`接口.所以`IBookAppService`完全满足这个条件. + +在你的服务中实现这个类,你可以使用[auto API controller system](Auto-API-Controllers.md)将你的服务暴漏为一个REST API 端点. + +## 客户端代理生成 + +首先,将[Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget包添加到你的客户端项目中: + +```` +Install-Package Volo.Abp.Http.Client +```` + +然后给你的模块添加`AbpHttpClientModule`依赖: + +````csharp +[DependsOn(typeof(AbpHttpClientModule))] //添加依赖 +public class MyClientAppModule : AbpModule +{ +} +```` + +现在,已经可以创建客户端代理了.例如: + +````csharp +[DependsOn( + typeof(AbpHttpClientModule), //用来创建客户端代理 + typeof(BookStoreApplicationModule) //包含应用服务接口 + )] +public class MyClientAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //创建动态客户端代理 + context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly + ); + } +} +```` + +`AddHttpClientproxies`方法获得一个程序集,找到这个程序集中所有的服务接口,创建并注册代理类. + +### Endpoint配置 + +`appsettings.json`文件中的`RemoteServices`节点被用来设置默认的服务地址.下面是最简单的配置: + +```` +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + } + } +} +```` + +查看下面的"AbpRemoteServiceOptions"章节获取更多详细配置. + +## 使用 + +可以很直接地使用.只需要在你的客户端程序中注入服务接口: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IBookAppService _bookService; + + public MyService(IBookAppService bookService) + { + _bookService = bookService; + } + + public async Task DoIt() + { + var books = await _bookService.GetListAsync(); + foreach (var book in books) + { + Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); + } + } +} +```` + +本例注入了上面定义的`IBookAppService`服务接口.当客户端调用服务方法的时候动态客户端代理就会创建一个HTTP调用. + +### IHttpClientProxy接口 + +你可以像上面那样注入`IBookAppService`来使用客户端代理,也可以注入`IHttpClientProxy`获取更多明确的用法.这种情况下你可以使用`IHttpClientProxy`接口的`Service`属性. + +## 配置 + +### AbpRemoteServiceOptions + +默认情况下`AbpRemoteServiceOptions`从`appsettings.json`获取.或者,你可以使用`Configure`方法来设置或重写它.如: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.Configure(options => + { + options.RemoteServices.Default = + new RemoteServiceConfiguration("http://localhost:53929/"); + }); + + //... +} +```` + +### 多个远程服务端点 + +上面的例子已经配置了"Default"远程服务端点.你可能需要为不同的服务创建不同的端点.(就像在微服务方法中一样,每个微服务具有不同的端点).在这种情况下,你可以在你的配置文件中添加其他的端点: + +````json +{ + "RemoteServices": { + "Default": { + "BaseUrl": "http://localhost:53929/" + }, + "BookStore": { + "BaseUrl": "http://localhost:48392/" + } + } +} +```` + +`AddHttpClientProxies`方法有一个可选的参数来定义远程服务的名字: + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + remoteServiceName: "BookStore" +); +```` + +`remoteServiceName`参数会匹配通过`AbpRemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. + +### 作为默认服务 + +当你为`IBookAppService`创建了一个服务代理,你可以直接注入`IBookAppService`来使用代理客户端(像上面章节中将的那样).你可以传递`asDefaultService:false`到`AddHttpClientProxies`方法来禁用此功能. + +````csharp +context.Services.AddHttpClientProxies( + typeof(BookStoreApplicationModule).Assembly, + asDefaultServices: false +); +```` + +如果你的程序中已经有一个服务的实现并且你不想用你的客户端代理重写或替换其他的实现,就需要使用`asDefaultServices:false` + +> 如果你禁用了`asDefaultService`,你只能使用`IHttpClientProxy`接口去使用客户端代理.(参见上面的相关章节). \ No newline at end of file diff --git a/docs/zh-Hans/API/JavaScript-API/Auth.md b/docs/zh-Hans/API/JavaScript-API/Auth.md new file mode 100644 index 0000000000..60c9eb8866 --- /dev/null +++ b/docs/zh-Hans/API/JavaScript-API/Auth.md @@ -0,0 +1,3 @@ +# abp.auth JavaScript API + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/API/JavaScript-API/Index.md b/docs/zh-Hans/API/JavaScript-API/Index.md new file mode 100644 index 0000000000..854ec8214b --- /dev/null +++ b/docs/zh-Hans/API/JavaScript-API/Index.md @@ -0,0 +1,23 @@ +# JavaScript API + +ABP为ASP.NET Core MVC / Razor页面应用程序提供了一些执行客户端常见需求的JavaScrpt Api. + +## APIs + +* abp.ajax +* [abp.auth](Auth.md) +* 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 diff --git a/docs/zh-Hans/Application-Services.md b/docs/zh-Hans/Application-Services.md index 41dac8edf7..72e0319cde 100644 --- a/docs/zh-Hans/Application-Services.md +++ b/docs/zh-Hans/Application-Services.md @@ -146,7 +146,6 @@ public interface IBookAppService : IApplicationService `BookDto`是一个简单的[DTO](Data-Transfer-Objects.md)类, 定义如下: ````csharp -[AbpAutoMapFrom(typeof(Book))] //Defines the mapping public class BookDto { public Guid Id { get; set; } @@ -159,7 +158,36 @@ public class BookDto } ```` -* `BookDto`定义了`[AbpAutoMapFrom(typeof(Book))]`属性来从创建对象映射Book到BookDto. +我们创建一个Automapper的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类. 例如: + +```csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap(); + } +} +``` + +然后使用`AbpAutoMapperOptions`注册配置文件: + +````csharp +[DependsOn(typeof(AbpAutoMapperModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + //Add all mappings defined in the assembly of the MyModule class + options.AddMaps(); + }); + } +} +```` + +`AddMaps` 注册给定类的程序集中所有的配置类,通常使用模块类. 它还会注册 [attribute 映射](https://docs.automapper.org/en/stable/Attribute-mapping.html). 更多信息请参考[对象到对象映射](Object-To-Object-Mapping.md)文档 然后你可以实现`GetAsync`方法. 如下所示: @@ -200,7 +228,7 @@ public async Task GetAsync(Guid id) ### CRUD应用服务 -如果需要创建具有Create,Update,Delete和Get方法的简单CRUD应用服务,则可以使用ABP的基类轻松构建服务. 你可以继承CrudAppService. +如果需要创建具有Create,Update,Delete和Get方法的简单**CRUD应用服务**,则可以使用ABP的基类轻松构建服务. 你可以继承CrudAppService. 示例: @@ -218,7 +246,9 @@ public interface IBookAppService : } ```` -* ICrudAppService有泛型参数来获取实体的主键类型和CRUD操作的DTO类型(它不获取实体类型,因为实体类型未向客户端公开使用此接口). +`ICrudAppService` 有泛型参数来获取实体的主键类型和CRUD操作的DTO类型(它不获取实体类型,因为实体类型未向客户端公开使用此接口). + +> 为应用程序服务创建一个接口是最佳做法,但是ABP框架并不强制你这么做,你可以跳过接口部分. `ICrudAppService`声明以下方法: @@ -247,7 +277,6 @@ public interface ICrudAppService< 示例中使用的DTO类是`BookDto`和`CreateUpdateBookDto`: ````csharp -[AbpAutoMapFrom(typeof(Book))] public class BookDto : AuditedEntityDto { public string Name { get; set; } @@ -257,7 +286,6 @@ public class BookDto : AuditedEntityDto public float Price { get; set; } } -[AbpAutoMapTo(typeof(Book))] public class CreateUpdateBookDto { [Required] @@ -272,6 +300,19 @@ public class CreateUpdateBookDto } ```` +DTO类的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类. + +```csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap(); + CreateMap(); + } +} +``` + * `CreateUpdateBookDto`由创建和更新操作共享,但你也可以使用单独的DTO类. 最后`BookAppService`实现非常简单: @@ -291,7 +332,52 @@ public class BookAppService : `CrudAppService`实现了`ICrudAppService`接口中声明的所有方法. 然后,你可以添加自己的自定义方法或覆盖和自定义实现. -### 生命周期 +> `CrudAppService` 有不同数量泛型参数的版本,你可以选择适合的使用. + +### AbstractKeyCrudAppService -应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. +`CrudAppService` 要求你的实体拥有一个Id属性做为主键. 如果你使用的是复合主键,那么你无法使用它. + +`AbstractKeyCrudAppService` 实现了相同的 `ICrudAppService` 接口,但它没有假设你的主键. + +#### 示例 + +假设你有实体 `District`,它的`CityId` 和 `Name` 做为复合主键,使用 `AbstractKeyCrudAppService` 时需要你自己实现 `DeleteByIdAsync` 和 `GetEntityByIdAsync` 方法: + +````csharp +public class DistrictAppService + : AbstractKeyCrudAppService +{ + public DistrictAppService(IRepository repository) + : base(repository) + { + } + + protected override async Task DeleteByIdAsync(DistrictKey id) + { + await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); + } + + protected override async Task GetEntityByIdAsync(DistrictKey id) + { + return await AsyncQueryableExecuter.FirstOrDefaultAsync( + Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) + ); + } +} +```` + +这个实现需要你创建一个类做为复合键: + +````csharp +public class DistrictKey +{ + public Guid CityId { get; set; } + + public string Name { get; set; } +} +```` + +### 生命周期 +应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. \ No newline at end of file diff --git a/docs/zh-Hans/AspNet-Boilerplate-Migration-Guide.md b/docs/zh-Hans/AspNet-Boilerplate-Migration-Guide.md new file mode 100644 index 0000000000..21cba49bfb --- /dev/null +++ b/docs/zh-Hans/AspNet-Boilerplate-Migration-Guide.md @@ -0,0 +1,3 @@ +# ASP.NET Boilerplate v5+ 迁移到 ABP Framework + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md b/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md index 93ac124913..19a26440eb 100644 --- a/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md +++ b/docs/zh-Hans/AspNetCore/Auto-API-Controllers.md @@ -1,140 +1,3 @@ -# 自动API控制器 +文档已经移动到其他位置. -创建[应用程序服务](Application-Services.md)后, 通常需要创建API控制器以将此服务公开为HTTP(REST)API端点. 典型的API控制器除了将方法调用重定向到应用程序服务并使用[HttpGet],[HttpPost],[Route]等属性配置REST API之外什么都不做. - -ABP可以按照惯例 **自动** 将你的应用程序服务配置为API控制器. 大多数时候你不关心它的详细配置,但它可以完全被自定义. - -## 配置 - -基本配置很简单. 只需配置`AbpAspNetCoreMvcOptions`并使用`ConventionalControllers.Create`方法,如下所示: - -````csharp -[DependsOn(BookStoreApplicationModule)] -public class BookStoreWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly); - }); - } -} -```` - -此示例代码配置包含类`BookStoreApplicationModule`的程序集中的所有应用程序服务.下图显示了[Swagger UI](https://swagger.io/tools/swagger-ui/)上的API内容. - -![bookstore-apis](../images/bookstore-apis.png) - -### 例子 - -一些示例方法名称和按约定生成的相应路由: - -| 服务方法名称 | HTTP Method | 路由 | -| ----------------------------------------------------- | ----------- | -------------------------- | -| GetAsync(Guid id) | GET | /api/app/book/{id} | -| GetListAsync() | GET | /api/app/book | -| CreateAsync(CreateBookDto input) | POST | /api/app/book | -| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | -| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | -| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | -| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | - -### HTTP Method - -ABP在确定服务方法的HTTP Method时使用命名约定: - -- **Get**: 如果方法名称以`GetList`,`GetAll`或`Get`开头. -- **Put**: 如果方法名称以`Put`或`Update`开头. -- **Delete**: 如果方法名称以`Delete`或`Remove`开头. -- **Post**: 如果方法名称以`Create`,`Add`,`Insert`或`Post`开头. -- **Patch**: 如果方法名称以`Patch`开头. -- 其他情况, **Post** 为 **默认方式**. - -如果需要为特定方法自定义HTTP Method, 则可以使用标准ASP.NET Core的属性([HttpPost], [HttpGet], [HttpPut]... 等等.). 这需要添加[Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core)的Nuget包. - -### 路由 - -路由根据一些惯例生成: - -* 它始终以 **/api**开头. -* 接着是**路由路径**. 默认值为"**/app**", 可以进行如下配置: - -````csharp -Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.RootPath = "volosoft/book-store"; - }); -}); -```` - -然后获得一本书的路由将是'**/api/volosoft/book-store/book/{id}**'. 此示例使用两级根路径,但通常使用单个级别的深度. - -* 接着 **标准化控制器/服务名称**. 会删除`AppService`,`ApplicationService`和`Service`的后缀并将其转换为 **camelCase**. 如果你的应用程序服务类名称为`BookAppService`.那么它将变为`/book`. - * 如果要自定义命名, 则设置`UrlControllerNameNormalizer`选项. 它是一个委托允许你自定义每个控制器/服务的名称. -* 如果该方法具有 '**id**'参数, 则会在路由中添加'**/{id}**'. -* 如有必要,它会添加操作名称. 操作名称从服务上的方法名称获取并标准化; - * 删除'**Async**'后缀. 如果方法名称为'GetPhonesAsync',则变为`GetPhones`. - * 删除**HTTP method前缀**. 基于的HTTP method删除`GetList`,`GetAll`,`Get`,`Put`,`Update`,`Delete`,`Remove`,`Create`,`Add`,`Insert`,`Post`和`Patch`前缀, 因此`GetPhones`变为`Phones`, 因为`Get`前缀和GET请求重复. - * 将结果转换为**camelCase**. - * 如果生成的操作名称为**空**,则它不会添加到路径中.否则它会被添加到路由中(例如'/phones').对于`GetAllAsync`方法名称,它将为空,因为`GetPhonesAsync`方法名称将为`phone`. - * 可以通过设置`UrlActionNameNormalizer`选项来自定义.It's an action delegate that is called for every method. -* 如果有另一个带有'Id'后缀的参数,那么它也会作为最终路线段添加到路线中(例如'/phoneId'). - -## 服务选择 - -创建的HTTP API控制器并不是应用服务所独有的功能. - -### IRemoteService 接口 - -如果一个类实现了`IRemoteService`接口, 那么它会被自动选择为API控制器. 由于应用程序服务本身实现了`IRemoteService`接口, 因此它自然就成为API控制器. - -### RemoteService Attribute - -`RemoteService`可用于将实现`IRemoteService`接口的类标记为远程服务或禁用它. 例如: - -````csharp -[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -### TypePredicate 选项 - -你可以通过提供`TypePedicate`选项进一步过滤类以成为API控制器: - -````csharp -services.Configure(options => -{ - options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.TypePredicate = type => { return true; }; - }); -}); -```` - -如果你不想将此类型公开为API控制器, 则可以在类型检查时返回`false`. - -## API Explorer - -API Explorer是可以由客户端获取API结构的服务. Swagger使用它为endpoint创建文档和test UI. - -默认情况下, HTTP API控制器会自动启用API Explorer, 可以使用`RemoteService`按类或方法的级别控制它. 例如: - -````csharp -[RemoteService(IsMetadataEnabled = false)] -public class PersonAppService : ApplicationService -{ - -} -```` - -禁用`IsMetadataEnabled`从而从API Explorer中隐藏此服务, 并且无法被发现. 但是它仍然可以被知道确切API路径/路由的客户端使用. \ No newline at end of file +[点击链接跳转到自动API控制器文档](../API/Auto-API-Controllers.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/AspNetCore/Bundling-Minification.md index b7c8c0a1b1..c102c89fd2 100644 --- a/docs/zh-Hans/AspNetCore/Bundling-Minification.md +++ b/docs/zh-Hans/AspNetCore/Bundling-Minification.md @@ -1,354 +1,3 @@ +文档已经移动到其他位置. -## ASP.NET Core MVC 捆绑 & 压缩 - -有许多方法可以捆绑&压缩客户端资源(JavaScript和CSS文件). 最常见的方式是: - -* 使用Visual Studio[捆绑&压缩](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier)扩展或者其它的[NuGet相关包](https://www.nuget.org/packages/BuildBundlerMinifier/). - -* 使用[Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/)及其插件. - -ABP内置了简单,动态,强大,模块化的方式. - -### Volo.Abp.AspNetCore.Mvc.UI.Bundling 包 - -> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. - -将`Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget包安装到你的项目中: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -然后将`AbpAspNetCoreMvcUiBundlingModule`依赖项添加到你的模块上: - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -### Razor Bundling Tag Helpers - -创建bundle的最简单方法是使用`abp-script-bundle`或`abp-style-bundle` tag helpers. 例如: - -````html - - - - - - -```` - -`abp-script-bundle`定义了一个带有**唯一名称**的样式包:`MyGlobalBundle`. 使用方法很容易理解. 让我们看看它是如何*工作的*: - -* 当首次请求时,ABP从提供的文件中 **(延迟)lazy** 创建. 后续将从 **缓存** 中返回内容. 这意味着如果你有条件地将文件添加到包中,它只执行一次, 并且条件的任何更改都不会影响下一个请求的包. -* 在`development`环境中ABP会将包文件**单独**添加到页面中, 其他环境(`staging`,`production`...)会自动捆绑和压缩. -* 捆绑文件可以是**物理**文件或[**虚拟/嵌入**](../Virtual-File-System.md)的文件. -* ABP自动将 **版本查询字符串(version query string)** 添加到捆绑文件的URL中,以防止浏览器缓存. 如:?_v=67872834243042(从文件的上次更改日期生成). 即使捆绑文件单独添加到页面(在`development`环境中), 版本控制仍然有效. - -#### 导入 Bundling Tag Helpers - -> 默认情况下已在启动模板导入. 大多数情况下,你不需要手动安装它. - -要使用`bundle tag helpers`, 你需要将其添加到`_ViewImports.cshtml`文件或页面中: - -```` -@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling -```` - -#### 未命名的 Bundles - -对于razor bundle tag helpers, `name`是**可选**. 如果没有定义一个名字,它将根据使用的捆绑文件名自动**计算生成**(they are **concatenated** and **hashed**) 例: - -````html - - - - - @if (ViewBag.IncludeCustomStyles != false) - { - - } - -```` - -这将潜在地创建**两个不同的bundles**(一个包括`my-global-style.css`而另一个则不包括). - -**未命名的** bundles优点: - -* 可以**有条件地将项目**添加到捆绑包中. 但这可能会导致基于条件的捆绑的存在多种变化. - -**命名** bundles优点: - -* 其他模块可以通过其名称为捆绑包做出贡献(参见下面的部分). - -#### 单个文件 - -如果你只需要在页面中添加一个文件, 你可以使用`abp-script`或`abp-style`而不需要包含在`abp-script-bundle`或`abp-style-bundle`中. 例: - -````xml - -```` - -对于上面的示例,包名称将是 *scripts.my-scripts*("/"替换为"."). 所有捆绑功能也可以按预期应用于单个文件. - -### Bundling 选项 - -如果你需要在 **多个页面中使用相同的包** 或想要使用更多 **强大功能**, 你可以在[模块](../Module-Development-Basics.md)类中进行**配置**. - -#### 创建一个新的捆绑包 - -用法示例: - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] -public class MyWebModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Add("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/libs/jquery/jquery.js", - "/libs/bootstrap/js/bootstrap.js", - "/libs/toastr/toastr.min.js", - "/scripts/my-global-scripts.js" - ); - }); - }); - } -} -```` - -> 你可以在脚本和样式包中使用相同的名称(*MyGlobalBundle*), 因为它们被添加到不同的集合(`ScriptBundles`和`StyleBundles`). - -在定义bundle之后, 可以使用上面定义的相同tag helpers将其包括在页面中. 例如: - -````html - -```` - -这次tag helper定义中没有定义文件, 因为捆绑文件是由代码定义的. - -#### 配置现有的 Bundle - -ABP也支持[模块化](../Module-Development-Basics.md)捆绑. 模块可以修改由依赖模块创建的捆绑包. -例如: - -````C# -[DependsOn(typeof(MyWebModule))] -public class MyWebExtensionModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddFiles( - "/scripts/my-extension-script.js" - ); - }); - }); - } -} -```` - -> 无法通过代码配置未命名的bundle tag helpers, 因为它们的名称在开发时是未知的. 建议始终使用bundle tag helper的名称. - -### Bundle 贡献者 - -将文件添加到现有bundle似乎很有用. 如果你需要**替换**bundle中的文件或者你想**有条件地**添加文件怎么办? 定义bundle贡献者可为此类情况提供额外的功能. - -一个bundle的贡献者使用自定义版本bootstrap.css替换示例: - -````C# -public class MyExtensionGlobalStyleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.ReplaceOne( - "/libs/bootstrap/css/bootstrap.css", - "/styles/extensions/bootstrap-customized.css" - ); - } -} -```` - -然后你可以按照下面的代码使用这个贡献者: - -````C# -services.Configure(options => -{ - options - .ScriptBundles - .Configure("MyGlobalBundle", bundle => { - bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); - }); -}); -```` - -贡献者也可以在bundle tag helpers中使用. -例如: - -````xml - - - - - -```` - -`abp-style`和`abp-script`标签可以使用`type`属性(而不是`src`属性), 如本示例所示. 添加bundle贡献者时, 其依赖关系也会自动添加到bundle中. - -#### 贡献者依赖关系 - -bundle贡献者可以与其他贡献者具有一个或多个依赖关系. -例如: - -````C# -[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -添加bundle贡献者时,其依赖关系将 **自动并递归** 添加. **依赖顺序** 通过阻止 **重复** 添加的依赖关系. 即使它们处于分离的bundle中,也会阻止重复. ABP在页面中组织所有bundle并消除重复. - -创建贡献者和定义依赖关系是一种跨不同模块组织bundle创建的方法. - - -#### 贡献者扩展 - -在某些高级应用场景中, 当用到一个bundle贡献者时,你可能想做一些额外的配置. 贡献者扩展可以和被扩展的贡献者无缝衔接. - -下面的示例为 prism.js 脚本库添加一些样式: - -````csharp -public class MyPrismjsStyleExtension : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); - } -} -```` - -然后你可以配置 `BundleContributorOptions` 去扩展已存在的 `PrismjsStyleBundleContributor`. - -````csharp -Configure(options => -{ - options - .Extensions() - .Add(); -}); -```` - -任何时候当 `PrismjsStyleBundleContributor` 被添加到bundle中时, `MyPrismjsStyleExtension` 也会被自动添加. - -#### 访问 IServiceProvider - -虽然很少需要它, 但是`BundleConfigurationContext`有一个`ServiceProvider`属性, 你可以在`ConfigureBundle`方法中解析服务依赖. - -#### 标准包装贡献者 - -将特定的NPM包资源(js,css文件)添加到包中对于该包非常简单. 例如, 你总是为bootstrap NPM包添加`bootstrap.css`文件. - -所有[标准NPM包](Client-Side-Package-Management.md)都有内置的贡献者. 例如,如果你的贡献者依赖于bootstrap,你可以声明它,而不是自己添加bootstrap.css. - -````C# -[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency -public class MyExtensionStyleBundleContributor : BundleContributor -{ - //... -} -```` - -使用标准包的内置贡献者: - -* 防止你输入**无效的资源路径**. -* 如果资源 **路径发生变化** (依赖贡献者将处理它),则防止更改你的贡献者. -* 防止多个模块添加**重复文件**. -* 以递归方式管理依赖项(如果需要,添加依赖项的依赖项). - -##### Volo.Abp.AspNetCore.Mvc.UI.Packages 包 - -> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. - -标准包贡献者在`Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet包中定义. -将它安装到你的项目中: - -```` -install-package Volo.Abp.AspNetCore.Mvc.UI.Packages -```` - -然后将`AbpAspNetCoreMvcUiPackagesModule`模块依赖项添加到你的模块中; - -````C# -using Volo.Abp.Modularity; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace MyCompany.MyProject -{ - [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] - public class MyWebModule : AbpModule - { - //... - } -} -```` - -#### Bundle 继承 - -在某些特定情况下, 可能需要从其他bundle创建一个 **新** bundle **继承**, 从bundle继承(递归)会继承该bundle的所有文件/贡献者. 然后派生的bundle可以添加或修改文件/贡献者**而无需修改**原始bundle. -例如: - -````c# -services.Configure(options => -{ - options - .StyleBundles - .Add("MyTheme.MyGlobalBundle", bundle => { - bundle - .AddBaseBundles("MyGlobalBundle") //Can add multiple - .AddFiles( - "/styles/mytheme-global-styles.css" - ); - }); -}); -```` - -### 主题 - -主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md). - -### 最佳实践 & 建议 - -建议为应用程序定义多个包, 每个包用于不同的目的. - -* **全局包**: 应用程序中的每个页面都包含全局样式/脚本包. 主题已经定义了全局样式和脚本包. 你的模块可以为他们做出贡献. -* **布局包**: 这是针对单个布局的特定包. 仅包含在所有页面之间共享的资源使用布局. 使用bundling tag helpers创建捆绑包是一种很好的做法. -* **模块包**: 用于单个模块页面之间的共享资源. -* **页面包**: 为每个页面创建的特定包. 使用bundling tag helpers创建捆绑包作为最佳实践. - -在性能,网络带宽使用和捆绑包的数量之间建立平衡. - -### 参见 - -* [客户端包管理](Client-Side-Package-Management.md) -* [主题](Theming.md) +[点击链接跳转到ASP.NET Core MVC 捆绑 & 压缩文档](../UI/AspNetCore/Bundling-Minification.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md b/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md index 4d45022da0..f61d4bd623 100644 --- a/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md +++ b/docs/zh-Hans/AspNetCore/Client-Side-Package-Management.md @@ -1,115 +1,3 @@ +文档已经移动到其他位置. -## ASP.NET Core MVC 客户端包管理 - -ABP框架可以与任何类型的客户端包管理系统一起使用. 甚至你可以决定不使用包管理系统并手动管理依赖项. - -但是, ABP框架最适用于**NPM/Yarn**. 默认情况下,内置模块配置为与NPM/Yarn一起使用. - -最后, 我们建议[**Yarn**](https://yarnpkg.com/)而不是NPM,因为它更快,更稳定并且与NPM兼容. - -### @ABP NPM Packages - -ABP是一个模块化平台. 每个开发人员都可以创建模块, 模块应该在**兼容**和**稳定**状态下协同工作. - -一个挑战是依赖NPM包的**版本**. 如果两个不同的模块使用相同的JavaScript库但其不同(并且可能不兼容)的版本会怎样. - -为了解决版本问题, 我们创建了一套**标准包**, 这取决于一些常见的第三方库. 一些示例包是[@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@ abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap)和[@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). 你可以从[Github存储库](https://github.com/volosoft/abp/tree/master/npm/packs)中查看**列表**. - -**标准包**的好处是: - -* 它取决于包装的**标准版本**. 取决于此包是**安全**,因为所有模块都依赖于相同的版本. -* 它包含将库资源(js,css,img...文件)从**node_modules**文件夹复制到**wwwroot/libs**文件夹的gulp任务. 有关更多信息, 请参阅 *映射库资源* 部分. - -依赖标准包装很容易. 只需像往常一样将它添加到**package.json**文件中. 例如: - -```` - { - ... - "dependencies": { - "@abp/bootstrap": "^1.0.0" - } - } -```` - -建议依赖于标准软件包, 而不是直接依赖于第三方软件包. - -#### 安装包 - -依赖于NPM包后, 你应该做的就是从命令行运行**yarn**命令来安装所有包及其依赖项: - -```` -yarn -```` - -虽然你可以使用`npm install`,但如前所述,建议使用[Yarn](https://yarnpkg.com/). - -#### 贡献包 - -如果你需要不在标准软件包中的第三方NPM软件包,你可以在Github[repository](https://github.com/volosoft/abp)上创建Pull请求. 接受遵循这些规则的拉取请求: - -* 对于NPM上的`package-name`, 包名称应该命名为`@abp/package-name`(例如:`bootstrap`包的`@abp/bootstrap`). -* 它应该是**最新的稳定**版本的包. -* 它应该只依赖于**单个**第三方包. 它可以依赖于多个`@abp/*`包. -* 包应包含一个`abp.resourcemapping.js`文件格式,如*映射库资源*部分中所定义. 此文件应仅映射所依赖包的资源. -* 你还需要为你创建的包创建[bundle贡献者](Bundling-Minification.md). - -有关示例, 请参阅当前标准包. - -### 映射库资源 - -使用NPM包和NPM/Yarn工具是客户端库的事实标准. NPM/Yarn工具在Web项目的根文件夹中创建一个**node_modules**文件夹. - -下一个挑战是将所需的资源(js,css,img ...文件)从`node_modules`复制到**wwwroot**文件夹内的文件夹中,以使其可供客户端/浏览器访问. - -ABP将基于[Gulp](https://gulpjs.com/)的任务定义为**将资源**从**node_modules**复制到**wwwroot/libs**文件夹. 每个**标准包**(参见*@ABP NPM Packages*部分)定义了自己文件的映射. 因此, 大多数情况你只配置依赖项. - -**启动模板**已经配置为开箱即用的所有这些. 本节将介绍配置选项. - -#### 资源映射定义文件 - -模块应该定义一个名为`abp.resourcemapping.js`的JavaScript文件,其格式如下例所示: - -````js -module.exports = { - aliases: { - "@node_modules": "./node_modules", - "@libs": "./wwwroot/libs" - }, - clean: [ - "@libs" - ], - mappings: { - - } -} -```` - -* **aliases**部分定义了可在映射路径中使用的标准别名(占位符). **@node_modules**和 **@libs**是必需的(通过标准包), 你可以定义自己的别名以减少重复. -* **clean**部分是在复制文件之前要清理的文件夹列表. -* **mappings**部分是要复制的文件/文件夹的映射列表.此示例不会复制任何资源本身,但取决于标准包. - -示例映射配置如下所示: - -````js -mappings: { - "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", - "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/" -} -```` - -#### 使用 Gulp - -正确配置`abp.resourcemapping.js`文件后, 可以从命令行运行gulp命令: - -```` -gulp -```` - -当你运行`gulp`时,所有包都会将自己的资源复制到**wwwroot/libs**文件夹中. 只有在**package.json**文件中对依赖项进行更改时, 才需要运行`yarn&gulp`. - -> 运行Gulp命令时, 使用package.json文件解析应用程序的依赖关系. Gulp任务自动发现并映射来自所有依赖项的所有资源(递归). - -#### 参见 - -* [捆绑 & 压缩](Bundling-Minification.md) -* [主题](Theming.md) +[点击链接跳转到ASP.NET Core MVC 客户端包管理文档](../UI/AspNetCore/Client-Side-Package-Management.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md b/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md index f964ac9013..6f45ba4549 100644 --- a/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md +++ b/docs/zh-Hans/AspNetCore/Dynamic-CSharp-API-Clients.md @@ -1,165 +1,3 @@ -# 动态 C# API 客户端 +文档已经移动到其他位置. -ABP可以自动创建C# API 客户端代理来调用远程HTTP服务(REST APIS).通过这种方式,你不需要通过 `HttpClient` 或者其他低级的HTTP功能调用远程服务并获取数据. - -## 服务接口 - -你的service或controller需要实现一个在服务端和客户端共享的接口.因此,首先需要在一个共享的类库项目中定义一个服务接口.例如: - -````csharp -public interface IBookAppService : IApplicationService -{ - Task> GetListAsync(); -} -```` - -为了能自动被发现,你的接口需要实现`IRemoteService`接口.由于`IApplicationService`继承自`IRemoteService`接口.所以`IBookAppService`完全满足这个条件. - -在你的服务中实现这个类,你可以使用[auto API controller system](Auto-API-Controllers.md)将你的服务暴漏为一个REST API 端点. - -## 客户端代理生成 - -首先,将[Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget包添加到你的客户端项目中: - -```` -Install-Package Volo.Abp.Http.Client -```` - -然后给你的模块添加`AbpHttpClientModule`依赖: - -````csharp -[DependsOn(typeof(AbpHttpClientModule))] //添加依赖 -public class MyClientAppModule : AbpModule -{ -} -```` - -现在,已经可以创建客户端代理了.例如: - -````csharp -[DependsOn( - typeof(AbpHttpClientModule), //用来创建客户端代理 - typeof(BookStoreApplicationModule) //包含应用服务接口 - )] -public class MyClientAppModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - //创建动态客户端代理 - context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly - ); - } -} -```` - -`AddHttpClientproxies`方法获得一个程序集,找到这个程序集中所有的服务接口,创建并注册代理类. - -### Endpoint配置 - -`appsettings.json`文件中的`RemoteServices`节点被用来设置默认的服务地址.下面是最简单的配置: - -```` -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - } - } -} -```` - -查看下面的"RemoteServiceOptions"章节获取更多详细配置. - -## 使用 - -可以很直接地使用.只需要在你的客户端程序中注入服务接口: - -````csharp -public class MyService : ITransientDependency -{ - private readonly IBookAppService _bookService; - - public MyService(IBookAppService bookService) - { - _bookService = bookService; - } - - public async Task DoIt() - { - var books = await _bookService.GetListAsync(); - foreach (var book in books) - { - Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); - } - } -} -```` - -本例注入了上面定义的`IBookAppService`服务接口.当客户端调用服务方法的时候动态客户端代理就会创建一个HTTP调用. - -### IHttpClientProxy接口 - -你可以像上面那样注入`IBookAppService`来使用客户端代理,也可以注入`IHttpClientProxy`获取更多明确的用法.这种情况下你可以使用`IHttpClientProxy`接口的`Service`属性. - -## 配置 - -### RemoteServiceOptions - -默认情况下`RemoteServiceOptions`从`appsettings.json`获取.或者,你可以使用`Configure`方法来设置或重写它.如: - -````csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - context.Services.Configure(options => - { - options.RemoteServices.Default = - new RemoteServiceConfiguration("http://localhost:53929/"); - }); - - //... -} -```` - -### 多个远程服务端点 - -上面的例子已经配置了"Default"远程服务端点.你可能需要为不同的服务创建不同的端点.(就像在微服务方法中一样,每个微服务具有不同的端点).在这种情况下,你可以在你的配置文件中添加其他的端点: - -````json -{ - "RemoteServices": { - "Default": { - "BaseUrl": "http://localhost:53929/" - }, - "BookStore": { - "BaseUrl": "http://localhost:48392/" - } - } -} -```` - -`AddHttpClientProxies`方法有一个可选的参数来定义远程服务的名字: - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - remoteServiceName: "BookStore" -); -```` - -`remoteServiceName`参数会匹配通过`RemoteServiceOptions`配置的服务端点.如果`BookStore`端点没有定义就会使用默认的`Default`端点. - -### 作为默认服务 - -当你为`IBookAppService`创建了一个服务代理,你可以直接注入`IBookAppService`来使用代理客户端(像上面章节中将的那样).你可以传递`asDefaultService:false`到`AddHttpClientProxies`方法来禁用此功能. - -````csharp -context.Services.AddHttpClientProxies( - typeof(BookStoreApplicationModule).Assembly, - asDefaultServices: false -); -```` - -如果你的程序中已经有一个服务的实现并且你不想用你的客户端代理重写或替换其他的实现,就需要使用`asDefaultServices:false` - -> 如果你禁用了`asDefaultService`,你只能使用`IHttpClientProxy`接口去使用客户端代理.(参见上面的相关章节). \ No newline at end of file +[点击链接跳转到动态 C# API 客户端文档](../API/Dynamic-CSharp-API-Clients.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md b/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md index 60c9eb8866..857606a0f2 100644 --- a/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md +++ b/docs/zh-Hans/AspNetCore/JavaScript-API/Auth.md @@ -1,3 +1,3 @@ -# abp.auth JavaScript API +文档已经移动到其他位置. -TODO \ No newline at end of file +[点击链接跳转到JavaScript Auth文档](../../API/JavaScript-API/Auth.md) diff --git a/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md b/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md index 87d3b3021d..9f3e4d1645 100644 --- a/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md +++ b/docs/zh-Hans/AspNetCore/JavaScript-API/Index.md @@ -1,24 +1,3 @@ -# JavaScript API - -ABP为ASP.NET Core MVC / Razor页面应用程序提供了一些执行客户端常见需求的JavaScrpt Api. - -## APIs - -* abp.ajax -* [abp.auth](Auth.md) -* 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 +文档已经移动到其他位置. +[点击链接跳转到JavaScript API文档](../../API/JavaScript-API/Index.md) diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md new file mode 100644 index 0000000000..848d043312 --- /dev/null +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Buttons.md @@ -0,0 +1,94 @@ +# 按钮 + +ABP框架定义了Tag Helper用于简单的创建bootstrap按钮. + +`` + +## 属性 + +`` 有7个不同的属性. + +* [`button-type`](#button-type) +* [`size`](#size) +* [`busy-text`](#busy-text) +* [`text`](#text) +* [`icon`](#icon) +* [`disabled`](#disabled) +* [`icon-type`](#icon-type) + +### `button-type` + +`button-type` 是一个可选参数. 它的默认值是 `Default`. + +`Button` + +你可以为按钮选择以下按钮类型: + +* `Default` +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Outline_Primary` +* `Outline_Secondary` +* `Outline_Success` +* `Outline_Danger` +* `Outline_Warning` +* `Outline_Info` +* `Outline_Light` +* `Outline_Dark` +* `Link` + +### `size` + +`size` 是一个可选参数. 它的默认值是 `Default`. + +`Button` + +你可以为按钮选择以下size类型: + +* `Default` +* `Small` +* `Medium` +* `Large` +* `Block` +* `Block_Small` +* `Block_Medium` +* `Block_Large` + +### `busy-text` + +`busy-text` 是一个字符串类型参数. 当按钮繁忙时设置该文本. + +### `text` + +`text` 是一个字符串类型参数,显示在按钮上. + +### `icon` + +`icon` 是一个字符串类型参数. 它的值取决于[`icon-type`](#`icon-type`). 默认情况下,我们对图标使用[Font Awesome](https://fontawesome.com/). 要使用它,你需要将 `icon` 参数设置为图标名称. + +##### 示例 + +[fa-address-card](https://fontawesome.com/icons/address-card): ![fa-address-card](fa-address-card.png "Address Card") + +`` + +> 不要忘记: 你不需要写前缀,如果你没有更改 `icon-type` ,它会为[Font Awesome](https://fontawesome.com/)图标自动添加 `fa` 前缀. + +### `disabled` + +`disabled` 是一个布尔类型参数. 如果你设值为 `true`, 按钮会被禁用. + +### `icon-type` + +`icon-type` 是一个可选参数。它的默认值是 `FontAwesome`. 你可以创建自己的图标类型提供程序并更改它. + +你可以为按钮选择以下图标类型: + +* `FontAwesome` +* `Other` diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md index ceaa3d3d69..71e25d25ea 100644 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -1,3 +1,3 @@ -## Dynamic Forms +文档已经移动到其他位置. -目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file +[点击链接跳转到Dynamic Forms文档](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md b/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md index d4cadc9b18..96ca033863 100644 --- a/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md +++ b/docs/zh-Hans/AspNetCore/Tag-Helpers/Index.md @@ -1,3 +1,3 @@ -## ABP Tag Helpers +文档已经移动到其他位置. -"ABP tag helpers" 目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/). \ No newline at end of file +[点击链接跳转到ABP Tag Helpers文档](../../UI/AspNetCore/Tag-Helpers/Index.md) diff --git a/docs/zh-Hans/AspNetCore/Tag-Helpers/fa-address-card.png b/docs/zh-Hans/AspNetCore/Tag-Helpers/fa-address-card.png new file mode 100644 index 0000000000..a3b2c815b8 Binary files /dev/null and b/docs/zh-Hans/AspNetCore/Tag-Helpers/fa-address-card.png differ diff --git a/docs/zh-Hans/AspNetCore/Theming.md b/docs/zh-Hans/AspNetCore/Theming.md index 470ef1a458..4a863567f1 100644 --- a/docs/zh-Hans/AspNetCore/Theming.md +++ b/docs/zh-Hans/AspNetCore/Theming.md @@ -1,3 +1,4 @@ -# Theming -TODO \ No newline at end of file +文档已经移动到其他位置. + +[点击链接跳转到Theming文档](../UI/AspNetCore/Theming.md) \ No newline at end of file diff --git a/docs/zh-Hans/AspNetCore/Widgets.md b/docs/zh-Hans/AspNetCore/Widgets.md index a10e7e462f..423169b6fc 100644 --- a/docs/zh-Hans/AspNetCore/Widgets.md +++ b/docs/zh-Hans/AspNetCore/Widgets.md @@ -1,274 +1,4 @@ -# 小部件 -ABP为创建**可重用的部件**提供了模型和基础设施. 部件系统是[ASP.NET Core ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components)的扩展. 在你有以下需求时,小部件会非常有用; +文档已经移动到其他位置. -* 在可复用的 **[模块](../Module-Development-Basics.md)** 中定义部件. -* 在部件中引用 **scripts & styles** 脚本. -* 使用部件创建 **[仪表盘](Dashboards.md)**. -* 支持 **[授权](../Authorization.md)** 与 **[捆绑`bundling`](Bundling-Minification.md)** 的部件 - -## 基本部件定义 - -### 创建一个视图组件 - -第一部,创建一个新的ASP.NET Core View Component: - -![widget-basic-files](../images/widget-basic-files.png) - -**MySimpleWidgetViewComponent.cs**: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -继承 `AbpViewComponent` 不是必需的. 你也可以继承ASP.NET Core的 `ViewComponent`. `AbpViewComponent` 只是定义了一些基本的实用属性. - -**Default.cshtml**: - -```xml -
-

My Simple Widget

-

This is a simple widget!

-
-``` - -### 定义部件 - -添加 `Widget` attribute 到 `MySimpleWidgetViewComponent` 类,将此视图组件标记为部件: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## 渲染部件 - -渲染部件的用法是ASP.NET Core的标准用法. 在razor view/page中使用 `Component.InvokeAsync` 方法, 就像渲染一个View Component一样. 例如: - -````xml -@await Component.InvokeAsync("MySimpleWidget") -@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) -```` - -第一行代码使用名称渲染了部件,第二行代码使用type渲染了View Comonent. - -## 部件名称 - -默认下名称是根据View Conponent组件的名称计算的, 比如你的视图组件名是 `MySimpleWidgetViewComponent`, 那么部件的名称就是 `MySimpleWidget` (删除`ViewComponent`后缀). 这与ASP.NET Core的默认视图组件名称的方式一样. - -想要自定义组件名称,只需要使用ASP.NET Core的 `ViewComponent` attribute: - -```csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget] - [ViewComponent(Name = "MyCustomNamedWidget")] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); - } - } -} -``` - -ABP会通过自定义的名称去处理部件. - -> 如果视图组件名与视图组件的文件夹名称不匹配,那么需要像本例中那样去手动编写视图路径. - -### 显示名称 - -你还可以定义对于使用者友好的本地化显示名称. 需要时在UI中使用显示名称. 显示名称是可选的,在 `Widget` attribute 的`DisplayName`属性中定义: - -````csharp -using DashboardDemo.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - DisplayName = "MySimpleWidgetDisplayName", //Localization key - DisplayNameResource = typeof(DashboardDemoResource) //localization resource - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -参阅 [本地化文档](../Localization.md) 学习关于本地化资源的更多内容. - -## 引用 Style & Script - -当部件含有样式和scirpt文件时,会存在一些挑战; - -* 使用部件的页面应该将 **script & styles** 文件引用到页面中. -* 页面还需要解析部件的 `依赖库/文件`. - -将资源与部件正确的关联在一起时,ABP会解决这些问题. 使用正确的方法,就不用担心部件的依赖关系. - -### 定义一个简单的文件路径 - -下面的示例中部件添加了样式和scirpt文件: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, - ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -ABP会考虑到这些依赖关系, 在view/page中使用正确的方法添加部件 . 样式和script可以是物理文件也可以是虚拟文件. 它于[虚拟文件系统](../Virtual-File-System.md)完全集成]. - -### 定义 Bundle - -页面中使用的组件的所有资源都做为捆绑包添加(如果没有其他配置,会在生产中合并和压缩). 除了简单的添加文件,你还可以充分的利用捆绑功能. - -下面的示例与上面的代码相同,但是在添加文件时文件路径替换成了 `BundleContributor`: - -````csharp -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget( - StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, - ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } - )] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } - - public class MySimpleWidgetStyleBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); - } - } - - public class MySimpleWidgetScriptBundleContributor : BundleContributor - { - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files - .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); - } - } -} - -```` - -捆绑系统非常强大,如果你的部件使用了JavaScript库来呈现图表, 你可以将它声明为依赖项, 如果之前未添加JavaScript库. 则会自动添加到页面中. 使用这种方式让页面使用部件时不用关心依赖项. - -参阅 [捆包&压缩 文档](Bundling-Minification.md) 了解更多内容. - -## 授权 - -某些组件可能只对通过身份验证或授权的用户可用,这时可以使用 `Widget` attribute 的以下属性: - -* `RequiresAuthentication` (`bool`): 设置为true,只有通过身份验证的用户(登录用户)可用. -* `RequiredPolicies` (`List`): 授权用户的策略名称列表. 有关策略的详细信息请参阅[授权文档](../Authorization.md). - -示例: - -````csharp -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc.UI.Widgets; - -namespace DashboardDemo.Web.Pages.Components.MySimpleWidget -{ - [Widget(RequiredPolicies = new[] { "MyPolicyName" })] - public class MySimpleWidgetViewComponent : AbpViewComponent - { - public IViewComponentResult Invoke() - { - return View(); - } - } -} -```` - -## 部件选项 - -`WidgetOptions` 是 `Widget` attribute 替代, 你可以使用它去配置部件: - -```csharp -Configure(options => -{ - options.Widgets.Add(); -}); -``` - -将上面的代码写到[模块](../Module-Development-Basics.md)的 `ConfigureServices` 方法中. `Widgetoptions` 可以完成 `Widget` attribute 的所有功能. 比如为组件添加样式: - -````csharp -Configure(options => -{ - options.Widgets - .Add() - .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); -}); -```` - -> 提示: `WidgetOptions` 还可以更改现有的部件配置. 如果要修改应用程序使用的模块内的组件配置,这会很有用. 使用 `options.Widgets.Find` 获取现有的 `WidgetDefinition`. \ No newline at end of file +[点击链接跳转到小部件文档](../UI/AspNetCore/Widgets.md) \ No newline at end of file diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index 0a15f6e2ca..a43565279e 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -143,6 +143,20 @@ myGroup.AddPermission( ); ```` +#### 启用/禁用权限 + +权限默认为启用. 它也可以被禁用,禁用权限所有的用户将无法使用它. 你仍然可以检查这个权限,但它总是会返回被禁止. + +定义示例: + +````csharp +myGroup.AddPermission("Author_Management", isEnabled: false); +```` + +通常你不需要定义禁用权限(除非您暂时想要禁用应用程序的功能). 无论怎样,你可能想要禁用依赖模块中定义的权限,这样你可以禁用相关的功能. 参阅下面的 "*更改依赖模块的权限定义*" 节,查看示例用法. + +> 注意:检查一个未定义的权限会抛出异常,而被禁用的权限的返回禁止(false). + #### 子权限 权限可以具有子权限,当你想要创建一个层次结构的权限树时它特别有用. 在这个树中一个权限可能含有子权限,并且子权限只有在授权父权限时才可用. @@ -205,6 +219,22 @@ public class AuthorAppService : ApplicationService, IAuthorAppService 参阅 [基于策略的授权](https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/policies) 文档了解如何自定义策略. +### 更改依赖模块的权限定义 + +从 `PermissionDefinitionProvider` 派生的类(就像上面的示例一样) 可以获取现有的权限定义(由依赖[模块](Module-Development-Basics.md)定义)并更改其定义. + +示例: + +````csharp +context + .GetPermissionOrNull(IdentityPermissions.Roles.Delete) + .IsEnabled = false; +```` + +当你在权限提供程序编写了这行代码,它会找到[身份模块](Modules/Identity.md)的 "role deletion" 权限并且禁用它,因此没有人可以在应用程序中删除角色. + +> 提供: 更好的方式应该检查 `GetPermissionOrNull` 返回值,如果权限未定义,它会返回null值. + ## IAuthorizationService ASP.NET Core 提供了 `IAuthorizationService` 用于检查权限. 注入后使用它进行条件控制权限. @@ -336,10 +366,10 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider * `PermissionGrantResult.Prohibited` 禁止授权用户,任何一个授权值提供程序返回了 `Prohibited`, 那么其他的提供程序返回的值都不再重要. * `PermissionGrantResult.Undefined` 代表当前无法确定是否授予或禁止权限, 返回`UnDefined`由其他权限值提供程序检查权限. -定义`Provider`后将其添加到 `PermissionOptions`,如下所示: +定义`Provider`后将其添加到 `AbpPermissionOptions`,如下所示: ````csharp -Configure(options => +Configure(options => { options.ValueProviders.Add(); }); @@ -367,4 +397,5 @@ public override void ConfigureServices(ServiceConfigurationContext context) ## 接下来 * [权限管理模块](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor 页面 JavaScript Auth API](AspNetCore/JavaScript-API/Auth.md) +* [ASP.NET Core MVC / Razor 页面 JavaScript Auth API](API/JavaScript-API/Auth.md) +* [Angular界面中的权限管理](UI/Angular/Permission-Management.md) diff --git a/docs/zh-Hans/Background-Jobs-Hangfire.md b/docs/zh-Hans/Background-Jobs-Hangfire.md index d35d8482ca..bbe9428bb9 100644 --- a/docs/zh-Hans/Background-Jobs-Hangfire.md +++ b/docs/zh-Hans/Background-Jobs-Hangfire.md @@ -40,4 +40,45 @@ public class YourModule : AbpModule ## 配置 -TODO... \ No newline at end of file +你可以安装任何Hangfire存储. 最常用的是SQL Server(参阅[Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer)NuGet包). + +当你安装NuGet包后,你需要为你的项目配置Hangfire. + +1.首先, 我们需要更改 `Module` 类 (例如: `HttpApiHostModule`) 的 `ConfigureServices` 方法去配置Hangfire存储和连接字符串: + +````csharp + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + var hostingEnvironment = context.Services.GetHostingEnvironment(); + + //... other configarations. + + ConfigureHangfire(context, configuration); + } + + private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) + { + context.Services.AddHangfire(config => + { + config.UseSqlServerStorage(configuration.GetConnectionString("Default")); + }); + } +```` + +1. 我们需要在 `Module` 类的 `OnApplicationInitialization` 方法添加 `UseHangfireServer` 方法调用. + +如果你想要使用Hangfire的面板,你可以使用: `UseHangfireDashboard` + +````csharp + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + // ... others + + app.UseHangfireServer(); + app.UseHangfireDashboard(); + + } +```` \ No newline at end of file diff --git a/docs/zh-Hans/Background-Jobs-Quartz.md b/docs/zh-Hans/Background-Jobs-Quartz.md new file mode 100644 index 0000000000..906d0b2d1f --- /dev/null +++ b/docs/zh-Hans/Background-Jobs-Quartz.md @@ -0,0 +1,73 @@ +# Quartz 后台作业管理 + +[Quartz](https://www.quartz-scheduler.net/)是一个高级的作业管理. 你可以用ABP框架集成Quartz代替[默认后台作业管理](Background-Jobs.md). 通过这种方式你可以使用相同的后台作业API,将你的代码独立于Quartz. 如果你喜欢也可以直接使用Quartz的API. + +> 参阅[后台作业文档](Background-Jobs.md),学习如何使用后台作业系统. 本文只介绍了如何安装和配置Quartz集成. + +## 安装 + +建议使用[ABP CLI](CLI.md)安装包. + +### 使用ABP CLI + +在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令: + +````bash +abp add-package Volo.Abp.BackgroundJobs.Quartz +```` + +### 手动安装 + +如果你想手动安装; + +1. 添加 [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet包添加到你的项目: + + ```` + Install-Package Volo.Abp.BackgroundJobs.Quartz + ```` + +2. 添加 `AbpBackgroundJobsQuartzModule` 到你的模块的依赖列表: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +## 配置 + +Quartz是一个可配置的类库,对此ABP框架提供了 `AbpQuartzPreOptions`. 你可以在模块预配置此选项,ABP在初始化Quartz模块时将使用它. 例: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + PreConfigure(options => + { + options.Properties = new NameValueCollection + { + ["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp", + ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", + ["quartz.jobStore.tablePrefix"] = "QRTZ_", + ["quartz.serializer.type"] = "json", + ["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"), + ["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer", + ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", + }; + }); + } +} +```` + +Quartz**默认**将作业与调度信息存储在**内存**中,示例中我们使用[选项模式](Options.md)的预配置将其更改为存储到数据库中. 有关Quartz的更多配置请参阅[Quartz文档](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/index.html). \ No newline at end of file diff --git a/docs/zh-Hans/Background-Jobs.md b/docs/zh-Hans/Background-Jobs.md index 7c2a694005..affe2d9afa 100644 --- a/docs/zh-Hans/Background-Jobs.md +++ b/docs/zh-Hans/Background-Jobs.md @@ -7,13 +7,13 @@ - 为执行**长时间运行的任务**而用户无需等待, 例如:用户按了一下"报告"按钮开始一个长时间运行的报告任务, 你把这个任务添加到**队列**里,并在完成后通过电子邮件将报告的结果发送给你的用户. - 创建**可重试**和**持久的任务**以**确保**代码将**成功执行**. 例如, 你可以在后台作业中发送电子邮件以克服**临时故障**并**保证**最终发送. 这样用户不需要在发送电子邮件时等待. -后台作业是**持久性的**这意味着即使你的应用程序崩溃了, 后台左右也会在稍后**重试**并**执行**. +后台作业是**持久性的**这意味着即使你的应用程序崩溃了, 后台作业也会在稍后**重试**并**执行**. ABP为后台作业提供了一个**抽象**模块和几个后台作业**实现**. 它具有内置/默认的实现以及与Hangfire和RabbitMQ的集成. ## 抽象模块 -ABP为后台作业提供了一个 **abstraction** 模块和 **多个实现**. 它有一个内置/默认实现以及Hangfire与RabbitMQ集成. +ABP为后台作业提供了一个 **抽象** 模块和 **多个实现**. 它有一个内置/默认实现以及Hangfire,RabbitMQ与Quartz集成. `Volo.Abp.BackgroundJobs.Abstractions` nuget package 提供了创建后台作业和队列作业所需要的服务. 如果你的模块只依赖这个包,那么它可以独立于其实现/集成. @@ -174,4 +174,8 @@ public class MyModule : AbpModule 请参阅预构建的作业管理器备选方案: * [Hangfire 后台作业管理器](Background-Jobs-Hangfire.md) -* [RabbitMQ 后台作业管理器](Background-Jobs-RabbitMq.md) \ No newline at end of file +* [RabbitMQ 后台作业管理器](Background-Jobs-RabbitMq.md) +* [Quartz 后台作业管理器](Background-Jobs-Quartz.md) + +## 另请参阅 +* [后台工作者](Background-Workers.md) \ No newline at end of file diff --git a/docs/zh-Hans/Background-Workers-Quartz.md b/docs/zh-Hans/Background-Workers-Quartz.md new file mode 100644 index 0000000000..4799633b07 --- /dev/null +++ b/docs/zh-Hans/Background-Workers-Quartz.md @@ -0,0 +1,68 @@ +# Quartz 后台工作者管理 + +[Quartz](https://www.quartz-scheduler.net/)是一个高级的后台工作者管理. 你可以用ABP框架集成Quartz代替[默认后台工作者管理](Background-Workers.md). ABP简单的集成了Quartz. + +## 安装 + +建议使用[ABP CLI](CLI.md)安装包. + +### 使用ABP CLI + +在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令: + +````bash +abp add-package Volo.Abp.BackgroundWorkers.Quartz +```` + +### 手动安装 + +如果你想手动安装; + +1. 添加 [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet包添加到你的项目: + + ```` + Install-Package Volo.Abp.BackgroundWorkers.Quartz + ```` + +2. 添加 `AbpBackgroundWorkersQuartzModule` 到你的模块的依赖列表: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +### 配置 + +参阅[配置](Background-Jobs-Quartz.md#配置). + +### 创建后台工作者 + +后台工作者是一个继承自 `QuartzBackgroundWorkerBase` 基类的类. 一个简单的工作者如下所示: + +```` csharp +public class MyLogWorker : QuartzBackgroundWorkerBase +{ + public MyLogWorker() + { + JobDetail = JobBuilder.Create().Build(); + Trigger = TriggerBuilder.Create().StartNow().Build(); + } + + public override Task Execute(IJobExecutionContext context) + { + Logger.LogInformation("Executed MyLogWorker..!"); + return Task.CompletedTask; + } +} +```` + +示例中我们重写了 `Execute` 方法写入日志. 后台工作者默认是**单例**. 如果你需要,也可以实现[依赖接口](Dependency-Injection.md#依赖接口)将其注册为其他的生命周期. + +### 更多 + +参阅Quartz[文档](https://www.quartz-scheduler.net/documentation/index.html)了解更多信息. \ No newline at end of file diff --git a/docs/zh-Hans/Background-Workers.md b/docs/zh-Hans/Background-Workers.md new file mode 100644 index 0000000000..dfd26c7b82 --- /dev/null +++ b/docs/zh-Hans/Background-Workers.md @@ -0,0 +1,138 @@ +# 后台工作者 + +## 介绍 + +背景工人在应用简单独立的线程在后台运行。一般来说,他们定期运行,以执行一些任务。例子; +后台工作者在应用程序后台运行的简单的独立线程,一般来说它们定期运行执行一些任务.例如; + +* 后台工作者可以定期**删除过时的日志**. +* 后台工作者可以定期检查**不活跃的用户**并且向其**发送邮件**使用户继续使用你的应用程序. + +## 创建一个后台工作者 + +后台工作者应该直接或间接的继承 `IBackgroundWorker` 接口. + +> 后台工作者是[单例](Dependency-Injection.md)的. 所以实例化运行你的工作者类的单个实例. + +### BackgroundWorkerBase + +`BackgroundWorkerBase` 是创建后台工作者的简单方法. + +````csharp +public class MyWorker : BackgroundWorkerBase +{ + public override Task StartAsync(CancellationToken cancellationToken = default) + { + //... + } + + public override Task StopAsync(CancellationToken cancellationToken = default) + { + //... + } +} +```` + +`StartAsync` 开始你的工作者(在应用程序启动时),`StopAsync` 停止它(在应用程序关闭时). + +> 你可以直接实现 `IBackgroundWorker`, 但 `BackgroundWorkerBase` 提供了一些像 `Logger` 的常用属性. + +### AsyncPeriodicBackgroundWorkerBase + +假设我们要设置用户为不活跃用户(如果用户最近30天未登录应用程序).`AsyncPeriodicBackgroundWorkerBase` 类简化了创建定期工作者的过程,我们在下面的示例中使用它: + +````csharp +public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase +{ + public PassiveUserCheckerWorker( + AbpTimer timer, + IServiceScopeFactory serviceScopeFactory + ) : base( + timer, + serviceScopeFactory) + { + Timer.Period = 600000; //10 minutes + } + + protected override async Task DoWorkAsync( + PeriodicBackgroundWorkerContext workerContext) + { + Logger.LogInformation("Starting: Setting status of inactive users..."); + + //Resolve dependencies + var userRepository = workerContext + .ServiceProvider + .GetRequiredService(); + + //Do the work + await userRepository.UpdateInactiveUserStatusesAsync(); + + Logger.LogInformation("Completed: Setting status of inactive users..."); + } +} +```` + +* `AsyncPeriodicBackgroundWorkerBase` 使用 `AbpTimer`(线程安全定时器)对象来确定**时间段**. 我们可以在构造函数中设置了`Period` 属性。 +* 它需要实现 `DoWorkAsync` 方法**执行**定期任务. +* 最好使用 `PeriodicBackgroundWorkerContext` **解析依赖** 而不是构造函数. 因为 `AsyncPeriodicBackgroundWorkerBase` 使用 `IServiceScope` 在你的任务执行结束时会对其 **disposed**. +* `AsyncPeriodicBackgroundWorkerBase` **捕获并记录** 由 `DoWorkAsync` 方法抛出的 **异常**. + +## 注册后台工作者 + +创建一个后台工作者后,你应该将其添加到 `IBackgroundWorkerManager`. 最常见的地方是模块类的 `OnApplicationInitialization` 方法: + +````csharp +[DependsOn(typeof(AbpBackgroundWorkersModule))] +public class MyModule : AbpModule +{ + public override void OnApplicationInitialization( + ApplicationInitializationContext context) + { + context.AddBackgroundWorker(); + } +} +```` + +`context.AddBackgroundWorker(...)` 是以下代码的简化扩展方法: + +````csharp +context.ServiceProvider + .GetRequiredService() + .Add( + context + .ServiceProvider + .GetRequiredService() + ); +```` + +所以,它解析了给定的后台工作者并添加到 `IBackgroundWorkerManager`. + +如果我们通常在 `OnApplicationInitialization` 添加工作者,但并不是强制的. 你可以在应用程序的任何地方注入 `IBackgroundWorkerManager` 并在运行时添加工作者. 在你的应用程序关闭时Background worker manager会释放所有已注册的后台工作者. + +## Options + +`AbpBackgroundWorkerOptions` 是用于设置后台工作者的选择. 目前只有一个选项: + +* `IsEnabled` (默认值: true): 用于为你的应用程序启动或禁用后台工作者系统. + +## 让应用程序始终运行 + +后台工作者只有在你的应用程序运行时才会工作. 如果你将后台作业托管在web应用程序中(这是默认行为),那么你应该确保你的web应用程序被配置为始终运行. 否则只有在你的应用程序正在运行时后台作业才会工作. + +## 在集群运行 + +如果你在集群环境中运行同时运行应用程序的多个实现,这种情况下要小心,每个应用程序都运行相同的后台工作者,如果你的工作者在相同的资源上运行(例如处理相同的数据),那么可能会产生冲突. + +如果这对你的工作者是一个问题,你有两个选项: + +* 使用上面提到的 `AbpBackgroundWorkerOptions` 禁用其他的后台工作者系统,只保留一个实例. +* 所有的应用程序都禁用后台工作者系统,创建一个特殊的应用程序在一个服务上运行执行工作者. + +## Quartz 集成 + +ABP框架的后台工作者系统可以很好的执行周期任务. 但是你可能需要使用更高级的任务调度,像[Quartz](https://www.quartz-scheduler.net/). 参阅社区贡献的[Quartz集成](Background-Workers-Quartz.md) + +## 另请参阅 + +* [后台工作者的Quartz集成](Background-Workers-Quartz.md) +* [后台作业](Background-Jobs.md) \ No newline at end of file diff --git a/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md b/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md index 4d98c6cbc5..b01f7eab28 100644 --- a/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md +++ b/docs/zh-Hans/Best-Practices/Entity-Framework-Core-Integration.md @@ -89,24 +89,27 @@ public static class IdentityDbContextModelBuilderExtensions builder.Entity(b => { - b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ToTable(options.TablePrefix + "Users", options.Schema); + b.ConfigureByConvention(); //code omitted for brevity }); builder.Entity(b => { b.ToTable(options.TablePrefix + "UserClaims", options.Schema); + b.ConfigureByConvention(); //code omitted for brevity - }); + }); //code omitted for brevity } } ```` -* **推荐** 通过继承 `ModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 例如: +* **推荐** 为每个Enttiy映射调用 `b.ConfigureByConvention();`(如上所示). +* **推荐** 通过继承 `AbpModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 例如: ````C# -public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions +public class IdentityModelBuilderConfigurationOptions : AbpModelBuilderConfigurationOptions { public IdentityModelBuilderConfigurationOptions() : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) diff --git a/docs/zh-Hans/Best-Practices/MongoDB-Integration.md b/docs/zh-Hans/Best-Practices/MongoDB-Integration.md index 4c71a58f90..b7b033b1fa 100644 --- a/docs/zh-Hans/Best-Practices/MongoDB-Integration.md +++ b/docs/zh-Hans/Best-Practices/MongoDB-Integration.md @@ -1,4 +1,4 @@ -## MongoDB 集成 +## MongoDB 集成 * **推荐** 为每个模块定义一个独立的 `MongoDbContext` 接口与实现类. @@ -90,11 +90,11 @@ public static class AbpIdentityMongoDbContextExtensions } ``` -- **推荐** 通过继承 `MongoModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 示例: +- **推荐** 通过继承 `AbpMongoModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 示例: ```c# public class IdentityMongoModelBuilderConfigurationOptions - : MongoModelBuilderConfigurationOptions + : AbpMongoModelBuilderConfigurationOptions { public IdentityMongoModelBuilderConfigurationOptions() : base(AbpIdentityConsts.DefaultDbTablePrefix) diff --git a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md b/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md index 1011b5a10a..7d633ee9e0 100644 --- a/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md +++ b/docs/zh-Hans/Blog-Posts/2018-09-24-Announcement/Post.md @@ -144,7 +144,7 @@ UI组合是主要目标之一.为此,主题系统将提供菜单,工具栏和其 这段代码通过包含bootstrap(及其依赖项,如果有)和另外两个css文件来动态创建一个新的样式包.这些文件在生产环境中捆绑和压缩,但将在开发环境中单独添加. -有关更多信息,请参阅[文档](https://github.com/abpframework/abp/blob/master/docs/AspNetCore/Bundling-Minification.md) +有关更多信息,请参阅[文档](https://github.com/abpframework/abp/blob/master/docs/UI/AspNetCore/Bundling-Minification.md) ### 分布式事件总线(Distributed Event Bus) diff --git a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md index dfb5a66123..831cc791e9 100644 --- a/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -27,7 +27,7 @@ Angular是第一个SPA UI选项,但它不是最后一个.在v1.0发布之后,我 ### Widget系统 -[Widget系统](https://docs.abp.io/en/abp/latest/AspNetCore/Widgets)允许为ASP.NET Core MVC应用程序**定义和重用**Widget.Widget可能有自己的脚本和样式资源以及由ABP框架管理的第三方库的依赖关系. +[Widget系统](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Widgets)允许为ASP.NET Core MVC应用程序**定义和重用**Widget.Widget可能有自己的脚本和样式资源以及由ABP框架管理的第三方库的依赖关系. ### 其他 diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md new file mode 100644 index 0000000000..a17bef2e1b --- /dev/null +++ b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/Post.md @@ -0,0 +1,142 @@ +# ABP框架v2.3.0已经发布! + +在**新冠病毒**的日子里,我们发布了**ABP框架v2.3**, 这篇文章将说明本次发布**新增内容**和过去的两周**我们做了什么**. + +## 关于新冠病毒和我们的团队 + +关于冠状病毒的状况**我们很难过**.在[Volosoft](https://volosoft.com/)的团队,我们有不同国家的**远程工作者**在自己家里工作.从上周开始,我们已经**完全开始在家远程工作**,包括我们的主要办公室的员工. + +我们相信并祈祷人类会在很短的时间内克服这个问题. + +## 关于发布周期 + +从ABP v2.1.0开始,我们开始**每两周**的周四发布功能版本.本次是该决定后的第3次发布,我们看到这种方式目前运转良好,并提高了我们的灵活性. + +我们将继续每两周发布**功能版本**(如v2.4,v2.5).另外,如果需要我们会随时发布**热修复版本**(如v2.3.1,v2.3.2). + +## ABP框架v2.3.0新增内容 + +我们已在这两周的开发周期内通过**393次提交**完成和合并了 **[104](https://github.com/abpframework/abp/milestone/30?closed=1)个issue和pull request**. + +我将介绍这个版本加入的一些新功能和改善. + +### React Native移动应用程序 + +我们终于完成了**react native移动应用程序**.目前,它可以让你**登录**,管理**用户**和**租户**.它利用ABP框架相同的设置,授权和本地化系统. + +应用程序的一些截图: + +![mobile-ui](react-native-ui.png) + +它没有太多的功能,但它是你的移动应用程序一个**完美的起点**,因为它是完全集成到后端并支持多租户. + +### Angular TypeScript代理生成器 + +从我们的Angular应用程序中调用服务器中的REST端点是很常见的.这种情况下,我们一般创建**服务**(在服务器上包含各个服务的方法)和**模型对象**(对应服务器上的[DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)). + +除了手动创建这样的与服务器交互的服务外,我们可以使用像[NSWAG](https://github.com/RicoSuter/NSwag)工具来为我们生成服务代理.但是NSWAG有以下几个我们遇到的问题: + +* 它产生一个**大,单一**的.ts文件; + * 当你的应用程序增长时,它变得**太大**了. + * 它不适合ABP框架的 **[模块化](https://docs.abp.io/en/abp/latest/Module-Development-Basics)方式**. +* 它创建了有点**丑陋的代码**.我们希望有一个干净的代码(就像我们手写的). +* 它不能生成服务器端声明的相同的**方法签名**(因为swagger.json不能准确地反映后端服务的方法签名).我们已创建了公开服务器端方法约定的端点,来允许客户端生成更好的客户端代理. + +因此,我们决定创建一个ABP CLI命令来自动生成typescript客户端代理([#2222](https://github.com/abpframework/abp/issues/2222)),用于在ABP框架中开发REST API. + +它用起来很简单.只需要在你Angular应用程序的**根文件夹**运行以下命令 + +````bash +abp generate-proxy +```` + +它只会为你自己的应用程序的服务创建代理.它(默认)不会为你使用的应用程序模块创建代理.有几个选项.参见[CLI文档](https://docs.abp.io/en/abp/latest/CLI). + +### 复合主键的CRUD应用服务 + +` CrudAppService `是一个很有用的基类,用来为你的实体创建CRUD应用服务.不过,它不支持**复合主键**的实体. `AbstractKeyCrudAppService`是新开发的基类以支持复合主键的实体.更多信息请浏览[文档](https://docs.abp.io/en/abp/latest/Application-Services#abstractkeycrudappservice). + +### 添加模块的源代码 + +应用程序启动模板带有一些[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index), 以**Nuget和NPM包**的方式**预先安装了** .这样做有几个重要的优点: + +* 当新版本可用时, 你可以 **轻松地[升级](https://docs.abp.io/en/abp/latest/CLI#update)** 这些模块. +* 你的解决方案**更干净**,这样你就可以专注于自己的代码. + +但是,当你需要对一个依赖的模块**大量定制**时,就不如它的代码在你的应用程序中那么容易.为了解决这个问题,我们引入了一个[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)的新命令, 在你的解决方案中用代码**替换**Nuget包.用法很简单: + +````bash +abp add-module --with-source-code +```` + +该命令以源代码方式添加模块, 或者如果模块已经以包引用方式添加了, 则替换为源代码,. + +> 建议在使用此命令前**保存你的更改**到源代码控制系统, 因为它会修改很多你的代码. + +此外,我们也创建了文档来说明如何定制依赖的模块而不改变它们的源代码(见下面的部分).仍然建议以包的方式使用模块,以便在以后可以轻松升级. + +> 免费模块的源代码是**MIT**许可,所以你可以自由更改它们并添加到您的解决方案中. + +### 切换到预览版 + +ABP框架正在迅速发展,我们经常发布新版本.不过,如果你想更紧密地追随它,你可以使用**每日预览包**. + +我们创建了一个ABP CLI命令来轻松地为你的解决方案**更新到最新的预览包**.在你的解决方案的根文件夹中运行以下命令: + +````bash +abp switch-to-preview +```` + +它会修改所有ABP相关的NuGet和NPM包的版本.当你需要时你也可以**切换回最新稳定版**: + +````bash +abp switch-to-stable +```` + +更多信息请浏览[ABP CLI文档](https://docs.abp.io/en/abp/latest/CLI#switch-to-preview). + +### 文档改进 + +#### 扩展/定制依赖应用模块 + +我们创建了一个巨大的文档来说明如何定制模块依赖而不改变其源代码.参见[文档](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide). + +除了文档以外,我们已经修订了所有模块([#3166](https://github.com/abpframework/abp/issues/3166)),来使他们的服务更容易扩展和定制. + +#### EF Core迁移指南 + +最近,我们创建了一个指南,说明ABP启动模板所使用的迁移系统. [该指南](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations)还介绍了如何定制迁移结构,拆分你的模块跨多个数据库,复用一个模块的表,等等. + +#### 从 ASP.NET Boilerplate迁移 + +如果你有建立在 ASP.NET Boilerplate的解决方案,我们[创建了一个指南](https://docs.abp.io/en/abp/latest/AspNet-Boilerplate-Migration-Guide),试着帮助迁移你的解决方案到新的ABP框架上. + +### 其他一些功能 + +#### 框架 + +* 添加`IRepository.GetAsync`和`IRepository.FindAsync`方法([#3184](https://github.com/abpframework/abp/issues/3148)). + +#### 模块 + +* 当创建新租户时获取管理员的密码和电子邮件地址,租户管理模块([#3088](https://github.com/abpframework/abp/issues/3088)). +* 集成Elastic全文检索, 文档模块([#2901](https://github.com/abpframework/abp/pull/2901)). +* 新的Quartz后台工作者模块([#2762](https://github.com/abpframework/abp/issues/2762)) + +#### 示例 + +* 微服务演示添加多租户支持([#3032](https://github.com/abpframework/abp/pull/3032)). + +所有的功能, 改善和BUG修复, 请浏览[发布说明](https://github.com/abpframework/abp/releases/tag/2.3.0). + +## 下一步? + +我们未来几个月的目标如下: + +* 完成**文档和示例**,写更多的教程. +* 使框架和现有模块的更加**可定制和可扩展**. +* 集成**gRPC**和为所有预置模块实现gRPC端点([#2882](https://github.com/abpframework/abp/issues/2882)). +* 为ABP框架创建**Blazor UI**, 并在所有模块和启动模板中实现它([#394](https://github.com/abpframework/abp/issues/394)). +* 为预置模块**添加新功能**,并为[ABP商业版](https://commercial.abp.io/)创建新模块. + +更多细节请浏览[GitHub里程碑](https://github.com/abpframework/abp/milestones). \ No newline at end of file diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png new file mode 100644 index 0000000000..8c0a2bf241 Binary files /dev/null and b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/abp-io-release-2-3-0-blog.png differ diff --git a/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png new file mode 100644 index 0000000000..47d889413c Binary files /dev/null and b/docs/zh-Hans/Blog-Posts/2020-03-19 v2_3_Release/react-native-ui.png differ diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index f5f230d9f2..e0ce6e3c74 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -1,4 +1,4 @@ -# ABP CLI +# ABP CLI ABP CLI (命令行接口) 是一个命令行工具,用来执行基于ABP解决方案的一些常见操作. @@ -22,7 +22,7 @@ dotnet tool update -g Volo.Abp.Cli 生成基于ABP[启动模板](Startup-Templates/Index.md)的新解决方案. -基本用法: +用法: ````bash abp new <解决方案名称> [options] @@ -43,18 +43,25 @@ abp new Acme.BookStore * `app` (default): [应用程序模板](Startup-Templates/Application.md). 其他选项: * `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项: * `mvc`: ASP.NET Core MVC.此模板的其他选项: - * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景. + * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景. * `angular`: Angular. 这个模板还有一些额外的选项: * `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. * `none`: 无UI. 这个模板还有一些额外的选项: * `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点. + * `--mobile` 或者 `-m`: 指定移动应用程序框架. 默认框架是 `react-native`. 其他选项: + * `none`: 不包含移动应用程序. + * `react-native`: React Native. * `--database-provider` 或者 `-d`: 指定数据库提供程序.默认是 `ef`.其他选项: * `ef`: Entity Framework Core. * `mongodb`: MongoDB. * `module`: [Module template](Startup-Templates/Module.md). 其他选项: * `--no-ui`: 不包含UI.仅创建服务模块(也称为微服务 - 没有UI). -* `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录. -* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本. + * +* `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录. +* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本. +* `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D\localTemplate` 或 `https://.zip`). +* `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中. +* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 如果你不想使用默认,你可以设置自己的连接字符串. 默认的数据库提供程序是 `SQL Server`, 所以你只能输入SQL Server连接字符串! ### add-package @@ -65,7 +72,7 @@ abp new Acme.BookStore > 需要注意的是添加的模块可能需要额外的配置,通常会在包的文档中指出. -基本用法: +用法: ````bash abp add-package <包名> [options] @@ -89,7 +96,7 @@ abp add-package Volo.Abp.MongoDB > 由于分层,不同的数据库提供程序选项或其他原因,业务模块通常由多个包组成. 使用`add-module`命令可以大大简化向模块添加模块的过程. 但是每个模块可能需要一些其他配置,这些配置通常在相关模块的文档中指出. -基本用法: +用法: ````bash abp add-module <模块名称> [options] @@ -108,6 +115,7 @@ abp add-module Volo.Blogging * `--solution` 或 `-s`: 指定解决方案 (.sln) 路径. 如果未指定,CLI会尝试在当前目录中寻找.sln文件. * `--skip-db-migrations`: 对于EF Core 数据库提供程序,它会自动添加新代码的第一次迁移 (`Add-Migration`) 并且在需要时更新数据库 (`Update-Database`). 指定此选项可跳过此操作. * `-sp` 或 `--startup-project`: 启动项目的项目文件夹的相对路径. 默认值是当前文件夹. +* `--with-source-code`: 添加模块的源代码,而不是NuGet/NPM软件包. ### update @@ -125,6 +133,68 @@ abp update [options] #### Options * `--include-previews` 或 `-p`: 将预览版, 测试版本 和 rc 包 同时更新到最新版本. +* `--npm`: 仅更新NPM包 +* `--nuget`: 仅更新的NuGet包 + +### 切换到每晚构建(预览)包 + +想要切换到ABP框架的最新预览版可以使用此命令. + +用法: + +````bash +abp switch-to-preview [options] +```` + +你也可以使用切换回稳定版本: + +````bash +abp switch-to-stable [options] +```` + +#### Options + +`--solution-directory` 或 `-sd`: 指定解决方案文件夹. 解决方案应该在指定文件夹或子文件夹中. 如果未指定,默认为当前目录. + +### login + +CLI的一些功能需要登录到abp.io平台. 使用你的用户名登录 + +```bash +abp login +``` + +请注意,新的登录将终止先前的会话并创建一个新的会话. + +### logout + +通过从计算机中删除会话令牌来注销. + +``` +abp logout +``` + +### generate-proxy + +为你的HTTP API生成客户端代码,简化客户端使用服务的成本. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行. + +用法: + +````bash +abp generate-proxy [options] +```` + +#### Options + +* `--apiUrl` 或者 `-a`:指定HTTP API的根URL. 如果未指定这个选项,默认使用你Angular应用程序的`environment.ts`文件API URL. 在运行 `generate-proxy` 命令之前,你的host必须启动正在运行. +* `--ui` 或者 `-u`: 指定UI框架,默认框架是angular.当前只有angular一个选项, 但我们会通过更改CLI增加新的选项. 尽请关注! +* `--module` 或者 `-m`:指定模块名. 默认模块名称为app. 如果你想所有模块,你可以指定 `--module all` 命令. + +示例: + +````bash +abp generate-proxy --apiUrl https://localhost:44305 --ui angular --module all +```` ### help diff --git a/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md b/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md new file mode 100644 index 0000000000..4cc73989fc --- /dev/null +++ b/docs/zh-Hans/Customizing-Application-Modules-Extending-Entities.md @@ -0,0 +1,177 @@ +# 自定义应用模块: 扩展实体 + +在某些情况下你可能希望为依赖模块中定义的实体添加一些额外的属性(和数据库字段). 本节将介绍一些实现这一目标的不同方法. + +## Extra Properties + +[Extra properties](Entities.md)是一种存储实体的一些额外数据但不用更改实体的方式. 实体应该实现 `IHasExtraProperties` 接口. 所有预构建模块定义的聚合根实体都实现了 `IHasExtraProperties` 接口,所以你可以在这些实体中存储额外的属性. + +示例: + +````csharp +//SET AN EXTRA PROPERTY +var user = await _identityUserRepository.GetAsync(userId); +user.SetProperty("Title", "My custom title value!"); +await _identityUserRepository.UpdateAsync(user); + +//GET AN EXTRA PROPERTY +var user = await _identityUserRepository.GetAsync(userId); +return user.GetProperty("Title"); +```` + +这种方法开箱即用并且非常简单,你可以使用不同的属性名称(如这里的`Title`)在同一时间存储多个属性. + +对于EF Core额外的属性被格式化成单个 `JSON` 字符值串存储在数据库中. 对于MongoDB它们做为单独的字段存储. + +参阅[实体文档](Entities.md)了解更多关于额外系统. + +> 可以基于额外的属性执行**业务逻辑**. 你可以[重写服务方法](Customizing-Application-Modules-Overriding-Services.md). 然后获取或设置如上所示的值. + +## 实体扩展 (EF Core) + +如上所述,实体所有的额外属性都作为单个JSON对象存储在数据库表中. 它不适用复杂的场景,特别是在你需要的时候. + +* 使用额外属性创建**索引**和**外键**. +* 使用额外属性编写**SQL**或**LINQ**(例如根据属性值搜索). +* 创建你**自己的实体**映射到相同的表,但在实体中定义一个额外属性做为 **常规属性**(参阅 [EF Core迁移文档](Entity-Framework-Core-Migrations.md)了解更多). + +为了解决上面的问题,用于EF Core的ABP框架实体扩展系统允许你使用上面定义相同的额外属性API,但将所需的属性存储在单独的数据库表字段中. + +假设你想要添加 `SocialSecurityNumber` 到[身份模块](Modules/Identity.md)的 `IdentityUser` 实体. 你可以使用 `ObjectExtensionManager` 类: + +````csharp +ObjectExtensionManager.Instance + .MapEfCoreProperty( + "SocialSecurityNumber", + b => { b.HasMaxLength(32); } + ); +```` + +* 你提供了 `IdentityUser` 作为实体名(泛型参数), `string` 做为新属性的类型, `SocialSecurityNumber` 做为属性名(也是数据库表的字段名). +* 你还需要提供一个使用[EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)定义数据库映射属性的操作. + +> 必须在使用相关的 `DbContext` 之前执行此代码. 应用程序启动模板定义了一个名为 `YourProjectNameEntityExtensions` 的静态类. 你可以在此类中定义扩展确保在正确的时间执行它. 否则你需要自己处理. + +定义实体扩展后你需要使用EF Core的[Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration)和[Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database)命令来创建code first迁移类并更新数据库. + +然后你可以使用上一部分中定义的相同额外属性系统来操纵实体上的属性. + +## 创建新实体映射到同一个数据库表/Collection + +尽管额外属性方法**易于使用**并且适用于一些场景,但它具有[实体文档](Entities.md)中描述的一些缺点. + +另一个方法是**创建你自己的实体**映射到**同一个数据库库**(对于MongoDB数据库是collection) + +[应用程序启动模板](Startup-Templates/Application.md)的 `AppUser` 已经实现了这种方法. [EF Core迁移文档](Entity-Framework-Core-Migrations.md)描述了在这些情况下如何实现和管理**EF Core数据库迁移**. 这种方法同样适用于MongoDB,但你不需要处理数据库迁移问题. + +## 创建一个拥有自己数据库表/Collection的新实体 + +映射你的实体到依赖模块的**已存在的表**有一些缺点; + +* 你需要处理EF Core的**数据库迁移架构**. 需要特别注意迁移代码,特别是当你需要在实体间添加**关系**时. +* 你的应用程序数据库和模块数据库将是 **同一个物理数据库**. 通常需要时可以将模块数据库分开,但使用相同的表会对其进行限制. + +如果你想要使你的实体或模块定义的实体**低耦合**,那么可以创建自己的数据库表/collection并且将你的实体映射到自己的数据库表. + +在这种情况下你需要处理**同步问题**,尤其是你要**复制**相关实体的某些属性/字段时,有一些解决方案; + +* 如果你构建的是一个 **单体** 应用程序(或者在同一进程管理你的实体和依赖模块的实体),那么你可以使用[本地事件总线](Local-Event-Bus.md)监听实体更改. +* 如果你构建的是一个 **分布式** 系统,模块的实体和你的实体在不同的 进程/服务 管理(创建/更新/删除),那么你可以使用[分布式事件总线](Distributed-Event-Bus.md)订阅实体的更改事件. + +在你处理事件时,你可以在自己的数据库中更改自己的实体. + +### 订阅本地事件总线 + +[本地事件总线](Local-Event-Bus.md)系统是发布和订阅同一应用程序中发生的事件的方法. + +假设你想要获取 `IdentityUser` 实体的更改信息(创建,更改或删除). 你可以创建一个类实现 `ILocalEventHandler>` 接口. + +````csharp +public class MyLocalIdentityUserChangeEventHandler : + ILocalEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityChangedEventData eventData) + { + var userId = eventData.Entity.Id; + var userName = eventData.Entity.UserName; + //... + } +} +```` + +* `EntityChangedEventData` 涵盖了给定实体的创建,更新或删除事件. 如果你需要你可以分别订阅创建,更新或删除事件(在同一个类或不同的类中). +* 这里的代码在**本地事务之外执行**,因为它监听 `EntityChanged` 事件. 如果当前[工作单元](Unit-Of-Work.md)是事务性的,你可以订阅 `EntityChangingEventData` 事件,它在**同一本地(进行)事务**中执行事件处理. + +> 提醒:这些方法需要在包含处理类的同一进程中更改 `IdentityUser` 实体. 即使在集群环境(同一应用程序的多个实例在不同的服务器进行),它也完美工作. + +### 订阅分布式事件总线 + +[分布式事件总线](Distributed-Event-Bus.md)是在一个应用程序中发布事件,并在相同服务器或不同服务器运行的相同应用程序或不同应用程序中接收事件的方法. + +假设你想要获取 `IdentityUser` 实体的创建,更改或删除信息. 你可以像以下一样创建一个类: + +````csharp +public class MyDistributedIdentityUserChangeEventHandler : + IDistributedEventHandler>, + IDistributedEventHandler>, + IDistributedEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityCreatedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "created" event + } + } + + public async Task HandleEventAsync(EntityUpdatedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "updated" event + } + } + + public async Task HandleEventAsync(EntityDeletedEto eventData) + { + if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") + { + var userId = Guid.Parse(eventData.Entity.KeysAsString); + //...handle the "deleted" event + } + } +} +```` + +* 它实现了多个 `IDistributedEventHandler` 接口: **创建**,**更改**和**删除**,因为分布式事件总线单独发布事件,没有本地事件总线那样的"Changed"事件. +* 它订阅了 `EntityEto`, 这是一个通用的事件类,ABP框架针对所有类型的实体**自动发布**. 这就是为什么它检查**实体类型**(因为我们没有假设有对 `IdentityUser` 实体有安全的类型引用,所以它是字符串类型的). + +预构建应用模块没有定义专门的事件类型(如`IdentityUserEto` - "ETO" 意思是 "事件传输对象"). 此功能在路线图上([关注这个issue](https://github.com/abpframework/abp/issues/3033)),一旦完成后,你就可以订阅独立的实体类型: + +````csharp +public class MyDistributedIdentityUserCreatedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + public async Task HandleEventAsync(EntityCreatedEto eventData) + { + var userId = eventData.Entity.Id; + var userName = eventData.Entity.UserName; + //...handle the "created" event + } + + //... +} +```` + +* 这个处理程序只会在新用户创建时执行. + +> 唯一预定义的专门事件类是 `UserEto`, 你可以订阅 `EntityCreatedEto` 获取用户创建时的通知. 此事件也适用于身份模块. + +## 另请参阅 + +* [自定义已存在的模块](Customizing-Application-Modules-Guide.md) \ No newline at end of file diff --git a/docs/zh-Hans/Customizing-Application-Modules-Guide.md b/docs/zh-Hans/Customizing-Application-Modules-Guide.md new file mode 100644 index 0000000000..7e799c885d --- /dev/null +++ b/docs/zh-Hans/Customizing-Application-Modules-Guide.md @@ -0,0 +1,62 @@ +# 自定义现有模块 + +ABP框架提供的设计旨在支持构建完全[模块化的应用程序](Module-Development-Basics.md)和系统. 它还提供了一些可以在任何类型的应用程序中**使用**的[预构建应用模块](Modules/Index.md) + +例如,你可以在你的应用程序中**重用**[身份管理模块](Modules/Identity.md)去添加用户,角色和权限管理. [应用程序启动模板](Startup-Templates/Application.md)已经**预装**了Identity和其他模块. + +## 复用应用模块 + +你有两个选项去复用应用模块: + +### 添加包引用 + +你可以添加相关模块的 **NuGet** 和 **NPM** 包引用到你的应用程序,并配置模块(根据它的文档)集成到你的应用程序中. + +正如前面提到,[应用程序启动模板](Startup-Templates/Application.md)已经**预装了一些基本模块**,它引用模块的NuGet和NPM包. + +这种方法具有以下优点: + +* 你的解决方案会非常**干净**,只包含你**自己的应用程序代码**. +* 你可以**很简单的**升级模块到最新的可用模板. `abp update` [CLI](CLI.md) 命令会使更新变的更加简单. 通过这种方式, 你可以获得**最新功能和Bus修复**. + +然而有一个缺点: + +* 你可能无法**自定义**模块,因为模块源码没有在你的解决方案中. + +本文档介绍了 **或者自定义或扩展** 依赖模块并且无需更改其源码,尽快与更改完整的源码比起是有限的,但仍有一些好的方法可以自定义. + +如果你不认为自己会对预构建的模块进行重大更改,那么使用包引用的方法复用模块是推荐的方法. + +### 包含源码 + +如果你想要在预构建的模块上进行**重大**更改或添加**主要功能**,但是可用的扩展点不够使用,那么可以考虑直接使用依赖模块的源码. + +这种情况下,你通常**添加模块源码**到你的解决方案中,并将**包引用替换**为本地项目引用. **[ABP CLI](CLI.md)** 可以为你自动化这一过程. + +#### 分离模块解决方案 + +你可能不希望将模块源代码**直接包含在解决方案**中. 每个模块都包含十多个项目文件,添加**多个模块**会使解决方案变的臃肿可能还会影响**开发时的加载速度**,另外你可能有不同的开发团队维护不同模块. + +无论如何,你都可以为需要的模块创建**单独的解决方案**,将依赖模块做为解决方案中的项目引用. 比如在[abp仓库](https://github.com/abpframework/abp/),我们就是这样做的. + +> 我们看到的一个问题是Visual Studio在这种方式下不能很好的工作(解决方案目录之外对本地项目的引用不能很好地支持). 如果在开发过程中出错(对于外部模块),请在Visual Studio打开应用程序的解决方案后,在命令行运行 `dotnet restore`命令. + +#### 发布的自定义模块的包 + +一个备选方案是将重新打包模块的源代码(NuGet/NPM包),使用包引用. 你可以为公司使用本地私人的Nuget/NPM服务器. + +## 模块自定义/扩展途径 + +如果你决定使用预构建模块的NuGet/NPM包引用方式. 下面的文档详细解释了如何自定义/扩展现有模块的方法: + +* [扩展实体](Customizing-Application-Modules-Extending-Entities.md) +* [重写服务](Customizing-Application-Modules-Overriding-Services.md) +* [重写界面](Customizing-Application-Modules-Overriding-User-Interface.md) + +### 另请参阅 + +另外,请参阅以下文档: + +* 参阅 [本地化文档](Localization.md) 学习如何扩展已存在的本地化资源. +* 参阅 [设置文档](Settings.md) 学习如何更改依赖模块的设置定义. +* 参阅 [授权文档](Authorization.md) 学习如何更改依赖模块的权限定义. \ No newline at end of file diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md new file mode 100644 index 0000000000..cb40de9bdb --- /dev/null +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -0,0 +1,166 @@ +# 自定义应用模块: 重写服务 + +你可能想要**更改**依赖模块的**行为(业务逻辑)**. 在这种情况下,你可以使用[依赖注入](Dependency-Injection.md)的能力替换服务,控制器甚至页面模型到你自己的实现. + +注册到依赖注入的任何类,包括ABP框架的服务都可以被**替换**. + +你可以根据自己的需求使用不同的选项,下面的章节中将介绍这些选项. + +> 请注意,某些服务方法可能不是virtual,你可能无法override,我们会通过设计将其virtual,如果你发现任何方法不可以被覆盖,请[创建一个issue](https://github.com/abpframework/abp/issues/new)或者你直接修改后并发送**pull request**到GitHub. + +## 替换接口 + +如果给定的服务定义了接口,像 `IdentityUserAppService` 类实现了 `IIdentityUserAppService` 接口,你可以为这个接口创建自己的实现并且替换当前的实现. 例如: + +````csharp +public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +`MyIdentityUserAppService` 通过命名约定替换了 `IIdentityUserAppService` 的当前实现. 如果你的类名不匹配,你需要手动公开服务接口: + +````csharp +[ExposeServices(typeof(IIdentityUserAppService))] +public class TestAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +依赖注入系统允许为一个接口注册多个服务. 注入接口时会解析最后一个注入的服务. 显式的替换服务是一个好习惯. + +示例: + +````csharp +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IIdentityUserAppService))] +public class TestAppService : IIdentityUserAppService, ITransientDependency +{ + //... +} +```` + +使用这种方法, `IIdentityUserAppService` 接口将只会有一个实现. 也可以使用以下方法替换服务: + +````csharp +context.Services.Replace( + ServiceDescriptor.Transient() +); +```` + +你可以在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法编写替换服务代码. + +## 重写一个服务类 + +大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法。 + +### 示例: 重写服务方法 + +````csharp +[Dependency(ReplaceServices = true)] +public class MyIdentityUserAppService : IdentityUserAppService +{ + //... + public MyIdentityUserAppService( + IdentityUserManager userManager, + IIdentityUserRepository userRepository, + IGuidGenerator guidGenerator + ) : base( + userManager, + userRepository, + guidGenerator) + { + } + + public override async Task CreateAsync(IdentityUserCreateDto input) + { + if (input.PhoneNumber.IsNullOrWhiteSpace()) + { + throw new AbpValidationException( + "Phone number is required for new users!", + new List + { + new ValidationResult( + "Phone number can not be empty!", + new []{"PhoneNumber"} + ) + } + ); } + + return await base.CreateAsync(input); + } +} +```` + +示例中**重写**了 `IdentityUserAppService` [应用程序](Application-Services.md) `CreateAsync` 方法检查手机号码. 然后调用了基类方法继续**基本业务逻辑**. 通过这种方法你可以在基本业务逻辑**之前**和**之后**执行其他业务逻辑. + +你也可以完全**重写**整个业务逻辑去创建用户,而不是调用基类方法. + +### 示例: 重写领域服务 + +````csharp +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IdentityUserManager))] +public class MyIdentityUserManager : IdentityUserManager +{ + public MyIdentityUserManager( + IdentityUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider + ) : base( + store, + optionsAccessor, + passwordHasher, + userValidators, + passwordValidators, + keyNormalizer, + errors, + services, + logger, + cancellationTokenProvider) + { + } + + public override async Task CreateAsync(IdentityUser user) + { + if (user.PhoneNumber.IsNullOrWhiteSpace()) + { + throw new AbpValidationException( + "Phone number is required for new users!", + new List + { + new ValidationResult( + "Phone number can not be empty!", + new []{"PhoneNumber"} + ) + } + ); + } + + return await base.CreateAsync(user); + } +} +```` + +示例中类继承了 `IdentityUserManager` [领域服务](Domain-Services.md),并且重写了 `CreateAsync` 方法进行了与之前相同的手机号码检查. 结果也是一样的,但是这次我们在领域服务实现了它,假设这是我们系统的**核心领域逻辑**. + +> 这里需要 `[ExposeServices(typeof(IdentityUserManager))]` attribute,因为 `IdentityUserManager` 没有定义接口 (像 `IIdentityUserManager`) ,依赖注入系统并不会按照约定公开继承类的服务(如已实现的接口). + +参阅[本地化系统](Localization.md)了解如何自定义错误消息. + +### 重写其他服务 + +控制器,框架服务,视图组件类以及其他类型注册到依赖注入的类都可以像上面的示例那样被重写. + +## 如何找到服务? + +[模块文档](Modules/Index.md) 包含了定义的主要服务列表. 另外 你也可以查看[源码](https://github.com/abpframework/abp/tree/dev/modules)找到所有的服务. \ No newline at end of file diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-User-Interface.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-User-Interface.md new file mode 100644 index 0000000000..eed56807ea --- /dev/null +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-User-Interface.md @@ -0,0 +1,6 @@ +# 重写用户界面 + +你可以想要重写页面,组件,JavaScript,CSS或你依赖模块的图片文件. 重写UI取决于你使用的UI框架. 选择UI框架以继续: + +* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) +* [Angular](UI/Angular/Customization-User-Interface.md) \ No newline at end of file diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md index 9c3066fef4..96d75895d2 100644 --- a/docs/zh-Hans/Entities.md +++ b/docs/zh-Hans/Entities.md @@ -1,34 +1,80 @@ -## 实体 +# 实体 实体是DDD(Domain Driven Design)中核心概念.Eric Evans是这样描述实体的 "一个没有从其属性,而是通过连续性和身份的线索来定义的对象" 实体通常映射到关系型数据库的表中. -### 实体类 +## 实体类 实体都继承自`Entity`类,如下所示: ```C# -public class Person : Entity +public class Book : Entity { public string Name { get; set; } - public DateTime CreationTime { get; set; } + public float Price { get; set; } +} +``` + +> 如果你不想继承基类`Entity`,也可以直接实现`IEntity`接口 + +`Entity`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`Guid`类型.可以是其他类型如`string`, `int`, `long`或其他你需要的类型. - public Person() +### Guid主键的实体 + +如果你的实体Id类型为 `Guid`,有一些好的实践可以实现: + +* 创建一个构造函数,获取ID作为参数传递给基类. + * 如果没有为GUID Id斌值,ABP框架会在保存时设置它,但是在将实体保存到数据库之前最好在实体上有一个有效的Id. +* 如果使用带参数的构造函数创建实体,那么还要创建一个 `protected` 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它. +* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中使用[`IGuidGenerator`服务](Guid-Generation.md)传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要. + +示例实体: + +````csharp +public class Book : Entity +{ + public string Name { get; set; } + public float Price { get; set; } + protected Book() + { + } + public Book(Guid id) + : base(id) { - CreationTime = DateTime.Now; } } -``` +```` -> 如果你不想继承基类`Entity`,也可以直接实现`IEntity`接口 +在[应用服务](Application-Services.md)中使用示例: -`Entity`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`int`类型.可以是其他类型如`string`, `Guid`, `long`或其他你需要的类型. +````csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; + public BookAppService(IRepository bookRepository) + { + _bookRepository = bookRepository; + } + public async Task CreateAsync(CreateBookDto input) + { + await _bookRepository.InsertAsync( + new Book(GuidGenerator.Create()) + { + Name = input.Name, + Price = input.Price + } + ); + } +} +```` -实体类还重写了 **equality** 运算符(==),以方便地检查两个实体是否相等(如果它们是相同的类型并且它们的Id相等,则它们是相等的). +* `BookAppService` 注入图书实体的默认[仓库](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中. +* `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md). +* 如果您想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. -#### 具有复合键的实体 +### 具有复合键的实体 有些实体可能需要 **复合键** .在这种情况下,可以从非泛型`Entity`类派生实体.如: @@ -53,30 +99,30 @@ public class UserRole : Entity } ```` -上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键. - -具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法. +上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键. 具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法. -你还需要在 **object-to-relational mapping**(ORM)中配置实体的键. +> 你还需要在**对象关系映射**(ORM)中配置实体的键. 参阅[Entity Framework Core](Entity-Framework-Core.md)集成文档查看示例. -> 复合主键在仓储中有限制.由于不知道Id属性,所以对于这些实体,不能使用`IRepository`.但是,可以使用`IRepository`.更多信息请参见[仓储](Repositories.md)的文档. +> 需要注意,复合主键实体不可以使用 `IRepository` 接口,因为它需要一个唯一的Id属性. 但你可以使用 `IRepository`.更多信息请参见[仓储](Repositories.md)的文档. -### 聚合根 +## 聚合根 "*聚合是域驱动设计中的一种模式.DDD的聚合是一组可以作为一个单元处理的域对象.例如,订单及订单系列的商品,这些是独立的对象,但将订单(连同订单系列的商品)视为一个聚合通常是很有用的*"( [查看详细介绍](http://martinfowler.com/bliki/DDD_Aggregate.html)) -`AggregateRoot`类继承自`Entity`类,所以默认有`Id`这个属性 +`AggregateRoot`类继承自`Entity`类,所以默认有`Id`这个属性 > 值得注意的是 ABP 会默认为聚合根创建仓储,当然,ABP也可以为所有的实体创建仓储,详情参见[仓储](Repositories.md). -ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现DDD并且创建聚合根,这里有一些最佳实践仅供参考: +ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现[领域驱动设计](Domain-Driven-Design.md)并且创建聚合根,这里有一些最佳实践仅供参考: * 聚合根需要维护自身的完整性,所有的实体也是这样.但是聚合根也要维护子实体的完整性.所以,聚合根必须一直有效. * 使用Id引用聚合根,而不使用导航属性 * 聚合根被视为一个单元.它是作为一个单元检索和更新的.它通常被认为是一个交易边界. * 不单独修改聚合根中的子实体 -#### 聚合根例子 +如果你想在应用程序中实现DDD,请参阅[实体设计最佳实践指南](Best-Practices/Entities.md). + +### 聚合根例子 这是一个具有子实体集合的聚合根例子: @@ -155,6 +201,11 @@ public class OrderLine : Entity { Count = newCount; } + + public override object[] GetKeys() + { + return new Object[] {OrderId, ProductId}; + } } ```` @@ -169,8 +220,169 @@ public class OrderLine : Entity * `Order.AddProduct`实现了业务规则将商品添加到订单中 * 所有属性都有`protected`的set.这是为了防止实体在实体外部任意改变.因此,在没有向订单中添加新产品的情况下设置 `TotalItemCount`将是危险的.它的值由`AddProduct`方法维护. -ABP不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则. +ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则. -#### 带有组合键的聚合根 +### 带有组合键的聚合根 虽然这种聚合根并不常见(也不建议使用),但实际上可以按照与上面提到的跟实体相同的方式定义复合键.在这种情况下,要使用非泛型的`AggregateRoot`基类. + +## 基类和接口的审计属性 + +有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**. + +### 审计接口 + +有很多的审计接口,你可以实现一个你需要的那个. + +> 虽然可以手动实现这些接口,但是可以使用下一节中定义的**基类**简化代码. + +* `IHasCreationTime` 定义了以下属性: + * `CreationTime` +* `IMayHaveCreator` 定义了以下属性: + * `CreatorId` +* `ICreationAuditedObject` 继承 `IHasCreationTime` 和 `IMayHaveCreator`, 所以它定义了以下属性: + * `CreationTime` + * `CreatorId` +* `IHasModificationTime` 定义了以下属性: + * `LastModificationTime` +* `IModificationAuditedObject` 扩展 `IHasModificationTime` 并添加了 `LastModifierId` 属性. 所以它定义了以下属性: + * `LastModificationTime` + * `LastModifierId` +* `IAuditedObject` 扩展 `ICreationAuditedObject` 和 `IModificationAuditedObject`, 所以它定义了以下属性: + * `CreationTime` + * `CreatorId` + * `LastModificationTime` + * `LastModifierId` +* `ISoftDelete` (参阅 [数据过滤文档](Data-Filtering.md)) 定义了以下属性: + * `IsDeleted` +* `IHasDeletionTime` 扩展 `ISoftDelete` 并添加了 `DeletionTime` 属性. 所以它定义了以下属性: + * `IsDeleted` + * `DeletionTime` +* `IDeletionAuditedObject` 扩展 `IHasDeletionTime` 并添加了 `DeleterId` 属性. 所以它定义了以下属性: + * `IsDeleted` + * `DeletionTime` + * `DeleterId` +* `IFullAuditedObject` 继承 `IAuditedObject` 和 `IDeletionAuditedObject`, 所以它定义了以下属性: + * `CreationTime` + * `CreatorId` + * `LastModificationTime` + * `LastModifierId` + * `IsDeleted` + * `DeletionTime` + * `DeleterId` + +当你实现了任意接口,或者从下一节定义的类派生,ABP框架就会尽可能地自动管理这些属性. + +> 实现 `ISoftDelete` , `IDeletionAuditedObject` 或 `IFullAuditedObject` 让你的实体**软删除**. 参阅[数据过滤文档](Data-Filtering.md),了解软删除模式. + +### 审计基类 + +虽然可以手动实现以上定义的任何接口,但建议从这里定义的基类继承: + +* `CreationAuditedEntity` 和 `CreationAuditedAggregateRoot` 实现了 `ICreationAuditedObject` 接口. +* `AuditedEntity` 和 `AuditedAggregateRoot` 实现了 `IAuditedObject` 接口. +* `FullAuditedEntity` and `FullAuditedAggregateRoot` 实现了 `IFullAuditedObject` 接口. + +所有这些基类都有非泛型版本,可以使用 `AuditedEntity` 和 `FullAuditedAggregateRoot` 来支持复合主键; + +所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式). + +## 额外的属性 + +ABP定义了 `IHasExtraProperties` 接口,可以由实体实现,以便能够动态地设置和获取的实体属性. `AggregateRoot` 基类已经实现了 `IHasExtraProperties` 接口. 如果你从这个类(或者上面定义的一个相关审计类)派生,那么你可以直接使用API​. + +### GetProperty 和 SetProperty 扩展方法 + +这些扩展方法是获取和设置实体数据的推荐方法. 例: + +````csharp +public class ExtraPropertiesDemoService : ITransientDependency +{ + private readonly IIdentityUserRepository _identityUserRepository; + + public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository) + { + _identityUserRepository = identityUserRepository; + } + + public async Task SetTitle(Guid userId, string title) + { + var user = await _identityUserRepository.GetAsync(userId); + + //SET A PROPERTY + user.SetProperty("Title", title); + + await _identityUserRepository.UpdateAsync(user); + } + + public async Task GetTitle(Guid userId) + { + var user = await _identityUserRepository.GetAsync(userId); + + //GET A PROPERTY + return user.GetProperty("Title"); + } +} +```` + +* 属性的**值是object**,可以是任何类型的对象(string,int,bool...等). +* 如果给定的属性未设置值, `GetProperty` 方法会返回 `null`. +* 你可以使用不同的**属性名称**(如这里的`Title`)同时存储多个属性. + +最好为属性名**定义一个常量**防止拼写错误. 最佳方式是定义**扩展方法**来利用智能感知. 例: + +````csharp +public static class IdentityUserExtensions +{ + private const string TitlePropertyName = "Title"; + + public static void SetTitle(this IdentityUser user, string title) + { + user.SetProperty(TitlePropertyName, title); + } + + public static string GetTitle(this IdentityUser user) + { + return user.GetProperty(TitlePropertyName); + } +} +```` + +然后你可以直接使用 `IdentityUser` 对象的 `user.SetTitle("...")` 和 `user.GetTitle()`. + +### HasProperty 和 RemoveProperty 扩展方法 + +* `HasProperty` 用于检查对象是否设置了属性. +* `RemoveProperty` 用于从对象中删除属性. 你可以使用它来替代设置 `null` 值. + +### 它是如何实现的? + +`IHasExtraProperties` 要求实现类定义一个名称为 `ExtraProperties` 的`Dictionary` 属性. + +所以,如果你需要你可以直接使用 `ExtraProperties` 属性来使用字典API,但是推荐使用 `SetProperty` 和 `GetProperty` 方法,因为它们会检查 `null` 值. + +#### 它是如何存储的? + +存储字典的方式取决于你使用的数据库提供程序. + +* 对于 [Entity Framework Core](Entity-Framework-Core.md),这是两种类型的配置; + * 默认它以 `JSON` 字符串形式存储在 `ExtraProperties` 字段中. 序列化到 `JSON` 和反序列化到 `JSON` 由ABP使用EF Core的[值转换](https://docs.microsoft.com/zh-cn/ef/core/modeling/value-conversions)系统自动完成. + * 如果需要,你可以使用 `ObjectExtensionManager` 为所需的额外属性定义一个单独的数据库字段. 那些使用 `ObjectExtensionManager` 配置的属性继续使用单个 `JSON` 字段. 当你使用预构建的[应用模块](Modules/Index.md)并且想要[扩展模块的实体](Customizing-Application-Modules-Extending-Entities.md). 参阅[EF Core迁移文档](Entity-Framework-Core.md)了解如何使用 `ObjectExtensionManager`. +* 对于 [MongoDB](MongoDB.md), 它以 **常规字段** 存储, 因为 MongoDB 天生支持这种 [额外](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) 系统. + +### 讨论额外的属性 + +如果你使用**可重复使用的模块**,其中定义了一个实体,你想使用简单的方式get/set此实体相关的一些数据,那么额外的属性系统是非常有用的. +你通常 **不需要** 为自己的实体使用这个系统,是因为它有以下缺点: + +* 它不是**完全类型安全的**,因为它使用字符串用作属性名称. +* 这些属性**不容易[自动映射](Object-To-Object-Mapping.md)到其他对象**. +* 它**不会**为EF Core在数据库表中**创建字段**,因此在数据库中针对这个字段创建索引或搜索/排序并不容易. + +### 额外属性背后的实体 + +`IHasExtraProperties` 不限于与实体一起使用. 你可以为任何类型的类实现这个接口,使用 `GetProperty`,`SetProperty` 和其他相关方法. + +## 另请参阅 + +* [实体设计最佳实践指南](Best-Practices/Entities.md) \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md new file mode 100644 index 0000000000..5da30614dd --- /dev/null +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -0,0 +1,881 @@ + +# EF Core数据库迁移 + +本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论您可能希望为自己的应用程序实现的**各种场景**. + +> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md). + +### 源码 + +你可以在[这里](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo)找到本文引用的示例项目的源代码; 但是为了理解示例项目的源代码,你需要阅读和理解这个文档. + +## 关于EF Core 代码优先迁移 + +Entity Framework Core 提供了一种简单强大[数据库迁移系统](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). ABP框架[启动模板](Startup-Templates/Index.md)使用这个系统,让你以标准的方式开发你的应用程序. + +但是EF Core迁移系统在**模块化环境中不是很好**,在模块化环境中,每个模块都维护**自己的数据库模式**,而实际上两个或多个模块可以**共享一个数据库**. + +由于ABP框架在所有方面都关心模块化,所以它为这个问题提供了**解决方案**. 如果你需要**自定义数据库结构**,那么应当了解这个解决方案. + +> 参阅[EF Core文档](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)充分了解EF Core Code First迁移,以及为什么需要这样的系统. + +## 默认解决方案与数据库配置 + +当你[创建一个新的Web应用程序](https://abp.io/get-started)(使用EF Core,它是默认的数据库提供程序),你的解决方案结构类似下图: + +![bookstore-visual-studio-solution-v3](images/bookstore-visual-studio-solution-v3.png) + +实际的解决方案结构可能会根据你的偏好有所不同,但是数据库部分是相同的. + +> 本文档将使用 `Acme.BookStore` 示例项目名称来引用项目和类. 你需要在解决方案中找到相应的类/项目. + +### 数据库模式 + +启动模板已预安装了一些[应用程序模块](Modules/Index.md). 解决方案的每一层都有相应的模块包引用. 所以 `.EntityFrameworkCore` 项目含有使用 `EntityFrameworkCore` 模块的Nuget的引用: + +![bookstore-efcore-dependencies](images/bookstore-efcore-dependencies.png) + +通过这种方式,你可以看到所有的 `.EntityFrameworkCore` 项目下的**EF Core的依赖**. + +> 除了模块引用之外,它还引用了 `Volo.Abp.EntityFrameworkCore.SqlServer` 包,因为启动模板预配置的是Sql Server. 参阅文档了解如何[切换到其它DBMS](Entity-Framework-Core-Other-DBMS.md). + +虽然每个模块在设计上有自己的 `DbContext` 类,并且可以使用其自己的**物理数据库**,但解决方案的配置是使用**单个共享数据库**如下图所示: + +![single-database-usage](images/single-database-usage.png) + +这是**最简单的配置**,适用于大部分的应用程序. `appsettings.json` 文件有名为`Default`**单个连接字符串**: + +````json +"ConnectionStrings": { + "Default": "..." +} +```` + +所以你有一个**单一的数据库模式**,其中包含**共享**此数据库的模块的所有表. + +ABP框架的[连接字符串](Connection-Strings.md)系统允许你轻松为所需的模块**设置不同的连接字符串**: + +````json +"ConnectionStrings": { + "Default": "...", + "AbpAuditLogging": "..." +} +```` + +示例配置告诉ABP框架[审计日志模块](Modules/Audit-Logging.md)应使用第二个连接字符串. + +**然而这仅仅只是开始**. 你还需要创建第二个数据库以及里面审计日志表并使用code frist迁移的方法维护数据库表. 本文档的主要目的之一就是指导你了解这样的数据库分离场景. + +#### 模块表 + +每个模块都使用**自己的数据库表**. 例如[身份模块](Modules/Identity.md)有一些表来管理系统中的用户和角色. + +##### 表前缀 + +由于所有模块都允许共享一个数据库(这是默认配置),所以模块通常使用**表前缀**来对自己的表进行分组. + +基础模块(如[身份](Modules/Identity.md), [租户管理](Modules/Tenant-Management.md) 和 [审计日志](Modules/Audit-Logging.md))使用 `Abp` 前缀, 其他的模块使用自己的前缀. 如[Identity Server](Modules/IdentityServer.md) 模块使用前缀 `IdentityServer`. + +如果你愿意,你可以为你的应用程序的模块**更改数据库表前缀**. +例: + +````csharp +Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; +```` + +这段代码更改了[Identity Server](Modules/IdentityServer.md)的前缀. 在应用程序的**最开始**编写这段代码. + +> 每个模块还定义了 `DbSchema` 属性,你可以在支持schema的数据库中使用它. + +### 项目 + +从数据库的角度来看.有三个重要的项目将在下一节中解释. + +#### .EntityFrameworkCore 项目 + +这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ). + +**每个模块都使用自己的 `DbContext` 类**来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: + +````csharp +[ConnectionStringName("Default")] +public class BookStoreDbContext : AbpDbContext +{ + public DbSet Users { get; set; } + + /* Add DbSet properties for your Aggregate Roots / Entities here. */ + + public BookStoreDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Configure the shared tables (with included modules) here */ + + builder.Entity(b => + { + //Sharing the same Users table with the IdentityUser + b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); + + b.ConfigureByConvention(); + b.ConfigureAbpUser(); + + /* Configure mappings for your additional properties + * Also see the MyProjectNameEntityExtensions class + */ + }); + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + builder.ConfigureBookStore(); + } +} +```` + +这个简单的 `DbContext` 类仍然需要一些解释: + +* 它定义了一个 `[connectionStringName]` Attribute,它告诉ABP始终为此 `Dbcontext` 使用 `Default` 连接字符串. +* 它从 `AbpDbContext` 而不是标准的 `DbContext` 类继承. 你可以参阅[EF Core集成](Entity-Framework-Core.md)文档了解更多. 现在你需要知道 `AbpDbContext` 基类实现ABP框架的一些约定,为你自动化一些常见的任务. +* 它为 `AppUser` 实体定义了 `DbSet` 属性. `AppUser` 与[身份模块](Modules/Identity.md)的 `IdentityUser` 实体共享同一个表(默认名为 `AbpUsers`). 启动模板在应用程序中提供这个实体,因为我们认为用户实体一般需要应用程序中进行定制. +* 构造函数接受一个 `DbContextOptions` 实例. +* 它覆盖了 `OnModelCreating` 方法定义EF Core 映射. + * 首先调用 `base.OnModelCreating` 方法让ABP框架为我们实现基础映射. + * 然后它配置了 `AppUser` 实体的映射. 这个实体有一个特殊的情况(它与Identity模块共享一个表),在下一节中进行解释. + * 最后它调用 `builder.ConfigureBookStore()` 扩展方法来配置应用程序的其他实体. + +在介绍其他数据库相关项目之后,将更详细地说明这个设计. + +#### .EntityFrameworkCore.DbMigrations 项目 + +正如前面所提到的,每个模块(和你的应用程序)有**它们自己**独立的 `DbContext` 类. 每个 `DbContext` 类只定义了自身模块的实体到表的映射,每个模块(包括你的应用程序)在**运行时**都使用相关的 `DbContext` 类. + +如你所知,EF Core Code First迁移系统依赖于 `DbContext` 类来跟踪和生成Code First迁移. 那么我们应该使用哪个 `DbContext` 进行迁移? 答案是它们都不是. `.EntityFrameworkCore.DbMigrations` 项目中定义了另一个 `DbContext` (示例解决方案中的 `BookStoreMigrationsDbContext`). + +##### MigrationsDbContext + +`MigrationsDbContext` 仅用于创建和应用数据库迁移. **不在运行时使用**. 它将所有使用的模块的所有实体到表的映射以及应用程序的映射**合并**. + +通过这种方式你可以创建和维护**单个数据库迁移路径**. 然而这种方法有一些困难,接下来的章节将解释ABP框架如何克服这些困难. 首先以 `BookStoreMigrationsDbContext` 类为例: + +````csharp +/* This DbContext is only used for database migrations. + * It is not used on runtime. See BookStoreDbContext for the runtime DbContext. + * It is a unified model that includes configuration for + * all used modules and your application. + */ +public class BookStoreMigrationsDbContext : AbpDbContext +{ + public BookStoreMigrationsDbContext( + DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentity(); + builder.ConfigureIdentityServer(); + builder.ConfigureFeatureManagement(); + builder.ConfigureTenantManagement(); + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + builder.ConfigureBookStore(); + } +} +```` + +##### 共享映射代码 + +第一个问题是: 一个模块使用自己的 `DbContext` 这就需要到数据库的映射. 该 `MigrationsDbContext` 也需要相同的映射创建此模块的数据库表. 我们绝对**不希望复制**的映射代码. + +解决方案是定义一个**扩展方法**(在`ModelBuilder`)由两个 `DbContext` 类调用. 所以每个模块都定义了这样的扩展方法. + +例如 `builder.ConfigureBackgroundJobs()` 方法调用[后台作业模块](Modules/Background-Jobs.md)配置数据库表. 扩展方法的定义如下: + +````csharp +public static class BackgroundJobsDbContextModelCreatingExtensions +{ + public static void ConfigureBackgroundJobs( + this ModelBuilder builder, + Action optionsAction = null) + { + var options = new BackgroundJobsModelBuilderConfigurationOptions( + BackgroundJobsDbProperties.DbTablePrefix, + BackgroundJobsDbProperties.DbSchema + ); + + optionsAction?.Invoke(options); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); + + b.ConfigureCreationTime(); + b.ConfigureExtraProperties(); + + b.Property(x => x.JobName) + .IsRequired() + .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); + + //... + }); + } +} +```` + +此u还获取选项用于更改此模块的数据库表前缀和模式,但在这里并不重要. + +最终的应用程序在 `MigrationsDbContext` 类中调用扩展方法, 因此它可以确定此 `MigrationsDbContext` 维护的数据库中包含哪些模块. 如果要创建第二个数据库并将某些模块表移动到第二个数据库,则需要有第二个`MigrationsDbContext` 类,该类仅调用相关模块的扩展方法. 下一部分将详细介绍该主题. + +同样 `ConfigureBackgroundJobs` 方法也被后台作业模块的 `DbContext` 调用: + +````csharp +[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)] +public class BackgroundJobsDbContext + : AbpDbContext, IBackgroundJobsDbContext +{ + public DbSet BackgroundJobs { get; set; } + + public BackgroundJobsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + //Reuse the same extension method! + builder.ConfigureBackgroundJobs(); + } +} +```` + +通过这种方式,可以在 `DbContext` 类之间共享模块的映射配置. 上面的代码在相关的模块NuGet包中,所以你不需要关心它. + +##### 重用模块的表 + +您可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择: + +1. 你可以**直接使用模块定义的实体**(你仍然可以在某种程度上[扩展实体](Customizing-Application-Modules-Extending-Entities.md)). +2. 你可以**创建一个新的实体**映射到同一个数据库表。 + +###### 使用由模块定义的实体 + +使用实体定义的模块有标准用法非常简单. 例如身份模块定义了 `IdentityUser` 实体. 你可以为注入 `IdentityUser` 仓储,为此实体执行标准仓储操作. +例: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Identity; + +namespace Acme.BookStore +{ + public class MyService : ITransientDependency + { + private readonly IRepository _identityUserRepository; + + public MyService(IRepository identityUserRepository) + { + _identityUserRepository = identityUserRepository; + } + + public async Task DoItAsync() + { + //Get all users + var users = await _identityUserRepository.GetListAsync(); + } + } +} +```` + +示例注入了 `IRepository`(默认仓储). 它定义了标准的存储库方法并实现了 `IQueryable` 接口. + +另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. + +###### 创建一个新的实体 + +如果你只是按原样使用实体,那么使用模块的实体是很容易的. 但是在下面的情况下你可能需要定义自己的实体并映射到模块实体相同的数据库表. + +* 你想要**添加一个新的字段**到表中并将其映射到实体的属性. 你无法使用模块的实体,因为它没有相关的属性. +* 你想要**使用的表字段的一个子集**. 你不希望访问实体的所有属性并隐藏不相关的属性(从安全的角度或仅出于设计目的). +* 你不希望直接**依赖于**模块实体类. + +无论哪一种情况,流程都是一样的. 假如你想要创建一个名为 `AppRole` 的实体,将其映射到与[身份模块](Modules/Identity.md)的 `IdentityRole` 实体相同的表 + +在这里我们将介绍实现方式, 然后**将讨论这种方法的局限性**. + +首先,在你的 `.Domain` 项目一个新的 `AppRole` 类: + +````csharp +using System; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace Acme.BookStore.Roles +{ + public class AppRole : AggregateRoot, IMultiTenant + { + // Properties shared with the IdentityRole class + + public Guid? TenantId { get; private set; } + public string Name { get; private set; } + + //Additional properties + + public string Title { get; set; } + + private AppRole() + { + + } + } +} +```` + +* 它继承了[`AggregateRoot`类](Entities.md)和实现了[`IMultiTenant`]接口(Multi-Tenancy.md),因为 `IdentityRole` 也做了同样的继承. +* 你可以添加 `IdentityRole` 实体定义的任何属性. 本例只加了 `TenantId` 和 `Name` 属性,因为我们这里只需要它们. 你可以把setters设置为私有(如同本例)以防意外更改身份模块的属性. +* 你可以添加自定义(附加)属性. 本例添加了 `Title` 属性. +* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体。创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制). + +现在是时候定义EF Core映射. 打开应用程序的 `DbContext` (此示例中是 `BookStoreDbContext` )添加以下属性: + +````csharp +public DbSet Roles { get; set; } +```` + +然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后): + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Configure the shared tables (with included modules) here */ + + //CONFIGURE THE AppRole ENTITY + builder.Entity(b => + { + b.ToTable("AbpRoles"); + b.ConfigureByConvention(); + b.Property(x => x.Title).HasMaxLength(128); + }); + + ... + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + + builder.ConfigureBookStore(); +} +```` + +我们添加了以下几行: + +````csharp +builder.Entity(b => +{ + b.ToTable("AbpRoles"); + b.ConfigureByConvention(); + b.Property(x => x.Title).HasMaxLength(128); +}); +```` + +* 它映射到 `AbpRoles` 表,与 `IdentityRole` 实体共享. +* `ConfigureByConvention()` 配置了标准/基本属性(像`TenantId`),建议总是调用它. + +你已经为你的 `DbContext` 配置自定义属性,该属性在应用程序运行时使用. +与其直接更改 `MigrationsDbContext`,我们应该使用ABP框架的实体扩展系统,在解决方案的 `.EntityFrameworkCore` 项目中找到 `YourProjectNameEntityExtensions` 类(本示例中是 `BookStoreEntityExtensions`)并且进行以下更改: + +````csharp +public static class MyProjectNameEntityExtensions +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance + .MapEfCoreProperty( + "Title", + builder => { builder.HasMaxLength(64); } + ); + }); + } +} +```` + +> 我们建议使用 `nameof(AppRole.Title)` 而不是硬编码 "Title" 字符串 + +`ObjectExtensionManager` 用于添加属性到现有的实体. 由于 `ObjectExtensionManager.Instance` 是静态实例(单例),因此应调用一次. `OneTimeRunner` 是ABP框架定义简单的工具类. + +参阅[EF Core集成文档](Entity-Framework-Core.md)了解更多关于实体扩展系统. + +我们在两个类中都重复了类似的数据库映射代码,例如 `HasMaxLength(128)`. + +现在你可以在包管理控制台(记得选择 `.EntityFrameworkCore.DbMigrations` 做为PMC的默认项目并将 `.Web` 项目设置为启动项目)使用标准的 `Add-Migration` 命令添加一个新的EF Core数据库迁移. + +![pmc-add-migration-role-title](images/pmc-add-migration-role-title.png) + +命令将创建一个新的Code First迁移类,如下所示: + +````csharp +public partial class Added_Title_To_Roles : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Title", + table: "AbpRoles", + maxLength: 128, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Title", + table: "AbpRoles"); + } +} +```` + +全部完成,只运行在PMC运行`Update-Database` 命令或运行你解决方案中的`.DbMigrator` 项目应用数据库更改. + +现在你可以像使用应用程序的其他实体一样使用 `AppRole` 实体. 下面是查询和更新角色的[应用服务](Application-Services.md). +示例: + +````csharp +public class AppRoleAppService : ApplicationService, IAppRoleAppService +{ + private readonly IRepository _appRoleRepository; + + public AppRoleAppService(IRepository appRoleRepository) + { + _appRoleRepository = appRoleRepository; + } + + public async Task> GetListAsync() + { + var roles = await _appRoleRepository.GetListAsync(); + + return roles + .Select(r => new AppRoleDto + { + Id = r.Id, + Name = r.Name, + Title = r.Title + }) + .ToList(); + } + + public async Task UpdateTitleAsync(Guid id, string title) + { + var role = await _appRoleRepository.GetAsync(id); + + role.Title = title; + + await _appRoleRepository.UpdateAsync(role); + } +} +```` + +创建新实体并将其映射到依赖模块的表存在一些**限制**: + +* 你的**自定义属性必须是可空的**. 例如 `AppRole.Title` 在这里是可空的. 否则身份模块会抛出异常,因为它在向数据库插入新角色时不知道如何如何填充标题. +* 好的做法是,你不应更新**模块定义的属性**,尤其是在需要业务逻辑的情况下. 通常你可以管理自己(自定义)的属性. + +##### 替代方法 + +你可以使用以下方法替代创建实体添加自定义属性. + +###### 使用ExtraProperties + +所有从 `AggregateRoot` 派生的实体都可以在 `ExtraProperties` 属性(因为它们都实现了 `IHasExtraProperties` 接口)中存储键值对, 它是 `Dictionary` 类型在数据库中被序列化为JSON. 所以你可以在字典中添加值用于查询,无需更改实体. + +例如你可以将查询属性 `Title` 存储在 `IdentityRole` 中,而不是创建一个新的实体. +例: + +````csharp +public class IdentityRoleExtendingService : ITransientDependency +{ + private readonly IIdentityRoleRepository _identityRoleRepository; + + public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) + { + _identityRoleRepository = identityRoleRepository; + } + + public async Task GetTitleAsync(Guid id) + { + var role = await _identityRoleRepository.GetAsync(id); + return role.GetProperty("Title"); + } + + public async Task SetTitleAsync(Guid id, string newTitle) + { + var role = await _identityRoleRepository.GetAsync(id); + role.SetProperty("Title", newTitle); + await _identityRoleRepository.UpdateAsync(role); + } +} +```` + +* `GetProperty` 和 `SetProperty` 方法是在 `role.ExtraProperties` 词典中获取和设置值的简化写法,推荐使用. + +通过这种方式你可以轻松任何类型的值附加到依赖模块的实体. 不过这种方法也有一些缺点: + +* 所有的额外属性都存储在数据库中的一个**JSON对象**,它们不是作为表的字段存储,与简单的表字段相比创建索引和针对此属性使用SQL查询将更加困难. +* 属性名称是字符串,他们**不是类型安全的**. 建议这些类型的属性定义常量,以防止拼写错误. + +###### 使用实体扩展系统 + +实体扩展系统解决了额外属性主要的问题: 它可以将额外属性做为**标准表字段**存储到数据库. + +你需要做的就是如上所诉使用 `ObjectExtensionManager` 定义额外属性, 然后你就可以使得 `GetProperty` 和 `SetProperty` 方法对实体的属性进行get/set,但是这时它存储在数据库表的单独字段中. + +###### 创建新表 + +你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `Name` 字段添加到你自己的表中,它是原表中 `Name` 字段的副本. + +在这种情况下你不需要处理迁移问题,但是需要处理数据复制问题. 当重复的值发生变化时,你应该在表中同步相同的变化. 你可以使用本地或分布式[事件总线](Event-Bus.md)订阅原始实体的更改事件. 这是根据来自另一个微服务的数据推荐的方法,特别是如果它们有单独的物理数据库(你可以在网络中搜索关于微服务设计的数据共享,这是一个广泛的主题). + +#### 讨论另一种场景:每个模块管理自己的迁移路径 + +上面提到, `.EntityFrameworkCore.DbMigrations` 合并所有模块的所有数据库映射(加上应用程序映射)创建统一的迁移路径. + +另一种方法是允许每个模块拥有自己的迁移来维护其数据库表. 虽然它在一开始看起来更模块化,但它有一些重要的缺点: + +* **EF Core迁移系统依赖于数据库DBMS提供程序**. 例如,如果一个模块为SQL Server创建迁移,那么你就不能使用MySQL使用这个迁移代码,对于模块而言为所有可用的DBMS提供程序维护迁移是不切实际的. 将迁移留给应用程序代码(如本文档中所解释的),你就可以在**应用程序代码中选择DBMS**. +* 在模块之间 **共享表** 或在应用程序中 **重用模块的表** 会更加困难. 因为EF Core迁移系统不能处理它,并会抛出异常,例如"数据库中已存在表XXX". +* **自定义/增强** 映射和生成迁移代码会更加困难. +* 当你使用多个模块时更难跟踪和**应用数据库更改**. + +## 使用多个数据库 + +默认的启动模板被组织为所有模块和应用程序使用单个数据库. 但是ABP框架和所有预构建的模块都被设计为**可以使用多个数据库**. 每个模块可以使用自己的数据库,也可以将模块分组到几个数据库中. + +本节将说明如何将审计日志,设置管理和权限管理模块表移至**第二个数据库**,而其余模块继续使用主("Default")数据库. + +结构如下图所示: + +![single-database-usage](images/multiple-database-usage.png) + +### 更改连接字符串部分 + +第一步是更改所有 `appsettings.json` 文件的连接字符串部分. 最初它是这样的: + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" +} +```` + +更改如下所示: + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", + "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", + "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", + "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" +} +```` + +为相关模块添加了**另外三个连接字符串**,以针对 `BookStore_SecondDb` 数据库(它们都是相同的). 例如 `AbpPermissionManagement` 是权限管理模块的连接字符串. + +`AbpPermissionManagement` 是权限管理模块[定义](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11)的一个常量. 如果定义值,ABP框架[连接字符串选择系统](Connection-Strings.md)将为权限管理模块选择此连接字符串. 如果不定义它将回退到 `Default` 连接字符串. + +### 创建第二个迁移项目 + +在**运行时**上定义如上所述的连接字符串就足够了. 但是 `BookStore_SecondDb` 数据库还不存在. 你需要为相关模块创建数据库表. + +就像主数据库我们要使用的EF Core Code Frist迁移系统创建和维护第二个数据库. + +一种简单的方法是为第二个迁移 `DbContext` 创建第二个项目(`.csproj`). + +在你的解决方案中创建一个名为 `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` 的**类库项目**(如果不喜欢你可以进行其他命名). + +`.csproj` 内容应该和以下类似: + +````xml + + + + + + netcoreapp3.1 + Acme.BookStore.DbMigrationsForSecondDb + + + + + + + + + + + +```` + +你可以仅复制和修改原始 `.DbMigrations` 项目的内容. 该项目引用 `.EntityFrameworkCore` 项目. **唯一的区别**是 `RootNamespace` 值. + +从 `.Web` 项目中添加对该项目的引用(否则,EF Core Tool不允许使用`Add-migration`命令). + +### 创建第二个DbMigrationDbContext + +为迁移创建一个新的 `DbContext` 并调用模块的**扩展方法**为相关模块配置数据库表: + +````csharp +[ConnectionStringName("AbpPermissionManagement")] +public class BookStoreSecondMigrationsDbContext : + AbpDbContext +{ + public BookStoreSecondMigrationsDbContext( + DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureAuditLogging(); + } +} +```` + +> `[ConnectionStringName(...)` Attribute 在这里非常重,它告诉ABP框架应该为这个 `DbContext` 使用哪个连接字符串. 我们使用了 `AbpPermissionManagement`. + +创建一个**Design Time Db Factory**类, EF Core Tool需要使用它(例如`Add-Migration`和`Update-Database` PCM命令): + +````csharp +/* This class is needed for EF Core console commands + * (like Add-Migration and Update-Database commands) */ +public class BookStoreSecondMigrationsDbContextFactory + : IDesignTimeDbContextFactory +{ + public BookStoreSecondMigrationsDbContext CreateDbContext(string[] args) + { + var configuration = BuildConfiguration(); + + var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement")); + + return new BookStoreSecondMigrationsDbContext(builder.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} +```` + +它与 `.EntityFrameworCore.DbMigrations` 项目中的类相似, 除了它使用 `AbpPermissionManagement` 连接字符串. + +现在你可以打开包管理器控制台, 选择 `.EntityFrameworkCore.DbMigrationsForSecondDb` 项目为默认项目(确保`.Web`项目是启动项目),运行 `Add-Migration "Initial` 和 `Update-Database` 命令,如下所示: + +![pmc-add-migration-initial-update-database](images/pmc-add-migration-initial-update-database.png) + +现在你应该有一个包含相关数据库表的新数据库. + +![bookstore-second-database](images/bookstore-second-database.png) + +### 从主数据库中删除模块 + +我们 **创建第二个数据库** 包含了审计日志,权限管理和设置管理模块表. 我们应该从**主数据库中删除这些表**,这是很容易的. +S +首先移除从 `MigrationsDbContext` 类以下行(本例中是`BookStoreMigrationsDbContext`): + +````csharp +builder.ConfigurePermissionManagement(); +builder.ConfigureSettingManagement(); +builder.ConfigureAuditLogging(); +```` + +打开包管理器控制台, 选择 `.EntityFrameworkCore.DbMigrations` 为默认的项目(确保`.Web`项目是启动项目)运行以下命令: + +```` +Add-Migration "Removed_Audit_Setting_Permission_Modules" +```` + +此命令将创建一个如下所示的迁移类: + +````csharp +public partial class Removed_Audit_Setting_Permission_Modules : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpAuditLogActions"); + + migrationBuilder.DropTable( + name: "AbpEntityPropertyChanges"); + + migrationBuilder.DropTable( + name: "AbpPermissionGrants"); + + migrationBuilder.DropTable( + name: "AbpSettings"); + + migrationBuilder.DropTable( + name: "AbpEntityChanges"); + + migrationBuilder.DropTable( + name: "AbpAuditLogs"); + } + + ... +} +```` + +在这一步需要小心: + +* 如果你是 **已经在生产的系统**, 那么你应该关心**数据丢失**,在删除表之前需要将表的内容移动到第二个数据库. +* 如果你还 **没有开始** 你的项目, 你可以考虑 **删除所有的迁移** 并且重新生成初始版本,让迁移历史更加清晰. + +运行 `Update-Database` 命令从主数据库中删除表. + +请注意如果你还没有将其复制到新数据库,那么还会**删除了一些初始种子数据**(例如授予管理员角色的权限). 如果你运行应用程序可能无法再登录. 解决方案很简单:在解决方案中重新运行 `.DbMigrator` 控制台应用程序,它会为新数据库添加种子数据. + +### 自动化第二个数据库模式迁移 + +`.DbMigrator` 控制台应用程序可以跨多个数据库运行数据库种子代码无需任何其他配置. 但它不能在第二个数据库迁移项目中运行EF Core First迁移. 下面介绍如何配置控制台迁移应用程序以处理两个数据库. + +#### 实现 IBookStoreDbSchemaMigrator + +`Acme.BookStore.EntityFrameworkCore.DbMigrations` 项目中的 `EntityFrameworkCoreBookStoreDbSchemaMigrator` 类负责为 `BookStoreMigrationsDbContext` 迁移数据库模式. 它应该是这样的: + +````csharp +[Dependency(ReplaceServices = true)] +public class EntityFrameworkCoreBookStoreDbSchemaMigrator + : IBookStoreDbSchemaMigrator, ITransientDependency +{ + private readonly IServiceProvider _serviceProvider; + + public EntityFrameworkCoreBookStoreDbSchemaMigrator( + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task MigrateAsync() + { + /* We are intentionally resolving the BookStoreMigrationsDbContext + * from IServiceProvider (instead of directly injecting it) + * to properly get the connection string of the current tenant in the + * current scope. + */ + + await _serviceProvider + .GetRequiredService() + .Database + .MigrateAsync(); + } +} +```` + +它实现了 `IBookStoreDbSchemaMigrator` 并**替换现有服务**(见第一行). + +删除 `[Dependency(ReplaceServices = true)]`,因为我们有这个接口的两个实现并且希望同时使用它们. + +在新的迁移项目(`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`)创建它的一个副本,内部使用 `BookStoreSecondMigrationsDbContext` . 示例实现: + +````csharp +public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator + : IBookStoreDbSchemaMigrator, ITransientDependency +{ + private readonly IServiceProvider _serviceProvider; + + public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator( + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task MigrateAsync() + { + /* We are intentionally resolving the BookStoreSecondMigrationsDbContext + * from IServiceProvider (instead of directly injecting it) + * to properly get the connection string of the current tenant in the + * current scope. + */ + + await _serviceProvider + .GetRequiredService() + .Database + .MigrateAsync(); + } +} +```` + +> 类的名称对于[依赖注入](Dependency-Injection.md)非常重要. 为了可以使用 `IBookStoreDbSchemaMigrator` 引用注入它的结尾应该是 `BookStoreDbSchemaMigrator`. + +现在我们有 `IBookStoreDbSchemaMigrator` 接口的两个实现,每个实现负责迁移相关的数据库模式. + +#### 为第二个迁移项目定义一个模块类 + +是时候为第二次迁移(`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`)项目定义[模块](Module-Development-Basics.md)类了: + +````csharp +[DependsOn( + typeof(BookStoreEntityFrameworkCoreModule) + )] +public class BookStoreEntityFrameworkCoreSecondDbMigrationsModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(); + } +} +```` + +在 `Acme.BookStore.DbMigrator` 项目中引用 `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` 项目并且将 `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` 依赖项添加到 `BookStoreDbMigratorModule` 的依赖项列表中. `BookStoreDbMigratorModule` 类应该是这样的: + +````csharp +[DependsOn( + typeof(AbpAutofacModule), + typeof(BookStoreEntityFrameworkCoreDbMigrationsModule), + typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule), // ADDED THIS! + typeof(BookStoreApplicationContractsModule) + )] +public class BookStoreDbMigratorModule : AbpModule +{ + ... +} +```` + +#### 运行数据库迁移程序 + +你可以运行 `.DbMigrator` 应用程序应用迁移和初始化种子数据. 你可以删除这两个数据库进行测试,然后再次运行 `.DbMigrator` 应用程序,它会创建两个数据库. + +## 结论 + +本文档说明了如何拆分数据库以及管理Entity Framework Core解决方案的数据库迁移. 简而言之,你需要为每个不同的数据库创建一个单独的迁移项目. \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md index 943667eb55..ca12d7524f 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md +++ b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md @@ -2,9 +2,13 @@ **[应用程序启动模板](Startup-Templates/Application.md)** 为EF Core预配置了Sql Server提供程序,EF Core支持许多其它DBMS,你可以在基于ABP的应用程序使用它们. -ABP框架为一些常见的DMBS提供了简化配置的集成包(有关可用集成包的列表,请参阅[EF Core文档](Entity-Framework-Core.md)),你也可以不使用集成包配置DBMS提供程序. +ABP框架为一些常见的DMBS提供了简化配置的**集成包**,你可以通过以下文档来学习如何**切换到你喜欢的DBMS**: -虽然总是建议使用集成包(它也使不同模块之间的依赖版本成为标准版本),但是如果没有用于DBMS提供程序的集成包,也可以手动集成. +* [MySQL](Entity-Framework-Core-MySQL.md) +* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md) +* [SQLite](Entity-Framework-Core-SQLite.md) + +你也可以不使用集成包配置DBMS提供程序,虽然总是建议使用集成包(它也使不同模块之间的依赖版本成为标准版本),但是如果没有用于DBMS提供程序的集成包,也可以手动集成. 本文介绍了如何在不使用[MySQL集成包](Entity-Framework-Core-MySQL.md)的情况下切换到MySQL. @@ -87,4 +91,18 @@ builder.ConfigureIdentityServer(options => ## 运行应用程序 -它已准备就绪, 只需要运行该应用程序与享受编码. \ No newline at end of file +它已准备就绪, 只需要运行该应用程序与享受编码. + +## DBMS限制 + +不同的DBMS可能存在一些限制, 如字段名称的最大长度, 索引长度等等. +模块可能提供了一些内置的解决方案. 你可以通过`ModelBuilder`来配置它 如: Identity Server模块 + +```csharp +builder.ConfigureIdentityServer(options => +{ + options.DatabaseProvider = EfCoreDatabaseProvider.MySql; +}); +``` + +相关讨论: https://github.com/abpframework/abp/issues/1920 \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md index b70630e38f..d2af31f4d2 100644 --- a/docs/zh-Hans/Entity-Framework-Core.md +++ b/docs/zh-Hans/Entity-Framework-Core.md @@ -32,14 +32,9 @@ namespace MyCompany.MyProject EF Core支持多种数据库管理系统([查看全部](https://docs.microsoft.com/en-us/ef/core/providers/)). ABP框架和本文档不依赖于任何特定的DBMS. -如果要创建一个可重用的库,应避免依赖于特定的DBMS包.但在最终的应用程序中,始终会选择一个DBMS. +如果要创建一个可重用的[应用程序模块](Modules/Index.md),应避免依赖于特定的DBMS包.但在最终的应用程序中,始终会选择一个DBMS. -ABP框架为一些常见的DBMS提供了集成包,使配置变得更加简单. [启动模板](Startup-Templates/Index.md)附带**预先配置的SQL Server (localdb)**.请参阅以下文档,了解如何配置其他DBMS提供程序: - -* [MySQL](Entity-Framework-Core-MySQL.md) -* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md) -* [SQLite](Entity-Framework-Core-SQLite.md) -* [Others](Entity-Framework-Core-Other-DBMS.md) +参阅[为Entity Framework Core切换到其他DBMS](Entity-Framework-Core-Other-DBMS.md)文档学习如何切换DBMS. ## 创建 DbContext @@ -63,6 +58,53 @@ namespace MyCompany.MyProject } ```` +### 关于EF Core Fluent Mapping + +[应用程序启动模板](Startup-Templates/Application.md)已配置使用[EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/)映射你的实体到数据库表. + +你依然为你的实体属性使用**data annotation attributes**(像`[Required]`),而ABP文档通常遵循**fluent mapping API** approach方法. 如何使用取决与你. + +ABP框架有一些**实体基类**和**约定**(参阅[实体文档](Entities.md))提供了一些有用的扩展方法来配置从基本实体类继承的属性. + +#### ConfigureByConvention 方法 + +`ConfigureByConvention()` 是主要的扩展方法,它对你的实体**配置所有的基本属性**和约定. 所以在你的流利映射代码中为你所有的实体调用这个方法是 **最佳实践**, + +**示例**: 假设你有一个直接继承 `AggregateRoot` 基类的 `Book` 实体: + +````csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } +} +```` + +你可以在你的 `DbContext` 重写 `OnModelCreating` 方法并且做以下配置: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + //Always call the base method + base.OnModelCreating(builder); + + builder.Entity(b => + { + b.ToTable("Books"); + + //Configure the base properties + b.ConfigureByConvention(); + + //Configure other properties (if you are using the fluent API) + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + }); +} +```` + +* 这里调用了 `b.ConfigureByConvention()` 它对于**配置基本属性**非常重要. +* 你可以在这里配置 `Name` 属性或者使用**data annotation attributes**(参阅[EF Core 文档](https://docs.microsoft.com/zh-cn/ef/core/modeling/entity-properties)). + +> 尽管有许多扩展方法可以配置基本属性,但如果需要 `ConfigureByConvention()` 内部会调用它们. 因此仅调用它就足够了. + ### 配置连接字符串选择 如果你的应用程序有多个数据库,你可以使用 `connectionStringName]` Attribute为你的DbContext配置连接字符串名称. @@ -230,7 +272,7 @@ public override async Task DeleteAsync( } ```` -### 访问 EF Core API +## 访问 EF Core API 大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目地). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例: @@ -256,9 +298,60 @@ public class BookService > 要点: 你必须在使用`DbContext`的项目里引用`Volo.Abp.EntityFrameworkCore`包. 这会破坏封装,但在这种情况下,这就是你需要的. -### 高级主题 +## Extra Properties & Object Extension Manager + +额外属性系统允许你为实现了 `IHasExtraProperties` 的实体set/get动态属性. 当你想将自定义属性添加到[应用程序模块](Modules/Index.md)中定义的实体时,它特别有用. + +默认,实体的所有额外属性存储在数据库的一个 `JSON` 对象中. -#### 设置默认仓储类 +实体扩展系统允许你存储额外属性在数据库的单独字段中. 有关额外属性和实体扩展系统的更多信息,请参阅下列文档: + +* [自定义应用模块: 扩展实体](Customizing-Application-Modules-Extending-Entities.md) +* [实体](Entities.md) + +本节只解释了 EF Core相关的 `ObjectExtensionManager` 及其用法. + +### ObjectExtensionManager.Instance + +`ObjectExtensionManager` 实现单例模式,因此你需要使用静态的 `ObjectExtensionManager.Instance` 来执行所有操作。 + +### MapEfCoreProperty + +`MapEfCoreProperty` 是一种快捷扩展方法,用于定义实体的扩展属性并映射到数据库. + +**示例**: 添加 `Title` 属性 (数据库字段)到 `IdentityRole` 实体: + +````csharp +ObjectExtensionManager.Instance + .MapEfCoreProperty( + "Title", + builder => { builder.HasMaxLength(64); } + ); +```` + +如果相关模块已实现此功能(通过使用下面说明的 `ConfigureEfCoreEntity`)则将新属性添加到模型中. 然后你需要运行标准的 `Add-Migration` 和 `Update-Database` 命令更新数据库以添加新字段. + +>`MapEfCoreProperty` 方法必须在使用相关的 `DbContext` 之前调用,它是一个静态方法. 最好的方法是尽早的应用程序中使用它. 应用程序启动模板含有 `YourProjectNameEntityExtensions` 类,可以在放心的在此类中使用此方法. + +### ConfigureEfCoreEntity + +如果你正在开发一个可重用使用的模块,并允许应用程序开发人员将属性添加到你的实体,你可以在实体映射使用 `ConfigureEfCoreEntity` 扩展方法,但是在配置实体映射时可以使用快捷的扩展方法 `ConfigureObjectExtensions`: + +````csharp +builder.Entity(b => +{ + b.ConfigureObjectExtensions(); + //... +}); +```` + +如果你调用 `ConfigureByConvention()` 扩展方法(在此示例中 `b.ConfigureByConvention`),ABP框架内部会调用 `ConfigureObjectExtensions` 方法. 使用 `ConfigureByConvention` 方法是**最佳实践**,因为它还按照约定配置基本属性的数据库映射. + +参阅上面提到的 "*ConfigureByConvention 方法*" 了解更多信息. + +## 高级主题 + +### 设置默认仓储类 默认的通用仓储的默认实现是`EfCoreRepository`类,你可以创建自己的实现,并将其做为默认实现 @@ -348,3 +441,7 @@ context.Services.AddAbpDbContext(options => ```` 在这个例子中,`OtherDbContext`实现了`IBookStoreDbContext`. 此功能允许你在开发时使用多个DbContext(每个模块一个),但在运行时可以使用单个DbContext(实现所有DbContext的所有接口). + +## 另请参阅 + +* [实体](Entities.md) \ No newline at end of file diff --git a/docs/zh-Hans/Exception-Handling.md b/docs/zh-Hans/Exception-Handling.md index a251c50f61..6bbbed56d4 100644 --- a/docs/zh-Hans/Exception-Handling.md +++ b/docs/zh-Hans/Exception-Handling.md @@ -190,7 +190,7 @@ throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage" "UserNameShouldBeUniqueMessage": "Username should be unique! '{0}' is already taken!" ```` -* `IUserFriendlyException`接口派生自`IBusinessException`,而 `UserFriendlyException `类派生自`BusinessException`类. +* `IUserFriendlyException`接口派生自`IBusinessException`,而 `UserFriendlyException`类派生自`BusinessException`类. #### 使用错误代码 @@ -204,7 +204,7 @@ throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage" 首先,在模块配置代码中将 **code-namespace** 映射至 **本地化资源**: ````C# -services.Configure(options => +services.Configure(options => { options.MapCodeNamespace("Volo.Qa", typeof(QaResource)); }); @@ -285,7 +285,7 @@ ABP尝试按照以下规则,自动映射常见的异常类型的HTTP状态代码 可以重写HTTP状态代码的自动映射,示例如下: ````C# -services.Configure(options => +services.Configure(options => { options.Map("Volo.Qa:010002", HttpStatusCode.Conflict); }); diff --git a/docs/zh-Hans/Getting-Started-Angular-Template.md b/docs/zh-Hans/Getting-Started-Angular-Template.md new file mode 100644 index 0000000000..d2ec83293b --- /dev/null +++ b/docs/zh-Hans/Getting-Started-Angular-Template.md @@ -0,0 +1,3 @@ +## Getting Started With the Angular Application Template + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md index d65573d9db..90291e968f 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md @@ -24,10 +24,10 @@ abp new Acme.BookStore 创建项目的要求: -* [Visual Studio 2019 (v16.3+)](https://visualstudio.microsoft.com/vs/) +* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) * [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/) * [Node v12+](https://nodejs.org) -* [Yarn v1.19+](https://yarnpkg.com/) +* [Yarn v1.19+](https://classic.yarnpkg.com/) ### 解决方案结构 diff --git a/docs/zh-Hans/Getting-Started-With-Startup-Templates.md b/docs/zh-Hans/Getting-Started-With-Startup-Templates.md new file mode 100644 index 0000000000..2b23350612 --- /dev/null +++ b/docs/zh-Hans/Getting-Started-With-Startup-Templates.md @@ -0,0 +1,6 @@ +# 启动模板入门 + +参阅下面的教程来学习如何开始使用的ABP框架预构建的应用程序启动模板: + +* [ASP.NET Core MVC/Razor页面模板入门](Getting-Started-AspNetCore-MVC-Template.md) +* [Angular UI模板入门](Getting-Started-Angular-Template.md) \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md new file mode 100644 index 0000000000..92b8596ee3 --- /dev/null +++ b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -0,0 +1,3 @@ +# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Customize-Login-Page-MVC.md b/docs/zh-Hans/How-To/Customize-Login-Page-MVC.md new file mode 100644 index 0000000000..fdb3c292f6 --- /dev/null +++ b/docs/zh-Hans/How-To/Customize-Login-Page-MVC.md @@ -0,0 +1,113 @@ +# 如何为MVC / Razor页面应用程序自定义登录页面 + +当你使用[应用程序启动模板](../Startup-Templates/Application.md)创建了一个新的应用程序, 登录页面的源代码并不在你的解决方案中,所以你不能直接更改. 它来自[账户模块](../Modules/Account.md),使用[NuGet包](https://www.nuget.org/packages/Volo.Abp.Account.Web)引用. + +本文介绍了如何为自己的应用程序自定义登录页面. + +## 创建登录 PageModel + +创建一个新的类继承账户模块的[LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs). + +````csharp +public class CustomLoginModel : LoginModel +{ + public CustomLoginModel( + Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, + Microsoft.Extensions.Options.IOptions accountOptions) + : base(schemeProvider, accountOptions) + { + } +} +```` + +> 在这里命令约定很重要. 如果你的类名不是以 `LoginModel` 结束,你需要手动在[依赖注入](../Dependency-Injection.md)系统替换 `LoginModel`. + +然后你可以覆盖任何方法并添加用户界面需要的新方法和属性. + +## 重写登录页面UI + +在 **Pages** 目录下创建名为 **Account** 的文件夹,并在这个文件夹中创建 `Login.cshtml` ,借助[虚拟文件系统](../Virtual-File-System.md)它会自动覆盖账户模块的页面文件. + +自定义页面一个很好的开始是复制它的源代码. [点击这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到登录页面的源码. 在编写本文档时,源代码如下: + +````xml +@page +@using Volo.Abp.Account.Settings +@using Volo.Abp.Settings +@model Acme.BookStore.Web.Pages.Account.CustomLoginModel +@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage +@inject Volo.Abp.Settings.ISettingProvider SettingProvider +@if (Model.EnableLocalLogin) +{ +
+
+

@L["Login"]

+ @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) + { + + @L["AreYouANewUser"] + @L["Register"] + + } +
+ + +
+ + + +
+
+ + + +
+
+ +
+ @L["Login"] +
+
+ + +
+} + +@if (Model.VisibleExternalProviders.Any()) +{ +
+

@L["UseAnotherServiceToLogIn"]

+
+ + + @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
+
+} + +@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) +{ +
+ @L["InvalidLoginRequest"] + @L["ThereAreNoLoginSchemesConfiguredForThisClient"] +
+} +```` + +只需更改 `@model` 为 `Acme.BookStore.Web.Pages.Account.CustomLoginModel` 使用自定义的 `PageModel` 类. 你可以做任何应用程序需要的更改. + +## 本文的源代码 + +你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. + +## 另请参阅 + +* [ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南](../UI/AspNetCore/Customization-User-Interface.md). \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md new file mode 100644 index 0000000000..2667509aca --- /dev/null +++ b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md @@ -0,0 +1,3 @@ +# 如何为ABP应用程序定制SignIn Manager + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/How-To/Index.md b/docs/zh-Hans/How-To/Index.md new file mode 100644 index 0000000000..3190ced7ca --- /dev/null +++ b/docs/zh-Hans/How-To/Index.md @@ -0,0 +1,9 @@ +# "如何" 指南 + +本部分包含一些常见问题的 "如何" 指南. 尽管其中是一些常见的开发任务和ABP并不直接相关,但我们认为有一些具体的示例可以直接与基于ABP的应用程序一起使用. + +## Authentication + +* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md) +* [如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证](Azure-Active-Directory-Authentication-MVC.md) +* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md) \ No newline at end of file diff --git a/docs/zh-Hans/Localization.md b/docs/zh-Hans/Localization.md index a3cc74c2ed..fce4b14407 100644 --- a/docs/zh-Hans/Localization.md +++ b/docs/zh-Hans/Localization.md @@ -46,7 +46,7 @@ public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { // "YourRootNameSpace" 是项目的根命名空间名字. 如果你的项目的根命名空间名字为空,则无需传递此参数. options.FileSets.AddEmbedded("YourRootNameSpace"); @@ -190,3 +190,7 @@ var testResource = abp.localization.getResource('Test'); ````js var str = testResource('HelloWorld'); ```` + +## See Also + +* [Angular UI中的本地化](UI/Angular/Localization.md) \ No newline at end of file diff --git a/docs/zh-Hans/Microservice-Architecture.md b/docs/zh-Hans/Microservice-Architecture.md index 3b96cdc7f6..856cfee034 100644 --- a/docs/zh-Hans/Microservice-Architecture.md +++ b/docs/zh-Hans/Microservice-Architecture.md @@ -12,8 +12,8 @@ ABP框架的主要目标之一就是提供**便捷的基础设施来创建微服 * 提供[架构模型](Best-Practices/Module-Architecture.md)来开发模块,与微服务开发和部署兼容. * 提供[最佳实践指南](Best-Practices/Index.md)制定模块开发标准. * 提供基础设施来实现微服务中的[领域驱动设计](Domain-Driven-Design.md). -* 提供从应用程序服务[自动生成REST风格的API](AspNetCore/Auto-API-Controllers.md)的服务. -* 提供[自动创建C#API客户端](AspNetCore/Dynamic-CSharp-API-Clients.md)服务,以便从其他服务/应用程序使用你服务. +* 提供从应用程序服务[自动生成REST风格的API](API/Auto-API-Controllers.md)的服务. +* 提供[自动创建C#API客户端](API/Dynamic-CSharp-API-Clients.md)服务,以便从其他服务/应用程序使用你服务. * 提供[分布式事件总线](Event-Bus.md)用于服务通信. * 提供更多其他服务,使日常开发更加简便. diff --git a/docs/zh-Hans/Module-Development-Basics.md b/docs/zh-Hans/Module-Development-Basics.md index 4519720161..e43c57d0b2 100644 --- a/docs/zh-Hans/Module-Development-Basics.md +++ b/docs/zh-Hans/Module-Development-Basics.md @@ -42,7 +42,7 @@ public class BlogModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { //为应用程序配置默认的连接字符串 - Configure(options => + Configure(options => { options.ConnectionStrings.Default = "......"; }); diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index f2a92334fb..20382d24f2 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -26,15 +26,19 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. 如果你没有现有的ABP项目, 这个步骤向你展示如何在[abp.io](https://abp.io)创建一个新项目并添加文档模块. 如果你本地已经有了一个ABP项目, 那么你可以跳过这一步. -打开 https://abp.io/Templates. 输入项目名称为 `Acme.MyProject`, 选择 `ASP.NET Core Mvc Application` 和选择 `Entity Framework Core` 做为数据库提供者. +推荐使用ABP CLI创建新项目,使用以下命令行: -请注意,本文档包含了 `Entity Framework Core` 提供者 不过你也可以选择 `MongoDB` 做为数据库提供者. +`abp new Acme.MyProject` + +你也可以在浏览器中导航到 https://abp.io/get-started. 输入项目名称为 `Acme.MyProject`, 其它保持默认选项. + +请注意,本文档包含了 `Entity Framework Core` 提供者 不过你也可以选择 `MongoDB` 做为数据库提供者. ![创建新项目](../images/docs-module_download-new-abp-project.png) ### 2- 运行这个空项目 -下载项目后, 解压压缩文档并且打开 `Acme.MyProject.sln`. 你可以看到这个解决方案包含了 `Application`, `Domain`, `EntityFrameworkCore` 和 `Web` 项目. 右键选择 `Acme.MyProject.Web` 项目**设置为启动项目**. +下载项目后, 解压压缩文档并且打开 `Acme.MyProject.sln`. 你可以看到这个解决方案包含了 `Application`, `Application.Contrawcts`, `DbMigrator`, `Domain`, `Domain.Shared`, `EntityFrameworkCore`, `EntityFrameworkCore.DbMigations`, `HttpApi`, `HttpApi.Client` 和 `Web` 项目. 右键选择 `Acme.MyProject.Web` 项目**设置为启动项目**. ![创建新项目](../images/docs-module_solution-explorer.png) @@ -43,12 +47,12 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. ```json { "ConnectionStrings": { - "Default": "Server=localhost;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" } } ``` -打开Visual Studio包管理控制台选择`src\Acme.MyProject.EntityFrameworkCore` 做为默认项目. 运行 `Update-Database` 命令创建数据库. 数据库`MyProject`将在数据库服务器中创建. +运行 `Acme.MyProject.DbMigrator` 项目,它会负责应用迁移与初始化种子数据. 数据库`MyProject`将在数据库服务器中创建. 现在一个空的ABP项目已经创建完成! 现在你可以运行项目并且查看网站. @@ -56,39 +60,28 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. ### 2- 引用文档模块包 -文档模块包托管在Nuget上面. 需要有四个包安装到你的应用程序中. 每个包必须安装到相关的项目. - -* [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) 需要安装到 `Acme.MyProject.Domain` 项目. +文档模块包托管在Nuget上面. 需要有四个包安装到你的应用程序中. 每个包必须安装到相关的项目. - * 修改 `Acme.MyProject.Domain.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. +建议使用ABP CLI安装模块,在解决方案文件 (`.sln`) 目录打开 `CMD` 窗口,运行以下命令: - ```csharp - - ``` +`abp add-module Volo.Docs` -* [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) 需要安装到 `Acme.MyProject.EntityFrameworkCore` 项目. +或者你也可以手动安装nuget包到每个项目: - * 修改 `Acme.MyProject.EntityFrameworkCore.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. +* 安装[Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Domain` 项目. - ```csharp - - ``` + `Install-Package Volo.Docs.Domain` -* [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) 需要安装到 `Acme.MyProject.Application` 项目. +* 安装[Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget包到 `Acme.MyProject.EntityFrameworkCore` 项目. - * 修改 `Acme.MyProject.Application.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. + `Install-Package Volo.Docs.EntityFrameworkCore` - ```csharp - - ``` +* 安装[Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget包到 `Acme.MyProject.Application` 项目. -* [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Web/) 需要安装到 `Acme.MyProject.Web` 项目. + `Install-Package Volo.Docs.Application` - * 修改 `Acme.MyProject.Web.csproj` 文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. - - ```csharp - - ``` +* 安装[Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Web` 项目. + `Install-Package Volo.Docs.Web` ### 3- 添加模块依赖 @@ -176,37 +169,58 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. #### 4.1- Entity Framework 集成 -如果你选择了Entity Framework 做为数据库供应者,你需要在DbContext中配置文档模块. 做以下操作; +如果你选择了Entity Framework 做为数据库供应者,你需要配置文档模块. 做以下操作; -* 打开 `MyProjectDbContext.cs` 并且添加 `modelBuilder.ConfigureDocs()` 到 `OnModelCreating()` 方法中 +* 打开 `MyProjectMigrationsDbContext.cs` 并且添加 `builder.ConfigureDocs()` 到 `OnModelCreating()` 方法中 ```csharp - [ConnectionStringName("Default")] - public class MyProjectDbContext : AbpDbContext - { - public MyProjectDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - //... - modelBuilder.ConfigureDocs(); - } - } + public class MyProjectMigrationsDbContext : AbpDbContext + { + public MyProjectMigrationsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentity(); + builder.ConfigureIdentityServer(); + builder.ConfigureFeatureManagement(); + builder.ConfigureTenantManagement(); + builder.ConfigureDocs(); //Add this line to configure the Docs Module + + /* Configure customizations for entities from the modules included */ + + builder.Entity(b => + { + b.ConfigureCustomUserProperties(); + }); + + /* Configure your own tables/entities inside the ConfigureQaDoc method */ + + builder.ConfigureMyProject(); + } + } ``` -* 打开 `Visual Studio` 的 `包管理控制台` 选择 `Acme.MyProject.EntityFrameworkCore` 做为默认项目. 然后编写以下命令为文档模块添加迁移. +* 打开 `Visual Studio` 的 `包管理控制台` 选择 `Acme.MyProject.EntityFrameworkCore.DbMigrations` 做为默认项目. 然后编写以下命令为文档模块添加迁移. ```csharp add-migration Added_Docs_Module ``` - 当命令执行成功后 , 你会看到`Acme.MyProject.EntityFrameworkCore\Migrations` 目录下有名为 `20181221111621_Added_Docs_Module` 的迁移文件. + 当命令执行成功后 , 你会看到`Acme.MyProject.EntityFrameworkCore.DbMigrations\Migrations` 目录下有名为 `20181221111621_Added_Docs_Module` 的迁移文件. - 现在更新数据库. 在 `Visual Studio` 的 `包管理控制台` 中执行以下代码. 要确认已 `Acme.MyProject.EntityFrameworkCore` 项目设置为默认项目. + 现在更新数据库. 在 `Visual Studio` 的 `包管理控制台` 中执行以下代码. 要确认已 `Acme.MyProject.EntityFrameworkCore.DbMigrations` 项目设置为默认项目. ```csharp update-database @@ -309,7 +323,7 @@ There are no projects yet! - ExtraProperties: ```json - {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***"} + {"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs/zh-Hans/","GitHubAccessToken":"***","GitHubUserAgent":""} ``` 注意 `GitHubAccessToken` 用 `***` 掩盖. 这是一个私人令牌,你必须从GitHub获取它. 请参阅 https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ @@ -321,11 +335,13 @@ There are no projects yet! 对于 `SQL` 数据库,你可以使用下面的 `T-SQL` 命令将指定的示例插入到 `DocsProjects` 表中: ```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***"}', N'/', N'master', N'') +INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939658', N'ABP framework (GitHub)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'GitHub', N'{"GitHubRootUrl":"https://github.com/abpframework/abp/tree/{version}/docs","GitHubAccessToken":"***","GitHubUserAgent":""}', N'/', N'master', N'') ``` 请注意,`GitHubAccessToken` 被屏蔽了.它是一个私人令牌,你必须获得自己的令牌并替换 `***` 字符串. +现在你可以运行应用程序并导航到 `/Documents`. + #### "FileSystem" 项目的示例记录 你可以使用 [ABP Framework](https://github.com/abpframework/abp/) GitHub文档来配置你的文件系统存储. @@ -556,6 +572,30 @@ This document assumes that you prefer to use **{{ UI_Value }}** as the UI framew 最后,为您的项目添加了一个新的Docs模块, 该模块由GitHub提供. +## 全文搜索(Elastic Search) + +文档模块支持使用Elastic Search对内容进行全文搜索. 默认没有启用, 你可以配置`DocsElasticSearchOptions`启用它. + +``` +Configure(options => +{ + options.Enable = true; + options.IndexName = "your_index_name"; //default IndexName is abp_documents +}); +``` + +应用程序启动后如果`Index`不存在则会自动创建`Index`. + +`DefaultElasticClientProvider`负责创建`IElasticClient`, 默认情况下它会从`IConfiguration`中读取Elastic Search的`Url`. +如果你的IElasticClient需要其它配置请使用重写IElasticClientProvider服务并在依赖注入系统中替换它. +``` +{ + "ElasticSearch": { + "Url": "http://localhost:9200" + } +} +``` + ## 下一步 文档模块也可以做为独立的应用程序. 查看 [VoloDocs](../Apps/VoloDocs). \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Identity.md b/docs/zh-Hans/Modules/Identity.md new file mode 100644 index 0000000000..ea50006de9 --- /dev/null +++ b/docs/zh-Hans/Modules/Identity.md @@ -0,0 +1,3 @@ +# 身份管理模块 + +参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Tentant-Management.md b/docs/zh-Hans/Modules/Tentant-Management.md new file mode 100644 index 0000000000..fa999428dd --- /dev/null +++ b/docs/zh-Hans/Modules/Tentant-Management.md @@ -0,0 +1,3 @@ +# 租户管理模块 + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/Multi-Tenancy.md b/docs/zh-Hans/Multi-Tenancy.md index 83d190bb38..8913c27ede 100644 --- a/docs/zh-Hans/Multi-Tenancy.md +++ b/docs/zh-Hans/Multi-Tenancy.md @@ -168,7 +168,7 @@ namespace MyCompany.MyProject { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { options.Tenants = new[] { @@ -214,7 +214,7 @@ namespace MyCompany.MyProject { var configuration = BuildConfiguration(); - Configure(configuration); + Configure(configuration); } private static IConfigurationRoot BuildConfiguration() @@ -303,6 +303,7 @@ TODO:... Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前Web请求(按优先级排序)中确定当前租户. +* **CurrentUserTenantResolveContributor**: 如果当前用户已登录,从当前用户的声明中获取租户Id. **出于安全考虑,应该始终将其做为第一个Contributor**. * **QueryStringTenantResolver**: 尝试从query string参数中获取当前租户,默认参数名为"__tenant". * **RouteTenantResolver**:尝试从当前路由中获取(URL路径),默认是变量名是"__tenant".所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户. * **HeaderTenantResolver**: 尝试从HTTP header中获取当前租户,默认的header名称是"__tenant". @@ -342,8 +343,8 @@ namespace MyCompany.MyProject { Configure(options => { - //子域名格式: {0}.mydomain.com (作为最高优先级解析器添加) - options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com")); + //子域名格式: {0}.mydomain.com (作为第二优先级解析器添加, 位于CurrentUserTenantResolveContributor之后) + options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com")); }); //... @@ -354,7 +355,7 @@ namespace MyCompany.MyProject {0}是用来确定当前租户唯一名称的占位符. -你可以使用下面的方法,代替``options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));``: +你可以使用下面的方法,代替``options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));``: ````C# options.AddDomainTenantResolver("{0}.mydomain.com"); diff --git a/docs/zh-Hans/Nightly-Builds.md b/docs/zh-Hans/Nightly-Builds.md index c37c9af9bc..3d34330ca3 100644 --- a/docs/zh-Hans/Nightly-Builds.md +++ b/docs/zh-Hans/Nightly-Builds.md @@ -1,8 +1,8 @@ - # 每日构建 +# 每日构建 -所有框架和模块包都每晚都部署到MyGet. 因此你可以使用或测试最新的代码,而无需等待下一个版本. +所有框架和模块包每晚都部署到MyGet. 因此你可以使用或测试最新的代码,而无需等待下一个版本. -## 在Visual Studio配置 +## 在Visual Studio配置 > 需要Visual Studio 2017以上 diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index aaa780188d..9b559797f7 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -24,82 +24,15 @@ ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服 下图展示了该系统: -![microservice-sample-diagram](../images/microservice-sample-diagram-2.png) +![microservice-sample-diagram](../images/microservice-sample-diagram-3.png) ### 源码 你可以从[GitHub仓库](https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo)获取源码. -### 状态 - -该示例的初始版本已完成.其他改进仍在开发中. - ## 运行解决方案 -您可以从 **源代码** 或者预先配置好的 **docker-compose** 文件运行. - -### 使用Docker容器 - -#### 预先要求 - -由于所有依赖项都已预先配置, 因此作为Docker容器运行更容易. 你只需要安装[最新的docker](https://docs.docker.com/compose/install/). - -#### 运行容器 - -- 克隆或下载 [ABP仓库](https://github.com/abpframework/abp). - -- 在存储库的`samples/MicroserviceDemo`文件夹中打开命令行. - -- 从Docker Hub中拉取image: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml pull - ``` - -- 如果要在本地构建映像, 可以跳过上述步骤, 使用build命令: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml build - ``` - - 根据你的电脑配置, 构建image可能需要**很长时间**. - -- 还原 SQL Server 数据库: - - ``` - docker-compose -f docker-compose.yml -f docker-compose.migrations.yml run restore-database - ``` - -- 启动容器: - - ``` - docker-compose up -d - ``` - -- 将此行添加到`hosts`文件的末尾: - - ``` - 127.0.0.1 auth-server - ``` - - hosts文件位于Windows上的`C:\Windows\System32\Drivers\etc\hosts`文件夹, Linux/MacOS的`/etc/hosts`中. - -#### 运行应用程序 - -你可能想要了解容器中运行一些应用程序: - -* 后端管理应用程序 (BackendAdminApp.Host): `http://localhost:51512` - *(用于管理系统中的用户和产品)* -* 公共网站 (PublicWebsite.Host): `http://localhost:51513` - *(用于列出产品并运行/管理博客模块)* -* 认证服务器 (AuthServer.Host): `http://auth-server:51511/` - *(用作使用IdentityServer4构建的单点登录和身份验证服务器)* -* Kibana UI: `http://localhost:51510` - *(用于显示/跟踪所有服务/应用程序/网关写入的日志)* - -### 从源代码运行 - -#### 预先要求 +### 预先要求 为了能够从源代码运行解决方案, 应在你的计算机上安装并运行以下工具: @@ -110,19 +43,19 @@ ABP框架的主要目标之一就是提供[便捷的基础设施来创建微服 * [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+ * [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (可选,建议显示日志) -#### 打开并构建Visual Studio解决方案 +### 打开并构建Visual Studio解决方案 * 在Visual Studio 2017 (15.9.0+)中打开`samples\MicroserviceDemo\MicroserviceDemo.sln`. * 在`samples\MicroserviceDemo`文件夹中的命令行运行`dotnet restore`命令. * 在Visual Studio中构建解决方案. -#### 创建数据库 +### 创建数据库 MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库的结构。其实你可以很轻松的创建数据库,因为这个解决方案配置了使用 Entity Core Code First 来做迁移。 这个解决方案中有两个 SQL server 数据库。 -##### MsDemo_Identity 数据库 +#### MsDemo_Identity 数据库 * 右键 `AuthServer.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) @@ -131,7 +64,7 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 ![microservice-sample-update-database-authserver](../images/microservice-sample-update-database-authserver.png) -##### MsDemo_ProductManagement +#### MsDemo_ProductManagement * 右键 `ProductService.Host` 项目,然后点击 `设置为启动项目`. * 打开 **程序包管理器控制台** (工具 -> NuGet 包管理器 -> 程序包管理器控制台) @@ -140,12 +73,13 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 ![microservice-sample-update-database-products](../images/microservice-sample-update-database-products.png) -#### 运行项目 +### 运行项目 按以下顺序运行项目(右键单击每个项目设置为启动项目,按Ctrl+F5运行,无需调试): * AuthServer.Host * IdentityService.Host +* TenantManagementService.Host * BloggingService.Host * ProductService.Host * InternalGateway.Host @@ -158,7 +92,7 @@ MongoDB 数据库是动态创建的,但是你需要创建 SQL server 数据库 Visual Studio解决方案由多个项目组成,每个项目在系统中具有不同的角色: -![microservice-sample-solution](../images/microservice-sample-solution.png) +![microservice-sample-solution](../images/microservice-sample-solution-2.png) ### 应用程序(Applications) @@ -182,6 +116,7 @@ Visual Studio解决方案由多个项目组成,每个项目在系统中具有不 微服务没有UI,但暴露了一些REST API. - **IdentityService.Host**: 托管用于管理用户和角色的ABP Identity模块. 它没有其他服务,仅托管Identity模块的API. +- **TenantManagementService.Host**: 托管用于管理角色的ABP租户管理模块. 它没有其他服务,仅托管租户管理模块的API. - **BloggingService.Host**: 托管ABP博客模块,该模块用于管理博客和帖子(典型的博客应用程序). 它没有其他服务,仅托管Blogging模块的API. - **ProductService.Host**: 托管用于管理产品的产品模块(位于解决方案内). 它还包含用于创建/更新产品管理数据库架构的EF Core迁移. @@ -193,7 +128,7 @@ Visual Studio解决方案由多个项目组成,每个项目在系统中具有不 此解决方案使用多个数据库: -* **MsDemo_Identity**: 一个SQL数据库. 默认使用**SQL Server**,但可以是EF Core支持的任何DBMS. 由AuthServer和IdentityService共享. 审计日志,权限和设置也存储在此数据库中(虽然它们可以轻松拥有自己的数据库,共享相同的数据库以保持简单). +* **MsDemo_Identity**: 一个SQL数据库. 默认使用**SQL Server**,但可以是EF Core支持的任何DBMS. 由AuthServer,IdentityService和TenantManagementService共享. 审计日志,权限和设置也存储在此数据库中(虽然它们可以轻松拥有自己的数据库,共享相同的数据库以保持简单). * **MsDemo_ProductManagement**: 一个SQL数据库. 同样默认使用 **SQL Server**,但可以是EF Core支持的任何DBMS. 由ProductService用作专用数据库. * **MsDemo_Blogging**: **MongoDB**数据库. 由BloggingService使用. * **Elasticsearch**: 用于在Serilog上写日志. @@ -341,7 +276,7 @@ BackendAdminApp.Host项目本身没有单个UI元素/页面. 它仅用于提供 ##### HTTP Clients -ABP应用程序模块通常提供C#客户端库以轻松地使用服务(API)(它们通常使用ABP框架的[Dynamic C# API客户端](../AspNetCore/Dynamic-CSharp-API-Clients.md)). 这意味着如果你需要使用Identity Service API, 你可以引用其客户端软件包,并通过提供的接口轻松使用API. +ABP应用程序模块通常提供C#客户端库以轻松地使用服务(API)(它们通常使用ABP框架的[Dynamic C# API客户端](../API/Dynamic-CSharp-API-Clients.md)). 这意味着如果你需要使用Identity Service API, 你可以引用其客户端软件包,并通过提供的接口轻松使用API. 为此`BackendAdminAppHostModule`类声明了`AbpIdentityHttpApiClientModule`和`ProductManagementHttpApiClientModule`的依赖关系. @@ -1104,7 +1039,7 @@ ABP提供强大的基础架构,通过提供服务和架构,使模块化应用程 * `ProductManagement.Application` 包含应用程序服务的实现. * `ProductManagement.EntityFrameworkCore` 包含DbContext和其他与EF Core相关的类和配置. * `ProductManagement.HttpApi` 包含API控制器. -* `ProductManagement.HttpApi.Client` 包含C#代理以远程直接使用HTTP API. 使用ABP的[Dynamic C#API客户端](../AspNetCore/Dynamic-CSharp-API-Clients.md)功能. +* `ProductManagement.HttpApi.Client` 包含C#代理以远程直接使用HTTP API. 使用ABP的[Dynamic C#API客户端](../API/Dynamic-CSharp-API-Clients.md)功能. * `ProductManagement.Web` 包含UI元素(页面,脚本,样式..等). @@ -1474,3 +1409,7 @@ ABP提供自动审计日志记录,详细保存每个请求(当前用户,浏览 所有服务和应用程序都配置为编写审核日志. 审核日志将保存到MsDemo_Identity SQL数据库中. 因此,您可以从单个点查询所有应用程序的所有审核日志. 审核日志记录具有`CorrelationId`属性,可用于跟踪请求. 当服务在单个Web请求中调用另一个服务时,它们都会使用相同的`CorrelationId`保存审核日志. 请参阅数据库中的`AbpAuditLogs`表. + +### 多租户 + +该解决方案已配置提供[多租户](../Multi-Tenancy.md)系统,其中每个租户可以拥有其隔离的用户,角色,权限和其他数据. \ No newline at end of file diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md index 2a0263d43a..78d43759db 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -2,11 +2,16 @@ ## 介绍 -应用程序启动模板是基于[领域驱动设计](../Domain-Driven-Design.md)(DDD)分层的应用程序结构. 在这篇文档中详细介绍了解决方案结构和项目,如果你想快速入门,请遵循以下指南: +应用程序启动模板是基于[领域驱动设计](../Domain-Driven-Design.md)(DDD)分层的应用程序结构. -* 参阅[ASP.NET Core MVC 模板入门](../Getting-Started-AspNetCore-MVC-Template.md)创建此模板的新解决方案并运行它. +在这篇文档中详细介绍了**解决方案结构**和项目,如果你想快速入门,请遵循以下指南: + +* 参阅[ASP.NET Core MVC 模板入门](../Getting-Started-With-Startup-Templates.md)创建此模板的新解决方案并运行它. * 参阅[ASP.NET Core MVC 教程](../Tutorials/AspNetCore-Mvc/Part-I.md)学习使用此模板开发应用程序. +* [入门文档](../Getting-Started-AspNetCore-MVC-Template.md) 介绍了如何在几分钟内创建一个新的应用程序. +* [应用程序开发教程](../Tutorials/AspNetCore-Mvc/Part-I.md) 学习使用此模板开发应用程序. + ## 如何开始 你可以使用[ABP CLI](../CLI.md)创建基于此启动模板的新项目,或者你也可以在[入门](https://abp.io/get-started)页面创建并下载项目. 在这里我们使用CLI创建新项目. @@ -120,6 +125,7 @@ ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 * 它依赖 `.EntityFrameworkCore` 项目,因为它重用了应用程序的 `DbContext` 配置 . > 只有在你使用了EF Core做为数据库提供程序时,此项目才会可用. +> 参阅[Entity Framework Core迁移指南](../Entity-Framework-Core-Migrations.md)了解这个项目的详细信息. #### .DbMigrator 项目 @@ -142,7 +148,7 @@ ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 用于定义API控制器. -大多数情况下,你不需要手动定义API控制器,因为ABP的[动态API](../AspNetCore/Auto-API-Controllers.md)功能会根据你的应用层自动创建API控制器. 但是,如果你需要编写API控制器,那么它是最合适的地方. +大多数情况下,你不需要手动定义API控制器,因为ABP的[动态API](../API/Auto-API-Controllers.md)功能会根据你的应用层自动创建API控制器. 但是,如果你需要编写API控制器,那么它是最合适的地方. * 它依赖 `.Application.Contracts` 项目,因为它需要注入应用服务接口. @@ -150,7 +156,7 @@ ABP是一个模块化的框架,理想的设计是让每个模块都有自己的 定义C#客户端代理使用解决方案的HTTP API项目. 可以将上编辑共享给第三方客户端,使其轻松的在DotNet应用程序中使用你的HTTP API(其他类型的应用程序可以手动或使用其平台的工具来使用你的API). -ABP有[动态 C# API 客户端](../AspNetCore/Dynamic-CSharp-API-Clients.md)功能,所以大多数情况下你不需要手动的创建C#客户端代理. +ABP有[动态 C# API 客户端](../API/Dynamic-CSharp-API-Clients.md)功能,所以大多数情况下你不需要手动的创建C#客户端代理. `.HttpApi.Client.ConsoleTestApp` 项目是一个用于演示客户端代理用法的控制台应用程序. diff --git a/docs/zh-Hans/Tutorials/Angular/Part-I.md b/docs/zh-Hans/Tutorials/Angular/Part-I.md new file mode 100644 index 0000000000..8e8c74ec59 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Angular/Part-I.md @@ -0,0 +1,3 @@ +## Angular 教程 - 第一章 + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md index 5394890f8d..176ccbc6af 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md @@ -16,7 +16,7 @@ ### 创建项目 -创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序. +创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序. ### 解决方案的结构 @@ -24,16 +24,16 @@ ![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) -> 你可以查看[应用程序模板文档](../../Startup-Templates/Application.md)以详细了解解决方案结构.但是,你将通过本教程了解基础知识. +> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构.但是,你将通过本教程了解基础知识. ### 创建Book实体 启动模板中的域层分为两个项目: - - `Acme.BookStore.Domain`包含你的[实体](../../Entities.md), [领域服务](../../Domain-Services.md)和其他核心域对象. + - `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. -在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](../../Entities.md). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: +在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: ````C# using System; @@ -66,7 +66,7 @@ namespace Acme.BookStore } ```` -* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](../../Entities.md). +* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities). * `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). * `Guid`是`Book`实体的主键类型. * 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling). @@ -138,7 +138,7 @@ PM> Update-Database ### 创建应用服务 -下一步是创建[应用服务](../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: +下一步是创建[应用服务](../../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: * `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口. * `Acme.BookStore.Application`包含应用程序服务的实现. @@ -166,7 +166,7 @@ namespace Acme.BookStore } ```` -* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](../../Data-Transfer-Objects.md)查看更多信息. +* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. * 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. * `BookDto`继承自 `AuditedEntityDto`.跟上面定义的`Book`类一样具有一些审计属性. @@ -217,7 +217,7 @@ namespace Acme.BookStore ```` * 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. -* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](../../Validation.md). +* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). 就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射: @@ -281,12 +281,12 @@ namespace Acme.BookStore ```` * `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. -* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](../../Repositories.md) +* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) * `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作. ### 自动生成API Controllers -你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](../../AspNetCore/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器. +你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器. #### Swagger UI @@ -392,7 +392,7 @@ context.Menu.AddItem( } ```` -* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](../../Localization.md). +* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization). * 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为). 运行该应用程序,看到新菜单项已添加到顶部栏: @@ -437,8 +437,8 @@ context.Menu.AddItem(
```` -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](../../AspNetCore/Bundling-Minification.md)获取更多信息. -* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](../../AspNetCore/Tag-Helpers/Index.md). +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息. +* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index). * 你可以像上面本地化菜单一样 **本地化** 列名. #### 添加脚本文件 diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md index e48cf6b710..a5e51134fa 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md @@ -68,7 +68,7 @@ namespace Acme.BookStore ```` * 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. -* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../Guid-Generation.md)了解更多信息). +* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息). ### 测试 BookAppService diff --git a/docs/zh-Hans/Tutorials/Index.md b/docs/zh-Hans/Tutorials/Index.md new file mode 100644 index 0000000000..f7eea987e4 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Index.md @@ -0,0 +1,6 @@ +# 教程 + +## 应用开发 + +* [使用ASP.NET Core MVC/ Razor Pages UI](AspNetCore-Mvc/Part-I.md) +* [使用Angular UI](Angular/Part-I.md) diff --git a/docs/zh-Hans/UI/Angular/AddingSettingTab.md b/docs/zh-Hans/UI/Angular/AddingSettingTab.md new file mode 100644 index 0000000000..c46a559957 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/AddingSettingTab.md @@ -0,0 +1,3 @@ +## Creating a Settings Tab + +TODO... diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md new file mode 100644 index 0000000000..d2369901dc --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -0,0 +1,3 @@ +# Component Replacement + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Customization-User-Interface.md b/docs/zh-Hans/UI/Angular/Customization-User-Interface.md new file mode 100644 index 0000000000..c86d967ec9 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Customization-User-Interface.md @@ -0,0 +1,3 @@ +# Angular用户界面自定义指南 + +* [替换组件](Component-Replacement.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Localization.md b/docs/zh-Hans/UI/Angular/Localization.md new file mode 100644 index 0000000000..c8e836a177 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Localization.md @@ -0,0 +1,3 @@ +# Localization + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/Permission-Management.md b/docs/zh-Hans/UI/Angular/Permission-Management.md new file mode 100644 index 0000000000..c562f7e9a7 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Permission-Management.md @@ -0,0 +1,3 @@ +# Permission Management + +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/images/component-replacement.gif b/docs/zh-Hans/UI/Angular/images/component-replacement.gif new file mode 100644 index 0000000000..3a88500e53 Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/component-replacement.gif differ diff --git a/docs/zh-Hans/UI/Angular/images/custom-settings.png b/docs/zh-Hans/UI/Angular/images/custom-settings.png new file mode 100644 index 0000000000..32d7516849 Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/custom-settings.png differ diff --git a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md new file mode 100644 index 0000000000..ebb402b273 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md @@ -0,0 +1,354 @@ + +## ASP.NET Core MVC 捆绑 & 压缩 + +有许多方法可以捆绑&压缩客户端资源(JavaScript和CSS文件). 最常见的方式是: + +* 使用Visual Studio[捆绑&压缩](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier)扩展或者其它的[NuGet相关包](https://www.nuget.org/packages/BuildBundlerMinifier/). + +* 使用[Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/)及其插件. + +ABP内置了简单,动态,强大,模块化的方式. + +### Volo.Abp.AspNetCore.Mvc.UI.Bundling 包 + +> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. + +将`Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget包安装到你的项目中: + +```` +install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +然后将`AbpAspNetCoreMvcUiBundlingModule`依赖项添加到你的模块上: + +````C# +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] + public class MyWebModule : AbpModule + { + //... + } +} +```` + +### Razor Bundling Tag Helpers + +创建bundle的最简单方法是使用`abp-script-bundle`或`abp-style-bundle` tag helpers. 例如: + +````html + + + + + + +```` + +`abp-script-bundle`定义了一个带有**唯一名称**的样式包:`MyGlobalBundle`. 使用方法很容易理解. 让我们看看它是如何*工作的*: + +* 当首次请求时,ABP从提供的文件中 **(延迟)lazy** 创建. 后续将从 **缓存** 中返回内容. 这意味着如果你有条件地将文件添加到包中,它只执行一次, 并且条件的任何更改都不会影响下一个请求的包. +* 在`development`环境中ABP会将包文件**单独**添加到页面中, 其他环境(`staging`,`production`...)会自动捆绑和压缩. +* 捆绑文件可以是**物理**文件或[**虚拟/嵌入**](../../Virtual-File-System.md)的文件. +* ABP自动将 **版本查询字符串(version query string)** 添加到捆绑文件的URL中,以防止浏览器缓存. 如:?_v=67872834243042(从文件的上次更改日期生成). 即使捆绑文件单独添加到页面(在`development`环境中), 版本控制仍然有效. + +#### 导入 Bundling Tag Helpers + +> 默认情况下已在启动模板导入. 大多数情况下,你不需要手动安装它. + +要使用`bundle tag helpers`, 你需要将其添加到`_ViewImports.cshtml`文件或页面中: + +```` +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +#### 未命名的 Bundles + +对于razor bundle tag helpers, `name`是**可选**. 如果没有定义一个名字,它将根据使用的捆绑文件名自动**计算生成**(they are **concatenated** and **hashed**) 例: + +````html + + + + + @if (ViewBag.IncludeCustomStyles != false) + { + + } + +```` + +这将潜在地创建**两个不同的bundles**(一个包括`my-global-style.css`而另一个则不包括). + +**未命名的** bundles优点: + +* 可以**有条件地将项目**添加到捆绑包中. 但这可能会导致基于条件的捆绑的存在多种变化. + +**命名** bundles优点: + +* 其他模块可以通过其名称为捆绑包做出贡献(参见下面的部分). + +#### 单个文件 + +如果你只需要在页面中添加一个文件, 你可以使用`abp-script`或`abp-style`而不需要包含在`abp-script-bundle`或`abp-style-bundle`中. 例: + +````xml + +```` + +对于上面的示例,包名称将是 *scripts.my-scripts*("/"替换为"."). 所有捆绑功能也可以按预期应用于单个文件. + +### Bundling 选项 + +如果你需要在 **多个页面中使用相同的包** 或想要使用更多 **强大功能**, 你可以在[模块](../../Module-Development-Basics.md)类中进行**配置**. + +#### 创建一个新的捆绑包 + +用法示例: + +````C# +[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] +public class MyWebModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .Add("MyGlobalBundle", bundle => { + bundle.AddFiles( + "/libs/jquery/jquery.js", + "/libs/bootstrap/js/bootstrap.js", + "/libs/toastr/toastr.min.js", + "/scripts/my-global-scripts.js" + ); + }); + }); + } +} +```` + +> 你可以在脚本和样式包中使用相同的名称(*MyGlobalBundle*), 因为它们被添加到不同的集合(`ScriptBundles`和`StyleBundles`). + +在定义bundle之后, 可以使用上面定义的相同tag helpers将其包括在页面中. 例如: + +````html + +```` + +这次tag helper定义中没有定义文件, 因为捆绑文件是由代码定义的. + +#### 配置现有的 Bundle + +ABP也支持[模块化](../../Module-Development-Basics.md)捆绑. 模块可以修改由依赖模块创建的捆绑包. +例如: + +````C# +[DependsOn(typeof(MyWebModule))] +public class MyWebExtensionModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .Configure("MyGlobalBundle", bundle => { + bundle.AddFiles( + "/scripts/my-extension-script.js" + ); + }); + }); + } +} +```` + +> 无法通过代码配置未命名的bundle tag helpers, 因为它们的名称在开发时是未知的. 建议始终使用bundle tag helper的名称. + +### Bundle 贡献者 + +将文件添加到现有bundle似乎很有用. 如果你需要**替换**bundle中的文件或者你想**有条件地**添加文件怎么办? 定义bundle贡献者可为此类情况提供额外的功能. + +一个bundle的贡献者使用自定义版本bootstrap.css替换示例: + +````C# +public class MyExtensionGlobalStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.ReplaceOne( + "/libs/bootstrap/css/bootstrap.css", + "/styles/extensions/bootstrap-customized.css" + ); + } +} +```` + +然后你可以按照下面的代码使用这个贡献者: + +````C# +services.Configure(options => +{ + options + .ScriptBundles + .Configure("MyGlobalBundle", bundle => { + bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); + }); +}); +```` + +贡献者也可以在bundle tag helpers中使用. +例如: + +````xml + + + + + +```` + +`abp-style`和`abp-script`标签可以使用`type`属性(而不是`src`属性), 如本示例所示. 添加bundle贡献者时, 其依赖关系也会自动添加到bundle中. + +#### 贡献者依赖关系 + +bundle贡献者可以与其他贡献者具有一个或多个依赖关系. +例如: + +````C# +[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency +public class MyExtensionStyleBundleContributor : BundleContributor +{ + //... +} +```` + +添加bundle贡献者时,其依赖关系将 **自动并递归** 添加. **依赖顺序** 通过阻止 **重复** 添加的依赖关系. 即使它们处于分离的bundle中,也会阻止重复. ABP在页面中组织所有bundle并消除重复. + +创建贡献者和定义依赖关系是一种跨不同模块组织bundle创建的方法. + + +#### 贡献者扩展 + +在某些高级应用场景中, 当用到一个bundle贡献者时,你可能想做一些额外的配置. 贡献者扩展可以和被扩展的贡献者无缝衔接. + +下面的示例为 prism.js 脚本库添加一些样式: + +````csharp +public class MyPrismjsStyleExtension : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); + } +} +```` + +然后你可以配置 `AbpBundleContributorOptions` 去扩展已存在的 `PrismjsStyleBundleContributor`. + +````csharp +Configure(options => +{ + options + .Extensions() + .Add(); +}); +```` + +任何时候当 `PrismjsStyleBundleContributor` 被添加到bundle中时, `MyPrismjsStyleExtension` 也会被自动添加. + +#### 访问 IServiceProvider + +虽然很少需要它, 但是`BundleConfigurationContext`有一个`ServiceProvider`属性, 你可以在`ConfigureBundle`方法中解析服务依赖. + +#### 标准包装贡献者 + +将特定的NPM包资源(js,css文件)添加到包中对于该包非常简单. 例如, 你总是为bootstrap NPM包添加`bootstrap.css`文件. + +所有[标准NPM包](Client-Side-Package-Management.md)都有内置的贡献者. 例如,如果你的贡献者依赖于bootstrap,你可以声明它,而不是自己添加bootstrap.css. + +````C# +[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency +public class MyExtensionStyleBundleContributor : BundleContributor +{ + //... +} +```` + +使用标准包的内置贡献者: + +* 防止你输入**无效的资源路径**. +* 如果资源 **路径发生变化** (依赖贡献者将处理它),则防止更改你的贡献者. +* 防止多个模块添加**重复文件**. +* 以递归方式管理依赖项(如果需要,添加依赖项的依赖项). + +##### Volo.Abp.AspNetCore.Mvc.UI.Packages 包 + +> 默认情况下已在启动模板安装此软件包. 大多数情况下,你不需要手动安装它. + +标准包贡献者在`Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet包中定义. +将它安装到你的项目中: + +```` +install-package Volo.Abp.AspNetCore.Mvc.UI.Packages +```` + +然后将`AbpAspNetCoreMvcUiPackagesModule`模块依赖项添加到你的模块中; + +````C# +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] + public class MyWebModule : AbpModule + { + //... + } +} +```` + +#### Bundle 继承 + +在某些特定情况下, 可能需要从其他bundle创建一个 **新** bundle **继承**, 从bundle继承(递归)会继承该bundle的所有文件/贡献者. 然后派生的bundle可以添加或修改文件/贡献者**而无需修改**原始bundle. +例如: + +````c# +services.Configure(options => +{ + options + .StyleBundles + .Add("MyTheme.MyGlobalBundle", bundle => { + bundle + .AddBaseBundles("MyGlobalBundle") //Can add multiple + .AddFiles( + "/styles/mytheme-global-styles.css" + ); + }); +}); +```` + +### 主题 + +主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md). + +### 最佳实践 & 建议 + +建议为应用程序定义多个包, 每个包用于不同的目的. + +* **全局包**: 应用程序中的每个页面都包含全局样式/脚本包. 主题已经定义了全局样式和脚本包. 你的模块可以为他们做出贡献. +* **布局包**: 这是针对单个布局的特定包. 仅包含在所有页面之间共享的资源使用布局. 使用bundling tag helpers创建捆绑包是一种很好的做法. +* **模块包**: 用于单个模块页面之间的共享资源. +* **页面包**: 为每个页面创建的特定包. 使用bundling tag helpers创建捆绑包作为最佳实践. + +在性能,网络带宽使用和捆绑包的数量之间建立平衡. + +### 参见 + +* [客户端包管理](Client-Side-Package-Management.md) +* [主题](Theming.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md b/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md new file mode 100644 index 0000000000..2c50064be0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Client-Side-Package-Management.md @@ -0,0 +1,115 @@ + +## ASP.NET Core MVC 客户端包管理 + +ABP框架可以与任何类型的客户端包管理系统一起使用. 甚至你可以决定不使用包管理系统并手动管理依赖项. + +但是, ABP框架最适用于**NPM/Yarn**. 默认情况下,内置模块配置为与NPM/Yarn一起使用. + +最后, 我们建议[**Yarn**](https://classic.yarnpkg.com/)而不是NPM,因为它更快,更稳定并且与NPM兼容. + +### @ABP NPM Packages + +ABP是一个模块化平台. 每个开发人员都可以创建模块, 模块应该在**兼容**和**稳定**状态下协同工作. + +一个挑战是依赖NPM包的**版本**. 如果两个不同的模块使用相同的JavaScript库但其不同(并且可能不兼容)的版本会怎样. + +为了解决版本问题, 我们创建了一套**标准包**, 这取决于一些常见的第三方库. 一些示例包是[@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@ abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap)和[@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). 你可以从[Github存储库](https://github.com/volosoft/abp/tree/master/npm/packs)中查看**列表**. + +**标准包**的好处是: + +* 它取决于包装的**标准版本**. 取决于此包是**安全**,因为所有模块都依赖于相同的版本. +* 它包含将库资源(js,css,img...文件)从**node_modules**文件夹复制到**wwwroot/libs**文件夹的gulp任务. 有关更多信息, 请参阅 *映射库资源* 部分. + +依赖标准包装很容易. 只需像往常一样将它添加到**package.json**文件中. 例如: + +```` + { + ... + "dependencies": { + "@abp/bootstrap": "^1.0.0" + } + } +```` + +建议依赖于标准软件包, 而不是直接依赖于第三方软件包. + +#### 安装包 + +依赖于NPM包后, 你应该做的就是从命令行运行**yarn**命令来安装所有包及其依赖项: + +```` +yarn +```` + +虽然你可以使用`npm install`,但如前所述,建议使用[Yarn](https://classic.yarnpkg.com/). + +#### 贡献包 + +如果你需要不在标准软件包中的第三方NPM软件包,你可以在Github[repository](https://github.com/volosoft/abp)上创建Pull请求. 接受遵循这些规则的拉取请求: + +* 对于NPM上的`package-name`, 包名称应该命名为`@abp/package-name`(例如:`bootstrap`包的`@abp/bootstrap`). +* 它应该是**最新的稳定**版本的包. +* 它应该只依赖于**单个**第三方包. 它可以依赖于多个`@abp/*`包. +* 包应包含一个`abp.resourcemapping.js`文件格式,如*映射库资源*部分中所定义. 此文件应仅映射所依赖包的资源. +* 你还需要为你创建的包创建[bundle贡献者](Bundling-Minification.md). + +有关示例, 请参阅当前标准包. + +### 映射库资源 + +使用NPM包和NPM/Yarn工具是客户端库的事实标准. NPM/Yarn工具在Web项目的根文件夹中创建一个**node_modules**文件夹. + +下一个挑战是将所需的资源(js,css,img ...文件)从`node_modules`复制到**wwwroot**文件夹内的文件夹中,以使其可供客户端/浏览器访问. + +ABP将基于[Gulp](https://gulpjs.com/)的任务定义为**将资源**从**node_modules**复制到**wwwroot/libs**文件夹. 每个**标准包**(参见*@ABP NPM Packages*部分)定义了自己文件的映射. 因此, 大多数情况你只配置依赖项. + +**启动模板**已经配置为开箱即用的所有这些. 本节将介绍配置选项. + +#### 资源映射定义文件 + +模块应该定义一个名为`abp.resourcemapping.js`的JavaScript文件,其格式如下例所示: + +````js +module.exports = { + aliases: { + "@node_modules": "./node_modules", + "@libs": "./wwwroot/libs" + }, + clean: [ + "@libs" + ], + mappings: { + + } +} +```` + +* **aliases**部分定义了可在映射路径中使用的标准别名(占位符). **@node_modules**和 **@libs**是必需的(通过标准包), 你可以定义自己的别名以减少重复. +* **clean**部分是在复制文件之前要清理的文件夹列表. +* **mappings**部分是要复制的文件/文件夹的映射列表.此示例不会复制任何资源本身,但取决于标准包. + +示例映射配置如下所示: + +````js +mappings: { + "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", + "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/" +} +```` + +#### 使用 Gulp + +正确配置`abp.resourcemapping.js`文件后, 可以从命令行运行gulp命令: + +```` +gulp +```` + +当你运行`gulp`时,所有包都会将自己的资源复制到**wwwroot/libs**文件夹中. 只有在**package.json**文件中对依赖项进行更改时, 才需要运行`yarn&gulp`. + +> 运行Gulp命令时, 使用package.json文件解析应用程序的依赖关系. Gulp任务自动发现并映射来自所有依赖项的所有资源(递归). + +#### 参见 + +* [捆绑 & 压缩](Bundling-Minification.md) +* [主题](Theming.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md new file mode 100644 index 0000000000..aa84c85a3d --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md @@ -0,0 +1,469 @@ +# ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南 + +本文档解释了如何重写ASP.NET Core MVC / Razor Page 应用程序依赖[应用模块](../../Modules/Index.md)的用户界面. + +## 重写页面 + +本节介绍了[Razor 页面](https://docs.microsoft.com/zh-cn/aspnet/core/razor-pages/)开发,它是ASP.NET Core推荐的服务端渲染用户页面的方法. 预构建的模块通常使用Razor页面替代经典的MVC方式(下一节也介绍MVC模式). + +你通过有三种重写页面的需求: + +* 仅**重写页面模型**(C#)端执行其他逻辑,不更改UI. +* 仅**重写Razor页面**(.cshtml文件),不更改逻辑. +* **完全重写** 页面. + +### 重写页面模型 (C#) + +````csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Identity.Web.Pages.Identity.Users; + +namespace Acme.BookStore.Web.Pages.Identity.Users +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(EditModalModel))] + public class MyEditModalModel : EditModalModel + { + public MyEditModalModel( + IIdentityUserAppService identityUserAppService, + IIdentityRoleAppService identityRoleAppService + ) : base( + identityUserAppService, + identityRoleAppService) + { + } + + public override async Task OnPostAsync() + { + //TODO: Additional logic + await base.OnPostAsync(); + //TODO: Additional logic + } + } +} +```` + +* 这个类继承并替换 `EditModalModel` ,重写了 `OnPostAsync` 方法在基类代码的前后执行附加逻辑 +* 它使用 `ExposeServices` 和 `Dependency` attributes去替换这个类. + +### 重写Razor页面 (.CSHTML) + +使用[虚拟文件系统](../../Virtual-File-System.md)可以重写 `.cshtml` 文件(razor page, razor view, view component... 等.) + +虚拟文件系统允许我们将**资源嵌入到程序集中**. 通过这个方式,预构建的模块在Nuget包中定义了Razor页面. 当你依赖模块时,可以覆盖这个模块向虚拟文件系统添加的任何文件,包括页面/视图. + +#### 示例 + +这个示例重写了[账户模块](../../Modules/Account.md)定义的**登录页面**UI + +物理文件可以覆盖相同位置的嵌入文件. 账户模块在 `Pages/Account` 文件夹下定义了 `Login.cshtml` 文件. 所以你可以在同一路径下创建文件覆盖它: +![overriding-login-cshtml](../../images/overriding-login-cshtml.png) + +通常你想要拷贝模块的 `.cshtml` 原文件,然后进行需要的更改. 你可以在[这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到源文件. 不要拷贝 `Login.cshtml.cs` 文件,它是隐藏razor页面的代码,我们不希望覆盖它(见下节). + +这就够了,接下来你可以对文件内容做你想要的更改. + +### 完全重写Razo页面 + +也许你想要完全重写页面,Razor和页面相关的C#文件. + +在这种情况下; + +1. 像上面描述过的那术重写C#页面模型类,但不需要替换已存在的页面模型类. +2. 像上面描述过的那样重写Razor页面,并且更改@model指向新的页面模型 + +#### 示例 + +这个示例重写了[账户模块](../../Modules/Account.md)定义的**登录页面** + +创建一个继承自 `LoginModel`(定义在`Volo.Abp.Account.Web.Pages.Account`命名空间下)的页面模型类: + +````csharp +public class MyLoginModel : LoginModel +{ + public MyLoginModel( + IAuthenticationSchemeProvider schemeProvider, + IOptions accountOptions + ) : base( + schemeProvider, + accountOptions) + { + + } + + public override Task OnPostAsync(string action) + { + //TODO: Add logic + return base.OnPostAsync(action); + } + + //TODO: Add new methods and properties... +} +```` + +如果需要,你可以重写任何方法或添加新的属性/方法 + +> 注意我们没有使用 `[Dependency(ReplaceServices = true)]` 或 `[ExposeServices(typeof(LoginModel))]`,因为我们不想替换依赖注入中已存在的类,我们定义了一个新的. + +拷贝 `Login.cshtml` 到你们解决方案,更改 **@model** 指定到 `MyLoginModel`: + +````xml +@page +... +@model Acme.BookStore.Web.Pages.Account.MyLoginModel +... +```` + +这就够了,接下来你可以做任何想要更改. + +#### 不使用继承替换页面模型 + +你不需要继承源页面模型类(像之前的示例). 你可以完全**重写实现**你自己的页面. 在这种事情下你可以从 `PageModel`,`AbpPageModel` 或任何你需要的合适的基类派生. + +## 重写视图组件 + +在ABP框架,预构建的模块和主题定义了一些**可重用的视图组件**. 这些视图组件可以像页面一样被替换. + +### 示例 + +下面是应用程序启动模板自带的 **基本主题** 的截图. + +![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) + +[基本主题](../../Themes/Basic.md) 为layout定义了一些视图组件. 例如上面带有红色矩形的突出显示区域称为 **Brand组件**, 你可能想添加自己的**自己的应用程序logo**来自定义此组件. 让我们来看看如何去做. + +首先创建你的logo并且放到你的web应用程序文件夹中,我们使用 `wwwroot/logos/bookstore-logo.png` 路径. 然后在 `Themes/Basic/Components/Brand` 文件夹下复制[Brand组件视图](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml). 结果应该是类似下面的图片: + +![bookstore-added-brand-files](../../images/bookstore-added-brand-files.png) + +然后对 `Default.cshtml` 文件做你想要的更改. 例如内容可以是这样的: + +````xml + + + +```` + +现在你可以运行应用程序看到结果: + +![bookstore-added-logo](../../images/bookstore-added-logo.png) + +如果你需要,你也可以仅使用依赖注入系统替换组件[背后的C#类代码](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/MainNavbarBrandViewComponent.cs) + +### 重写主题 + +正如上所解释的,你可以更改任何组件,layout或c#类. 参阅[主题文档]了解更多关于主题系统的信息. + +## 重写静态资源 + +重写模块的静态资源(像JavaScript,Css或图片文件)是很简单的. 只需要在解决方案的相同路径创建文件,虚拟文件系统会自动处理它. + +## 操作捆绑 + +[捆绑 & 压缩](Bundling-Minification.md) 系统提供了**动态可扩展的** 系统去创建**script**和**style**捆绑. 它允许你扩展和操作现有的包. + +### 示例: 添加全局CSS文件 + +例如APP框架定义了一个**全局样式捆绑**添加到所有的页面(事实上由主题添加layout). 让我们添加一个**自定义样式文件**到这个捆绑文件的最后,我们可以覆盖任何全局样式. + +创建在 `wwwroot` 文件夹下创建一个CSS文件 + +![bookstore-global-css-file](../../images/bookstore-global-css-file.png) + +在CSS文件中定义一些规则. 例如: + +````css +.card-title { + color: orange; + font-size: 2em; + text-decoration: underline; +} + +.btn-primary { + background-color: red; +} +```` + +然后在你的[模块](../../Module-Development-Basics.md) `ConfigureServices` 方法添加这个文件到标准的全局样式捆绑包: + +````csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, //The bundle name! + bundleConfiguration => + { + bundleConfiguration.AddFiles("/styles/my-global-styles.css"); + } + ); +}); +```` + +#### 全局脚本捆绑包 + +就像 `StandardBundles.Styles.Global` 一样,还有一个 `StandardBundles.Scripts.Global`,你可以添加文件或操作现有文件. + +### 示例: 操作捆绑包文件 + +上面的示例中添加了新文件到捆绑包. 如果你创建 **bundle contributor** 类则可以做到更多. 示例: + +````csharp +public class MyGlobalStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Clear(); + context.Files.Add("/styles/my-global-styles.css"); + } +} +```` + +然后你可以添加这个contributor到已存在的捆绑中: + +````csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundleConfiguration => + { + bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor)); + } + ); +}); +```` + +示例中清除了所有的CSS文件,在现实中这并不是一个好主意,你可以找到某个特定的文件替换成你自己的文件. + +### 示例: 为特定页面添加JavaScript文件 + +上面的示例将全局包添加到布局中. 如果要在依赖模块中为特定页面定义添加CSS/JavaScript文件(或替换文件)怎么做? + +假设你想要用户进入身份模块的**角色管理**页面时运行**JavaScript代码**. + +首先在 `wwwroot`, `Pages` 或 `Views` 文件夹下创建一个标准的JavaScript文件(默认ABP支持这些文件夹下的静态文件). 根据约定我们推荐 `Pages/Identity/Roles` 文件夹: + +![bookstore-added-role-js-file](../../images/bookstore-added-role-js-file.png) + +该文件的内容很简单: + +````js +$(function() { + abp.log.info('My custom role script file has been loaded!'); +}); +```` + +然后将这个文件添加到角色管页面理捆绑包中: + +````csharp +Configure(options => +{ + options.ScriptBundles + .Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName, + bundleConfig => + { + bundleConfig.AddFiles("/Pages/Identity/Roles/my-role-script.js"); + }); +}); +```` + +`typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName` 是获取角色管理页面捆绑包名称的安全方式: + +> 请注意并非每个页面都定义了这个页面的捆绑包. 它们仅在需要时定义. + +除了添加新的CSS/JavaScript文件到页面,你也可以以替换(通过捆绑包contributor)已存在. + +## 布局定制 + +布局由主题([参阅主题](Theming.md))定义设计. 它们不包含在下载的应用程序解决方案中. 通过这种方式你可以轻松的**更改**主题并获取新的功能. 你不能**直接更改**应用程序中的布局代码,除非你用自己的布局替换它(在下一部分中说明). + +有一些通用的方法可以**自定义布局**,将在下一节中介绍. + +### 菜单贡献者 + +ABP框架定义了两个**标准菜单**: + +![bookstore-menus-highlighted](../../images/bookstore-menus-highlighted.png) + +* `StandardMenus.Main`: 应用程序的主菜单. +* `StandardMenus.User`: 用户菜单 (通常在屏幕的右上方). + +显示菜单是主题的责任,但**菜单项**由模板和你的应用程序代码决定. 只需要实现 `IMenuContributor` 接口并在 `ConfigureMenuAsync` 方法操作菜单项. + +渲染菜单时需要执行菜单贡献者. **应用程序启动模板** 已经定义了菜单贡献者,所以你可以使用它. 参阅[导航菜单](Navigation-Menu.md)文档了解更多. + +### 工具栏贡献者 + +[工具栏系统](Toolbars.md)用于在用户界面定义 **工具栏** . 模块 (或你的应用程序)可以将 **项** 添加到工具栏, 随后主题将在**布局**上呈现工具栏. + +只有一个 **标准工具栏** (名称为 "Main" - 定义为常量: `StandardToolbars.Main`). 对于基本主题,按如下呈现:![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) + +在上面的屏幕快照中,主工具栏添加了两个项目:语言开关组件和用户菜单. 你可以在此处添加自己的项. + +#### 示例: 添加通知图标 + +在这个示例中,我们会添加一个**通知(响铃)图标**到语言切换项的左侧. 工具栏的项项目是一个**视图组件**. 所以,在你的项目中创建一个新的视图组件: + +![bookstore-notification-view-component](../../images/bookstore-notification-view-component.png) + +**NotificationViewComponent.cs** + +````csharp +public class NotificationViewComponent : AbpViewComponent +{ + public async Task InvokeAsync() + { + return View("/Pages/Shared/Components/Notification/Default.cshtml"); + } +} +```` + +**Default.cshtml** + +````xml +
+ +
+```` + +现在,我们创建一个类实现 `IToolbarContributor` 接口: + +````csharp +public class MyToolbarContributor : IToolbarContributor +{ + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items + .Insert(0, new ToolbarItem(typeof(NotificationViewComponent))); + } + + return Task.CompletedTask; + } +} +```` + +这个类向 `Main` 工具栏的第一项添加了 `NotificationViewComponent`. + +最后你需要将这个贡献者添加到 `AbpToolbarOptions`,在你模块类的 `ConfigureServices` 方法: + +````csharp +Configure(options => +{ + options.Contributors.Add(new MyToolbarContributor()); +}); +```` + +这就够了,当你运行应用程序后会看到工具栏上的通知图标: + +![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) + +示例中的 `NotificationViewComponent` 返回没有任何数据的视图. 在实际场景中,你可能想**查询数据库**(或调用HTTP API)获取通知并传递给视图. 如果需要可以将 `JavaScript` 或 `CSS` 文件添加到工具栏的全局捆绑包中(如前所述). + +参阅[工具栏文档](Toolbars.md)了解更多关于工具栏系统. + +### 布局钩子 + +[布局钩子](Layout-Hooks.md) 系统允许你在布局页面的某些特定部分 **添加代码** . 所有主题的所有布局都应该实现这些钩子. 然后你可以将**视图组件**添加到钩子. + +#### 示例: 添加谷歌统计 + +假设你想要添加谷歌统计脚本到布局(将适用所有的页面). 首先在你的项目中**创建一个视图组件**: + +![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 + +```` + +在你自己的代码中更改 `UA-xxxxxx-1` . + +然后你可以在你模块的 `ConfigureServices` 方法将这个组件添加到任何的钩子点: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, //The hook name + typeof(GoogleAnalyticsViewComponent) //The component to add + ); +}); +```` + +现在谷歌统计代码将在页面的 `head` 所为最后一项插入. 你(或你在使用的模块)可以将多个项添加到相同的钩子,它们都会添加到布局. + +在上面我们添加 `GoogleAnalyticsViewComponent` 到所有的布局,你可能只想添加到指定的布局: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, + typeof(GoogleAnalyticsViewComponent), + layout: StandardLayouts.Application //Set the layout to add + ); +}); +```` + +参阅下面的布局部分,以了解有关布局系统的更多信息. + +### 布局 + +布局系统允许主题定义标准,命名布局并且允许任何页面选择使用合适的布局. 有三种预定义的布局: + +* "**Application**": 应用程序的主要(和默认)布局. 它通常包含页眉,菜单(侧栏),页脚,工具栏等. +* "**Account**": 登录,注册和其他类似页面使用此布局. 默认它用于 `/Pages/Account` 文件夹下的页面. +* "**Empty**": 空的最小的布局. + +这些名称在 `StandardLayouts` 类定义为常量. 这是标准的布局名称,所有的主题开箱即用的实现. 你也可以创建自己的布局. + +#### 布局位置 + +你可以在[这里](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts)找到基本主题的布局文件. 你可以将它们作用构建自己的布局的参考,也可以在必要时覆盖它们. + +#### ITheme + +ABP框架使用 `ITheme` 服务通过局部名称获取布局位置. 你可以替换此服务动态的选择布局位置. + +#### IThemeManager + +`IThemeManager` 用于获取当前主题,并得到了布局路径. 任何页面可以都决定自己的布局. 例: + +````html +@using Volo.Abp.AspNetCore.Mvc.UI.Theming +@inject IThemeManager ThemeManager +@{ + Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty); +} +```` + +此页面将使用空白布局. 它使用 `ThemeManager.CurrentTheme.GetEmptyLayout()` 扩展方法. + +如果你设置特定目录下所有页面的布局,可以在该文件夹下的 `_ViewStart.cshtml` 文件编写以上代码. diff --git a/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md b/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md new file mode 100644 index 0000000000..49b334c6c0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Layout-Hooks.md @@ -0,0 +1,3 @@ +# Layout Hooks + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md b/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md new file mode 100644 index 0000000000..4e0164ec72 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Navigation-Menu.md @@ -0,0 +1,3 @@ +# Navigation Menu + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Alerts.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Alerts.md new file mode 100644 index 0000000000..fa70c1c9b0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Alerts.md @@ -0,0 +1,80 @@ +# Alerts + +## 介绍 + +`abp-alert` 是创建alert的主要元素. + +基本用法: + +````xml + + A simple primary alert—check it out! + +```` + +## Demo + +参阅[alerts demo页面](https://bootstrap-taghelpers.abp.io/Components/Alerts)查看示例. + +## Attributes + +### alert-type + +值做为alert的Type,应为以下值之一: + +* `Default` (默认值) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +示例: + +````xml + + A simple warning alert—check it out! + +```` + +### alert-link + +alert的链接. + +示例: + +````xml + + A simple danger alert with an example link. Give it a click if you like. + +```` + +### dismissible + +使alert可被忽略: + +示例: + +````xml + + Holy guacamole! You should check in on some of those fields below. + +```` + +### Additional content + +`abp-alert`还可以包含其他HTML元素,例如标题,段落和分隔符. + +示例: + +````xml + +

Well done!

+

Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.

+
+

Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

+
+```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md new file mode 100644 index 0000000000..f628f17ef4 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md @@ -0,0 +1,86 @@ +# 按钮 + +## 介绍 + +`abp-button` 是创建按钮的主要元素. + +基本用法: + +````xml +Click Me +```` + +## Demo + +参阅[按钮Demo页面](https://bootstrap-taghelpers.abp.io/Components/Buttons)查看示例. + +## Attributes + +### `button-type` + +指定按钮的主样式/类型. 应为以下值之一: + +* `Default` (默认值) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Outline_Primary` +* `Outline_Secondary` +* `Outline_Success` +* `Outline_Danger` +* `Outline_Warning` +* `Outline_Info` +* `Outline_Light` +* `Outline_Dark` +* `Link` + +### `size` + +指定按钮的大小. 应为以下值之一: + +* `Default` +* `Small` +* `Medium` +* `Large` +* `Block` +* `Block_Small` +* `Block_Medium` +* `Block_Large` + +### `busy-text` + +当按钮busy时显示的文本. + +### `text` + +按钮的文本. 如果你只想为为按钮设置文本,这是一种快捷方式. 例: + +````xml + +```` + +在这个示例中,你可以使用 self-closing 标签将其缩短. + +### `icon` + +设置按钮的图标. 默认情况下它使用[Font Awesome](https://fontawesome.com/)图标库. 例: + +````xml + +```` + +### `icon-type` + +如果你不想使用font-awesome,你有两个选项: + +1. 设置 `icon-type` 为 `Other`,并为你的按钮编写图标样式. +2. 如果你不使用图标,请手动使用opening和closing标签,并在标签内写任何代码. + +### `disabled` + +设置为 `true` 禁用按钮. \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Cards.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Cards.md new file mode 100644 index 0000000000..4cb75ea3b0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Cards.md @@ -0,0 +1,180 @@ +# 卡片 + +## 介绍 + +`abp-card` 是从bootstrap card元素派生的内容容器. + +基本用法: + +````xml + + + + Card Title + Some quick example text to build on the card title and make up the bulk of the card's content. + Go somewhere + + +```` + +##### 使用标题,文本和链接: + +`abp-card` 可以使用以下标签 + +* `abp-card-title` +* `abp-card-subtitle` +* `a abp-card-link` + +示例: + +````xml + + + Card title + Card subtitle + Some quick example text to build on the card title and make up the bulk of the card's content. + Card link + Another link + + +```` + +##### 使用列表组: + +* `abp-list-group flush="true"` : `flush` 属性渲染到 bootstrap `list-group-flush` 的 calss 中,该类用于删除边界和圆角以使列表组项在父容器中并排显示. +* `abp-list-group-item` + +示例: + +````xml + + + + Card Title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + Cras justo odio + Dapibus ac facilisis in + Vestibulum at eros + + + Card link + Another link + + +```` + +##### 使用页眉,页脚和块引用: + +* `abp-card-header` +* `abp-card-footer` +* `abp-blockquote` + +示例: + +```xml + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + +``` + +引用示例: + +```xml + + Quote + + +

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

+
Someone famous in Source Title
+
+
+
+``` + +页脚示例: + +```xml + + Featured + + +

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

+
Someone famous in Source Title
+
+
+ 2 days ago +
+``` + +## Demo + +参阅[卡片demo页面](https://bootstrap-taghelpers.abp.io/Components/Cards)查看示例. + +## abp-card Attributes + +- **background:** 值指定卡片背景的颜色. +- **text-color**: 值指定卡片内文本的颜色. +- **border:** 值指定卡片边框的颜色. + +应为以下值之一: + +* `Default` (默认值) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +示例: + +````xml + +```` + +### sizing + +卡片的默认值为100%,可以使用自定义CSS,栅格类,栅格Sass mixins或[utilities](https://getbootstrap.com/docs/4.0/utilities/sizing/)进行更改. + +````xml + +```` + +### card-deck 和 card-columns + +`abp-card` 可以在 `card-deck` 或 `card-columns` 里使用. + +````xml +
+ + First Deck + + Ace + Here is the content for Ace. + + + + Second Deck + + Beta + Beta content. + + + + Third Deck + + Epsilon + Content for Epsilon. + + +
+```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Collapse.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Collapse.md new file mode 100644 index 0000000000..65327de438 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Collapse.md @@ -0,0 +1,90 @@ +# Collapse + +## 介绍 + +`abp-collapse-body` 是显示和隐藏内容的主要容器. `abp-collapse-id` 用于显示和隐藏内容容器. 可以通过 `abp-button` 和 `a` 标签触发. + +基本用法: + +````xml + + Link with href + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + +```` + +## Demo + +参阅[collapse demo 页面](https://bootstrap-taghelpers.abp.io/Components/Collapse)查看示例. + +## Attributes + +### show + +指定折叠主体初始化时是折叠还是展开. 应为以下值之一: + +* `false` (默认值) +* `true` + +### multi + +指定 `abp-collapse-body` 是否可以通过显示/隐藏多个折叠体的元素显示或隐藏. 此属性做为 "multi-collapse" class 添加到 `abp-collapse-body`. 应为以下值之一: + +* `false` (默认值) +* `true` + +示例: + +````xml + Toggle first element + + + + + + + Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit + + + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. + + + +```` + +## 手风琴示例 + +`abp-accordion` 是手风琴项的主容器. + +基本用法: + +````xml + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry rtat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + +```` + +## Attributes + +### active + +指定手风琴项目在初始化时是显示还是隐藏. 应为以下值之一: + +* `false` (默认值) +* `true` + +### title + +指定手风琴项的标题. 应为字符串类型的值. diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dropdowns.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dropdowns.md new file mode 100644 index 0000000000..971dce7a1c --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dropdowns.md @@ -0,0 +1,93 @@ +# 下拉菜单 + +## 介绍 + +`abp-dropdown` 是下拉菜单的主要容器. + +基本用法: + +````xml + + + + Action + Another action + Something else here + + +```` + +## Demo + +参阅 [下拉菜单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例. + +## Attributes + +### direction + +指定下拉菜单的方向. 应为以下值之一: + +* `Down` (默认值) +* `Up` +* `Right` +* `Left` + +### dropdown-style + +指定 `abp-dropdown-button` 是否具有用于拆分的拆分图标. 应为以下值之一: + +* `Single` (默认值) +* `Split` + +## Menu items + +`abp-dropdown-menu` 下拉菜单项的主要容器. + +基本用法: + +````xml + + + + Dropdown Header + Action + Active action + Disabled action + + Dropdown Item Text + Something else here + + +```` + +## Attributes + +### align + +指定 `abp-dropdown-menu` 在哪个方向对齐. 应为以下值之一: + +* `Left` (默认值) +* `Right` + +### Additional content + +`abp-dropdown-menu` 也可以包含其他HTML元素,例如标题,段落,分隔符或form元素. + +示例: + +````xml + + + +
+ + + + + + + New around here? Sign up + Forgot password? +
+
+```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md new file mode 100644 index 0000000000..ceaa3d3d69 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md @@ -0,0 +1,3 @@ +## Dynamic Forms + +目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Grids.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Grids.md new file mode 100644 index 0000000000..0605e893f0 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Grids.md @@ -0,0 +1,273 @@ +# 栅格 + +## 介绍 + +ABP标签助手基于bootstrap栅格系统. + +## Demo + +参阅[网络demo页面](https://bootstrap-taghelpers.abp.io/Components/Grids)查看示例. + +### Sizing + +**等宽:** 创建等宽的列. + +示例: + +````xml + + + 1 of 2 + 2 of 2 + + + 1 of 3 + 2 of 3 + 3 of 3 + + +```` + +**列中断:** `abp-column-breaker` 用于中断当前行的自动放置宽度,然后在新行中开始. + +示例: + +````xml + + + column + column + + column + column + + +```` + +**设置列宽度:** size属性用于设置特定列的宽度. + +示例: + +```xml + + + 1 of 3 + 2 of 3 (wider) + 3 of 3 + + + 1 of 3 + 2 of 3 (wider) + 3 of 3 + + +``` + +**可变宽度内容:** 根据内容自动调整列的大小. + +```xml + + + 1 of 3 + Contrary to popular belief, Lorem Ipsum is not simply random text. + 3 of 3 + + + 1 of 3 + Variable width content + 3 of 3 + + +``` + +### 响应类 + +响应式类可以在abp标签中强类型使用. + +```xml + + col-sm-8 + col-sm-4 + + + col-sm + col-sm + col-sm + col-sm + + + + .col-12 .col-md-8 + .col-6 .col-md-4 + + + + + .col-6 .col-md-4 + .col-6 .col-md-4 + .col-6 .col-md-4 + + + + + .col-6 + .col-6 + +``` + +### 对齐 + +可以使用强类型的abp标签在垂直和水平方向上进行列对齐. + +**垂直对齐**: `v-align` 属性值用于对垂直齐列. + +示例: + +```xml + + + column + column + column + + + column + column + column + + + column + column + column + + +``` + +**水平对齐**: `h-align` 属性值用于对水平齐列. + +示例: + +```xml + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + +``` + +**无间隙**: 可以使用 `gutters="false"` 删除预定义栅格类中列之间的栅格线. 这会从 `abp-row` 中消除负边距,并从所有直接子列中消除水平边距. + +示例: + +```xml + + One of two columns + One of two columns + +``` + +**列包装**: 如果在一行中放置超过12列,则将每组额外的列作为一个单元包装到新行上. + +示例: + +```xml + + .col-9 + .col-4
Since 9 + 4 = 13 > 12, this 4-column-wide div gets wrapped onto a new line as one contiguous unit.
+ .col-6
Subsequent columns continue along the new line.s
+
+``` + +### 重新排序 + +**Order类**: `order` 属性用于控制内容的视觉顺序. + +示例: + +```xml + + + First, but Last + Second, but unordered + Third, but Second + + +``` + +**偏移列**: `offset` 属性用于设置栅格列的偏移量. + +示例: + +```xml + + + .col-md-4 + .col-md-4 .offset-md-4 + + + .col-md-3 .offset-md-3 + .col-md-3 .offset-md-3 + + + .col-md-6 .offset-md-3 + + + .col-sm-5 .col-md-6 + .col-sm-5 .offset-sm-2 .col-md-6 .offset-md-0 + + + col-sm-6 .col-md-5 .col-lg-6 + .col-sm-6 .col-md-5 .offset-md-2 .col-lg-6 .offset-lg-0 + + +``` + +## abp-row Attributes + +- **v-align:** 指定包含列的垂直位置. 应为以下值之一: + * `Default` (默认值) + * `Start` + * `Center` + * `End` + +- **h-align**: 指定包含列的水平位置. 应为以下值之一: + * `Default` (默认值) + * `Start` + * `Center` + * `Around` + * `Between` + * `End` +- **gutter**: 指定是否将从所有子列中删除负边距和水平填充. 如果未设置,默认为 `true`. 应为以下值之一: + * `true` + * `false` + +## abp-column Attributes + +- **size:** 指定列的宽度: `_`, `Undefined`, `_1`..`_12`, `Auto`. 或者可以与预定义值一起使用,例如: + - `size-sm` + - `size-md` + - `size-lg` + - `size-xl` +- **order**: 指定列的顺序: `Undefined`, `_1`..`_12`, `First` 和 `Last`. +- **offset:** 指定列的偏移量: `_`, `Undefined`, `_1`..`_12`, `Auto`. 或者可以与预定义值一起使用,例如: + - `offset-sm` + - `offset-md` + - `offset-lg` + - `offset-xl` \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md new file mode 100644 index 0000000000..20d1119969 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md @@ -0,0 +1,38 @@ +# ABP Tag Helpers + +ABP框架定义了一组**标签助手组件**. 简化开发ASP.NET Core (MVC / Razor Pages) 应用程序界面. + +## bootstrap 组件包装 + +大多数标签助手是[Bootstrap](https://getbootstrap.com/) (v4+)的包装. 编写bootstrap代码并不是那么简单,其中包含太多的重复HTML标签并且也没有类型安全. ABP标签助手使其 **简单** 并且 **类型安全**. + +我们的目标并不是100%的包装bootstrap组件. 仍然可以编写 **原生bootstrap代码** (实际上标签助手生成的也是原生的bootstrap代码), 但我们建议尽量使用标签助手. + +ABP框架还向标准bootstrap组件添加了一些**实用的功能**. + +这里是ABP框架包装的组件列表: + +* [Buttons](Buttons.md) +* [Cards](Cards.md) +* [Alerts](Alerts.md) +* [Tabs](Tabs.md) +* [Grids](Grids.md) +* [Modals](Modals.md) +* [Collapse](Collapse.md) +* [Dropdowns](Dropdowns.md) +* [List Groups](List-Groups.md) +* [Paginator](Paginator.md) +* [Popovers](Popovers.md) +* [Progress Bars](Progress-Bars.md) +* [Tooltips](Tooltips.md) +* ... + +> 在为所有的标签助手完成文档之前,你可以访问 https://bootstrap-taghelpers.abp.io/ 查看在线示例. + +## 表单元素 + +参阅 [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). + +## 动态表单 + +参阅 [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/List-Groups.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/List-Groups.md new file mode 100644 index 0000000000..411a8a3b8a --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/List-Groups.md @@ -0,0 +1,76 @@ +# 列表组 + +## 介绍 + +`abp-list-group` 是列表组内容的主要容器. + +基本用法: + +````xml + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + +```` + +## Demo + +参阅[列表组demo页面](https://bootstrap-taghelpers.abp.io/Components/ListGroups)查看示例. + +## Attributes + +### flush + +指定 `abp-list-group` 项可删除某些边框和圆角,以在父容器中无边框呈现列表组项. 应为以下值之一: + +* `false` (默认值) +* `true` + +### active + +指定 `abp-list-group-item` 是否处于active. 应为以下值之一: + +* `false` (默认值) +* `true` + +### disabled + +指定 `abp-list-group-item` 是否被禁用. 应为以下值之一: + +* `false` (默认值) +* `true` + +### href + +指定 `abp-list-group-item` 是否含有链接. 应该是一个字符串链接值. + +### type + +指定 `abp-list-group-item` 具有状态背景和颜色的样式类. 应为以下值之一: + +* `Default` (默认值) +* `Primary` +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` +* `Link` + +### Additional content + +`abp-list-group-item` 还可以包含其他HTML元素(例如span). + +示例: + +````xml + + Cras justo odio 14 + Dapibus ac facilisis in 2 + Morbi leo risus 1 + +```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md new file mode 100644 index 0000000000..28b92254bd --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md @@ -0,0 +1,79 @@ +# 模态框 + +## 介绍 + +`abp-modal` 是创建模态框的主要元素. + +基本用法: + +````xml +Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + +```` + +## Demo + +参阅[模态框demo页面](https://bootstrap-taghelpers.abp.io/Components/Modals)查看示例. + +## Attributes + +### centered + +指定模态框的位置. 应为以下值之一: + +* `false` (默认值) +* `true` + +### size + +指定模态框的大小. 应为以下值之一: + +* `Default` (默认值) +* `Small` +* `Large` +* `ExtraLarge` + +### static + +指定模态框是否是静态的. 应为以下值之一: + +* `false` (默认值) +* `true` + +### Additional content + +`abp-modal-footer` 可以有多个带有对齐选项的按钮. + +添加 `@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal` 到你的页面. + +示例: + +````xml +Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + +```` + +### button-alignment + +指定模态页脚按钮的位置. 应为以下值之一: + +* `Default` (默认值) +* `Start` +* `Center` +* `Around` +* `Between` +* `End` \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md new file mode 100644 index 0000000000..7a9b3de126 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -0,0 +1,55 @@ +# 分页器 + +## 介绍 + +`abp-paginator` 是分页器的abp标签. 需要 `Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` 类型的模型. + +基本用法: + +````xml + +```` + +模型: + +````csharp +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components +{ + public class PaginatorModel : PageModel + { + public PagerModel PagerModel { get; set; } + + public void OnGet(int currentPage, string sort) + { + PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + } + } +} +```` + +## Demo + +参阅[分页器demo页面](https://bootstrap-taghelpers.abp.io/Components/Paginator)查看示例. + +## Attributes + +### model + +`Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination.PagerModel` 类型模型可以用以下数据初始化: + +* `totalCount` +* `shownItemsCount` +* `currentPage` +* `pageSize` +* `pageUrl` +* `sort` (默认值为null) + +### show-info + +指定是否显示开始,结束和总记录的其他信息. 应为以下值之一: + +* `false` (默认值) +* `true` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Progress-Bars.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Progress-Bars.md new file mode 100644 index 0000000000..e77580cd99 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Progress-Bars.md @@ -0,0 +1,68 @@ +# 进度条 + +## 介绍 + +`abp-progress-bar` 是进度条状态的abp标签. + +基本用法: + +````xml + + + %25 + + + + %50 + + + + %10 + + +```` + +## Demo + +参阅[进度条demo页面](https://bootstrap-taghelpers.abp.io/Components/Progress-Bars)查看示例. + +## Attributes + +### value + +指定当前进度条的进度. + +### type + +指定进度条的背景颜色. 应为下列值之一: + +* `Default` (默认值) +* `Secondary` +* `Success` +* `Danger` +* `Warning` +* `Info` +* `Light` +* `Dark` + +### min-value + +进度条的最小值. 默认值是0。 + +### max-value + +进度条的最大值. 默认值是100. + +### strip + +指定进度条的背景样式是否被去除. 应为以下值之一: + +* `false` (默认值) +* `true` + +### animation + +指定进度条的背景样式是否为动画. 应为以下值之一: + +* `false` (默认值) +* `true` \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tabs.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tabs.md new file mode 100644 index 0000000000..3aacbb5e0d --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tabs.md @@ -0,0 +1,91 @@ +# 标签页 + +## 介绍 + +`abp-tab` 是从引导标签元素派生的基本标签导航内容容器. + +基本用法: + +````xml + + + Content_Home + + + + Content_Profile + + + + Content_1_Content + + + Content_2_Content + + + +```` + +## Demo + +参阅[标签页demo页面](https://bootstrap-taghelpers.abp.io/Components/Tabs)查看示例. + +## abp-tab Attributes + +- **title**: 设置标签页菜单文字. +- **name:** 设置生成元素的"id"属性. 默认值为Guid. 除非使用Jquery更改或修改了选项卡,否则不需要. +- **active**: 设置active标签页. + +示例: + +````xml + + + Content_Home + + + Content_Profile + + + Content_Contact + + +```` + +### Pills + +示例: + +````xml + + + Content_Home + + + Content_Profile + + + Content_Contact + + +```` + +### Vertical + +**vertical-header-size**: 设置标签标题的列宽. + +示例: + +````xml + + + Content_Home + + + Content_Profile + + + Content_Contact + + +```` diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tooltips.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tooltips.md new file mode 100644 index 0000000000..0f404911b7 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Tooltips.md @@ -0,0 +1,33 @@ +# 提示工具 + +## 介绍 + +`abp-tooltip` 是提示工具的abp标签. + +基本用法: + +````xml + + Tooltip Default + + + + Tooltip on top + + + + Tooltip on right + + + + Tooltip on bottom + + + + Disabled button Tooltip + +```` + +## Demo + +参阅[提示工具demo页面](https://bootstrap-taghelpers.abp.io/Components/Tooltips)查看示例. diff --git a/docs/zh-Hans/UI/AspNetCore/Theming.md b/docs/zh-Hans/UI/AspNetCore/Theming.md new file mode 100644 index 0000000000..10486dd6c5 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Theming.md @@ -0,0 +1,3 @@ +# ASP.NET Core MVC / Razor Pages 主题 + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Toolbars.md b/docs/zh-Hans/UI/AspNetCore/Toolbars.md new file mode 100644 index 0000000000..cc3bcd95be --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Toolbars.md @@ -0,0 +1,3 @@ +# Toolbars + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Widgets.md b/docs/zh-Hans/UI/AspNetCore/Widgets.md new file mode 100644 index 0000000000..5f86595ba2 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Widgets.md @@ -0,0 +1,274 @@ +# 小部件 + +ABP为创建**可重用的部件**提供了模型和基础设施. 部件系统是[ASP.NET Core ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components)的扩展. 在你有以下需求时,小部件会非常有用; + +* 在可复用的 **[模块](../../Module-Development-Basics.md)** 中定义部件. +* 在部件中引用 **scripts & styles** 脚本. +* 使用部件创建 **仪表盘**. +* 支持 **[授权](../../Authorization.md)** 与 **[捆绑`bundling`](Bundling-Minification.md)** 的部件 + +## 基本部件定义 + +### 创建一个视图组件 + +第一部,创建一个新的ASP.NET Core View Component: + +![widget-basic-files](../../images/widget-basic-files.png) + +**MySimpleWidgetViewComponent.cs**: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +继承 `AbpViewComponent` 不是必需的. 你也可以继承ASP.NET Core的 `ViewComponent`. `AbpViewComponent` 只是定义了一些基本的实用属性. + +**Default.cshtml**: + +```xml +
+

My Simple Widget

+

This is a simple widget!

+
+``` + +### 定义部件 + +添加 `Widget` attribute 到 `MySimpleWidgetViewComponent` 类,将此视图组件标记为部件: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## 渲染部件 + +渲染部件的用法是ASP.NET Core的标准用法. 在razor view/page中使用 `Component.InvokeAsync` 方法, 就像渲染一个View Component一样. 例如: + +````xml +@await Component.InvokeAsync("MySimpleWidget") +@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) +```` + +第一行代码使用名称渲染了部件,第二行代码使用type渲染了View Comonent. + +## 部件名称 + +默认下名称是根据View Conponent组件的名称计算的, 比如你的视图组件名是 `MySimpleWidgetViewComponent`, 那么部件的名称就是 `MySimpleWidget` (删除`ViewComponent`后缀). 这与ASP.NET Core的默认视图组件名称的方式一样. + +想要自定义组件名称,只需要使用ASP.NET Core的 `ViewComponent` attribute: + +```csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget] + [ViewComponent(Name = "MyCustomNamedWidget")] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); + } + } +} +``` + +ABP会通过自定义的名称去处理部件. + +> 如果视图组件名与视图组件的文件夹名称不匹配,那么需要像本例中那样去手动编写视图路径. + +### 显示名称 + +你还可以定义对于使用者友好的本地化显示名称. 需要时在UI中使用显示名称. 显示名称是可选的,在 `Widget` attribute 的`DisplayName`属性中定义: + +````csharp +using DashboardDemo.Localization; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + DisplayName = "MySimpleWidgetDisplayName", //Localization key + DisplayNameResource = typeof(DashboardDemoResource) //localization resource + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +参阅 [本地化文档](../../Localization.md) 学习关于本地化资源的更多内容. + +## 引用 Style & Script + +当部件含有样式和scirpt文件时,会存在一些挑战; + +* 使用部件的页面应该将 **script & styles** 文件引用到页面中. +* 页面还需要解析部件的 `依赖库/文件`. + +将资源与部件正确的关联在一起时,ABP会解决这些问题. 使用正确的方法,就不用担心部件的依赖关系. + +### 定义一个简单的文件路径 + +下面的示例中部件添加了样式和scirpt文件: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, + ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +ABP会考虑到这些依赖关系, 在view/page中使用正确的方法添加部件 . 样式和script可以是物理文件也可以是虚拟文件. 它于[虚拟文件系统](../../Virtual-File-System.md)完全集成]. + +### 定义 Bundle + +页面中使用的组件的所有资源都做为捆绑包添加(如果没有其他配置,会在生产中合并和压缩). 除了简单的添加文件,你还可以充分的利用捆绑功能. + +下面的示例与上面的代码相同,但是在添加文件时文件路径替换成了 `BundleContributor`: + +````csharp +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget( + StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, + ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } + )] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } + + public class MySimpleWidgetStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); + } + } + + public class MySimpleWidgetScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files + .AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); + } + } +} + +```` + +捆绑系统非常强大,如果你的部件使用了JavaScript库来呈现图表, 你可以将它声明为依赖项, 如果之前未添加JavaScript库. 则会自动添加到页面中. 使用这种方式让页面使用部件时不用关心依赖项. + +参阅 [捆包&压缩 文档](Bundling-Minification.md) 了解更多内容. + +## 授权 + +某些组件可能只对通过身份验证或授权的用户可用,这时可以使用 `Widget` attribute 的以下属性: + +* `RequiresAuthentication` (`bool`): 设置为true,只有通过身份验证的用户(登录用户)可用. +* `RequiredPolicies` (`List`): 授权用户的策略名称列表. 有关策略的详细信息请参阅[授权文档](../../Authorization.md). + +示例: + +````csharp +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace DashboardDemo.Web.Pages.Components.MySimpleWidget +{ + [Widget(RequiredPolicies = new[] { "MyPolicyName" })] + public class MySimpleWidgetViewComponent : AbpViewComponent + { + public IViewComponentResult Invoke() + { + return View(); + } + } +} +```` + +## 部件选项 + +`AbpWidgetOptions` 是 `Widget` attribute 替代, 你可以使用它去配置部件: + +```csharp +Configure(options => +{ + options.Widgets.Add(); +}); +``` + +将上面的代码写到[模块](../../Module-Development-Basics.md)的 `ConfigureServices` 方法中. `AbpWidgetOptions` 可以完成 `Widget` attribute 的所有功能. 比如为组件添加样式: + +````csharp +Configure(options => +{ + options.Widgets + .Add() + .WithStyles("/Pages/Components/MySimpleWidget/Default.css"); +}); +```` + +> 提示: `AbpWidgetOptions` 还可以更改现有的部件配置. 如果要修改应用程序使用的模块内的组件配置,这会很有用. 使用 `options.Widgets.Find` 获取现有的 `WidgetDefinition`. \ No newline at end of file diff --git a/docs/zh-Hans/_resources/Diagrams.docx b/docs/zh-Hans/_resources/Diagrams.docx new file mode 100644 index 0000000000..424e281470 Binary files /dev/null and b/docs/zh-Hans/_resources/Diagrams.docx differ diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index ad811cc5b3..f01ce04bc9 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -5,6 +5,7 @@ "items": [ { "text": "从启动模板开始", + "path": "Getting-Started-With-Startup-Templates.md", "items": [ { "text": "ASP.NET Core MVC 模板", @@ -29,6 +30,7 @@ }, { "text": "教程", + "path": "Tutorials/Index.md", "items": [ { "text": "应用开发", @@ -41,6 +43,37 @@ } ] }, + { + "text": "指南", + "items": [ + { + "text": "自定义应用模块", + "path": "Customizing-Application-Modules-Guide.md", + "items": [ + { + "text": "扩展实体", + "path": "Customizing-Application-Modules-Extending-Entities.md" + }, + { + "text": "重写服务", + "path": "Customizing-Application-Modules-Overriding-Services.md" + }, + { + "text": "重写用户界面", + "path": "Customizing-Application-Modules-Overriding-User-Interface.md" + } + ] + }, + { + "text": "\"如何\" 指南", + "path": "How-To/Index.md" + }, + { + "text": "从ASP.NET Boilerplate迁移", + "path": "AspNet-Boilerplate-Migration-Guide.md" + } + ] + }, { "text": "CLI", "path": "CLI.md" @@ -197,7 +230,8 @@ "text": "领域服务" }, { - "text": "规约" + "text": "规约", + "path": "Specifications.md" } ] }, @@ -219,46 +253,67 @@ ] }, { - "text": "ASP.NET Core", + "text": "API", "items": [ { - "text": "API", - "items": [ - { - "text": "自动API控制器", - "path": "AspNetCore/Auto-API-Controllers.md" - }, - { - "text": "动态C# API客户端", - "path": "AspNetCore/Dynamic-CSharp-API-Clients.md" - } - ] + "text": "自动API控制器", + "path": "API/Auto-API-Controllers.md" }, { - "text": "用户界面", + "text": "动态C# API客户端", + "path": "API/Dynamic-CSharp-API-Clients.md" + } + ] + }, + { + "text": "用户界面", + "items": [ + { + "text": "ASP.NET Core", "items": [ { "text": "客户端包管理", - "path": "AspNetCore/Client-Side-Package-Management.md" + "path": "UI/AspNetCore/Client-Side-Package-Management.md" }, { "text": "捆绑&压缩", - "path": "AspNetCore/Bundling-Minification.md" + "path": "UI/AspNetCore/Bundling-Minification.md" }, { "text": "Tag Helpers", - "path": "AspNetCore/Tag-Helpers/Index.md" + "path": "UI/AspNetCore/Tag-Helpers/Index.md" }, { "text": "仪表板和小部件(Widget)系统", - "path": "AspNetCore/Widgets.md" + "path": "UI/AspNetCore/Widgets.md" + }, + { + "text": "自定义/扩展UI", + "path": "UI/AspNetCore/Customization-User-Interface.md" }, { "text": "主题化", - "path": "AspNetCore/Theming.md" + "path": "UI/AspNetCore/Theming.md" } ] - } + }, + { + "text": "Angular", + "items": [ + { + "text": "本地化", + "path": "UI/Angular/Localization.md" + }, + { + "text": "权限管理", + "path": "UI/Angular/Permission-Management.md" + }, + { + "text": "替换组件", + "path": "UI/Angular/Component-Replacement.md" + } + ] + } ] }, { @@ -277,20 +332,27 @@ "path": "Entity-Framework-Core.md", "items": [ { - "text": "切换到MySql", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "切换到PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - }, - { - "text": "切换到SQLite", - "path": "Entity-Framework-Core-SQLite.md" + "text": "数据库迁移", + "path": "Entity-Framework-Core-Migrations.md" }, { - "text": "切换到其他DBMS", - "path": "Entity-Framework-Core-Other-DBMS.md" + + "text": "切换DMBS", + "path": "Entity-Framework-Core-Other-DBMS.md", + "items":[ + { + "text": "到MySql", + "path": "Entity-Framework-Core-MySQL.md" + }, + { + "text": "到PostgreSQL", + "path": "Entity-Framework-Core-PostgreSQL.md" + }, + { + "text": "到SQLite", + "path": "Entity-Framework-Core-SQLite.md" + } + ] } ] }, @@ -320,11 +382,39 @@ { "text": "RabbitMQ 集成", "path": "Background-Jobs-RabbitMq.md" + }, + { + "text": "Quartz 集成", + "path": "Background-Jobs-Quartz.md" + } + ] + }, + { + "text": "后台工作者", + "path": "Background-Workers.md", + "items": [ + { + "text": "Quartz 集成", + "path": "Background-Workers-Quartz.md" } ] } ] }, + { + "text": "启动模板", + "path": "Startup-Templates/Index.md", + "items": [ + { + "text": "应用程序", + "path": "Startup-Templates/Application.md" + }, + { + "text": "模块", + "path": "Startup-Templates/Module.md" + } + ] + }, { "text": "示例", "items": [ diff --git a/docs/zh-Hans/images/angular-folder-structure.png b/docs/zh-Hans/images/angular-folder-structure.png new file mode 100644 index 0000000000..e17eb135ce Binary files /dev/null and b/docs/zh-Hans/images/angular-folder-structure.png differ diff --git a/docs/zh-Hans/images/angular-template-structure-diagram.png b/docs/zh-Hans/images/angular-template-structure-diagram.png new file mode 100644 index 0000000000..d27d75bf7f Binary files /dev/null and b/docs/zh-Hans/images/angular-template-structure-diagram.png differ diff --git a/docs/zh-Hans/images/bookstore-added-brand-files.png b/docs/zh-Hans/images/bookstore-added-brand-files.png new file mode 100644 index 0000000000..15ef283a2a Binary files /dev/null and b/docs/zh-Hans/images/bookstore-added-brand-files.png differ diff --git a/docs/zh-Hans/images/bookstore-added-logo.png b/docs/zh-Hans/images/bookstore-added-logo.png new file mode 100644 index 0000000000..55af77a5f4 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-added-logo.png differ diff --git a/docs/zh-Hans/images/bookstore-added-role-js-file.png b/docs/zh-Hans/images/bookstore-added-role-js-file.png new file mode 100644 index 0000000000..00881bf808 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-added-role-js-file.png differ diff --git a/docs/zh-Hans/images/bookstore-brand-area-highlighted.png b/docs/zh-Hans/images/bookstore-brand-area-highlighted.png new file mode 100644 index 0000000000..7ab1b8fb7f Binary files /dev/null and b/docs/zh-Hans/images/bookstore-brand-area-highlighted.png differ diff --git a/docs/zh-Hans/images/bookstore-efcore-dependencies.png b/docs/zh-Hans/images/bookstore-efcore-dependencies.png new file mode 100644 index 0000000000..3de5a679ef Binary files /dev/null and b/docs/zh-Hans/images/bookstore-efcore-dependencies.png differ diff --git a/docs/zh-Hans/images/bookstore-global-css-file.png b/docs/zh-Hans/images/bookstore-global-css-file.png new file mode 100644 index 0000000000..e764fef8ee Binary files /dev/null and b/docs/zh-Hans/images/bookstore-global-css-file.png differ diff --git a/docs/zh-Hans/images/bookstore-google-analytics-view-component.png b/docs/zh-Hans/images/bookstore-google-analytics-view-component.png new file mode 100644 index 0000000000..e548a00935 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-google-analytics-view-component.png differ diff --git a/docs/zh-Hans/images/bookstore-menus-highlighted.png b/docs/zh-Hans/images/bookstore-menus-highlighted.png new file mode 100644 index 0000000000..c4f3237b00 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-menus-highlighted.png differ diff --git a/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png b/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png new file mode 100644 index 0000000000..817491e9eb Binary files /dev/null and b/docs/zh-Hans/images/bookstore-notification-icon-on-toolbar.png differ diff --git a/docs/zh-Hans/images/bookstore-notification-view-component.png b/docs/zh-Hans/images/bookstore-notification-view-component.png new file mode 100644 index 0000000000..3fc300efe1 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-notification-view-component.png differ diff --git a/docs/zh-Hans/images/bookstore-second-database.png b/docs/zh-Hans/images/bookstore-second-database.png new file mode 100644 index 0000000000..3faf19496c Binary files /dev/null and b/docs/zh-Hans/images/bookstore-second-database.png differ diff --git a/docs/zh-Hans/images/bookstore-toolbar-highlighted.png b/docs/zh-Hans/images/bookstore-toolbar-highlighted.png new file mode 100644 index 0000000000..f2b7c3f57e Binary files /dev/null and b/docs/zh-Hans/images/bookstore-toolbar-highlighted.png differ diff --git a/docs/zh-Hans/images/docs-module_download-new-abp-project.png b/docs/zh-Hans/images/docs-module_download-new-abp-project.png index 0da3b7a67a..6424ad7622 100644 Binary files a/docs/zh-Hans/images/docs-module_download-new-abp-project.png and b/docs/zh-Hans/images/docs-module_download-new-abp-project.png differ diff --git a/docs/zh-Hans/images/docs-module_solution-explorer.png b/docs/zh-Hans/images/docs-module_solution-explorer.png index cafc38f0b0..2988ec4134 100644 Binary files a/docs/zh-Hans/images/docs-module_solution-explorer.png and b/docs/zh-Hans/images/docs-module_solution-explorer.png differ diff --git a/docs/zh-Hans/images/microservice-sample-diagram-3.png b/docs/zh-Hans/images/microservice-sample-diagram-3.png new file mode 100644 index 0000000000..b8066ef6be Binary files /dev/null and b/docs/zh-Hans/images/microservice-sample-diagram-3.png differ diff --git a/docs/zh-Hans/images/microservice-sample-solution-2.png b/docs/zh-Hans/images/microservice-sample-solution-2.png new file mode 100644 index 0000000000..3da5d160f8 Binary files /dev/null and b/docs/zh-Hans/images/microservice-sample-solution-2.png differ diff --git a/docs/zh-Hans/images/multiple-database-usage.png b/docs/zh-Hans/images/multiple-database-usage.png new file mode 100644 index 0000000000..ada6177629 Binary files /dev/null and b/docs/zh-Hans/images/multiple-database-usage.png differ diff --git a/docs/zh-Hans/images/overriding-login-cshtml.png b/docs/zh-Hans/images/overriding-login-cshtml.png new file mode 100644 index 0000000000..d6daa4a141 Binary files /dev/null and b/docs/zh-Hans/images/overriding-login-cshtml.png differ diff --git a/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png b/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png new file mode 100644 index 0000000000..3cc45e5ddf Binary files /dev/null and b/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png differ diff --git a/docs/zh-Hans/images/pmc-add-migration-role-title.png b/docs/zh-Hans/images/pmc-add-migration-role-title.png new file mode 100644 index 0000000000..69fc5b7bc5 Binary files /dev/null and b/docs/zh-Hans/images/pmc-add-migration-role-title.png differ diff --git a/docs/zh-Hans/images/react-native-folder-structure.png b/docs/zh-Hans/images/react-native-folder-structure.png new file mode 100644 index 0000000000..cf703ea061 Binary files /dev/null and b/docs/zh-Hans/images/react-native-folder-structure.png differ diff --git a/docs/zh-Hans/images/react-native-navigation-structure.png b/docs/zh-Hans/images/react-native-navigation-structure.png new file mode 100644 index 0000000000..9f17375b68 Binary files /dev/null and b/docs/zh-Hans/images/react-native-navigation-structure.png differ diff --git a/docs/zh-Hans/images/react-native-store-folder.png b/docs/zh-Hans/images/react-native-store-folder.png new file mode 100644 index 0000000000..e90ae9debb Binary files /dev/null and b/docs/zh-Hans/images/react-native-store-folder.png differ diff --git a/docs/zh-Hans/images/single-database-usage.png b/docs/zh-Hans/images/single-database-usage.png new file mode 100644 index 0000000000..108173fd86 Binary files /dev/null and b/docs/zh-Hans/images/single-database-usage.png differ diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index b731aff187..3db0998855 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28922.388 MinimumVisualStudioVersion = 10.0.40219.1 @@ -252,13 +253,31 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ObjectMapping.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Ddd.Application.Contracts", "src\Volo.Abp.Ddd.Application.Contracts\Volo.Abp.Ddd.Application.Contracts.csproj", "{73559227-EBF0-475F-835B-1FF0CD9132AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify", "src\Volo.Abp.Minify\Volo.Abp.Minify.csproj", "{928DC30D-C078-4BB4-A9F8-FE7252C67DC6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Minify", "src\Volo.Abp.Minify\Volo.Abp.Minify.csproj", "{928DC30D-C078-4BB4-A9F8-FE7252C67DC6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify.Tests", "test\Volo.Abp.Minify.Tests\Volo.Abp.Minify.Tests.csproj", "{E69182B3-350A-43F5-A935-5EBBEBECEF97}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Minify.Tests", "test\Volo.Abp.Minify.Tests\Volo.Abp.Minify.Tests.csproj", "{E69182B3-350A-43F5-A935-5EBBEBECEF97}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Serilog", "src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj", "{3B801003-BE74-49ED-9749-DA5E99F45EBF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Serilog", "src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj", "{3B801003-BE74-49ED-9749-DA5E99F45EBF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Serilog.Tests", "test\Volo.Abp.AspNetCore.Serilog.Tests\Volo.Abp.AspNetCore.Serilog.Tests.csproj", "{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Serilog.Tests", "test\Volo.Abp.AspNetCore.Serilog.Tests\Volo.Abp.AspNetCore.Serilog.Tests.csproj", "{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.IdentityModel.Web", "src\Volo.Abp.Http.Client.IdentityModel.Web\Volo.Abp.Http.Client.IdentityModel.Web.csproj", "{925AF101-2203-409C-9C3B-03917316858F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BackgroundJobs.Quartz", "src\Volo.Abp.BackgroundJobs.Quartz\Volo.Abp.BackgroundJobs.Quartz.csproj", "{2307198B-5837-4F05-AA84-D6EC2A923D69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Quartz", "src\Volo.Abp.Quartz\Volo.Abp.Quartz.csproj", "{9467418B-4A9B-4093-9B31-01A9DEF5B372}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BackgroundWorkers.Quartz", "src\Volo.Abp.BackgroundWorkers.Quartz\Volo.Abp.BackgroundWorkers.Quartz.csproj", "{CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo", "src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj", "{29E42ADB-85F8-44AE-A9B0-078F84C1B866}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo", "test\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj", "{0C498CF2-D052-4BF7-AD35-509A90F69707}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.IdentityModel.Web.Tests", "test\Volo.Abp.Http.Client.IdentityModel.Web.Tests\Volo.Abp.Http.Client.IdentityModel.Web.Tests.csproj", "{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ObjectExtending", "src\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj", "{D1815C77-16D6-4F99-8814-69065CD89FB3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.ObjectExtending.Tests", "test\Volo.Abp.ObjectExtending.Tests\Volo.Abp.ObjectExtending.Tests.csproj", "{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -774,6 +793,42 @@ Global {9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Debug|Any CPU.Build.0 = Debug|Any CPU {9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Release|Any CPU.ActiveCfg = Release|Any CPU {9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Release|Any CPU.Build.0 = Release|Any CPU + {925AF101-2203-409C-9C3B-03917316858F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {925AF101-2203-409C-9C3B-03917316858F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {925AF101-2203-409C-9C3B-03917316858F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {925AF101-2203-409C-9C3B-03917316858F}.Release|Any CPU.Build.0 = Release|Any CPU + {2307198B-5837-4F05-AA84-D6EC2A923D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2307198B-5837-4F05-AA84-D6EC2A923D69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2307198B-5837-4F05-AA84-D6EC2A923D69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2307198B-5837-4F05-AA84-D6EC2A923D69}.Release|Any CPU.Build.0 = Release|Any CPU + {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Release|Any CPU.Build.0 = Release|Any CPU + {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Release|Any CPU.Build.0 = Release|Any CPU + {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Release|Any CPU.Build.0 = Release|Any CPU + {0C498CF2-D052-4BF7-AD35-509A90F69707}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C498CF2-D052-4BF7-AD35-509A90F69707}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C498CF2-D052-4BF7-AD35-509A90F69707}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C498CF2-D052-4BF7-AD35-509A90F69707}.Release|Any CPU.Build.0 = Release|Any CPU + {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Release|Any CPU.Build.0 = Release|Any CPU + {D1815C77-16D6-4F99-8814-69065CD89FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1815C77-16D6-4F99-8814-69065CD89FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1815C77-16D6-4F99-8814-69065CD89FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1815C77-16D6-4F99-8814-69065CD89FB3}.Release|Any CPU.Build.0 = Release|Any CPU + {17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -906,6 +961,15 @@ Global {E69182B3-350A-43F5-A935-5EBBEBECEF97} = {447C8A77-E5F0-4538-8687-7383196D04EA} {3B801003-BE74-49ED-9749-DA5E99F45EBF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {925AF101-2203-409C-9C3B-03917316858F} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {2307198B-5837-4F05-AA84-D6EC2A923D69} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {9467418B-4A9B-4093-9B31-01A9DEF5B372} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {29E42ADB-85F8-44AE-A9B0-078F84C1B866} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {0C498CF2-D052-4BF7-AD35-509A90F69707} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {E1963439-2BE5-4DB5-8438-2A9A792A1ADA} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {D1815C77-16D6-4F99-8814-69065CD89FB3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} 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 e2392c2b53..544099c653 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/MultiTenancyMiddleware.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/MultiTenancyMiddleware.cs index ba8802a80e..2c82095dac 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/MultiTenancyMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/MultiTenancyMiddleware.cs @@ -14,9 +14,9 @@ namespace Volo.Abp.AspNetCore.MultiTenancy private readonly ITenantResolveResultAccessor _tenantResolveResultAccessor; public MultiTenancyMiddleware( - ITenantResolver tenantResolver, - ITenantStore tenantStore, - ICurrentTenant currentTenant, + ITenantResolver tenantResolver, + ITenantStore tenantStore, + ICurrentTenant currentTenant, ITenantResolveResultAccessor tenantResolveResultAccessor) { _tenantResolver = tenantResolver; @@ -34,11 +34,13 @@ namespace Volo.Abp.AspNetCore.MultiTenancy if (resolveResult.TenantIdOrName != null) { tenant = await FindTenantAsync(resolveResult.TenantIdOrName); + if (tenant == null) { - //TODO: A better exception? - throw new AbpException( - "There is no tenant with given tenant id or name: " + resolveResult.TenantIdOrName + 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 ); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs index 44e9076751..cc62a1e1b9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs @@ -1,4 +1,5 @@ using System; +using Volo.Abp.AspNetCore.Mvc.MultiTenancy; namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations { @@ -14,5 +15,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public CurrentUserDto CurrentUser { get; set; } public ApplicationFeatureConfigurationDto Features { get; set; } + + public MultiTenancyInfoDto MultiTenancy { get; set; } + + public CurrentTenantDto CurrentTenant { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/CurrentTenantDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/CurrentTenantDto.cs new file mode 100644 index 0000000000..06b24f1235 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/CurrentTenantDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.AspNetCore.Mvc.MultiTenancy +{ + public class CurrentTenantDto + { + public Guid? Id { get; set; } + + public string Name { get; set; } + + public bool IsAvailable { get; set; } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/MultiTenancyInfoDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/MultiTenancyInfoDto.cs new file mode 100644 index 0000000000..165a0e5c21 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/MultiTenancyInfoDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.AspNetCore.Mvc.MultiTenancy +{ + public class MultiTenancyInfoDto + { + public bool IsEnabled { get; set; } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs index ee2a920980..6f8ccf285a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs @@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form SetFormAttributes(context, output); - SetSubmitButton(context, output); + await SetSubmitButton(context, output); } protected virtual async Task ConvertToMvcForm(TagHelperContext context, TagHelperOutput output) @@ -107,14 +107,14 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form output.Content.SetHtmlContent(childContent); } - protected virtual void SetSubmitButton(TagHelperContext context, TagHelperOutput output) + protected virtual async Task SetSubmitButton(TagHelperContext context, TagHelperOutput output) { if (!TagHelper.SubmitButton ?? true) { return; } - var buttonHtml = ProcessSubmitButtonAndGetContentAsync(context, output); + var buttonHtml = await ProcessSubmitButtonAndGetContentAsync(context, output); output.PostContent.SetHtmlContent(output.PostContent.GetContent() + buttonHtml); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelper.cs index 2df1bf158a..30de6d25f5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelper.cs @@ -1,8 +1,12 @@ -namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal { + [HtmlTargetElement("abp-modal-footer")] public class AbpModalFooterTagHelper : AbpTagHelper { public AbpModalButtons Buttons { get; set; } + public ButtonsAlign ButtonAlignment { get; set; } = ButtonsAlign.Default; public AbpModalFooterTagHelper(AbpModalFooterTagHelperService tagHelperService) : base(tagHelperService) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs index cff4646917..38cac85a6d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs @@ -24,6 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal { output.PostContent.SetHtmlContent(CreateContent()); } + ProcessButtonsAlignment(output); } protected virtual string CreateContent() @@ -49,9 +50,18 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal sb.AppendLine(""); sb.AppendLine(""); break; - } - + } + return sb.ToString(); } + + protected virtual void ProcessButtonsAlignment(TagHelperOutput output) + { + if (TagHelper.ButtonAlignment == ButtonsAlign.Default) + { + 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/AbpModalSize.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSize.cs index ce33f4e073..fae811f79d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSize.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSize.cs @@ -4,6 +4,7 @@ { Default, Small, - Large + Large, + ExtraLarge } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSizeExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSizeExtensions.cs index ca58dd918e..faf89abc18 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSizeExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalSizeExtensions.cs @@ -10,6 +10,8 @@ return "modal-sm"; case AbpModalSize.Large: return "modal-lg"; + case AbpModalSize.ExtraLarge: + return "modal-xl"; case AbpModalSize.Default: return ""; default: diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelper.cs index aa61832ab9..51688afceb 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalTagHelper.cs @@ -6,6 +6,8 @@ public bool? Centered { get; set; } = false; + public bool? Static { get; set; } = false; + public AbpModalTagHelper(AbpModalTagHelperService tagHelperService) : base(tagHelperService) { 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 8106db4661..3524b27d35 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 @@ -18,9 +18,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal var sb = new StringBuilder(); var attritubutes = output.Attributes.Select(a => " " + a.Name + "=\"" + a.Value + "\" ").ToList(); - var attritubutesAsJoin = string.Join(" ", attritubutes.ToArray()); - - sb.AppendLine("
"); + var attritubutesAsJoin = string.Join(" ", attritubutes.ToArray()); + + sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("
"); @@ -56,6 +56,15 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal return "modal-content"; } + protected virtual string GetDataAttributes() + { + if (TagHelper.Static == true) + { + return "data-backdrop=\"static\" "; + } + return string.Empty; + } + protected virtual string CreatePostContent() { var sb = new StringBuilder(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/ButtonsAlign.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/ButtonsAlign.cs new file mode 100644 index 0000000000..7f6ba7b984 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/ButtonsAlign.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +{ + public enum ButtonsAlign + { + Default, + Start, + Center, + Around, + Between, + End + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs index d23ac6c912..dfa2c2cfb1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs @@ -18,6 +18,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover public bool? Dismissible { get; set; } + public bool? Hoverable { get; set; } + public string AbpPopover { get; set; } public string AbpPopoverRight { get; set; } 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 b7bda04134..7ecb351290 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,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover @@ -13,34 +14,67 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover SetDataPlacement(context, output); SetPopoverData(context, output); SetDataTriggerIfDismissible(context, output); + SetDataTriggerIfHoverable(context, output); } else { SetDisabled(context, output); } - } - + } protected virtual void SetDisabled(TagHelperContext context, TagHelperOutput output) { - var triggerAsHtml = TagHelper.Dismissible ?? false ? "datatrigger=\"focus\" " : ""; + var triggerAsHtml = TagHelper.Dismissible ?? false ? "data-trigger=\"focus\" " : ""; + if (TagHelper.Hoverable ?? false) + { + if (triggerAsHtml.Contains("focus")) + { + triggerAsHtml = triggerAsHtml.Replace("focus", "focus hover"); + } + else + { + triggerAsHtml = "data-trigger=\"hover\" "; + } + } var dataPlacementAsHtml = "data-placement=\"" + 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"); + } var titleAttribute = output.Attributes.FirstOrDefault(at => at.Name == "title"); var titleAsHtml = titleAttribute == null ? "" : "title=\"" + titleAttribute.Value + "\" "; - var preElementHtml = ""; + var preElementHtml = ""; var postElementHtml = ""; output.PreElement.SetHtmlContent(preElementHtml); output.PostElement.SetHtmlContent(postElementHtml); output.Attributes.Add("style", "pointer-events: none;"); - } - + } protected virtual void SetDataTriggerIfDismissible(TagHelperContext context, TagHelperOutput output) { if (TagHelper.Dismissible ?? false) { output.Attributes.Add("data-trigger", "focus"); } + } + + protected virtual void SetDataTriggerIfHoverable(TagHelperContext context, TagHelperOutput output) + { + if (TagHelper.Hoverable ?? false) + { + //If already has focus data trigger + if (output.Attributes.TryGetAttribute("data-trigger", out _)) + { + output.Attributes.SetAttribute(new TagHelperAttribute("data-trigger", "focus hover")); + } + else + { + output.Attributes.Add("data-trigger", "hover"); + } + } } protected virtual void SetDataToggle(TagHelperContext context, TagHelperOutput output) @@ -101,5 +135,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover return PopoverDirectory.Default; } + protected virtual bool IsDismissibleOrHoverable() + { + if (TagHelper.Dismissible ?? false) + { + return true; + } + if (TagHelper.Hoverable ?? false) + { + return true; + } + return false; + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs index 8389f7b9ed..42a0cd9c5f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs @@ -39,6 +39,43 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling AddWithDependencies(contributorType); } + public void Replace(bool includeDependencies = false) + where TSourceContributor : IBundleContributor, new() + where TDestContributorType : IBundleContributor, new() + { + Replace(typeof(TSourceContributor), typeof(TDestContributorType), includeDependencies); + } + + public void Replace([NotNull] Type sourceContributorType, [NotNull] Type destContributorType, bool includeDependencies = false) + { + Check.NotNull(sourceContributorType, nameof(sourceContributorType)); + Check.NotNull(destContributorType, nameof(destContributorType)); + + if (!includeDependencies) + { + _contributors.ReplaceOne(x => x.GetType() == sourceContributorType, + contributor => (IBundleContributor) Activator.CreateInstance(destContributorType)); + } + else + { + RemoveWithDependencies(sourceContributorType); + Add(destContributorType); + } + } + + public void Remove(bool includeDependencies = false) + where TContributor : IBundleContributor, new() + { + if (!includeDependencies) + { + _contributors.RemoveAll(x => x.GetType() == typeof(TContributor)); + } + else + { + RemoveWithDependencies(typeof(TContributor)); + } + } + public IReadOnlyList GetAll() { return _contributors.ToImmutableList(); @@ -58,6 +95,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling AddInstanceToContributors(contributorType); } + + private void RemoveWithDependencies(Type contributorType) + { + foreach (var dependedType in GetDirectDependencies(contributorType)) + { + RemoveWithDependencies(dependedType); //Recursive call + } + + _contributors.RemoveAll(x => x.GetType() == contributorType); + } private IEnumerable GetDirectDependencies(Type contributorType) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs index 73b6e025ea..88387e8937 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs @@ -63,9 +63,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers foreach (var bundleFile in bundleFiles) { var file = WebContentFileProvider.GetFileInfo(bundleFile); - if (file == null) + + if (file == null || !file.Exists) { - throw new AbpException($"Could not find the bundle file from {nameof(IWebContentFileProvider)}"); + throw new AbpException($"Could not find the bundle file '{bundleFile}' from {nameof(IWebContentFileProvider)}"); } AddHtmlTag(context, output, bundleFile + "?_v=" + file.LastModified.UtcTicks); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/sl.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/sl.json new file mode 100644 index 0000000000..51938a569e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/sl.json @@ -0,0 +1,12 @@ +{ + "culture": "sl", + "texts": { + "GivenTenantIsNotAvailable": "Najemnik ni na voljo: {0}", + "Tenant": "Najemnik", + "Switch": "Zamenjaj", + "Name": "Naziv", + "SwitchTenantHint": "Pustite polje z imenom prazno, da preklopite na stran za upravnike.", + "SwitchTenant": "Zamenjaj najemnika", + "NotSelected": "Ni izbran" + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs index 8de698c723..7e21c8cef5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs @@ -21,7 +21,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago ? "en" : CultureInfo.CurrentUICulture.Name; - if (TryAddCultureFile(context, cultureName)) + if (TryAddCultureFile(context, MapCultureName(cultureName))) { return; } @@ -39,5 +39,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago context.Files.AddIfNotContains(filePath); return true; } + + protected virtual string MapCultureName(string cultureName) + { + return cultureName; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs index 2ac0d58acf..b4acf05ec6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs @@ -25,7 +25,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation var cultureName = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Replace('-', '_'); - if (TryAddCultureFile(context, cultureName)) + if (TryAddCultureFile(context, MapCultureName(cultureName))) { return; } @@ -35,7 +35,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation return; } - TryAddCultureFile(context, cultureName.Substring(0, cultureName.IndexOf('_'))); + TryAddCultureFile(context, MapCultureName(cultureName.Substring(0, cultureName.IndexOf('_')))); } protected virtual bool TryAddCultureFile(BundleConfigurationContext context, string cultureName) @@ -51,5 +51,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation context.Files.AddIfNotContains(filePath); return true; } + + protected virtual string MapCultureName(string cultureName) + { + return cultureName; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs index 586bcd3db9..e788c612cf 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs @@ -17,7 +17,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor public override void ConfigureBundle(BundleConfigurationContext context) { context.Files.AddIfNotContains("/libs/to-mark/to-mark.min.js"); - context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.min.js"); + + if (context.FileProvider.GetFileInfo("/libs/tui-code-snippet/tui-code-snippet.min.js").Exists) + { + context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.min.js"); + } + else + { + context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.js"); + } + context.Files.AddIfNotContains("/libs/squire-rte/squire.js"); context.Files.AddIfNotContains("/libs/tui-editor/tui-editor-Editor.min.js"); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs index 01d3a35db2..60d1307b38 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/BasicTheme.cs @@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic { public const string Name = "Basic"; - public string GetLayout(string name, bool fallbackToDefault = true) + public virtual string GetLayout(string name, bool fallbackToDefault = true) { switch (name) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml index ae3255abbf..65ca8d940e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml @@ -1,4 +1,5 @@ @using System.Linq +@using Microsoft.AspNetCore.Http.Extensions @using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar.LanguageSwitch @model LanguageSwitchViewComponentModel @if (Model.OtherLanguages.Any()) @@ -11,7 +12,7 @@
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 13d438441d..9dd98b5fda 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 @@ -23,6 +23,7 @@ Layout = null; AbpAntiForgeryManager.SetCookie(); var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options + } @@ -37,6 +38,8 @@ @(ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title) + + @await RenderSectionAsync("styles", false) @@ -54,32 +57,32 @@ @if (MultiTenancyOptions.Value.IsEnabled && - (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true)) - { -
-
-
-
- @MultiTenancyStringLocalizer["Tenant"]
-
- @if (CurrentTenant.Id == null) - { - - @MultiTenancyStringLocalizer["NotSelected"] - - } - else - { - @(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString()) - } -
-
-
- @MultiTenancyStringLocalizer["Switch"] + (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true)) + { +
+
+
+
+ @MultiTenancyStringLocalizer["Tenant"]
+
+ @if (CurrentTenant.Id == null) + { + + @MultiTenancyStringLocalizer["NotSelected"] + + } + else + { + @(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString()) + } +
+
+
-
} @(await Component.InvokeAsync()) @RenderBody() 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 1fe4639d25..0f0fd43fa0 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 @@ -39,7 +39,10 @@ @pageTitle - + @if (ViewBag.Description!=null) + { + + } @await Component.InvokeAsync(typeof(WidgetStylesViewComponent)) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/AbpAspNetCoreMvcUiThemeSharedDemoModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/AbpAspNetCoreMvcUiThemeSharedDemoModule.cs new file mode 100644 index 0000000000..cd312db532 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/AbpAspNetCoreMvcUiThemeSharedDemoModule.cs @@ -0,0 +1,19 @@ +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo +{ + [DependsOn( + typeof(AbpAspNetCoreMvcUiThemeSharedModule) + )] + public class AbpAspNetCoreMvcUiThemeSharedDemoModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded("Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo"); + }); + } + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xml rename to framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xsd similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xsd rename to framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xsd diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/AlertsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/AlertsDemoViewComponent.cs new file mode 100644 index 0000000000..2599c317cf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/AlertsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.AlertsDemo +{ + [Widget] + public class AlertsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml new file mode 100644 index 0000000000..8c132b9197 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml @@ -0,0 +1,70 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.AlertsDemo + + + + A simple primary alert—check it out! + + + A simple secondary alert—check it out! + + + A simple success alert—check it out! + + + A simple danger alert—check it out! + + + A simple warning alert—check it out! + + + A simple info alert—check it out! + + + A simple light alert—check it out! + + + A simple dark alert—check it out! + + + + + + A simple primary alert with an example link. Give it a click if you like. + + + A simple secondary alert with an example link. Give it a click if you like. + + + A simple success alert with an example link. Give it a click if you like. + + + A simple danger alert with an example link. Give it a click if you like. + + + A simple warning alert with an example link. Give it a click if you like. + + + A simple info alert with an example link. Give it a click if you like. + + + A simple light alert with an example link. Give it a click if you like. + + + A simple dark alert with an example link. Give it a click if you like. + + + + + +

Well done!

+

Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.

+
+

Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

+
+
+ + + + Holy guacamole! You should check in on some of those fields below. + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/BadgesDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/BadgesDemoViewComponent.cs new file mode 100644 index 0000000000..bdc47336cd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/BadgesDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BadgesDemo +{ + [Widget] + public class BadgesDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml new file mode 100644 index 0000000000..9ed21ebe3e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml @@ -0,0 +1,49 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BadgesDemo + + +

Example heading New

+

Example heading New

+

Example heading New

+

Example heading New

+
Example heading New
+
Example heading New
+
+ + + + Notifications 4 + + + + + Primary + Secondary + Success + Danger + Warning + Info + Light + Dark + + + + Primary + Secondary + Success + Danger + Warning + Info + Light + Dark + + + + Primary + Secondary + Success + Danger + Warning + Info + Light + Dark + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/BordersDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/BordersDemoViewComponent.cs new file mode 100644 index 0000000000..2ba33feb54 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/BordersDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BordersDemo +{ + [Widget] + public class BordersDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..281f6c73ed --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml @@ -0,0 +1,46 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BordersDemo + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/BreadcrumbsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/BreadcrumbsDemoViewComponent.cs new file mode 100644 index 0000000000..e700acb667 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/BreadcrumbsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BreadcrumbsDemo +{ + [Widget] + public class BreadcrumbsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml new file mode 100644 index 0000000000..8338bdfc1f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml @@ -0,0 +1,18 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BreadcrumbsDemo + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/ButtonGroupsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/ButtonGroupsDemoViewComponent.cs new file mode 100644 index 0000000000..ff5b917d40 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/ButtonGroupsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonGroupsDemo +{ + [Widget] + public class ButtonGroupsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml new file mode 100644 index 0000000000..30c6eb5ef1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml @@ -0,0 +1,75 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonGroupsDemo + + + + Left + Middle + Right + + + + + + + 1 + 2 + 3 + 4 + + + 5 + 6 + 7 + + + 8 + + + + + + + Left + Middle + Right + + + Left + Middle + Right + + + Left + Middle + Right + + + + + + 1 + 2 + + + + Dropdown link + Dropdown link + + + + + + + + Button + Button + Button + + + + Dropdown link + Dropdown link + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/ButtonsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/ButtonsDemoViewComponent.cs new file mode 100644 index 0000000000..9d7a2dd3f9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/ButtonsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo +{ + [Widget] + public class ButtonsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml new file mode 100644 index 0000000000..39b079f802 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml @@ -0,0 +1,55 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo + + + + + Secondary + Success + Danger + Warning + Info + Light + Dark + Link + + + + Primary + Secondary + Success + Danger + Warning + Info + Light + Dark + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/CardsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/CardsDemoViewComponent.cs new file mode 100644 index 0000000000..f192285f80 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/CardsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CardsDemo +{ + [Widget] + public class CardsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml new file mode 100644 index 0000000000..d40637735c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml @@ -0,0 +1,305 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CardsDemo + + + + + + Card Title + Some quick example text to build on the card title and make up the bulk of the card's content. + Go somewhere + + + + + + + + Card title + Card subtitle + Some quick example text to build on the card title and make up the bulk of the card's content. + Card link + Another link + + + + + + + + Cras justo odio + Dapibus ac facilisis in + Vestibulum at eros + + + + + + + Featured + + Cras justo odio + Dapibus ac facilisis in + Vestibulum at eros + + + + + + + + + Card TitleSome quick example text to build on the card title and make up the bulk of the card's content. + + + Cras justo odio + Dapibus ac facilisis in + Vestibulum at eros + + + Card link + Another link + + + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + + + + + + Quote + + +

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

+
Someone famous in Source Title
+
+
+
+
+ + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + 2 days ago + + + + +
+
+ + Quote + + +

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

+
Someone famous in Source Title
+
+
+
+
+
+ + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + 2 days ago + +
+
+
+ + + + Quote + + +

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

+
Someone famous in Source Title
+
+
+
+ + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + 2 days ago + +
+ + + + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + + + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + + + + Special title treatment + With supporting text below as a natural lead-in to additional content. + Go somewhere + + + + + + + +
+ Card Title + Some quick example text to build on the card title and make up the bulk of the card's content. + Go somewhere +
+
+
+ + + + Header + + Primary card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Secondary card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Success card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Danger card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Warning card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Info card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Light card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + Header + + Dark card title + Some quick example text to build on the card title and make up the bulk of the card's content. + + + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + + + Featured + + Special title treatment + With supporting text below as a natural lead-in to additional content. + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CarouselDemo/CarouselDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CarouselDemo/CarouselDemoViewComponent.cs new file mode 100644 index 0000000000..8ef48c1da5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CarouselDemo/CarouselDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CarouselDemo +{ + [Widget] + public class CarouselDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/CarouselDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CarouselDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CarouselDemo/Default.cshtml new file mode 100644 index 0000000000..54ac50eea7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CarouselDemo/Default.cshtml @@ -0,0 +1,25 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CarouselDemo + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CollapseDemo/CollapseDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CollapseDemo/CollapseDemoViewComponent.cs new file mode 100644 index 0000000000..28ccbcb447 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CollapseDemo/CollapseDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CollapseDemo +{ + [Widget] + public class CollapseDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/CollapseDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CollapseDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CollapseDemo/Default.cshtml new file mode 100644 index 0000000000..6455bef2fc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CollapseDemo/Default.cshtml @@ -0,0 +1,43 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CollapseDemo + + + + Link with href + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + + + Toggle first element + + + + + + + Curabitur porta porttitor libero eu luctus. Praesent ultrices mattis commodo. Integer sodales massa risus, in molestie enim sagittis blandit + + + + + Anim pariatur wolf moon tempor,,, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. + + + + + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry rtat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + Anim pariatur wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS. + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/Default.cshtml new file mode 100644 index 0000000000..1a9587b600 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/Default.cshtml @@ -0,0 +1,190 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DropdownsDemo +@model DropDownDemoDemoModel + + + + + + Action + Another action + Something else here + + + + + + Action + Another action + Something else here + + + + + + Action + Another action + Something else here + + Separated link + + + + + + + + + Action + Another action + Something else here + + Separated link + + + + + + Action + Another action + Something else here + + Separated link + + + + + + Action + Another action + Something else here + + Separated link + + + + + + Action + Another action + Something else here + + Separated link + + + + + + Action + Another action + Something else here + + Separated link + + + + + + + + + + Action + Another action + Something else here + + + + + + Action + Another action + Something else here + + + + + + Action + Another action + Something else here + + + + + + + + + Dropdown Header + Action + Active action + Disabled action + + Dropdown Item Text + Something else here + + + + + + + + + Action + Active action + Disabled action + + + + + + + + +

+ Some example text that's free-flowing within the dropdown menu. +

+

+ And this is more example text. +

+
+
+
+ +
+
+

The form model is:

+
+ +
+
+ + + + + +
+ + + + + + + New around here? Sign up + Forgot password? +
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/DropDownDemoModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/DropDownDemoModel.cs new file mode 100644 index 0000000000..2baf2b25bd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/DropDownDemoModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DropdownsDemo +{ + public class DropDownDemoDemoModel + { + [Required] + [EmailAddress] + public string EmailAddress { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + public bool RememberMe { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/DropdownsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/DropdownsDemoViewComponent.cs new file mode 100644 index 0000000000..136ad70481 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DropdownsDemo/DropdownsDemoViewComponent.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DropdownsDemo +{ + [Widget] + public class DropdownsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/DropdownsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + var Model = new DropDownDemoDemoModel(); + + return View(ViewPath, Model); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/Default.cshtml new file mode 100644 index 0000000000..affecebcb3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/Default.cshtml @@ -0,0 +1,159 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DynamicFormsDemo +@model DynamicFormsDemoModel +
+
+

The form model is:

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

The form model is:

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

The form model is:

+
+ +
+
+ + + + + + +
+ First Div!
+ --------- +
+ + + +
+ ---------
+ Second Div! +
+
\ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/DynamicFormsDemoModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/DynamicFormsDemoModel.cs new file mode 100644 index 0000000000..ae1bb9d28b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/DynamicFormsDemoModel.cs @@ -0,0 +1,139 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DynamicFormsDemo +{ + public class DynamicFormsDemoModel + { + public List CountryList { get; set; } = new List + { + new SelectListItem { Value = "CA", Text = "Canada"}, + new SelectListItem { Value = "US", Text = "USA"}, + new SelectListItem { Value = "UK", Text = "United Kingdom"}, + new SelectListItem { Value = "RU", Text = "Russia"} + }; + + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe + } + + public class DetailedModel + { + [Required] + [Placeholder("Enter your name...")] + [Display(Name = "Name")] + public string Name { get; set; } + + [TextArea(Rows = 4)] + [Display(Name = "Description")] + [InputInfoText("Describe Yourself")] + public string Description { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Is Active")] + public bool IsActive { get; set; } + + [Required] + [Display(Name = "Age")] + public int Age { get; set; } + + [Required] + [Display(Name = "My Car Type")] + public CarType MyCarType { get; set; } + + [Required] + [AbpRadioButton(Inline = true)] + [Display(Name = "Your Car Type")] + public CarType YourCarType { get; set; } + + [DataType(DataType.Date)] + [Display(Name = "Day")] + public DateTime Day { get; set; } + + [SelectItems(nameof(CountryList))] + [Display(Name = "Country")] + public string Country { get; set; } + + [SelectItems(nameof(CountryList))] + [Display(Name = "Neighbor Countries")] + public List NeighborCountries { get; set; } + + public DetailedModel() + { + Name = ""; + Description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + IsActive = true; + Age = 65; + Day = DateTime.Now; + MyCarType = CarType.Coupe; + YourCarType = CarType.Sedan; + Country = "RU"; + NeighborCountries = new List() { "UK", "CA" }; + } + } + + public class OrderExampleModel + { + [DisplayOrder(10005)] + public string Surname { get; set; } + + //Default 10000 + public string EmailAddress { get; set; } + + [DisplayOrder(10003)] + public string Name { get; set; } + + [DisplayOrder(9999)] + public string City { get; set; } + } + + public class AttributeExamplesModel + { + [HiddenInput] + public string HiddenInput { get; set; } + + [DisabledInput] + public string DisabledInput { get; set; } + + [ReadOnlyInput] + public string ReadonlyInput { get; set; } + + [FormControlSize(AbpFormControlSize.Large)] + public string LargeInput { get; set; } + + [FormControlSize(AbpFormControlSize.Small)] + public string SmallInput { get; set; } + } + + public DetailedModel MyDetailedModel { get; set; } + + public OrderExampleModel MyOrderExampleModel { get; set; } + + public AttributeExamplesModel MyAttributeExamplesModel { get; set; } + + public DynamicFormsDemoModel() + { + MyDetailedModel = new DetailedModel(); + + MyOrderExampleModel = new OrderExampleModel(); + + MyAttributeExamplesModel = new AttributeExamplesModel(); + MyAttributeExamplesModel.DisabledInput = "Disabled Input"; + MyAttributeExamplesModel.ReadonlyInput = "Readonly Input"; + MyAttributeExamplesModel.LargeInput = "Large Input"; + MyAttributeExamplesModel.SmallInput = "Small Input"; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/DynamicFormsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/DynamicFormsDemoViewComponent.cs new file mode 100644 index 0000000000..3681eda627 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/DynamicFormsDemoViewComponent.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DynamicFormsDemo +{ + [Widget] + public class DynamicFormsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/DynamicFormsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + var model = new DynamicFormsDemoModel(); + + return View(ViewPath, model); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/Default.cshtml new file mode 100644 index 0000000000..54146c8953 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/Default.cshtml @@ -0,0 +1,124 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.FormElementsDemo +@model FormElementsDemoModel + +
+
+

The form model is:

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/FormElementsDemoModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/FormElementsDemoModel.cs new file mode 100644 index 0000000000..ea85aaefd8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/FormElementsDemoModel.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.FormElementsDemo +{ + public class FormElementsDemoModel + { + public enum CarType + { + Sedan, + Hatchback, + StationWagon, + Coupe + } + + public List CityList { get; set; } = new List + { + new SelectListItem { Value = "NY", Text = "New York"}, + new SelectListItem { Value = "LDN", Text = "London"}, + new SelectListItem { Value = "IST", Text = "Istanbul"}, + new SelectListItem { Value = "MOS", Text = "Moscow"} + }; + + public class InformMeModel + { + [Required] + public string Name { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + public bool CheckMeOut { get; set; } + } + + public class DetailsModel + { + [Required] + public string EmailAddress { get; set; } + + public string City { get; set; } + + public List Cities { get; set; } + + [TextArea] + public string Description { get; set; } + } + + public class CheckboxModel + { + public bool DefaultCheckbox { get; set; } + + public bool DisabledCheckbox { get; set; } + } + + public class CityRadioModel + { + [Display(Name = "City")] + public string CityRadio { get; set; } + } + + public class EnumModel + { + public CarType CarType { get; set; } + } + + public InformMeModel MyInformMeModel { get; set; } + public DetailsModel MyDetailsModel { get; set; } + public CheckboxModel MyCheckboxModel { get; set; } + public CityRadioModel MyCityRadioModel { get; set; } + public EnumModel MyEnumModel { get; set; } + + + public FormElementsDemoModel() + { + MyInformMeModel = new InformMeModel(); + MyDetailsModel = new DetailsModel(); + MyCheckboxModel = new CheckboxModel(); + MyCityRadioModel = new CityRadioModel() { CityRadio = "IST" }; + MyEnumModel = new EnumModel(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/FormElementsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/FormElementsDemoViewComponent.cs new file mode 100644 index 0000000000..01c477ecb1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/FormElementsDemo/FormElementsDemoViewComponent.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.FormElementsDemo +{ + [Widget] + public class FormElementsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/FormElementsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + var model = new FormElementsDemoModel(); + + return View(ViewPath, model); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/GridsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/GridsDemo/Default.cshtml new file mode 100644 index 0000000000..ee043b71e1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/GridsDemo/Default.cshtml @@ -0,0 +1,223 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.GridsDemo + + + + + 1 of 2 + 2 of 2 + + + 1 of 3 + 2 of 3 + 3 of 3 + + + + + + + + column + column + + column + column + + + + + + + + 1 of 3 + 2 of 3 (wider) + 3 of 3 + + + 1 of 3 + 2 of 3 (wider) + 3 of 3 + + + + + + + + 1 of 3 + Variable width content + 3 of 3 + + + 1 of 3 + Variable width content + 3 of 3 + + + + + + + col + col + col + col + + + col-8 + col-4 + + + + + + col-sm-8 + col-sm-4 + + + col-sm + col-sm + col-sm + col-sm + + + + + + + .col-12 .col-md-8 + .col-6 .col-md-4 + + + + + .col-6 .col-md-4 + .col-6 .col-md-4 + .col-6 .col-md-4 + + + + + .col-6 + .col-6 + + + + + + + column + column + column + + + column + column + column + + + column + column + column + + + + + + + + column + column + column + + + + + + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + One of two columns + One of two columns + + + + + + + One of two columns + One of two columns + + + + + + .col-9 + .col-4
Since 9 + 4 = 13 > 12, this 4-column-wide div gets wrapped onto a new line as one contiguous unit.
+ .col-6
Subsequent columns continue along the new line.s
+
+
+ + + + + First, but Last + Second, but unordered + Third, but Second + + + + + + + + First, but Last + Second, but unordered + Third, but First + + + + + + + + .col-md-4 + .col-md-4 .offset-md-4 + + + .col-md-3 .offset-md-3 + .col-md-3 .offset-md-3 + + + .col-md-6 .offset-md-3 + + + + + + + + .col-sm-5 .col-md-6 + .col-sm-5 .offset-sm-2 .col-md-6 .offset-md-0 + + + col-sm-6 .col-md-5 .col-lg-6 + .col-sm-6 .col-md-5 .offset-md-2 .col-lg-6 .offset-lg-0 + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/GridsDemo/GridsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/GridsDemo/GridsDemoViewComponent.cs new file mode 100644 index 0000000000..99f99186cd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/GridsDemo/GridsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.GridsDemo +{ + [Widget] + public class GridsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/GridsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ListGroupsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ListGroupsDemo/Default.cshtml new file mode 100644 index 0000000000..69dfbd24c4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ListGroupsDemo/Default.cshtml @@ -0,0 +1,82 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ListGroupsDemo + + + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + + + + + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + + + + + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + + + + + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + + + + + + Cras justo odio + Dapibus ac facilisis in + Morbi leo risus + Vestibulum at eros + + + + + + Cras justo odio + A simple Primary list group item + A simple Secondary list group item + A simple Success list group item + A simple Danger list group item + A simple Warning list group item + A simple Info list group item + A simple Light list group item + A simple Dark list group item + + + + + + Cras justo odio + A simple Primary list group item + A simple Secondary list group item + A simple Success list group item + A simple Danger list group item + A simple Warning list group item + A simple Info list group item + A simple Light list group item + A simple Dark list group item + + + + + + Cras justo odio 14 + Dapibus ac facilisis in 2 + Morbi leo risus 1 + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ListGroupsDemo/ListGroupsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ListGroupsDemo/ListGroupsDemoViewComponent.cs new file mode 100644 index 0000000000..9e0f89e325 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ListGroupsDemo/ListGroupsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ListGroupsDemo +{ + [Widget] + public class ListGroupsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ListGroupsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ModalsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ModalsDemo/Default.cshtml new file mode 100644 index 0000000000..91ed5b8bff --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ModalsDemo/Default.cshtml @@ -0,0 +1,15 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ModalsDemo +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal + + + + Launch modal + + + + + Woohoo, you're reading this text in a modal! + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ModalsDemo/ModalsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ModalsDemo/ModalsDemoViewComponent.cs new file mode 100644 index 0000000000..272ea4a480 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ModalsDemo/ModalsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ModalsDemo +{ + [Widget] + public class ModalsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ModalsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavbarsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavbarsDemo/Default.cshtml new file mode 100644 index 0000000000..f0c5113054 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavbarsDemo/Default.cshtml @@ -0,0 +1,71 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.NavbarsDemo + + + + Navbar + + + + Home (current) + + + Link + + + + + + Dropdown header + Action + Another disabled action + Something else here + + Separated link + + + + + Disabled + + + + Sample Text + + + + + + + + Navbar + + + + Home (current) + + + Link + + + + + + Dropdown header + Action + Another disabled action + Something else here + + Separated link + + + + + Disabled + + + + Sample Text + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavbarsDemo/NavbarsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavbarsDemo/NavbarsDemoViewComponent.cs new file mode 100644 index 0000000000..e282f1b923 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavbarsDemo/NavbarsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.NavbarsDemo +{ + [Widget] + public class NavbarsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/NavbarsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavsDemo/Default.cshtml new file mode 100644 index 0000000000..1423aea2b6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavsDemo/Default.cshtml @@ -0,0 +1,130 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.NavsDemo + + + + + Active + + + Longer nav link + + + + + + Action + Another action + Something else here + + + + + link + + + disabled + + + + + + + + Active + + + Longer nav link + + + + + + Action + Another action + Something else here + + + + + link + + + disabled + + + + + Active + + + Longer nav link + + + + + + Action + Another action + Something else here + + + + + link + + + disabled + + + + + Active + + + Longer nav link + + + + + + Action + Another action + Something else here + + + + + link + + + disabled + + + + + + + + Active + + + Longer nav link + + + + + + Action + Another action + Something else here + + + + + link + + + disabled + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavsDemo/NavsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavsDemo/NavsDemoViewComponent.cs new file mode 100644 index 0000000000..5bc0363193 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/NavsDemo/NavsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.NavsDemo +{ + [Widget] + public class NavsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/NavsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PaginatorDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PaginatorDemo/Default.cshtml new file mode 100644 index 0000000000..9dd61c6ff1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PaginatorDemo/Default.cshtml @@ -0,0 +1,5 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.PaginatorDemo + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PaginatorDemo/PaginatorDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PaginatorDemo/PaginatorDemoViewComponent.cs new file mode 100644 index 0000000000..46b8cb2db1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PaginatorDemo/PaginatorDemoViewComponent.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.PaginatorDemo +{ + [Widget] + public class PaginatorDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/PaginatorDemo/Default.cshtml"; + + public PagerModel PagerModel { get; set; } + + public IViewComponentResult Invoke(PagerModel pagerModel) + { + PagerModel = pagerModel; + + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PopoversDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PopoversDemo/Default.cshtml new file mode 100644 index 0000000000..1efee90533 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PopoversDemo/Default.cshtml @@ -0,0 +1,16 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.PopoversDemo + + + + Popover Default + + + Popover With Title + + + Dismissible Popover + + + Disabled Popover + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PopoversDemo/PopoversDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PopoversDemo/PopoversDemoViewComponent.cs new file mode 100644 index 0000000000..6114918165 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/PopoversDemo/PopoversDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.PopoversDemo +{ + [Widget] + public class PopoversDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/PopoversDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/Default.cshtml new file mode 100644 index 0000000000..00f917b28a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/Default.cshtml @@ -0,0 +1,17 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ProgressBarsDemo + + + + + %25 + + + + %50 + + + + %10 + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/ProgressBarsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/ProgressBarsDemoViewComponent.cs new file mode 100644 index 0000000000..acaf9b4135 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/ProgressBarsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ProgressBarsDemo +{ + [Widget] + public class ProgressBarsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ProgressBarsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TablesDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TablesDemo/Default.cshtml new file mode 100644 index 0000000000..d1ddbf16df --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TablesDemo/Default.cshtml @@ -0,0 +1,134 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TablesDemo + + + + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + mdo + + + 2 + Jacob + Thornton + fat + + + 3 + Larry + the Bird + twitter + + + + + + + + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + mdo + + + 2 + Jacob + Thornton + fat + + + 3 + Larry + the Bird + twitter + + + + + + + + List of users + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + mdo + + + 2 + Jacob + Thornton + fat + + + 3 + Larry + the Bird + twitter + + + + + + + + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + mdo + + + 2 + Jacob + Thornton + fat + + + 3 + Larry + the Bird + twitter + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TablesDemo/TablesDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TablesDemo/TablesDemoViewComponent.cs new file mode 100644 index 0000000000..e40ecae321 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TablesDemo/TablesDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TablesDemo +{ + [Widget] + public class TablesDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/TablesDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TabsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TabsDemo/Default.cshtml new file mode 100644 index 0000000000..e91c67d02c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TabsDemo/Default.cshtml @@ -0,0 +1,63 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TabsDemo + + + + + Content_Home + + + + Content_Profile + + + + Content_1_Content + + + Content_2_Content + + + + + + + + + Content_Home + + + Content_Profile + + + Content_Contact + + + + + + + + Content_Home + + + Content_Profile + + + Content_Contact + + + + + + + + Content_Home + + + Content_Profile + + + Content_Contact + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TabsDemo/TabsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TabsDemo/TabsDemoViewComponent.cs new file mode 100644 index 0000000000..00f3b5c2c4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TabsDemo/TabsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TabsDemo +{ + [Widget] + public class TabsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/TabsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TooltipsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TooltipsDemo/Default.cshtml new file mode 100644 index 0000000000..b611237b29 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TooltipsDemo/Default.cshtml @@ -0,0 +1,23 @@ +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TooltipsDemo + + + + Tooltip Default + + + + Tooltip on top + + + + Tooltip on right + + + + Tooltip on bottom + + + + Disabled button Tooltip + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TooltipsDemo/TooltipsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TooltipsDemo/TooltipsDemoViewComponent.cs new file mode 100644 index 0000000000..a23fede627 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/TooltipsDemo/TooltipsDemoViewComponent.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TooltipsDemo +{ + [Widget] + public class TooltipsDemoViewComponent : AbpViewComponent + { + public const string ViewPath = "/Views/Components/Themes/Shared/Demos/TooltipsDemo/Default.cshtml"; + + public IViewComponentResult Invoke() + { + return View(ViewPath); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs new file mode 100644 index 0000000000..5b3bd7791a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs @@ -0,0 +1,105 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.FileProviders; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers; +using Volo.Abp.VirtualFileSystem; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.TagHelpers +{ + public class AbpComponentDemoSectionTagHelper : AbpTagHelper + { + private const string DemoSectionOpeningTag = ""); + output.PreContent.AppendHtml($"

{Title}

"); + output.PreContent.AppendHtml("
"); + /* component rendering here */ + output.PostContent.AppendHtml("
"); //abp-component-demo-section-body + AppendRawSource(output); + AppendBootstrapSource(output, content); + output.PostContent.AppendHtml("
"); //abp-component-demo-section + } + + private static void AppendBootstrapSource(TagHelperOutput output, TagHelperContent content) + { + output.PostContent.AppendHtml("
"); + output.PostContent.AppendHtml("

Bootstrap

"); + output.PostContent.AppendHtml("
");
+            output.PostContent.Append(content.GetContent());
+            output.PostContent.AppendHtml("
"); + output.PostContent.AppendHtml("
"); + } + + private void AppendRawSource(TagHelperOutput output) + { + output.PostContent.AppendHtml("
"); + output.PostContent.AppendHtml("

ABP Tag Helpers

"); + output.PostContent.AppendHtml("
");
+            output.PostContent.Append(GetRawDemoSource());
+            output.PostContent.AppendHtml("
"); + output.PostContent.AppendHtml("
"); + } + + private string GetRawDemoSource() + { + StringBuilder sourceBuilder = null; + + var lines = GetFileContent().SplitToLines(); + + foreach (var line in lines) + { + if (line.Contains(DemoSectionOpeningTag) && GetName(line) == Title) + { + sourceBuilder = new StringBuilder(); + } + else if (line.Contains(DemoSectionClosingTag, StringComparison.InvariantCultureIgnoreCase)) + { + if (sourceBuilder == null) + { + continue; + } + + return sourceBuilder.ToString(); + } + else if (sourceBuilder != null) + { + sourceBuilder.AppendLine(line); + } + } + + throw new AbpException($"Could not find {Title} demo section inside {ViewPath}"); + } + + private string GetFileContent() + { + var viewFileInfo = _virtualFileProvider.GetFileInfo(ViewPath); + return viewFileInfo.ReadAsString(); + } + + private string GetName(string line) + { + var str = line.Substring(line.IndexOf("title=\"", StringComparison.OrdinalIgnoreCase) + "title=\"".Length); + str = str.Left(str.IndexOf("\"", StringComparison.OrdinalIgnoreCase)); + return str; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/_ViewImports.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/_ViewImports.cshtml new file mode 100644 index 0000000000..8015b96c4d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using System.Globalization +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo 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 new file mode 100644 index 0000000000..66015b14a7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj @@ -0,0 +1,34 @@ + + + + + + + netcoreapp3.1 + true + Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo + Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + true + Library + + + + + + + + + + + + + + + + + + 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 1fb4c12cf2..9682ec838e 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 @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; @@ -7,6 +8,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Views.Error; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers { @@ -16,20 +18,23 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers private readonly IHttpExceptionStatusCodeFinder _statusCodeFinder; private readonly IStringLocalizer _localizer; private readonly AbpErrorPageOptions _abpErrorPageOptions; + private readonly IExceptionNotifier _exceptionNotifier; public ErrorController( IExceptionToErrorInfoConverter exceptionToErrorInfoConverter, IHttpExceptionStatusCodeFinder httpExceptionStatusCodeFinder, IOptions abpErrorPageOptions, - IStringLocalizer localizer) + IStringLocalizer localizer, + IExceptionNotifier exceptionNotifier) { _errorInfoConverter = exceptionToErrorInfoConverter; _statusCodeFinder = httpExceptionStatusCodeFinder; _localizer = localizer; + _exceptionNotifier = exceptionNotifier; _abpErrorPageOptions = abpErrorPageOptions.Value; } - public IActionResult Index(int httpStatusCode) + public async Task Index(int httpStatusCode) { var exHandlerFeature = HttpContext.Features.Get(); @@ -37,6 +42,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Controllers ? exHandlerFeature.Error : new Exception(_localizer["UnhandledException"]); + await _exceptionNotifier.NotifyAsync(new ExceptionNotificationContext(exception)); + var errorInfo = _errorInfoConverter.Convert(exception); if (httpStatusCode == 0) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs index c808250a3e..31768b5e6a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Microsoft/AspNetCore/Http/AbpFormFileExtensions.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http { @@ -11,5 +12,13 @@ namespace Microsoft.AspNetCore.Http return stream.GetAllBytes(); } } + + public static async Task GetAllBytesAsync(this IFormFile file) + { + using (var stream = file.OpenReadStream()) + { + return await stream.GetAllBytesAsync(); + } + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj index dabe2e30d4..90500a456b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj @@ -26,9 +26,9 @@ - + - + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index 06ce49551b..21a675ddc4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -43,21 +43,6 @@ namespace Volo.Abp.AspNetCore.Mvc public override void ConfigureServices(ServiceConfigurationContext context) { - //Configure Razor - context.Services.Insert(0, - ServiceDescriptor.Singleton>( - new ConfigureOptions(options => - { - options.FileProviders.Add( - new RazorViewEngineVirtualFileProvider( - context.Services.GetSingletonInstance>() - ) - ); - } - ) - ) - ); - Configure(options => { options.IgnoredInterfaces.AddIfNotContains(typeof(IAsyncActionFilter)); @@ -101,7 +86,16 @@ namespace Volo.Abp.AspNetCore.Mvc }; }) .AddViewLocalization(); //TODO: How to configure from the application? Also, consider to move to a UI module since APIs does not care about it. - + + Configure(options => + { + options.FileProviders.Add( + new RazorViewEngineVirtualFileProvider( + context.Services.GetSingletonInstance>() + ) + ); + }); + context.Services.ExecutePreConfiguredActions(mvcBuilder); //TODO: AddViewLocalization by default..? diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs index ae72975486..e1c2d1f859 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs @@ -14,9 +14,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ApiExploring } [HttpGet] - public ApplicationApiDescriptionModel Get() + public ApplicationApiDescriptionModel Get(ApplicationApiDescriptionModelRequestDto model) { - return _modelProvider.CreateApiModel(); + return _modelProvider.CreateApiModel(model); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 69f75a31b2..4b4cf521c2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -8,9 +8,11 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Volo.Abp.Application.Services; +using Volo.Abp.AspNetCore.Mvc.MultiTenancy; using Volo.Abp.Authorization; using Volo.Abp.Features; using Volo.Abp.Localization; +using Volo.Abp.MultiTenancy; using Volo.Abp.Settings; using Volo.Abp.Users; @@ -19,6 +21,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApplicationConfigurationAppService { private readonly AbpLocalizationOptions _localizationOptions; + private readonly AbpMultiTenancyOptions _multiTenancyOptions; private readonly IServiceProvider _serviceProvider; private readonly IAbpAuthorizationPolicyProvider _abpAuthorizationPolicyProvider; private readonly IAuthorizationService _authorizationService; @@ -30,6 +33,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public AbpApplicationConfigurationAppService( IOptions localizationOptions, + IOptions multiTenancyOptions, IServiceProvider serviceProvider, IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider, IAuthorizationService authorizationService, @@ -48,6 +52,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations _featureDefinitionManager = featureDefinitionManager; _languageProvider = languageProvider; _localizationOptions = localizationOptions.Value; + _multiTenancyOptions = multiTenancyOptions.Value; } public virtual async Task GetAsync() @@ -60,11 +65,31 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations Features = await GetFeaturesConfigAsync(), Localization = await GetLocalizationConfigAsync(), CurrentUser = GetCurrentUser(), - Setting = await GetSettingConfigAsync() + Setting = await GetSettingConfigAsync(), + MultiTenancy = GetMultiTenancy(), + CurrentTenant = GetCurrentTenant() }; } + protected virtual CurrentTenantDto GetCurrentTenant() + { + return new CurrentTenantDto() + { + Id = CurrentTenant.Id, + Name = CurrentTenant.Name, + IsAvailable = CurrentTenant.IsAvailable + }; + } + + protected virtual MultiTenancyInfoDto GetMultiTenancy() + { + return new MultiTenancyInfoDto + { + IsEnabled = _multiTenancyOptions.IsEnabled + }; + } + protected virtual CurrentUserDto GetCurrentUser() { return new CurrentUserDto diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index 3069d72e9d..f957fd894f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -12,10 +13,13 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Volo.Abp.Application.Services; +using Volo.Abp.AspNetCore.Mvc.ApiExploring; using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.Utils; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Modeling; +using Volo.Abp.Reflection; +using Volo.Abp.Threading; namespace Volo.Abp.AspNetCore.Mvc { @@ -39,7 +43,7 @@ namespace Volo.Abp.AspNetCore.Mvc Logger = NullLogger.Instance; } - public ApplicationApiDescriptionModel CreateApiModel() + public ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input) { //TODO: Can cache the model? @@ -54,19 +58,22 @@ namespace Volo.Abp.AspNetCore.Mvc continue; } - AddApiDescriptionToModel(apiDescription, model); + AddApiDescriptionToModel(apiDescription, model, input); } } return model; } - private void AddApiDescriptionToModel(ApiDescription apiDescription, ApplicationApiDescriptionModel model) + private void AddApiDescriptionToModel( + ApiDescription apiDescription, + ApplicationApiDescriptionModel applicationModel, + ApplicationApiDescriptionModelRequestDto input) { var controllerType = apiDescription.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(); var setting = FindSetting(controllerType); - var moduleModel = model.GetOrAddModule(GetRootPath(controllerType, setting)); + var moduleModel = applicationModel.GetOrAddModule(GetRootPath(controllerType, setting), GetRemoteServiceName(controllerType, setting)); var controllerModel = moduleModel.GetOrAddController(controllerType.FullName, CalculateControllerName(controllerType, setting), controllerType, _modelOptions.IgnoredInterfaces); @@ -80,6 +87,7 @@ namespace Volo.Abp.AspNetCore.Mvc } Logger.LogDebug($"ActionApiDescriptionModel.Create: {controllerModel.ControllerName}.{uniqueMethodName}"); + var actionModel = controllerModel.AddAction(uniqueMethodName, ActionApiDescriptionModel.Create( uniqueMethodName, method, @@ -88,6 +96,11 @@ namespace Volo.Abp.AspNetCore.Mvc GetSupportedVersions(controllerType, method, setting) )); + if (input.IncludeTypes) + { + AddCustomTypesToModel(applicationModel, method); + } + AddParameterDescriptionsToModel(actionModel, method, apiDescription); } @@ -149,6 +162,68 @@ namespace Volo.Abp.AspNetCore.Mvc return supportedVersions.Select(v => v.ToString()).Distinct().ToList(); } + private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, MethodInfo method) + { + foreach (var parameterInfo in method.GetParameters()) + { + AddCustomTypesToModel(applicationModel, parameterInfo.ParameterType); + } + + AddCustomTypesToModel(applicationModel, method.ReturnType); + } + + private static void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, [CanBeNull] Type type) + { + if (type == null) + { + return; + } + + type = AsyncHelper.UnwrapTask(type); + + if (type == typeof(object) || + type == typeof(void) || + type == typeof(Enum) || + type == typeof(ValueType) || + TypeHelper.IsPrimitiveExtended(type)) + { + return; + } + + if (TypeHelper.IsEnumerable(type, out var itemType)) + { + AddCustomTypesToModel(applicationModel, itemType); + return; + } + + if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) + { + AddCustomTypesToModel(applicationModel, keyType); + AddCustomTypesToModel(applicationModel, valueType); + return; + } + + /* TODO: Add interfaces + */ + + var typeName = ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(type); + + if (applicationModel.Types.ContainsKey(typeName)) + { + return; + } + + var typeModel = TypeApiDescriptionModel.Create(type); + applicationModel.Types[typeName] = typeModel; + + AddCustomTypesToModel(applicationModel, type.BaseType); + + foreach (var propertyInfo in type.GetProperties()) + { + AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType); + } + } + private void AddParameterDescriptionsToModel(ActionApiDescriptionModel actionModel, MethodInfo method, ApiDescription apiDescription) { if (!apiDescription.ParameterDescriptions.Any()) @@ -214,6 +289,22 @@ namespace Volo.Abp.AspNetCore.Mvc return ModuleApiDescriptionModel.DefaultRootPath; } + private string GetRemoteServiceName(Type controllerType, [CanBeNull] ConventionalControllerSetting setting) + { + if (setting != null) + { + return setting.RemoteServiceName; + } + + var remoteServiceAttr = controllerType.GetCustomAttributes().OfType().FirstOrDefault(); + if (remoteServiceAttr?.Name != null) + { + return remoteServiceAttr.Name; + } + + return ModuleApiDescriptionModel.DefaultRemoteServiceName; + } + [CanBeNull] private ConventionalControllerSetting FindSetting(Type controllerType) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditActionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditActionFilter.cs index 33932137cb..0ee62f9b65 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditActionFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Auditing/AbpAuditActionFilter.cs @@ -84,12 +84,6 @@ namespace Volo.Abp.AspNetCore.Mvc.Auditing return false; } - //TODO: This is partially duplication of AuditHelper.ShouldSaveAudit method. Check why it does not work for controllers - if (!AuditingInterceptorRegistrar.ShouldAuditTypeByDefault(context.Controller.GetType())) - { - return false; - } - auditLog = auditLogScope.Log; auditLogAction = _auditingHelper.CreateAuditLogAction( auditLog, diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs index e28ff37717..e21e6ee939 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs @@ -23,9 +23,16 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions }; } - public AbpConventionalControllerOptions Create(Assembly assembly, [CanBeNull] Action optionsAction = null) + public AbpConventionalControllerOptions Create( + Assembly assembly, + [CanBeNull] Action optionsAction = null) { - var setting = new ConventionalControllerSetting(assembly, ModuleApiDescriptionModel.DefaultRootPath); + var setting = new ConventionalControllerSetting( + assembly, + ModuleApiDescriptionModel.DefaultRootPath, + ModuleApiDescriptionModel.DefaultRemoteServiceName + ); + optionsAction?.Invoke(setting); setting.Initialize(); ConventionalControllerSettings.Add(setting); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index 9875ccd808..0254a6022b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs @@ -12,6 +12,7 @@ using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.Http.Modeling; +using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc.Conventions @@ -81,7 +82,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions continue; } - if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType)) + if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType, includeEnums: true)) { if (CanUseFormBodyBinding(action, prm)) { @@ -94,7 +95,15 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual bool CanUseFormBodyBinding(ActionModel action, ParameterModel parameter) { - if (_options.ConventionalControllers.FormBodyBindingIgnoredTypes.Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) + //We want to use "id" as path parameter, not body! + if (parameter.ParameterName == "id") + { + return false; + } + + if (_options.ConventionalControllers + .FormBodyBindingIgnoredTypes + .Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType))) { return false; } @@ -251,7 +260,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions if (!selector.ActionConstraints.OfType().Any()) { - selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] {httpMethod})); + selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod })); } } } @@ -295,9 +304,24 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions var url = $"api/{rootPath}/{controllerNameInUrl.ToCamelCase()}"; //Add {id} path if needed - if (action.Parameters.Any(p => p.ParameterName == "id")) + var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id"); + if (idParameterModel != null) { - url += "/{id}"; + if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true)) + { + url += "/{id}"; + } + else + { + var properties = idParameterModel + .ParameterType + .GetProperties(BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in properties) + { + url += "/{" + property.Name + "}"; + } + } } //Add action name if needed @@ -341,7 +365,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual string NormalizeUrlControllerName(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration) { - if(configuration?.UrlControllerNameNormalizer == null) + if (configuration?.UrlControllerNameNormalizer == null) { return controllerName; } @@ -364,7 +388,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions protected virtual bool IsEmptySelector(SelectorModel selector) { - return selector.AttributeRouteModel == null + return selector.AttributeRouteModel == null && selector.ActionConstraints.IsNullOrEmpty() && selector.EndpointMetadata.IsNullOrEmpty(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs index fac627d182..bae2522d20 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalControllerSetting.cs @@ -30,6 +30,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions } private string _rootPath; + [NotNull] + public string RemoteServiceName + { + get => _remoteServiceName; + set + { + Check.NotNull(value, nameof(value)); + _remoteServiceName = value; + } + } + private string _remoteServiceName; + [CanBeNull] public Func TypePredicate { get; set; } @@ -46,15 +58,16 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions public Action ApiVersionConfigurer { get; set; } - public ConventionalControllerSetting([NotNull] Assembly assembly, [NotNull] string rootPath) + public ConventionalControllerSetting( + [NotNull] Assembly assembly, + [NotNull] string rootPath, + [NotNull] string remoteServiceName) { - Check.NotNull(assembly, rootPath); - - Assembly = assembly; - RootPath = rootPath; + Assembly = Check.NotNull(assembly, nameof(assembly)); + RootPath = Check.NotNull(rootPath, nameof(rootPath)); + RemoteServiceName = Check.NotNull(remoteServiceName, nameof(remoteServiceName)); ControllerTypes = new HashSet(); - ApiVersions = new List(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs index 28c1c18fda..1ce177ba44 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/AbpExceptionFilter.cs @@ -1,18 +1,21 @@ using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Json; namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { - public class AbpExceptionFilter : IExceptionFilter, ITransientDependency + public class AbpExceptionFilter : IAsyncExceptionFilter, ITransientDependency { public ILogger Logger { get; set; } @@ -32,14 +35,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling Logger = NullLogger.Instance; } - public virtual void OnException(ExceptionContext context) + public async Task OnExceptionAsync(ExceptionContext context) { if (!ShouldHandleException(context)) { return; } - HandleAndWrapException(context); + await HandleAndWrapException(context); } protected virtual bool ShouldHandleException(ExceptionContext context) @@ -65,7 +68,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling return false; } - protected virtual void HandleAndWrapException(ExceptionContext context) + protected virtual async Task HandleAndWrapException(ExceptionContext context) { //TODO: Trigger an AbpExceptionHandled event or something like that. @@ -82,6 +85,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling Logger.LogWithLevel(logLevel, _jsonSerializer.Serialize(remoteServiceErrorInfo, indented: true)); Logger.LogException(context.Exception, logLevel); + await context.HttpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(context.Exception) + ); + context.Exception = null; //Handled! } } diff --git a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj index 38d9a34867..2ff1d539a6 100644 --- a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj +++ b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj @@ -24,7 +24,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs index f59c5b6b3a..d3f261a3f7 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs index cdd921dae4..9a7a8f1676 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.Net.Http.Headers; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Uow; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Json; @@ -73,6 +74,13 @@ namespace Volo.Abp.AspNetCore.ExceptionHandling ) ) ); + + await httpContext + .RequestServices + .GetRequiredService() + .NotifyAsync( + new ExceptionNotificationContext(exception) + ); } private Task ClearCacheHeaders(object state) diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs index b333e07bf9..e53ba5b9a6 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs @@ -86,6 +86,44 @@ namespace Volo.Abp.Auditing return defaultValue; } + public virtual bool IsEntityHistoryEnabled(Type entityType, bool defaultValue = false) + { + if (!entityType.IsPublic) + { + return false; + } + + if (Options.IgnoredTypes.Any(t => t.IsAssignableFrom(entityType))) + { + return false; + } + + if (entityType.IsDefined(typeof(AuditedAttribute), true)) + { + return true; + } + + foreach (var propertyInfo in entityType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if(propertyInfo.IsDefined(typeof(AuditedAttribute))) + { + return true; + } + } + + if (entityType.IsDefined(typeof(DisableAuditingAttribute), true)) + { + return false; + } + + if (Options.EntityHistorySelectors.Any(selector => selector.Predicate(entityType))) + { + return true; + } + + return defaultValue; + } + public virtual AuditLogInfo CreateAuditLogInfo() { var auditInfo = new AuditLogInfo diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingManager.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingManager.cs index 2a9dd8bf5a..8cc9fb7675 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingManager.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingManager.cs @@ -114,20 +114,7 @@ namespace Volo.Abp.Auditing { BeforeSave(saveHandle); - if (ShouldSave(saveHandle.AuditLog)) - { - await _auditingStore.SaveAsync(saveHandle.AuditLog); - } - } - - protected bool ShouldSave(AuditLogInfo auditLog) - { - if (!auditLog.Actions.Any() && !auditLog.EntityChanges.Any()) - { - return false; - } - - return true; + await _auditingStore.SaveAsync(saveHandle.AuditLog); } protected class DisposableSaveHandle : IAuditLogSaveHandle diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs index e6992491b8..6ee13a57d1 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs @@ -4,11 +4,13 @@ using System.Reflection; namespace Volo.Abp.Auditing { - //TODO: Move ShouldSaveAudit and rename to IAuditingFactory + //TODO: Move ShouldSaveAudit & IsEntityHistoryEnabled and rename to IAuditingFactory public interface IAuditingHelper { bool ShouldSaveAudit(MethodInfo methodInfo, bool defaultValue = false); + bool IsEntityHistoryEnabled(Type entityType, bool defaultValue = false); + AuditLogInfo CreateAuditLogInfo(); AuditLogActionInfo CreateAuditLogAction( diff --git a/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/AbpAuthorizationServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/AbpAuthorizationServiceCollectionExtensions.cs index 093c9f3884..1cd3f5a0b7 100644 --- a/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/AbpAuthorizationServiceCollectionExtensions.cs +++ b/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/AbpAuthorizationServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection { services.Replace(ServiceDescriptor.Singleton()); services.Replace(ServiceDescriptor.Singleton()); + services.Replace(ServiceDescriptor.Singleton()); return services.Replace(ServiceDescriptor.Singleton()); } } diff --git a/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj b/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj index 8c6482837f..e5183688df 100644 --- a/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj +++ b/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AlwaysAllowMethodInvocationAuthorizationService.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AlwaysAllowMethodInvocationAuthorizationService.cs new file mode 100644 index 0000000000..d4a30b56f5 --- /dev/null +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AlwaysAllowMethodInvocationAuthorizationService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Authorization +{ + public class AlwaysAllowMethodInvocationAuthorizationService : IMethodInvocationAuthorizationService + { + public Task CheckAsync(MethodInvocationAuthorizationContext context) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs index e5541caf3f..3c700dfdf6 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System; +using JetBrains.Annotations; using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; @@ -7,13 +8,35 @@ namespace Volo.Abp.Authorization.Permissions public interface IPermissionDefinitionContext { //TODO: Add Get methods to find and modify a permission or group. + + IServiceProvider ServiceProvider { get; } + + /// + /// Gets a pre-defined permission group. + /// Throws if can not find the given group. + /// + /// Name of the group + /// + PermissionGroupDefinition GetGroup([NotNull] string name); + + /// + /// Tries to get a pre-defined permission group. + /// Returns null if can not find the given group. + /// + /// Name of the group + /// + [NotNull] PermissionGroupDefinition GetGroupOrNull(string name); + [CanBeNull] PermissionGroupDefinition AddGroup( [NotNull] string name, ILocalizableString displayName = null, MultiTenancySides multiTenancySide = MultiTenancySides.Both); void RemoveGroup(string name); + + [CanBeNull] + PermissionDefinition GetPermissionOrNull([NotNull] string name); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionProvider.cs index ef92125595..206176ac52 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionProvider.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IPermissionDefinitionProvider.cs @@ -2,6 +2,10 @@ { public interface IPermissionDefinitionProvider { + void PreDefine(IPermissionDefinitionContext context); + void Define(IPermissionDefinitionContext context); + + void PostDefine(IPermissionDefinitionContext context); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs index 51c8530ee6..931c9d1d23 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionChecker.cs @@ -32,12 +32,19 @@ namespace Volo.Abp.Authorization.Permissions return await IsGrantedAsync(PrincipalAccessor.Principal, name); } - public virtual async Task IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name) + public virtual async Task IsGrantedAsync( + ClaimsPrincipal claimsPrincipal, + string name) { Check.NotNull(name, nameof(name)); var permission = PermissionDefinitionManager.Get(name); + if (!permission.IsEnabled) + { + return false; + } + var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ?? CurrentTenant.GetMultiTenancySide(); diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs index 2e274965b2..602fdd9c50 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs @@ -46,6 +46,19 @@ namespace Volo.Abp.Authorization.Permissions /// public Dictionary Properties { get; } + /// + /// Indicates whether this permission is enabled or disabled. + /// A permission is normally enabled. + /// A disabled permission can not be granted to anyone, but it is still + /// will be available to check its value (while it will always be false). + /// + /// Disabling a permission would be helpful to hide a related application + /// functionality from users/clients. + /// + /// Default: true. + /// + public bool IsEnabled { get; set; } + /// /// Gets/sets a key-value on the . /// @@ -63,11 +76,13 @@ namespace Volo.Abp.Authorization.Permissions protected internal PermissionDefinition( [NotNull] string name, ILocalizableString displayName = null, - MultiTenancySides multiTenancySide = MultiTenancySides.Both) + MultiTenancySides multiTenancySide = MultiTenancySides.Both, + bool isEnabled = true) { Name = Check.NotNull(name, nameof(name)); DisplayName = displayName ?? new FixedLocalizableString(name); MultiTenancySide = multiTenancySide; + IsEnabled = isEnabled; Properties = new Dictionary(); Providers = new List(); @@ -77,12 +92,14 @@ namespace Volo.Abp.Authorization.Permissions public virtual PermissionDefinition AddChild( [NotNull] string name, ILocalizableString displayName = null, - MultiTenancySides multiTenancySide = MultiTenancySides.Both) + MultiTenancySides multiTenancySide = MultiTenancySides.Both, + bool isEnabled = true) { var child = new PermissionDefinition( name, displayName, - multiTenancySide) + multiTenancySide, + isEnabled) { Parent = this }; diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs index 777cd1f7b0..4e52cdc2ee 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using JetBrains.Annotations; using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; @@ -6,10 +8,13 @@ namespace Volo.Abp.Authorization.Permissions { public class PermissionDefinitionContext : IPermissionDefinitionContext { + public IServiceProvider ServiceProvider { get; } + internal Dictionary Groups { get; } - internal PermissionDefinitionContext() + internal PermissionDefinitionContext(IServiceProvider serviceProvider) { + ServiceProvider = serviceProvider; Groups = new Dictionary(); } @@ -28,7 +33,20 @@ namespace Volo.Abp.Authorization.Permissions return Groups[name] = new PermissionGroupDefinition(name, displayName, multiTenancySide); } - public virtual PermissionGroupDefinition GetGroupOrNull(string name) + [NotNull] + public virtual PermissionGroupDefinition GetGroup([NotNull] string name) + { + var group = GetGroupOrNull(name); + + if (group == null) + { + throw new AbpException($"Could not find a permission definition group with the given name: {name}"); + } + + return group; + } + + public virtual PermissionGroupDefinition GetGroupOrNull([NotNull] string name) { Check.NotNull(name, nameof(name)); @@ -51,5 +69,22 @@ namespace Volo.Abp.Authorization.Permissions Groups.Remove(name); } + + public virtual PermissionDefinition GetPermissionOrNull([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + foreach (var groupDefinition in Groups.Values) + { + var permissionDefinition = groupDefinition.GetPermissionOrNull(name); + + if (permissionDefinition != null) + { + return permissionDefinition; + } + } + + return null; + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContextExtensions.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContextExtensions.cs new file mode 100644 index 0000000000..e1bdcb8057 --- /dev/null +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionContextExtensions.cs @@ -0,0 +1,34 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Authorization.Permissions +{ + public static class PermissionDefinitionContextExtensions + { + /// + /// Finds and disables a permission with the given . + /// Returns false if given permission was not found. + /// + /// Permission definition context + /// Name of the permission + /// + /// Returns true if given permission was found. + /// Returns false if given permission was not found. + /// + public static bool TryDisablePermission( + [NotNull] this IPermissionDefinitionContext context, + [NotNull] string name) + { + Check.NotNull(context, nameof(context)); + Check.NotNull(name, nameof(name)); + + var permission = context.GetPermissionOrNull(name); + if (permission == null) + { + return false; + } + + permission.IsEnabled = false; + return true; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs index 7e193feda0..fba618fc78 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs @@ -83,7 +83,7 @@ namespace Volo.Abp.Authorization.Permissions } protected virtual void AddPermissionToDictionaryRecursively( - Dictionary permissions, + Dictionary permissions, PermissionDefinition permission) { if (permissions.ContainsKey(permission.Name)) @@ -101,22 +101,32 @@ namespace Volo.Abp.Authorization.Permissions protected virtual Dictionary CreatePermissionGroupDefinitions() { - var context = new PermissionDefinitionContext(); - using (var scope = _serviceProvider.CreateScope()) { + var context = new PermissionDefinitionContext(scope.ServiceProvider); + var providers = Options - .DefinitionProviders - .Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider) - .ToList(); + .DefinitionProviders + .Select(p => scope.ServiceProvider.GetRequiredService(p) as IPermissionDefinitionProvider) + .ToList(); + + foreach (var provider in providers) + { + provider.PreDefine(context); + } foreach (var provider in providers) { provider.Define(context); } - } - return context.Groups; + foreach (var provider in providers) + { + provider.PostDefine(context); + } + + return context.Groups; + } } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionProvider.cs index 0584ffe426..b6fcff3429 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionProvider.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionProvider.cs @@ -4,6 +4,16 @@ namespace Volo.Abp.Authorization.Permissions { public abstract class PermissionDefinitionProvider : IPermissionDefinitionProvider, ITransientDependency { + public virtual void PreDefine(IPermissionDefinitionContext context) + { + + } + public abstract void Define(IPermissionDefinitionContext context); + + public virtual void PostDefine(IPermissionDefinitionContext context) + { + + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs index 5582739a11..5038e8e064 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; +using JetBrains.Annotations; using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; @@ -60,9 +61,15 @@ namespace Volo.Abp.Authorization.Permissions public virtual PermissionDefinition AddPermission( string name, ILocalizableString displayName = null, - MultiTenancySides multiTenancySide = MultiTenancySides.Both) + MultiTenancySides multiTenancySide = MultiTenancySides.Both, + bool isEnabled = true) { - var permission = new PermissionDefinition(name, displayName, multiTenancySide); + var permission = new PermissionDefinition( + name, + displayName, + multiTenancySide, + isEnabled + ); _permissions.Add(permission); @@ -95,5 +102,33 @@ namespace Volo.Abp.Authorization.Permissions { return $"[{nameof(PermissionGroupDefinition)} {Name}]"; } + + [CanBeNull] + public PermissionDefinition GetPermissionOrNull([NotNull] string name) + { + Check.NotNull(name, nameof(name)); + + return GetPermissionOrNullRecursively(Permissions, name); + } + + private PermissionDefinition GetPermissionOrNullRecursively( + IReadOnlyList permissions, string name) + { + foreach (var permission in permissions) + { + if (permission.Name == name) + { + return permission; + } + + var childPermission = GetPermissionOrNullRecursively(permission.Children, name); + if (childPermission != null) + { + return childPermission; + } + } + + return null; + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs b/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs new file mode 100644 index 0000000000..40be84cb6f --- /dev/null +++ b/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace AutoMapper +{ + public static class AbpAutoMapperExtensibleDtoExtensions + { + public static IMappingExpression MapExtraProperties( + this IMappingExpression mappingExpression, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + where TDestination : IHasExtraProperties + where TSource : IHasExtraProperties + { + return mappingExpression + .ForMember( + x => x.ExtraProperties, + y => y.MapFrom( + (source, destination, extraProps) => + { + var result = extraProps.IsNullOrEmpty() + ? new Dictionary() + : new Dictionary(extraProps); + + ExtensibleObjectMapper + .MapExtraPropertiesTo( + source.ExtraProperties, + result, + definitionChecks, + ignoredProperties + ); + + return result; + }) + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj b/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj index e6146fedde..327f2078d1 100644 --- a/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj +++ b/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj @@ -15,6 +15,7 @@ + diff --git a/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs b/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs index ba3ef2ada8..1590e879b1 100644 --- a/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs +++ b/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs @@ -3,11 +3,14 @@ using AutoMapper; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectMapping; namespace Volo.Abp.AutoMapper { - [DependsOn(typeof(AbpObjectMappingModule))] + [DependsOn( + typeof(AbpObjectMappingModule), + typeof(AbpObjectExtendingModule))] public class AbpAutoMapperModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj index 868f7e6719..45cc6e2d94 100644 --- a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj +++ b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj @@ -17,7 +17,7 @@ - + diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs index d4ce5cd03a..a5027daec6 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.BackgroundJobs { @@ -51,6 +53,10 @@ namespace Volo.Abp.BackgroundJobs { Logger.LogException(ex); + await context.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + throw new BackgroundJobExecutionException("A background job execution is failed. See inner exception for details.", ex) { JobType = context.JobType.AssemblyQualifiedName, diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.RabbitMq/FodyWeavers.xml b/framework/src/Volo.Abp.BackgroundJobs.Quartz/FodyWeavers.xml similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.RabbitMq/FodyWeavers.xml rename to framework/src/Volo.Abp.BackgroundJobs.Quartz/FodyWeavers.xml diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.RabbitMq/FodyWeavers.xsd b/framework/src/Volo.Abp.BackgroundJobs.Quartz/FodyWeavers.xsd similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.RabbitMq/FodyWeavers.xsd rename to framework/src/Volo.Abp.BackgroundJobs.Quartz/FodyWeavers.xsd diff --git a/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo.Abp.BackgroundJobs.Quartz.csproj b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo.Abp.BackgroundJobs.Quartz.csproj new file mode 100644 index 0000000000..67c07f77d6 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo.Abp.BackgroundJobs.Quartz.csproj @@ -0,0 +1,22 @@ + + + + + + + netstandard2.0 + Volo.Abp.BackgroundJobs.Quartz + Volo.Abp.BackgroundJobs.Quartz + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + diff --git a/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/AbpBackgroundJobsQuartzModule.cs b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/AbpBackgroundJobsQuartzModule.cs new file mode 100644 index 0000000000..d5f84ed91e --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/AbpBackgroundJobsQuartzModule.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; +using Volo.Abp.Quartz; + +namespace Volo.Abp.BackgroundJobs.Quartz +{ + [DependsOn( + typeof(AbpBackgroundJobsAbstractionsModule), + typeof(AbpQuartzModule) + )] + public class AbpBackgroundJobsQuartzModule :AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddTransient(typeof(QuartzJobExecutionAdapter<>)); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzBackgroundJobManager.cs b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzBackgroundJobManager.cs new file mode 100644 index 0000000000..2e7b8193b4 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzBackgroundJobManager.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Quartz; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs.Quartz +{ + [Dependency(ReplaceServices = true)] + public class QuartzBackgroundJobManager : IBackgroundJobManager, ITransientDependency + { + private readonly IScheduler _scheduler; + + public QuartzBackgroundJobManager(IScheduler scheduler) + { + _scheduler = scheduler; + } + + public async Task EnqueueAsync(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, + TimeSpan? delay = null) + { + var jobDetail = JobBuilder.Create>().SetJobData(new JobDataMap { { nameof(TArgs), args } }).Build(); + var trigger = !delay.HasValue ? TriggerBuilder.Create().StartNow().Build() : TriggerBuilder.Create().StartAt(new DateTimeOffset(DateTime.Now.Add(delay.Value))).Build(); + await _scheduler.ScheduleJob(jobDetail, trigger); + return jobDetail.Key.ToString(); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzJobExecutionAdapter.cs b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzJobExecutionAdapter.cs new file mode 100644 index 0000000000..85a1ef736f --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzJobExecutionAdapter.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Quartz; + +namespace Volo.Abp.BackgroundJobs.Quartz +{ + public class QuartzJobExecutionAdapter : IJob + { + protected AbpBackgroundJobOptions Options { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } + protected IBackgroundJobExecuter JobExecuter { get; } + + public QuartzJobExecutionAdapter( + IOptions options, + IBackgroundJobExecuter jobExecuter, + IServiceScopeFactory serviceScopeFactory) + { + JobExecuter = jobExecuter; + ServiceScopeFactory = serviceScopeFactory; + Options = options.Value; + } + + public async Task Execute(IJobExecutionContext context) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var args = (TArgs)context.JobDetail.JobDataMap.Get(nameof(TArgs)); + var jobType = Options.GetJob(typeof(TArgs)).JobType; + var jobContext = new JobExecutionContext(scope.ServiceProvider, jobType, args); + await JobExecuter.ExecuteAsync(jobContext); + } + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs index 4cb4f04a09..879cf4f8ff 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Nito.AsyncEx; using RabbitMQ.Client; using RabbitMQ.Client.Events; +using Volo.Abp.ExceptionHandling; using Volo.Abp.RabbitMQ; using Volo.Abp.Threading; @@ -31,6 +32,7 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ protected IRabbitMqSerializer Serializer { get; } protected IBackgroundJobExecuter JobExecuter { get; } protected IServiceScopeFactory ServiceScopeFactory { get; } + protected IExceptionNotifier ExceptionNotifier { get; } protected SemaphoreSlim SyncObj = new SemaphoreSlim(1, 1); protected bool IsDiposed { get; private set; } @@ -41,13 +43,15 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ IChannelPool channelPool, IRabbitMqSerializer serializer, IBackgroundJobExecuter jobExecuter, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + IExceptionNotifier exceptionNotifier) { AbpBackgroundJobOptions = backgroundJobOptions.Value; AbpRabbitMqBackgroundJobOptions = rabbitMqAbpBackgroundJobOptions.Value; Serializer = serializer; JobExecuter = jobExecuter; ServiceScopeFactory = serviceScopeFactory; + ExceptionNotifier = exceptionNotifier; ChannelPool = channelPool; JobConfiguration = AbpBackgroundJobOptions.GetJob(typeof(TArgs)); diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/FodyWeavers.xml b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/FodyWeavers.xml similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/FodyWeavers.xml rename to framework/src/Volo.Abp.BackgroundWorkers.Quartz/FodyWeavers.xml diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/FodyWeavers.xsd b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/FodyWeavers.xsd similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/FodyWeavers.xsd rename to framework/src/Volo.Abp.BackgroundWorkers.Quartz/FodyWeavers.xsd diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo.Abp.BackgroundWorkers.Quartz.csproj b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo.Abp.BackgroundWorkers.Quartz.csproj new file mode 100644 index 0000000000..1a3ca4e254 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo.Abp.BackgroundWorkers.Quartz.csproj @@ -0,0 +1,24 @@ + + + + + + + + netstandard2.0 + Volo.Abp.BackgroundWorkers.Quartz + Volo.Abp.BackgroundWorkers.Quartz + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs new file mode 100644 index 0000000000..1f23b350aa --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Modularity; +using Volo.Abp.Quartz; + +namespace Volo.Abp.BackgroundWorkers.Quartz +{ + [DependsOn( + typeof(AbpBackgroundWorkersModule), + typeof(AbpQuartzModule) + )] + public class AbpBackgroundWorkersQuartzModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddConventionalRegistrar(new AbpQuartzConventionalRegistrar()); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var backgroundWorkerManager = context.ServiceProvider.GetService(); + var works = context.ServiceProvider.GetServices(); + + foreach (var work in works) + { + backgroundWorkerManager.Add(work); + } + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpQuartzConventionalRegistrar.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpQuartzConventionalRegistrar.cs new file mode 100644 index 0000000000..8d709c449c --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpQuartzConventionalRegistrar.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers.Quartz +{ + public class AbpQuartzConventionalRegistrar : DefaultConventionalRegistrar + { + public override void AddType(IServiceCollection services, Type type) + { + if (!typeof(IQuartzBackgroundWorker).IsAssignableFrom(type)) + { + return; + } + + var dependencyAttribute = GetDependencyAttributeOrNull(type); + var lifeTime = GetLifeTimeOrNull(type, dependencyAttribute); + + if (lifeTime == null) + { + return; + } + + services.Add(ServiceDescriptor.Describe(typeof(IQuartzBackgroundWorker), type, lifeTime.Value)); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorker.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorker.cs new file mode 100644 index 0000000000..ffc2117e6f --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/IQuartzBackgroundWorker.cs @@ -0,0 +1,11 @@ +using Quartz; + +namespace Volo.Abp.BackgroundWorkers.Quartz +{ + public interface IQuartzBackgroundWorker : IBackgroundWorker, IJob + { + ITrigger Trigger { get; set; } + + IJobDetail JobDetail { get; set; } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerBase.cs new file mode 100644 index 0000000000..e468ac80f5 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerBase.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Quartz; + +namespace Volo.Abp.BackgroundWorkers.Quartz +{ + public abstract class QuartzBackgroundWorkerBase : BackgroundWorkerBase, IQuartzBackgroundWorker + { + public ITrigger Trigger { get; set; } + + public IJobDetail JobDetail { get; set; } + + public abstract Task Execute(IJobExecutionContext context); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs new file mode 100644 index 0000000000..46568a5a11 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using Quartz; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundWorkers.Quartz +{ + [Dependency(ReplaceServices = true)] + public class QuartzBackgroundWorkerManager : IBackgroundWorkerManager, ISingletonDependency + { + private readonly IScheduler _scheduler; + + public QuartzBackgroundWorkerManager(IScheduler scheduler) + { + _scheduler = scheduler; + } + + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _scheduler.ResumeAll(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + if (!_scheduler.IsShutdown) + { + await _scheduler.PauseAll(cancellationToken); + } + } + + public void Add(IBackgroundWorker worker) + { + if (worker is IQuartzBackgroundWorker quartzWork) + { + Check.NotNull(quartzWork.Trigger, nameof(quartzWork.Trigger)); + Check.NotNull(quartzWork.JobDetail, nameof(quartzWork.JobDetail)); + + AsyncHelper.RunSync(() => _scheduler.ScheduleJob(quartzWork.JobDetail, quartzWork.Trigger)); + } + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs index 62da163eb9..cb2f6dbfae 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AsyncPeriodicBackgroundWorkerBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers @@ -35,18 +36,24 @@ namespace Volo.Abp.BackgroundWorkers private void Timer_Elapsed(object sender, System.EventArgs e) { - try + using (var scope = ServiceScopeFactory.CreateScope()) { - using (var scope = ServiceScopeFactory.CreateScope()) + try { AsyncHelper.RunSync( () => DoWorkAsync(new PeriodicBackgroundWorkerContext(scope.ServiceProvider)) ); } - } - catch (Exception ex) - { - Logger.LogException(ex); + catch (Exception ex) + { + AsyncHelper.RunSync( + () => scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)) + ); + + Logger.LogException(ex); + } } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs index 1ce71ca3f9..3326c4e65f 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkerBase.cs @@ -1,5 +1,7 @@ +using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -11,14 +13,34 @@ namespace Volo.Abp.BackgroundWorkers public abstract class BackgroundWorkerBase : IBackgroundWorker { //TODO: Add UOW, Localization and other useful properties..? + public IServiceProvider ServiceProvider { get; set; } + protected readonly object ServiceProviderLock = new object(); - public ILogger Logger { protected get; set; } + protected TService LazyGetRequiredService(ref TService reference) + => LazyGetRequiredService(typeof(TService), ref reference); - protected BackgroundWorkerBase() + protected TRef LazyGetRequiredService(Type serviceType, ref TRef reference) { - Logger = NullLogger.Instance; + if (reference == null) + { + lock (ServiceProviderLock) + { + if (reference == null) + { + reference = (TRef)ServiceProvider.GetRequiredService(serviceType); + } + } + } + + return reference; } - + + public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory); + private ILoggerFactory _loggerFactory; + + protected ILogger Logger => _lazyLogger.Value; + private Lazy _lazyLogger => new Lazy(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); + public virtual Task StartAsync(CancellationToken cancellationToken = default) { Logger.LogDebug("Started background worker: " + ToString()); diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs new file mode 100644 index 0000000000..ccb3beac5d --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs @@ -0,0 +1,38 @@ +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers +{ + public static class BackgroundWorkersApplicationInitializationContextExtensions + { + public static ApplicationInitializationContext AddBackgroundWorker([NotNull] this ApplicationInitializationContext context) + where TWorker : IBackgroundWorker + { + Check.NotNull(context, nameof(context)); + + context.AddBackgroundWorker(typeof(TWorker)); + + return context; + } + + public static ApplicationInitializationContext AddBackgroundWorker([NotNull] this ApplicationInitializationContext context, [NotNull] Type workerType) + { + Check.NotNull(context, nameof(context)); + Check.NotNull(workerType, nameof(workerType)); + + if (!workerType.IsAssignableTo()) + { + throw new AbpException($"Given type ({workerType.AssemblyQualifiedName}) must implement the {typeof(IBackgroundWorker).AssemblyQualifiedName} interface, but it doesn't!"); + } + + context.ServiceProvider + .GetRequiredService() + .Add( + (IBackgroundWorker)context.ServiceProvider.GetRequiredService(workerType) + ); + + return context; + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs index 091bcc917f..c63adaf798 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/PeriodicBackgroundWorkerBase.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.BackgroundWorkers @@ -38,16 +39,21 @@ namespace Volo.Abp.BackgroundWorkers private void Timer_Elapsed(object sender, System.EventArgs e) { - try + using (var scope = ServiceScopeFactory.CreateScope()) { - using (var scope = ServiceScopeFactory.CreateScope()) + try { + DoWork(new PeriodicBackgroundWorkerContext(scope.ServiceProvider)); } - } - catch (Exception ex) - { - Logger.LogException(ex); + catch (Exception ex) + { + scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } } } diff --git a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj index f59d9fe116..db744f410e 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 c23fb05aa7..267ab259f5 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -2,10 +2,13 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; 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; @@ -23,12 +26,14 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer) : base( + IDistributedCacheKeyNormalizer keyNormalizer, + IHybridServiceScopeFactory serviceScopeFactory) : base( distributedCacheOption: distributedCacheOption, cache: cache, cancellationTokenProvider: cancellationTokenProvider, serializer: serializer, - keyNormalizer: keyNormalizer) + keyNormalizer: keyNormalizer, + serviceScopeFactory: serviceScopeFactory) { } @@ -56,6 +61,8 @@ namespace Volo.Abp.Caching protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } + protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected SemaphoreSlim SyncSemaphore { get; } protected DistributedCacheEntryOptions DefaultCacheOptions; @@ -67,7 +74,8 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer) + IDistributedCacheKeyNormalizer keyNormalizer, + IHybridServiceScopeFactory serviceScopeFactory) { _distributedCacheOption = distributedCacheOption.Value; Cache = cache; @@ -75,6 +83,7 @@ namespace Volo.Abp.Caching Logger = NullLogger>.Instance; Serializer = serializer; KeyNormalizer = keyNormalizer; + ServiceScopeFactory = serviceScopeFactory; SyncSemaphore = new SemaphoreSlim(1, 1); @@ -139,7 +148,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return null; } @@ -181,7 +190,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return null; } @@ -298,7 +307,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return; } @@ -337,7 +346,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } @@ -364,7 +373,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); return; } @@ -393,7 +402,7 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } @@ -420,7 +429,8 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); + return; } throw; @@ -449,12 +459,24 @@ namespace Volo.Abp.Caching { if (hideErrors == true) { - Logger.LogException(ex, LogLevel.Warning); + await HandleExceptionAsync(ex); return; } throw; } } + + protected virtual async Task HandleExceptionAsync(Exception ex) + { + Logger.LogException(ex, LogLevel.Warning); + + using (var scope = ServiceScopeFactory.CreateScope()) + { + await scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); + } + } } } \ No newline at end of file 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 34bf95fdf1..f5a5a44980 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 @@ -28,8 +28,11 @@ namespace Volo.Abp.Cli options.Commands["add-package"] = typeof(AddPackageCommand); options.Commands["add-module"] = typeof(AddModuleCommand); options.Commands["login"] = typeof(LoginCommand); - options.Commands["logout"] = typeof(LogoutCommand); + options.Commands["logout"] = typeof(LogoutCommand); + options.Commands["generate-proxy"] = typeof(GenerateProxyCommand); options.Commands["suite"] = typeof(SuiteCommand); + options.Commands["switch-to-preview"] = typeof(SwitchNightlyPreviewCommand); + options.Commands["switch-to-stable"] = typeof(SwitchStableCommand); }); } } 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 58adc65fc4..b37f67fd1a 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 @@ -16,10 +16,12 @@ namespace Volo.Abp.Cli.Commands public ILogger Logger { get; set; } protected SolutionModuleAdder SolutionModuleAdder { get; } + public SolutionAbpVersionFinder SolutionAbpVersionFinder { get; } - public AddModuleCommand(SolutionModuleAdder solutionModuleAdder) + public AddModuleCommand(SolutionModuleAdder solutionModuleAdder, SolutionAbpVersionFinder solutionAbpVersionFinder) { SolutionModuleAdder = solutionModuleAdder; + SolutionAbpVersionFinder = solutionAbpVersionFinder; Logger = NullLogger.Instance; } @@ -34,14 +36,26 @@ namespace Volo.Abp.Cli.Commands ); } + var withSourceCode = commandLineArgs.Options.ContainsKey("with-source-code"); + var skipDbMigrations = Convert.ToBoolean( commandLineArgs.Options.GetOrNull(Options.DbMigrations.Skip) ?? "false"); + var solutionFile = GetSolutionFile(commandLineArgs); + + var version = commandLineArgs.Options.GetOrNull(Options.Version.Short, Options.Version.Long); + if (version == null) + { + version = SolutionAbpVersionFinder.Find(solutionFile); + } + await SolutionModuleAdder.AddAsync( - GetSolutionFile(commandLineArgs), + solutionFile, commandLineArgs.Target, commandLineArgs.Options.GetOrNull(Options.StartupProject.Short, Options.StartupProject.Long), - skipDbMigrations + version, + skipDbMigrations, + withSourceCode ); } @@ -57,8 +71,9 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(" abp add-module [options]"); sb.AppendLine(""); sb.AppendLine("Options:"); - 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(" --with-source-code Downloads the source code of the module and adds it to your solution."); + 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."); sb.AppendLine(""); sb.AppendLine("Examples:"); @@ -126,6 +141,11 @@ namespace Volo.Abp.Cli.Commands public const string Short = "s"; public const string Long = "solution"; } + public static class Version + { + public const string Short = "v"; + public const string Long = "version"; + } public static class DbMigrations { 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 new file mode 100644 index 0000000000..accf5eddb9 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs @@ -0,0 +1,749 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json.Linq; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.ProjectBuilding; +using Volo.Abp.Cli.ProjectBuilding.Building; +using Volo.Abp.DependencyInjection; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Volo.Abp.Cli.Commands +{ + public class GenerateProxyCommand : IConsoleCommand, ITransientDependency + { + public static Dictionary> propertyList = new Dictionary>(); + public ILogger Logger { get; set; } + + protected TemplateProjectBuilder TemplateProjectBuilder { get; } + + public GenerateProxyCommand(TemplateProjectBuilder templateProjectBuilder) + { + TemplateProjectBuilder = templateProjectBuilder; + + Logger = NullLogger.Instance; + } + + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + var angularPath = $"angular.json"; + if (!File.Exists(angularPath)) + { + throw new CliUsageException( + "angular.json file not found. You must run this command in angular folder." + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } + + var module = commandLineArgs.Options.GetOrNull(Options.Module.Short, Options.Module.Long); + module = module == null ? "app" : module.ToLower(); + + var apiUrl = commandLineArgs.Options.GetOrNull(Options.ApiUrl.Short, Options.ApiUrl.Long); + if (string.IsNullOrWhiteSpace(apiUrl)) + { + var environmentJson = File.ReadAllText("src/environments/environment.ts").Split("export const environment = ")[1].Replace(";", " "); + var environment = JObject.Parse(environmentJson); + apiUrl = environment["apis"]["default"]["url"].ToString(); + } + apiUrl += "/api/abp/api-definition?IncludeTypes=true"; + + var uiFramework = GetUiFramework(commandLineArgs); + + WebClient client = new WebClient(); + string json = ""; + try + { + json = client.DownloadString(apiUrl); + } + catch (Exception ex) + { + throw new CliUsageException( + "Cannot connect to the host {" + apiUrl + "}! Check that the host is up and running." + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } + + Logger.LogInformation("Downloading api definition..."); + Logger.LogInformation("Api Url: " + apiUrl); + + var data = JObject.Parse(json); + + Logger.LogInformation("Modules are combining"); + var apiNameList = new Dictionary(); + var moduleList = GetCombinedModules(data, module, out apiNameList); + + if (moduleList.Count < 1) + { + throw new CliUsageException( + "Module can not find!" + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } + + Logger.LogInformation("Modules and types are creating"); + + foreach (var moduleItem in moduleList) + { + var moduleValue = JObject.Parse(moduleItem.Value); + + var rootPath = moduleItem.Key; + var apiName = apiNameList.Where(p => p.Key == rootPath).Select(p => p.Value).FirstOrDefault(); + + Logger.LogInformation($"{rootPath} directory is creating"); + + Directory.CreateDirectory($"src/app/{rootPath}/shared/models"); + Directory.CreateDirectory($"src/app/{rootPath}/shared/services"); + + var serviceIndexList = new List(); + var modelIndexList = new List(); + + foreach (var controller in moduleValue.Root.ToList().Select(item => item.First)) + { + var serviceFileText = new StringBuilder(); + + serviceFileText.AppendLine("[firstTypeList]"); + serviceFileText.AppendLine("import { Injectable } from '@angular/core';"); + serviceFileText.AppendLine("import { Observable } from 'rxjs';"); + serviceFileText.AppendLine("[secondTypeList]"); + serviceFileText.AppendLine(""); + serviceFileText.AppendLine("@Injectable({providedIn: 'root'})"); + serviceFileText.AppendLine("export class [controllerName]Service {"); + serviceFileText.AppendLine(" apiName = '" + apiName + "';"); + serviceFileText.AppendLine(""); + serviceFileText.AppendLine(" constructor(private restService: RestService) {}"); + serviceFileText.AppendLine(""); + + var firstTypeList = new List(); + var secondTypeList = new List(); + + var controllerName = (string)controller["controllerName"]; + var controllerServiceName = controllerName.PascalToKebabCase() + ".service.ts"; + + foreach (var actionItem in controller["actions"]) + { + var action = actionItem.First; + var actionName = (string)action["uniqueName"]; + + actionName = (char.ToLower(actionName[0]) + actionName.Substring(1)).Replace("Async", "").Replace("Controller", ""); + + var returnValueType = (string)action["returnValue"]["type"]; + + var parameters = action["parameters"]; + var parametersText = new StringBuilder(); + var parametersIndex = 0; + var bodyExtra = ""; + var modelBindingExtra = ""; + var modelBindingExtraList = new List(); + var parameterModel = new List(); + + foreach (var parameter in parameters.OrderBy(p => p["bindingSourceId"])) + { + var bindingSourceId = (string)parameter["bindingSourceId"]; + bindingSourceId = char.ToLower(bindingSourceId[0]) + bindingSourceId.Substring(1); + + var name = (string)parameter["name"]; + var typeSimple = (string)parameter["typeSimple"]; + var typeArray = ((string)parameter["type"]).Split("."); + var type = (typeArray[typeArray.Length - 1]).TrimEnd('>'); + var isOptional = (bool)parameter["isOptional"]; + var defaultValue = (string)parameter["defaultValue"]; + + var modelIndex = CreateType(data, (string)parameter["type"], rootPath, modelIndexList); + + if (!string.IsNullOrWhiteSpace(modelIndex)) + { + modelIndexList.Add(modelIndex); + } + + if (bindingSourceId == "body") + { + bodyExtra = ", body"; + parameterModel = AddParameter(bindingSourceId, type, isOptional, defaultValue, bindingSourceId, parameterModel); + } + else if (bindingSourceId == "path") + { + parameterModel = AddParameter(name, typeSimple, isOptional, defaultValue, bindingSourceId, parameterModel); + } + else if (bindingSourceId == "modelBinding") + { + var parameterNameOnMethod = (string)parameter["nameOnMethod"]; + + var parametersOnMethod = action["parametersOnMethod"]; + foreach (var parameterOnMethod in parametersOnMethod) + { + var parametersOnMethodName = (string)parameterOnMethod["name"]; + if (parametersOnMethodName == parameterNameOnMethod) + { + typeSimple = (string)parameterOnMethod["typeSimple"]; + typeArray = ((string)parameterOnMethod["type"]).Split("."); + type = typeArray[typeArray.Length - 1]; + isOptional = (bool)parameterOnMethod["isOptional"]; + defaultValue = (string)parameterOnMethod["defaultValue"]; + + if (typeSimple == "string" || typeSimple == "boolean" || typeSimple == "number") + { + parameterModel = AddParameter(name, typeSimple, isOptional, defaultValue, bindingSourceId, parameterModel); + } + + modelIndex = CreateType(data, (string)parameterOnMethod["type"], rootPath, modelIndexList); + + if (!string.IsNullOrWhiteSpace(modelIndex)) + { + modelIndexList.Add(modelIndex); + } + } + } + + if (typeSimple != "string" && typeSimple != "boolean" && typeSimple != "number") + { + parametersText.Append($"params = {{}} as {type}"); + modelBindingExtra = ", params"; + if (!string.IsNullOrWhiteSpace(modelIndex)) + { + secondTypeList.Add(type); + } + else + { + firstTypeList.Add(type); + } + break; + } + } + } + + if (parameterModel != null && parameterModel.Count > 0) + { + foreach (var parameterItem in parameterModel.OrderBy(p => p.DisplayOrder)) + { + var parameterItemModelName = parameterItem.Type.PascalToKebabCase() + ".ts"; + var parameterItemModelPath = $"src/app/{rootPath}/shared/models/{parameterItemModelName}"; + if (parameterItem.BindingSourceId == "body" && !File.Exists(parameterItemModelPath)) + { + parameterItem.Type = "any"; + } + + parametersIndex++; + + if (parametersIndex > 1) + { + parametersText.Append(", "); + } + + parametersText.Append(parameterItem.Name + (parameterItem.IsOptional ? "?" : "") + ": " + parameterItem.Type + (parameterItem.Value != null ? (" = " + (string.IsNullOrWhiteSpace(parameterItem.Value) ? "''" : parameterItem.Value)) : "")); + + if (parameterItem.BindingSourceId == "modelBinding") + { + modelBindingExtraList.Add(parameterItem.Name); + } + else if (parameterItem.BindingSourceId == "body" && File.Exists(parameterItemModelPath)) + { + secondTypeList.Add(parameterItem.Type); + } + } + } + + if (returnValueType != null) + { + if (returnValueType.IndexOf('<') > -1) + { + var firstTypeArray = returnValueType.Split("<")[0].Split("."); + var firstType = firstTypeArray[firstTypeArray.Length - 1]; + + var secondTypeArray = returnValueType.Split("<")[1].Split("."); + var secondType = secondTypeArray[secondTypeArray.Length - 1].TrimEnd('>'); + + var secondTypeModelName = secondType.PascalToKebabCase() + ".ts"; + var secondTypeModelPath = $"src/app/{rootPath}/shared/models/{secondTypeModelName}"; + if (firstType == "List" && !File.Exists(secondTypeModelPath)) + { + secondType = "any"; + } + + serviceFileText.AppendLine( + firstType == "List" + ? $" {actionName}({parametersText}): Observable<{secondType}[]> {{" + : $" {actionName}({parametersText}): Observable<{firstType}<{secondType}>> {{"); + + if (firstType != "List") + { + firstTypeList.Add(firstType); + } + + if (secondType != "any") + { + secondTypeList.Add(secondType); + } + } + else + { + var typeArray = returnValueType.Split("."); + var type = typeArray[typeArray.Length - 1].TrimEnd('>'); + + type = type switch + { + "Void" => "void", + "String" => "string", + "IActionResult" => "void", + "ActionResult" => "void", + _ => type + }; + + serviceFileText.AppendLine( + $" {actionName}({parametersText}): Observable<{type}> {{"); + + if (type != "void" && type != "string") + { + secondTypeList.Add(type); + } + } + + var modelIndex = CreateType(data, returnValueType, rootPath, modelIndexList); + + if (!string.IsNullOrWhiteSpace(modelIndex)) + { + modelIndexList.Add(modelIndex); + } + } + + if (modelBindingExtraList != null && modelBindingExtraList.Count > 0) + { + modelBindingExtra = ", params: { " + string.Join(", ", modelBindingExtraList.ToArray()) + " }"; + } + + var url = ((string)action["url"]).Replace("/{", "/${"); + var httpMethod = (string)action["httpMethod"]; + + serviceFileText.AppendLine( + url.Contains("${") + ? $" return this.restService.request({{ url: `/{url}`, method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }},{{ apiName: this.apiName }});" + : $" return this.restService.request({{ url: '/{url}', method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }},{{ apiName: this.apiName }});"); + + + serviceFileText.AppendLine(" }"); + } + + serviceIndexList.Add(controllerServiceName.Replace(".ts", "")); + + if (firstTypeList != null && firstTypeList.Count > 0) + { + var firstTypeListDistinct = ", " + string.Join(", ", firstTypeList.Where(p => p != "void").Distinct().ToArray()); + serviceFileText.Replace("[firstTypeList]", + $"import {{ RestService {firstTypeListDistinct}}} from '@abp/ng.core';"); + } + else + { + serviceFileText.Replace("[firstTypeList]", + $"import {{ RestService }} from '@abp/ng.core';"); + } + + if (secondTypeList != null && secondTypeList.Count > 0) + { + var secondTypeListDistinct = string.Join(", ", secondTypeList.Where(p => p != "void").Distinct().ToArray()); + serviceFileText.Replace("[secondTypeList]", + $"import {{{secondTypeListDistinct}}} from '../models';"); + } + else + { + serviceFileText.Replace("[secondTypeList]", ""); + } + + serviceFileText.AppendLine("}"); + + serviceFileText.Replace("[controllerName]", controllerName); + File.WriteAllText($"src/app/{rootPath}/shared/services/{controllerServiceName}", serviceFileText.ToString()); + } + + var serviceIndexFileText = new StringBuilder(); + + foreach (var serviceIndexItem in serviceIndexList.Distinct()) + { + serviceIndexFileText.AppendLine($"export * from './{serviceIndexItem}';"); + } + + File.WriteAllText($"src/app/{rootPath}/shared/services/index.ts", serviceIndexFileText.ToString()); + + var modelIndexFileText = new StringBuilder(); + + foreach (var modelIndexItem in modelIndexList.Distinct()) + { + modelIndexFileText.AppendLine($"export * from './{modelIndexItem}';"); + } + + File.WriteAllText($"src/app/{rootPath}/shared/models/index.ts", modelIndexFileText.ToString()); + } + + Logger.LogInformation("Completed!"); + } + + private Dictionary GetCombinedModules(JToken data, string module, out Dictionary apiNameList) + { + var moduleList = new Dictionary(); + apiNameList = new Dictionary(); + + foreach (var moduleItem in data["modules"]) + { + var rootPath = ((string)moduleItem.First["rootPath"]).ToLower(); + var apiName = (string)moduleItem.First["remoteServiceName"]; + + if (moduleList.Any(p => p.Key == rootPath)) + { + var value = moduleList[rootPath]; + + moduleList[rootPath] = value.TrimEnd('}') + "," + moduleItem.First["controllers"].ToString().TrimStart('{'); + } + else + { + apiNameList.Add(rootPath, apiName); + moduleList.Add(rootPath, moduleItem.First["controllers"].ToString()); + } + } + + if (module != "all") + { + moduleList = moduleList.Where(p => p.Key.ToLower() == module).ToDictionary(p => p.Key, s => s.Value); + } + + return moduleList; + } + + private static string CreateType(JObject data, string returnValueType, string rootPath, List modelIndexList) + { + var type = data["types"][returnValueType]; + + if (type == null) + { + return null; + } + + if (returnValueType.StartsWith("Volo.Abp.Application.Dtos") + || returnValueType.StartsWith("System.Collections") + || returnValueType == "System.String" + || returnValueType == "System.Void" + || returnValueType.Contains("System.Net.HttpStatusCode?") + || returnValueType.Contains("IActionResult") + || returnValueType.Contains("ActionResult") + || returnValueType.Contains("IStringValueType") + || returnValueType.Contains("IValueValidator") + ) + { + return null; + } + + var typeNameSplit = returnValueType.Split("."); + var typeName = typeNameSplit[typeNameSplit.Length - 1]; + + if (typeName.Contains("HttpStatusCode")) + { + + } + + var typeModelName = typeName.Replace("<", "").Replace(">", "").Replace("?", "").PascalToKebabCase() + ".ts"; + + var path = $"src/app/{rootPath}/shared/models/{typeModelName}"; + + var modelFileText = new StringBuilder(); + + var baseType = (string)type["baseType"]; + var extends = ""; + var customBaseTypeName = ""; + var baseTypeName = ""; + + if (!string.IsNullOrWhiteSpace(baseType)) + { + var baseTypeSplit = baseType.Split("."); + baseTypeName = baseTypeSplit[baseTypeSplit.Length - 1].Replace("<", "").Replace(">", ""); + var baseTypeKebabCase = "./" + baseTypeName.PascalToKebabCase(); + + if (baseType != "System.Enum") + { + if (baseType.Contains("Volo.Abp.Application.Dtos")) + { + baseTypeKebabCase = "@abp/ng.core"; + + baseTypeName = baseType.Split("Volo.Abp.Application.Dtos")[1].Split("<")[0].TrimStart('.'); + customBaseTypeName = baseType.Split("Volo.Abp.Application.Dtos")[1].Replace("System.Guid", "string").TrimStart('.'); + } + + if (baseTypeName.Contains("guid") || baseTypeName.Contains("Guid")) + { + baseTypeName = "string"; + } + + modelFileText.AppendLine($"import {{ {baseTypeName} }} from '{baseTypeKebabCase}';"); + extends = "extends " + (!string.IsNullOrWhiteSpace(customBaseTypeName) ? customBaseTypeName : baseTypeName); + + var modelIndex = CreateType(data, baseType, rootPath, modelIndexList); + if (!string.IsNullOrWhiteSpace(modelIndex)) + { + modelIndexList.Add(modelIndex); + } + } + } + + if (baseType == "System.Enum" && (bool)type["isEnum"]) + { + modelFileText.AppendLine($"export enum {typeName} {{"); + + var enumNameList = type["enumNames"].ToArray(); + var enumValueList = type["enumValues"].ToArray(); + + for (var i = 0; i < enumNameList.Length; i++) + { + modelFileText.AppendLine($" {enumNameList[i]} = {enumValueList[i]},"); + } + + modelFileText.AppendLine("}"); + } + else + { + modelFileText.AppendLine(""); + modelFileText.AppendLine($"export class {typeName} {extends} {{"); + + foreach (var property in type["properties"]) + { + var propertyName = (string)property["name"]; + propertyName = (char.ToLower(propertyName[0]) + propertyName.Substring(1)); + var typeSimple = (string)property["typeSimple"]; + + var modelIndex = CreateType(data, (string)property["type"], rootPath, modelIndexList); + + if (typeSimple.IndexOf("[") > -1 && typeSimple.IndexOf("]") > -1) + { + typeSimple = typeSimple.Replace("[", "").Replace("]", "") + "[]"; + } + + if (typeSimple.StartsWith("Volo.Abp")) + { + var typeSimpleSplit = typeSimple.Split("."); + typeSimple = typeSimpleSplit[typeSimpleSplit.Length - 1]; + } + + if (typeSimple.StartsWith("System.Object")) + { + typeSimple = typeSimple.Replace("System.Object", "object"); + } + + if (typeSimple.Contains("?")) + { + typeSimple = typeSimple.Replace("?", ""); + propertyName += "?"; + } + + if (typeSimple != "boolean" + && typeSimple != "string" + && typeSimple != "number" + && typeSimple != "boolean[]" + && typeSimple != "string[]" + && typeSimple != "number[]" + ) + { + var typeSimpleModelName = typeSimple.PascalToKebabCase() + ".ts"; + var modelPath = $"src/app/{rootPath}/shared/models/{typeSimpleModelName}"; + if (!File.Exists(modelPath)) + { + typeSimple = "any" + (typeSimple.Contains("[]") ? "[]" : ""); + } + } + + if (propertyList.Any(p => p.Key == baseTypeName && p.Value.Any(q => q.Key == propertyName && q.Value == typeSimple))) + { + continue; + } + + if (!string.IsNullOrWhiteSpace(modelIndex)) + { + var propertyTypeSplit = ((string)property["type"]).Split("."); + var propertyType = propertyTypeSplit[propertyTypeSplit.Length - 1]; + modelFileText.Insert(0, ""); + modelFileText.Insert(0, $"import {{ {propertyType} }} from '../models';"); + modelFileText.Insert(0, ""); + modelIndexList.Add(modelIndex); + } + + if (propertyList.Any(p => p.Key == typeName && !p.Value.Any(q => q.Key == propertyName))) + { + propertyList[typeName].Add(propertyName, typeSimple); + } + else if (!propertyList.Any(p => p.Key == typeName)) + { + var newProperty = new Dictionary(); + newProperty.Add(propertyName, typeSimple); + propertyList.Add(typeName, newProperty); + } + + modelFileText.AppendLine($" {propertyName}: {typeSimple};"); + } + + modelFileText.AppendLine(""); + + modelFileText.AppendLine($" constructor(initialValues: Partial<{typeName}> = {{}}) {{"); + + if (!string.IsNullOrWhiteSpace(baseType)) + { + modelFileText.AppendLine(" super(initialValues);"); + modelFileText.AppendLine(" }"); + } + else + { + modelFileText.AppendLine(" if (initialValues) {"); + modelFileText.AppendLine(" for (const key in initialValues) {"); + modelFileText.AppendLine(" if (initialValues.hasOwnProperty(key)) {"); + modelFileText.AppendLine(" this[key] = initialValues[key];"); + + modelFileText.AppendLine(" }"); + modelFileText.AppendLine(" }"); + modelFileText.AppendLine(" }"); + modelFileText.AppendLine(" }"); + } + + modelFileText.AppendLine("}"); + } + + File.WriteAllText($"src/app/{rootPath}/shared/models/{typeModelName}", modelFileText.ToString()); + + return typeModelName.Replace(".ts", ""); + } + + private static List AddParameter(string parameterName, string type, bool parameterIsOptional, string parameterDefaultValue, string bindingSourceId, List parameterModel) + { + if (parameterDefaultValue != "null") + { + parameterModel.Add(new ParameterModel + { + DisplayOrder = 3, + Name = parameterName, + Type = type, + Value = parameterDefaultValue, + BindingSourceId = bindingSourceId + }); + } + else if (parameterDefaultValue == "null" && parameterIsOptional) + { + parameterModel.Add(new ParameterModel + { + DisplayOrder = 2, + Name = parameterName, + IsOptional = true, + Type = type, + BindingSourceId = bindingSourceId + }); + } + else + { + parameterModel.Add(new ParameterModel + { + DisplayOrder = 1, + Name = parameterName, + Type = type, + BindingSourceId = bindingSourceId + }); + } + + return parameterModel; + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(""); + sb.AppendLine(" abp generate-proxy [options]"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine(""); + sb.AppendLine("-a|--apiUrl (default: environment.ts>apis>default>url)"); + sb.AppendLine("-u|--ui (default: angular)"); + sb.AppendLine("-m|--module (default: app)"); + sb.AppendLine(""); + sb.AppendLine("Examples:"); + sb.AppendLine(""); + sb.AppendLine(" abp generate-proxy --apiUrl https://www.volosoft.com"); + 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 "Generates typescript service proxies and DTOs"; + } + + private UiFramework GetUiFramework(CommandLineArgs commandLineArgs) + { + var optionValue = commandLineArgs.Options.GetOrNull(Options.UiFramework.Short, Options.UiFramework.Long); + switch (optionValue) + { + case "none": + return UiFramework.None; + case "mvc": + return UiFramework.Mvc; + case "angular": + return UiFramework.Angular; + default: + return UiFramework.Angular; + } + } + + public static class Options + { + public static class Module + { + public const string Short = "m"; + public const string Long = "module"; + } + + public static class ApiUrl + { + public const string Short = "a"; + public const string Long = "apiUrl"; + } + + public static class UiFramework + { + public const string Short = "u"; + public const string Long = "ui"; + } + } + } + + public static class StringExtensions + { + public static string PascalToKebabCase(this string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + return Regex.Replace( + value, + "(? Logger { get; set; } - public GetSourceCommand(ModuleProjectBuilder moduleProjectBuilder) + public GetSourceCommand(ModuleProjectBuilder moduleProjectBuilder, SourceCodeDownloadService sourceCodeDownloadService) { + _sourceCodeDownloadService = sourceCodeDownloadService; ModuleProjectBuilder = moduleProjectBuilder; Logger = NullLogger.Instance; } @@ -36,29 +39,13 @@ namespace Volo.Abp.Cli.Commands ); } - Logger.LogInformation("Downloading source code of " + commandLineArgs.Target); - var version = commandLineArgs.Options.GetOrNull(Options.Version.Short, Options.Version.Long); if (version != null) { Logger.LogInformation("Version: " + version); } - var outputFolder = commandLineArgs.Options.GetOrNull(Options.OutputFolder.Short, Options.OutputFolder.Long); - if (outputFolder != null) - { - if (!Directory.Exists(outputFolder)) - { - Directory.CreateDirectory(outputFolder); - } - - outputFolder = Path.GetFullPath(outputFolder); - } - else - { - outputFolder = Directory.GetCurrentDirectory(); - } - + var outputFolder = GetOutPutFolder(commandLineArgs); Logger.LogInformation("Output folder: " + outputFolder); var gitHubLocalRepositoryPath = commandLineArgs.Options.GetOrNull(Options.GitHubLocalRepositoryPath.Long); @@ -68,53 +55,29 @@ namespace Volo.Abp.Cli.Commands } commandLineArgs.Options.Add(CliConsts.Command, commandLineArgs.Command); + + await _sourceCodeDownloadService.DownloadAsync( + commandLineArgs.Target, outputFolder, version, gitHubLocalRepositoryPath, commandLineArgs.Options); + } - var result = await ModuleProjectBuilder.BuildAsync( - new ProjectBuildArgs( - SolutionName.Parse(commandLineArgs.Target), - commandLineArgs.Target, - version, - DatabaseProvider.NotSpecified, - UiFramework.NotSpecified, - gitHubLocalRepositoryPath, - commandLineArgs.Options - ) - ); - - using (var templateFileStream = new MemoryStream(result.ZipContent)) + private static string GetOutPutFolder(CommandLineArgs commandLineArgs) + { + var outputFolder = commandLineArgs.Options.GetOrNull(Options.OutputFolder.Short, Options.OutputFolder.Long); + if (outputFolder != null) { - using (var zipInputStream = new ZipInputStream(templateFileStream)) + if (!Directory.Exists(outputFolder)) { - var zipEntry = zipInputStream.GetNextEntry(); - while (zipEntry != null) - { - var fullZipToPath = Path.Combine(outputFolder, zipEntry.Name); - var directoryName = Path.GetDirectoryName(fullZipToPath); - - if (!string.IsNullOrEmpty(directoryName)) - { - Directory.CreateDirectory(directoryName); - } - - var fileName = Path.GetFileName(fullZipToPath); - if (fileName.Length == 0) - { - zipEntry = zipInputStream.GetNextEntry(); - continue; - } - - var buffer = new byte[4096]; // 4K is optimum - using (var streamWriter = File.Create(fullZipToPath)) - { - StreamUtils.Copy(zipInputStream, streamWriter, buffer); - } - - zipEntry = zipInputStream.GetNextEntry(); - } + Directory.CreateDirectory(outputFolder); } + + outputFolder = Path.GetFullPath(outputFolder); + } + else + { + outputFolder = Directory.GetCurrentDirectory(); } - Logger.LogInformation($"'{commandLineArgs.Target}' has been successfully downloaded to '{outputFolder}'"); + return outputFolder; } public string GetUsageInfo() diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs index 9cfa702fd7..8a0c21a254 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs @@ -30,7 +30,7 @@ namespace Volo.Abp.Cli.Commands public async Task ExecuteAsync(CommandLineArgs commandLineArgs) { var projectName = NamespaceHelper.NormalizeNamespace(commandLineArgs.Target); - + if (projectName == null) { throw new CliUsageException( @@ -39,7 +39,7 @@ namespace Volo.Abp.Cli.Commands GetUsageInfo() ); } - + Logger.LogInformation("Creating your project..."); Logger.LogInformation("Project name: " + projectName); @@ -55,6 +55,12 @@ namespace Volo.Abp.Cli.Commands Logger.LogInformation("Version: " + version); } + var isTiered = commandLineArgs.Options.ContainsKey(Options.Tiered.Long); + if (isTiered) + { + Logger.LogInformation("Tiered: yes"); + } + var databaseProvider = GetDatabaseProvider(commandLineArgs); if (databaseProvider != DatabaseProvider.NotSpecified) { @@ -67,22 +73,47 @@ namespace Volo.Abp.Cli.Commands Logger.LogInformation("UI Framework: " + uiFramework); } + var connectionString = GetConnectionString(commandLineArgs); + if (connectionString != null) + { + Logger.LogInformation("Connection string: " + connectionString); + } + + var mobileApp = GetMobilePreference(commandLineArgs); + if (mobileApp != MobileApp.None) + { + Logger.LogInformation("Mobile App: " + mobileApp); + } + var gitHubLocalRepositoryPath = commandLineArgs.Options.GetOrNull(Options.GitHubLocalRepositoryPath.Long); if (gitHubLocalRepositoryPath != null) { Logger.LogInformation("GitHub Local Repository Path: " + gitHubLocalRepositoryPath); } - var outputFolder = commandLineArgs.Options.GetOrNull(Options.OutputFolder.Short, Options.OutputFolder.Long); - - outputFolder = Path.Combine(outputFolder != null ? Path.GetFullPath(outputFolder) : Directory.GetCurrentDirectory(), - SolutionName.Parse(projectName).FullName); + var templateSource = commandLineArgs.Options.GetOrNull(Options.TemplateSource.Short, Options.TemplateSource.Long); + if (templateSource != null) + { + Logger.LogInformation("Template Source: " + templateSource); + } - if (!Directory.Exists(outputFolder)) + var createSolutionFolder = (commandLineArgs.Options.GetOrNull(Options.CreateSolutionFolder.Short, Options.CreateSolutionFolder.Long) ?? "true").ToLowerInvariant() != "false"; + if (!createSolutionFolder) { - Directory.CreateDirectory(outputFolder); + Logger.LogInformation("Create Solution Folder: no"); } + var outputFolder = commandLineArgs.Options.GetOrNull(Options.OutputFolder.Short, Options.OutputFolder.Long); + + var outputFolderRoot = + outputFolder != null ? Path.GetFullPath(outputFolder) : Directory.GetCurrentDirectory(); + + outputFolder = createSolutionFolder ? + Path.Combine(outputFolderRoot, SolutionName.Parse(projectName).FullName) : + outputFolderRoot; + + Volo.Abp.IO.DirectoryHelper.CreateIfNotExists(outputFolder); + Logger.LogInformation("Output folder: " + outputFolder); commandLineArgs.Options.Add(CliConsts.Command, commandLineArgs.Command); @@ -94,8 +125,11 @@ namespace Volo.Abp.Cli.Commands version, databaseProvider, uiFramework, + mobileApp, gitHubLocalRepositoryPath, - commandLineArgs.Options + templateSource, + commandLineArgs.Options, + connectionString ) ); @@ -106,6 +140,12 @@ namespace Volo.Abp.Cli.Commands var zipEntry = zipInputStream.GetNextEntry(); while (zipEntry != null) { + if (string.IsNullOrWhiteSpace(zipEntry.Name)) + { + zipEntry = zipInputStream.GetNextEntry(); + continue; + } + var fullZipToPath = Path.Combine(outputFolder, zipEntry.Name); var directoryName = Path.GetDirectoryName(fullZipToPath); @@ -135,6 +175,12 @@ namespace Volo.Abp.Cli.Commands Logger.LogInformation($"'{projectName}' has been successfully created to '{outputFolder}'"); } + private static string GetConnectionString(CommandLineArgs commandLineArgs) + { + var connectionString = commandLineArgs.Options.GetOrNull(Options.ConnectionString.Short, Options.ConnectionString.Long); + return string.IsNullOrWhiteSpace(connectionString) ? null : connectionString; + } + public string GetUsageInfo() { var sb = new StringBuilder(); @@ -148,9 +194,11 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(""); sb.AppendLine("-t|--template (default: app)"); sb.AppendLine("-u|--ui (if supported by the template)"); + sb.AppendLine("-m|--mobile (if supported by the template)"); sb.AppendLine("-d|--database-provider (if supported by the template)"); sb.AppendLine("-o|--output-folder (default: current folder)"); sb.AppendLine("-v|--version (default: latest version)"); + sb.AppendLine("-ts|--template-source (your local or network abp template source)"); sb.AppendLine("--tiered (if supported by the template)"); sb.AppendLine("--no-ui (if supported by the template)"); sb.AppendLine("--separate-identity-server (if supported by the template)"); @@ -162,11 +210,15 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(" abp new Acme.BookStore --tiered"); sb.AppendLine(" abp new Acme.BookStore -u angular"); sb.AppendLine(" abp new Acme.BookStore -u angular -d mongodb"); + sb.AppendLine(" abp new Acme.BookStore -m none"); + sb.AppendLine(" abp new Acme.BookStore -m react-native"); sb.AppendLine(" abp new Acme.BookStore -d mongodb"); sb.AppendLine(" abp new Acme.BookStore -d mongodb -o d:\\my-project"); sb.AppendLine(" abp new Acme.BookStore -t module"); sb.AppendLine(" abp new Acme.BookStore -t module --no-ui"); + sb.AppendLine(" abp new Acme.BookStore -ts \"D:\\localTemplate\\abp\""); sb.AppendLine(" abp new Acme.BookStore --local-framework-ref --abp-path \"D:\\github\\abp\""); + sb.AppendLine(" abp new Acme.BookStore --connection-string \"Server=myServerName\\myInstanceName;Database=myDatabase;User Id=myUsername;Password=myPassword\""); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); @@ -192,7 +244,7 @@ namespace Volo.Abp.Cli.Commands } } - private UiFramework GetUiFramework(CommandLineArgs commandLineArgs) + protected virtual UiFramework GetUiFramework(CommandLineArgs commandLineArgs) { var optionValue = commandLineArgs.Options.GetOrNull(Options.UiFramework.Short, Options.UiFramework.Long); switch (optionValue) @@ -208,6 +260,20 @@ namespace Volo.Abp.Cli.Commands } } + protected virtual MobileApp GetMobilePreference(CommandLineArgs commandLineArgs) + { + var optionValue = commandLineArgs.Options.GetOrNull(Options.Mobile.Short, Options.Mobile.Long); + switch (optionValue) + { + case "none": + return MobileApp.None; + case "react-native": + return MobileApp.ReactNative; + default: + return MobileApp.ReactNative; + } + } + public static class Options { public static class Template @@ -244,6 +310,35 @@ namespace Volo.Abp.Cli.Commands public const string Short = "u"; public const string Long = "ui"; } + + public static class Mobile + { + public const string Short = "m"; + public const string Long = "mobile"; + } + + public static class TemplateSource + { + public const string Short = "ts"; + public const string Long = "template-source"; + } + + public static class ConnectionString + { + public const string Short = "cs"; + public const string Long = "connection-string"; + } + + public static class CreateSolutionFolder + { + public const string Short = "csf"; + public const string Long = "create-solution-folder"; + } + + public static class Tiered + { + public const string Long = "tiered"; + } } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpNuGetIndexUrlService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpNuGetIndexUrlService.cs new file mode 100644 index 0000000000..314c568030 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpNuGetIndexUrlService.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.Cli.Licensing; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands.Services +{ + public class AbpNuGetIndexUrlService : ITransientDependency + { + private readonly IApiKeyService _apiKeyService; + public ILogger Logger { get; set; } + + public AbpNuGetIndexUrlService(IApiKeyService apiKeyService) + { + _apiKeyService = apiKeyService; + Logger = NullLogger.Instance; + } + + public async Task GetAsync() + { + var apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync(); + + if (apiKeyResult == null) + { + Logger.LogWarning("You are not signed in! Use the CLI command \"abp login \" to sign in, then try again."); + return null; + } + + if (!string.IsNullOrWhiteSpace(apiKeyResult.ErrorMessage)) + { + Logger.LogWarning(apiKeyResult.ErrorMessage); + return null; + } + + if (string.IsNullOrEmpty(apiKeyResult.ApiKey)) + { + Logger.LogError("Couldn't retrieve your NuGet API key! You can re-sign in with the CLI command \"abp login \"."); + return null; + } + + return CliUrls.GetNuGetServiceIndexUrl(apiKeyResult.ApiKey); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SourceCodeDownloadService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SourceCodeDownloadService.cs new file mode 100644 index 0000000000..7c8d068e1e --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SourceCodeDownloadService.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +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.ProjectBuilding; +using Volo.Abp.Cli.ProjectBuilding.Building; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands.Services +{ + public class SourceCodeDownloadService : ITransientDependency + { + public ModuleProjectBuilder ModuleProjectBuilder { get; } + public ILogger Logger { get; set; } + + public SourceCodeDownloadService(ModuleProjectBuilder moduleProjectBuilder) + { + ModuleProjectBuilder = moduleProjectBuilder; + Logger = NullLogger.Instance; + } + + public async Task DownloadAsync(string moduleName, string outputFolder, string version, string gitHubLocalRepositoryPath, AbpCommandLineOptions options) + { + Logger.LogInformation("Downloading source code of " + moduleName); + Logger.LogInformation("Version: " + version); + Logger.LogInformation("Output folder: " + outputFolder); + + var result = await ModuleProjectBuilder.BuildAsync( + new ProjectBuildArgs( + SolutionName.Parse(moduleName), + moduleName, + version, + DatabaseProvider.NotSpecified, + UiFramework.NotSpecified, + null, + gitHubLocalRepositoryPath, + null, + options + ) + ); + + using (var templateFileStream = new MemoryStream(result.ZipContent)) + { + using (var zipInputStream = new ZipInputStream(templateFileStream)) + { + var zipEntry = zipInputStream.GetNextEntry(); + while (zipEntry != null) + { + var fullZipToPath = Path.Combine(outputFolder, zipEntry.Name); + var directoryName = Path.GetDirectoryName(fullZipToPath); + + if (!string.IsNullOrEmpty(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + + var fileName = Path.GetFileName(fullZipToPath); + if (fileName.Length == 0) + { + zipEntry = zipInputStream.GetNextEntry(); + continue; + } + + var buffer = new byte[4096]; // 4K is optimum + using (var streamWriter = File.Create(fullZipToPath)) + { + StreamUtils.Copy(zipInputStream, streamWriter, buffer); + } + + zipEntry = zipInputStream.GetNextEntry(); + } + } + } + + Logger.LogInformation($"'{moduleName}' has been successfully downloaded to '{outputFolder}'"); + } + } +} 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 a13621ea22..b4a4ff8bdd 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 @@ -1,11 +1,10 @@ using System; -using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Cli.Args; -using Volo.Abp.Cli.Licensing; +using Volo.Abp.Cli.Commands.Services; using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; @@ -13,13 +12,13 @@ namespace Volo.Abp.Cli.Commands { public class SuiteCommand : IConsoleCommand, ITransientDependency { + private readonly AbpNuGetIndexUrlService _nuGetIndexUrlService; private const string SuitePackageName = "Volo.Abp.Suite"; public ILogger Logger { get; set; } - private readonly IApiKeyService _apiKeyService; - public SuiteCommand(IApiKeyService apiKeyService) + public SuiteCommand(AbpNuGetIndexUrlService nuGetIndexUrlService) { - _apiKeyService = apiKeyService; + _nuGetIndexUrlService = nuGetIndexUrlService; Logger = NullLogger.Instance; } @@ -53,7 +52,7 @@ namespace Volo.Abp.Cli.Commands private async Task InstallSuiteAsync() { - var nugetIndexUrl = await GetNuGetIndexUrlAsync(); + var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync(); if (nugetIndexUrl == null) { @@ -71,7 +70,7 @@ namespace Volo.Abp.Cli.Commands private async Task UpdateSuiteAsync() { - var nugetIndexUrl = await GetNuGetIndexUrlAsync(); + var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync(); if (nugetIndexUrl == null) { @@ -104,31 +103,6 @@ namespace Volo.Abp.Cli.Commands CmdHelper.RunCmd("abp-suite"); } - private async Task GetNuGetIndexUrlAsync() - { - var apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync(); - - if (apiKeyResult == null) - { - Logger.LogWarning("You are not signed in! Use the CLI command \"abp login \" to sign in, then try again."); - return null; - } - - if (!string.IsNullOrWhiteSpace(apiKeyResult.ErrorMessage)) - { - Logger.LogWarning(apiKeyResult.ErrorMessage); - return null; - } - - if (string.IsNullOrEmpty(apiKeyResult.ApiKey)) - { - Logger.LogError("Couldn't retrieve your NuGet API key! You can re-sign in with the CLI command \"abp login \"."); - return null; - } - - return CliUrls.GetNuGetServiceIndexUrl(apiKeyResult.ApiKey); - } - public string GetUsageInfo() { var sb = new StringBuilder(); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs new file mode 100644 index 0000000000..883ac4db2f --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs @@ -0,0 +1,44 @@ +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.ProjectModification; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands +{ + public class SwitchNightlyPreviewCommand : IConsoleCommand, ITransientDependency + { + private readonly PackageSourceSwitcher _packageSourceSwitcher; + + public SwitchNightlyPreviewCommand(PackageSourceSwitcher packageSourceSwitcher) + { + _packageSourceSwitcher = packageSourceSwitcher; + } + + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + await _packageSourceSwitcher.SwitchToPreview(commandLineArgs); + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(" abp switch-to-preview [options]"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine("-sd|--solution-directory"); + 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 "Switches packages to nightly preview ABP version."; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs new file mode 100644 index 0000000000..54db7bc082 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs @@ -0,0 +1,44 @@ +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.ProjectModification; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands +{ + public class SwitchStableCommand : IConsoleCommand, ITransientDependency + { + private readonly PackageSourceSwitcher _packageSourceSwitcher; + + public SwitchStableCommand(PackageSourceSwitcher packageSourceSwitcher) + { + _packageSourceSwitcher = packageSourceSwitcher; + } + + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + await _packageSourceSwitcher.SwitchToStable(commandLineArgs); + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(" abp switch-to-stable [options]"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine("-sd|--solution-directory"); + 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 "Switches packages to stable ABP version from preview version."; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs index d201790c94..c44f77b749 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs @@ -42,13 +42,13 @@ namespace Volo.Abp.Cli.Commands if (updateNpm || !updateNuget) { - UpdateNpmPackages(directory); + await UpdateNpmPackages(directory); } } - private void UpdateNpmPackages(string directory) + private async Task UpdateNpmPackages(string directory) { - _npmPackagesUpdater.Update(directory); + await _npmPackagesUpdater.Update(directory); } private async Task UpdateNugetPackages(CommandLineArgs commandLineArgs, string directory) @@ -94,7 +94,7 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(""); sb.AppendLine("Usage:"); sb.AppendLine(""); - sb.AppendLine(" abp update [options]"); + sb.AppendLine(" abp update [options]"); sb.AppendLine(""); sb.AppendLine("Options:"); sb.AppendLine("-p|--include-previews (if supported by the template)"); 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 3cdd658403..c0eb6fd209 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 @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Polly; using Polly.Extensions.Http; +using Volo.Abp.Cli.Auth; using Volo.Abp.Cli.Http; using Volo.Abp.Cli.Licensing; using Volo.Abp.Cli.ProjectBuilding; @@ -32,7 +33,8 @@ namespace Volo.Abp.Cli.NuGet public NuGetService( IJsonSerializer jsonSerializer, IRemoteServiceExceptionHandler remoteServiceExceptionHandler, - ICancellationTokenProvider cancellationTokenProvider, IApiKeyService apiKeyService) + ICancellationTokenProvider cancellationTokenProvider, + IApiKeyService apiKeyService) { JsonSerializer = jsonSerializer; RemoteServiceExceptionHandler = remoteServiceExceptionHandler; @@ -43,21 +45,33 @@ namespace Volo.Abp.Cli.NuGet public async Task GetLatestVersionOrNullAsync(string packageId, bool includePreviews = false, bool includeNightly = false) { - var url = includeNightly ? - $"https://www.myget.org/F/abp-nightly/api/v3/flatcontainer/{packageId.ToLowerInvariant()}/index.json" : - $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json"; + List proPackageList = null; + + if (AuthService.IsLoggedIn()) + { + proPackageList = await GetProPackageListAsync(); + } + + string url; + if (includeNightly) + { + url = + $"https://www.myget.org/F/abp-nightly/api/v3/flatcontainer/{packageId.ToLowerInvariant()}/index.json"; + } + else if (proPackageList?.Contains(packageId) ?? false) + { + url = await GetNuGetUrlForCommercialPackage(packageId); + } + else + { + url = $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json"; + } using (var client = new CliHttpClient(setBearerToken: false)) { var responseMessage = await GetHttpResponseMessageWithRetryAsync(client, url); - if (responseMessage.StatusCode == HttpStatusCode.NotFound) - { - var commercialNuGetUrl = await GetNuGetUrlForCommercialPackage(packageId); - responseMessage = await GetHttpResponseMessageWithRetryAsync(client, commercialNuGetUrl); - } - if (!responseMessage.IsSuccessStatusCode) { throw new Exception($"ERROR: Remote server returns '{responseMessage.StatusCode}'"); @@ -95,8 +109,8 @@ namespace Volo.Abp.Cli.NuGet .OrResult(msg => !msg.IsSuccessStatusCode) .WaitAndRetryAsync(new[] { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(7) }, (responseMessage, timeSpan, retryCount, context) => @@ -117,6 +131,30 @@ namespace Volo.Abp.Cli.NuGet .ExecuteAsync(async () => await client.GetAsync(url, CancellationTokenProvider.Token)); } + private async Task> GetProPackageListAsync() + { + using var client = new CliHttpClient(); + + var responseMessage = await client.GetAsync( + $"{CliUrls.WwwAbpIo}api/app/nugetPackage/proPackageNames", + CancellationTokenProvider.Token + ); + + if (!responseMessage.IsSuccessStatusCode) + { + var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. "; + var remoteServiceErrorMessage = await RemoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + + if (remoteServiceErrorMessage != null) + { + exceptionMessage += remoteServiceErrorMessage; + } + Logger.LogInformation(exceptionMessage); + return null; + } + + return JsonSerializer.Deserialize>(await responseMessage.Content.ReadAsStringAsync()); + } public class NuGetVersionResultDto { 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 fd0a0e5246..0aa2587e20 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 @@ -7,8 +7,11 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Volo.Abp.Cli.Http; +using Volo.Abp.Cli.ProjectBuilding.Templates.App; +using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.IO; @@ -46,22 +49,48 @@ namespace Volo.Abp.Cli.ProjectBuilding public async Task GetAsync( string name, string type, - string version = null) + string version = null, + string templateSource = null) { + DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache); + var latestVersion = await GetLatestSourceCodeVersionAsync(name, type); if (version == null) { + if (latestVersion == null) + { + Logger.LogWarning("The remote service is currently unavailable, please specify the version."); + Logger.LogWarning(string.Empty); + Logger.LogWarning("Find the following template in your cache directory: "); + Logger.LogWarning("\t Template Name\tVersion"); + + var templateList = GetLocalTemplates(); + foreach (var cacheFile in templateList) + { + Logger.LogWarning($"\t {cacheFile.TemplateName}\t\t{cacheFile.Version}"); + } + + Logger.LogWarning(string.Empty); + throw new CliUsageException("Use command: abp new Acme.BookStore -v version"); + } + version = latestVersion; } - DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache); + var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version; + + if (!string.IsNullOrWhiteSpace(templateSource) && !IsNetworkSource(templateSource)) + { + Logger.LogInformation("Using local " + type + ": " + name + ", version: " + version); + return new TemplateFile(File.ReadAllBytes(Path.Combine(templateSource, name + "-" + version + ".zip")), version, latestVersion, nugetVersion); + } var localCacheFile = Path.Combine(CliPaths.TemplateCache, name + "-" + version + ".zip"); - if (Options.CacheTemplates && File.Exists(localCacheFile)) + if (Options.CacheTemplates && File.Exists(localCacheFile) && templateSource.IsNullOrWhiteSpace()) { Logger.LogInformation("Using cached " + type + ": " + name + ", version: " + version); - return new TemplateFile(File.ReadAllBytes(localCacheFile), version, latestVersion); + return new TemplateFile(File.ReadAllBytes(localCacheFile), version, latestVersion, nugetVersion); } Logger.LogInformation("Downloading " + type + ": " + name + ", version: " + version); @@ -71,61 +100,147 @@ namespace Volo.Abp.Cli.ProjectBuilding { Name = name, Type = type, + TemplateSource = templateSource, Version = version } ); - if (Options.CacheTemplates) + if (Options.CacheTemplates && templateSource.IsNullOrWhiteSpace()) { File.WriteAllBytes(localCacheFile, fileContent); } - return new TemplateFile(fileContent, version, latestVersion); + return new TemplateFile(fileContent, version, latestVersion, nugetVersion); } private async Task GetLatestSourceCodeVersionAsync(string name, string type) { - using (var client = new CliHttpClient()) + var url = $"{CliUrls.WwwAbpIo}api/download/{type}/get-version/"; + + try { - var response = await client.PostAsync( - $"{CliUrls.WwwAbpIo}api/download/{type}/get-version/", - new StringContent( - JsonSerializer.Serialize( - new GetLatestSourceCodeVersionDto { Name = name } + using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + { + var response = await client.PostAsync( + url, + new StringContent( + JsonSerializer.Serialize( + new GetLatestSourceCodeVersionDto { Name = name } + ), + Encoding.UTF8, + MimeTypes.Application.Json ), - Encoding.UTF8, - MimeTypes.Application.Json - ), - CancellationTokenProvider.Token - ); + CancellationTokenProvider.Token + ); - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); - var result = await response.Content.ReadAsStringAsync(); + var result = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(result).Version; + return JsonSerializer.Deserialize(result).Version; + } + } + catch (Exception ex) + { + Console.WriteLine("Error occured while getting the latest version from {0} : {1}", url, ex.Message); + return null; + } + } + + private async Task GetTemplateNugetVersionAsync(string name, string type, string version) + { + var url = $"{CliUrls.WwwAbpIo}api/download/{type}/get-nuget-version/"; + + try + { + using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + { + var response = await client.PostAsync( + url, + new StringContent( + JsonSerializer.Serialize( + new GetTemplateNugetVersionDto { Name = name, Version = version } + ), + Encoding.UTF8, + MimeTypes.Application.Json + ), + CancellationTokenProvider.Token + ); + + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + + var result = await response.Content.ReadAsStringAsync(); + + return JsonSerializer.Deserialize(result).Version; + } + } + catch (Exception ex) + { + Console.WriteLine("Error occured while getting the NuGet version from {0} : {1}", url, ex.Message); + return null; } } private async Task DownloadSourceCodeContentAsync(SourceCodeDownloadInputDto input) { - var postData = JsonSerializer.Serialize(input); + var url = $"{CliUrls.WwwAbpIo}api/download/{input.Type}/"; - using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + try { - var responseMessage = await client.PostAsync( - $"{CliUrls.WwwAbpIo}api/download/{input.Type}/", - new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), - CancellationTokenProvider.Token - ); + using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + { + HttpResponseMessage responseMessage; - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + if (input.TemplateSource.IsNullOrWhiteSpace()) + { + responseMessage = await client.PostAsync( + url, + new StringContent(JsonSerializer.Serialize(input), Encoding.UTF8, MimeTypes.Application.Json), + CancellationTokenProvider.Token + ); + } + else + { + responseMessage = await client.GetAsync(input.TemplateSource, CancellationTokenProvider.Token); + } - return await responseMessage.Content.ReadAsByteArrayAsync(); + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + + return await responseMessage.Content.ReadAsByteArrayAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine("Error occured while downloading source-code from {0} : {1}", url, ex.Message); + throw; } } + private bool IsNetworkSource(string source) + { + return source.ToLower().StartsWith("http"); + } + + private List<(string TemplateName, string Version)> GetLocalTemplates() + { + var templateList = new List<(string TemplateName, string Version)>(); + + var stringBuilder = new StringBuilder(); + foreach (var cacheFile in Directory.GetFiles(CliPaths.TemplateCache)) + { + stringBuilder.AppendLine(cacheFile); + } + + var matches = Regex.Matches(stringBuilder.ToString(),$"({AppTemplate.TemplateName}|{AppProTemplate.TemplateName}|{ModuleTemplate.TemplateName}|{ModuleProTemplate.TemplateName})-(.+).zip"); + foreach (Match match in matches) + { + templateList.Add((match.Groups[1].Value, match.Groups[2].Value)); + } + + return templateList; + } + public class SourceCodeDownloadInputDto { public string Name { get; set; } @@ -133,6 +248,8 @@ namespace Volo.Abp.Cli.ProjectBuilding public string Version { get; set; } public string Type { get; set; } + + public string TemplateSource { get; set; } } public class GetLatestSourceCodeVersionDto @@ -140,7 +257,14 @@ namespace Volo.Abp.Cli.ProjectBuilding public string Name { get; set; } } - public class GetLatestSourceCodeVersionResultDto + public class GetTemplateNugetVersionDto + { + public string Name { get; set; } + + public string Version { get; set; } + } + + public class GetVersionResultDto { public string Version { get; set; } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs index fdfbb8b5d3..621631af2a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Analyticses/CliAnalyticsCollect.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -32,27 +33,36 @@ namespace Volo.Abp.Cli.ProjectBuilding.Analyticses public async Task CollectAsync(CliAnalyticsCollectInputDto input) { var postData = _jsonSerializer.Serialize(input); - using (var client = new CliHttpClient()) + var url = $"{CliUrls.WwwAbpIo}api/clianalytics/collect"; + + try { - var responseMessage = await client.PostAsync( - $"{CliUrls.WwwAbpIo}api/clianalytics/collect", - new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), - _cancellationTokenProvider.Token - ); - - if (!responseMessage.IsSuccessStatusCode) + using (var client = new CliHttpClient()) { - var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. "; - var remoteServiceErrorMessage = await _remoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + var responseMessage = await client.PostAsync( + url, + new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), + _cancellationTokenProvider.Token + ); - if (remoteServiceErrorMessage != null) + if (!responseMessage.IsSuccessStatusCode) { - exceptionMessage += remoteServiceErrorMessage; - } + var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. "; + var remoteServiceErrorMessage = await _remoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + + if (remoteServiceErrorMessage != null) + { + exceptionMessage += remoteServiceErrorMessage; + } - _logger.LogInformation(exceptionMessage); + _logger.LogInformation(exceptionMessage); + } } } + catch (Exception ex) + { + // ignored + } } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs new file mode 100644 index 0000000000..97132e8f79 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/MobileApp.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.Cli.ProjectBuilding.Building +{ + public enum MobileApp + { + None, + ReactNative + } + + public static class MobileAppExtensions{ + public static string GetFolderName(this MobileApp mobileApp) + { + switch (mobileApp) + { + case MobileApp.ReactNative: + return "react-native"; + } + + throw new Exception("Mobile app folder name is not set!"); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs index 5eba01b388..9b66f17834 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs @@ -11,6 +11,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building pipeline.Steps.Add(new FileEntryListReadStep()); pipeline.Steps.Add(new ProjectReferenceReplaceStep()); pipeline.Steps.Add(new ReplaceCommonPropsStep()); + pipeline.Steps.Add(new ReplaceConfigureAwaitPropsStep()); pipeline.Steps.Add(new CreateProjectResultZipStep()); return pipeline; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs new file mode 100644 index 0000000000..b1e118ee8b --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Volo.Abp.Cli.ProjectBuilding.Files; +using Volo.Abp.Text; + +namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps +{ + public class ConnectionStringChangeStep : ProjectBuildPipelineStep + { + private const string DefaultConnectionStringKey = "Default"; + + public override void Execute(ProjectBuildContext context) + { + var appSettingsJsonFiles = context.Files.Where(f => + f.Name.EndsWith("appsettings.json", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + if (!appSettingsJsonFiles.Any()) + { + return; + } + + var newConnectionString = $"\"{DefaultConnectionStringKey}\": \"{context.BuildArgs.ConnectionString}\""; + + foreach (var appSettingsJson in appSettingsJsonFiles) + { + try + { + var appSettingJsonContentWithoutBom = StringHelper.ConvertFromBytesWithoutBom(appSettingsJson.Bytes); + var jsonObject = JObject.Parse(appSettingJsonContentWithoutBom); + + var connectionStringContainer = (JContainer)jsonObject?["ConnectionStrings"]; + if (connectionStringContainer == null) + { + continue; + } + + if (!connectionStringContainer.Any()) + { + continue; + } + + var connectionStrings = connectionStringContainer.ToList(); + + foreach (var connectionString in connectionStrings) + { + var property = ((JProperty)connectionString); + var connectionStringName = property.Name; + + if (connectionStringName == DefaultConnectionStringKey) + { + var defaultConnectionString = property.ToString(); + if (defaultConnectionString == null) + { + continue; + } + + appSettingsJson.ReplaceText(defaultConnectionString, newConnectionString); + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine("Cannot change the connection string in " + appSettingsJson.Name + ". Error: " + ex.Message); + } + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs index 29f5afb19c..0d985df192 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using System.Xml; using Volo.Abp.Cli.ProjectBuilding.Files; +using Volo.Abp.Cli.Utils; namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps { @@ -28,7 +30,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps } else { - var nugetPackageVersion = context.TemplateFile.Version; + var nugetPackageVersion = context.TemplateFile.RepositoryNugetVersion; if (IsBranchName(nugetPackageVersion)) { @@ -92,7 +94,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps var doc = new XmlDocument() { PreserveWhitespace = true }; - doc.Load(GenerateStreamFromString(content)); + doc.Load(StreamHelper.GenerateStreamFromString(content)); return ProcessReferenceNodes(doc, content); } @@ -108,7 +110,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps var oldNodeIncludeValue = oldNode.Attributes["Include"].Value; // ReSharper disable once PossibleNullReferenceException : Can not be null because nodes are selected with include attribute filter in previous method - if (oldNodeIncludeValue.Contains(_projectName)) + if (oldNodeIncludeValue.Contains(_projectName) && _entries.Any(e=>e.Name.EndsWith($"{oldNodeIncludeValue}.csproj"))) { continue; } @@ -125,16 +127,6 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps protected abstract XmlElement GetNewReferenceNode(XmlDocument doc, string oldNodeIncludeValue); - private static Stream GenerateStreamFromString(string s) - { - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(s); - writer.Flush(); - stream.Position = 0; - return stream; - } - public class NugetReferenceReplacer : ProjectReferenceReplacer { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveRootFolderStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveRootFolderStep.cs new file mode 100644 index 0000000000..b6e8cc3852 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveRootFolderStep.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Volo.Abp.Cli.ProjectBuilding.Files; + +namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps +{ + public class RemoveRootFolderStep : ProjectBuildPipelineStep + { + public override void Execute(ProjectBuildContext context) + { + foreach (var file in context.Files) + { + file.SetName(RemoveRootFolder(file.Name)); + } + } + + private string RemoveRootFolder(string fileName) + { + if (!fileName.Contains('/')) + { + return fileName; + } + + return string.Join("/", fileName.Split('/').Skip(1)); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceCommonPropsStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceCommonPropsStep.cs index 07b2081738..862ac39b09 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceCommonPropsStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceCommonPropsStep.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text.RegularExpressions; using System.Xml; using Volo.Abp.Cli.ProjectBuilding.Files; +using Volo.Abp.Cli.Utils; namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps { @@ -41,7 +42,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps var doc = new XmlDocument() { PreserveWhitespace = true }; - doc.Load(GenerateStreamFromString(content)); + doc.Load(StreamHelper.GenerateStreamFromString(content)); return ProcessReferenceNodes(doc, content); } @@ -81,16 +82,6 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps return doc.OuterXml; } - - private static Stream GenerateStreamFromString(string s) - { - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(s); - writer.Flush(); - stream.Position = 0; - return stream; - } } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceConfigureAwaitPropsStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceConfigureAwaitPropsStep.cs new file mode 100644 index 0000000000..1541284009 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ReplaceConfigureAwaitPropsStep.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Xml; +using Volo.Abp.Cli.ProjectBuilding.Files; +using Volo.Abp.Cli.Utils; + +namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps +{ + public class ReplaceConfigureAwaitPropsStep : ProjectBuildPipelineStep + { + public override void Execute(ProjectBuildContext context) + { + new ConfigureAwaitPropsReplacer(context.Files).Run(); + } + + private class ConfigureAwaitPropsReplacer + { + private readonly List _entries; + + public ConfigureAwaitPropsReplacer( + List entries) + { + _entries = entries; + } + + public void Run() + { + foreach (var fileEntry in _entries) + { + if (fileEntry.Name.EndsWith(".csproj")) + { + fileEntry.SetContent(ProcessFileContent(fileEntry.Content)); + } + } + } + + private string ProcessFileContent(string content) + { + Check.NotNull(content, nameof(content)); + + var doc = new XmlDocument() { PreserveWhitespace = true }; + + doc.Load(StreamHelper.GenerateStreamFromString(content)); + + return ProcessReferenceNodes(doc, content); + } + + private string ProcessReferenceNodes(XmlDocument doc, string content) + { + Check.NotNull(content, nameof(content)); + + var importNodes = doc.SelectNodes("/Project/Import[@Project]"); + + if (importNodes == null) + { + return doc.OuterXml; + } + + foreach (XmlNode node in importNodes) + { + if (!(node.Attributes?["Project"]?.Value?.EndsWith("\\configureawait.props") ?? false)) + { + continue; + } + + node.ParentNode?.RemoveChild(node); + } + + return doc.OuterXml; + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs index e5916f3cbc..46d2b77f95 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs @@ -1,6 +1,7 @@ using System; using Volo.Abp.Cli.ProjectBuilding.Building.Steps; using Volo.Abp.Cli.ProjectBuilding.Templates.App; +using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule; namespace Volo.Abp.Cli.ProjectBuilding.Building { @@ -18,11 +19,22 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building pipeline.Steps.Add(new TemplateCodeDeleteStep()); pipeline.Steps.Add(new SolutionRenameStep()); - if (context.Template.Name == AppProTemplate.TemplateName) + if (context.Template.Name == AppProTemplate.TemplateName || + context.Template.Name == ModuleProTemplate.TemplateName) { pipeline.Steps.Add(new LicenseCodeReplaceStep()); } + if (context.BuildArgs.UiFramework == UiFramework.Mvc && context.BuildArgs.MobileApp == MobileApp.None) + { + pipeline.Steps.Add(new RemoveRootFolderStep()); + } + + if (context.BuildArgs.ConnectionString != null) + { + pipeline.Steps.Add(new ConnectionStringChangeStep()); + } + pipeline.Steps.Add(new CreateProjectResultZipStep()); return pipeline; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ISourceCodeStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ISourceCodeStore.cs index 4d1ed7aa0c..de082d4bcf 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ISourceCodeStore.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ISourceCodeStore.cs @@ -8,7 +8,8 @@ namespace Volo.Abp.Cli.ProjectBuilding Task GetAsync( string name, string type, - [CanBeNull] string version = null + [CanBeNull] string version = null, + [CanBeNull] string templateSource = null ); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ModuleProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ModuleProjectBuilder.cs index 48f109e1d3..6a7132769a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ModuleProjectBuilder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ModuleProjectBuilder.cs @@ -83,6 +83,8 @@ namespace Volo.Abp.Cli.ProjectBuilding !x.Key.Equals(NewCommand.Options.OutputFolder.Short, StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.Key.Equals(NewCommand.Options.Version.Long, StringComparison.InvariantCultureIgnoreCase) && !x.Key.Equals(NewCommand.Options.Version.Short, StringComparison.InvariantCultureIgnoreCase)) + .Where(x => !x.Key.Equals(NewCommand.Options.TemplateSource.Short, StringComparison.InvariantCultureIgnoreCase) && + !x.Key.Equals(NewCommand.Options.TemplateSource.Long, StringComparison.InvariantCultureIgnoreCase)) .Select(x => x.Key).ToList(); await CliAnalyticsCollect.CollectAsync(new CliAnalyticsCollectInputDto diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ProjectBuildArgs.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ProjectBuildArgs.cs index 5586321010..70cb98dff0 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ProjectBuildArgs.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/ProjectBuildArgs.cs @@ -19,28 +19,42 @@ namespace Volo.Abp.Cli.ProjectBuilding public UiFramework UiFramework { get; set; } + public MobileApp? MobileApp { get; set; } + [CanBeNull] public string AbpGitHubLocalRepositoryPath { get; set; } + [CanBeNull] + public string TemplateSource { get; set; } + + [CanBeNull] + public string ConnectionString { get; set; } + [NotNull] public Dictionary ExtraProperties { get; set; } public ProjectBuildArgs( - [NotNull] SolutionName solutionName, + [NotNull] SolutionName solutionName, [CanBeNull] string templateName = null, [CanBeNull] string version = null, DatabaseProvider databaseProvider = DatabaseProvider.NotSpecified, UiFramework uiFramework = UiFramework.NotSpecified, + MobileApp? mobileApp = null, [CanBeNull] string abpGitHubLocalRepositoryPath = null, - Dictionary extraProperties = null) + [CanBeNull] string templateSource = null, + Dictionary extraProperties = null, + [CanBeNull] string connectionString = null) { SolutionName = Check.NotNull(solutionName, nameof(solutionName)); TemplateName = templateName; Version = version; DatabaseProvider = databaseProvider; UiFramework = uiFramework; + MobileApp = mobileApp; AbpGitHubLocalRepositoryPath = abpGitHubLocalRepositoryPath; + TemplateSource = templateSource; ExtraProperties = extraProperties ?? new Dictionary(); + ConnectionString = connectionString; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs index c7f9e77cb2..fc514e8a74 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateFile.cs @@ -6,13 +6,16 @@ public string LatestVersion { get; } + public string RepositoryNugetVersion { get; } + public byte[] FileBytes { get; } - public TemplateFile(byte[] fileBytes, string version, string latestVersion) + public TemplateFile(byte[] fileBytes, string version, string latestVersion, string repositoryNugetVersion) { FileBytes = fileBytes; Version = version; LatestVersion = latestVersion; + RepositoryNugetVersion = repositoryNugetVersion; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateProjectBuilder.cs index 9d31e10200..924fdaf2c9 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateProjectBuilder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/TemplateProjectBuilder.cs @@ -8,6 +8,7 @@ using Volo.Abp.Cli.Commands; using Volo.Abp.Cli.Licensing; using Volo.Abp.Cli.ProjectBuilding.Analyticses; using Volo.Abp.Cli.ProjectBuilding.Building; +using Volo.Abp.Cli.ProjectBuilding.Templates.App; using Volo.Abp.DependencyInjection; using Volo.Abp.Json; @@ -24,11 +25,11 @@ namespace Volo.Abp.Cli.ProjectBuilding protected IJsonSerializer JsonSerializer { get; } protected IApiKeyService ApiKeyService { get; } - public TemplateProjectBuilder(ISourceCodeStore sourceCodeStore, + public TemplateProjectBuilder(ISourceCodeStore sourceCodeStore, ITemplateInfoProvider templateInfoProvider, - ICliAnalyticsCollect cliAnalyticsCollect, + ICliAnalyticsCollect cliAnalyticsCollect, IOptions options, - IJsonSerializer jsonSerializer, + IJsonSerializer jsonSerializer, IApiKeyService apiKeyService) { SourceCodeStore = sourceCodeStore; @@ -40,7 +41,7 @@ namespace Volo.Abp.Cli.ProjectBuilding Logger = NullLogger.Instance; } - + public async Task BuildAsync(ProjectBuildArgs args) { var templateInfo = GetTemplateInfo(args); @@ -50,13 +51,21 @@ namespace Volo.Abp.Cli.ProjectBuilding var templateFile = await SourceCodeStore.GetAsync( args.TemplateName, SourceCodeTypes.Template, - args.Version + args.Version, + args.TemplateSource ); - + var apiKeyResult = await ApiKeyService.GetApiKeyOrNullAsync(); - if (apiKeyResult?.ApiKey != null) + if (apiKeyResult != null) { - args.ExtraProperties["api-key"] = apiKeyResult.ApiKey; + if (apiKeyResult.ApiKey != null) + { + args.ExtraProperties["api-key"] = apiKeyResult.ApiKey; + } + else if (templateInfo.Name == AppProTemplate.TemplateName) + { + throw new UserFriendlyException(apiKeyResult.ErrorMessage); + } } if (apiKeyResult?.LicenseCode != null) @@ -81,15 +90,19 @@ namespace Volo.Abp.Cli.ProjectBuilding // Exclude unwanted or known options. var options = args.ExtraProperties .Where(x => !x.Key.Equals(CliConsts.Command, StringComparison.InvariantCultureIgnoreCase)) - .Where(x => !x.Key.Equals("tiered", StringComparison.InvariantCultureIgnoreCase)) - .Where(x => !x.Key.Equals(NewCommand.Options.DatabaseProvider.Long, StringComparison.InvariantCultureIgnoreCase) && + .Where(x => !x.Key.Equals(NewCommand.Options.Tiered.Long, StringComparison.InvariantCultureIgnoreCase)) + .Where(x => !x.Key.Equals(NewCommand.Options.DatabaseProvider.Long, StringComparison.InvariantCultureIgnoreCase) && !x.Key.Equals(NewCommand.Options.DatabaseProvider.Short, StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.Key.Equals(NewCommand.Options.OutputFolder.Long, StringComparison.InvariantCultureIgnoreCase) && !x.Key.Equals(NewCommand.Options.OutputFolder.Short, StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.Key.Equals(NewCommand.Options.UiFramework.Long, StringComparison.InvariantCultureIgnoreCase) && !x.Key.Equals(NewCommand.Options.UiFramework.Short, StringComparison.InvariantCultureIgnoreCase)) + .Where(x => !x.Key.Equals(NewCommand.Options.Mobile.Long, StringComparison.InvariantCultureIgnoreCase) && + !x.Key.Equals(NewCommand.Options.Mobile.Short, StringComparison.InvariantCultureIgnoreCase)) .Where(x => !x.Key.Equals(NewCommand.Options.Version.Long, StringComparison.InvariantCultureIgnoreCase) && !x.Key.Equals(NewCommand.Options.Version.Short, StringComparison.InvariantCultureIgnoreCase)) + .Where(x => !x.Key.Equals(NewCommand.Options.TemplateSource.Short, StringComparison.InvariantCultureIgnoreCase) && + !x.Key.Equals(NewCommand.Options.TemplateSource.Long, StringComparison.InvariantCultureIgnoreCase)) .Select(x => x.Key).ToList(); await CliAnalyticsCollect.CollectAsync(new CliAnalyticsCollectInputDto @@ -97,7 +110,7 @@ namespace Volo.Abp.Cli.ProjectBuilding Tool = Options.ToolName, Command = args.ExtraProperties.ContainsKey(CliConsts.Command) ? args.ExtraProperties[CliConsts.Command] : "", DatabaseProvider = args.DatabaseProvider.ToProviderName(), - IsTiered = args.ExtraProperties.ContainsKey("tiered"), + IsTiered = args.ExtraProperties.ContainsKey(NewCommand.Options.Tiered.Long), UiFramework = args.UiFramework.ToFrameworkName(), Options = JsonSerializer.Serialize(options), ProjectName = args.SolutionName.FullName, 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 254e8d80ae..07ada2fe3d 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 @@ -74,6 +74,11 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App { steps.Add(new RemoveFolderStep("/angular")); } + + if (context.BuildArgs.MobileApp != MobileApp.ReactNative) + { + steps.Add(new RemoveFolderStep(MobileApp.ReactNative.GetFolderName().EnsureStartsWith('/'))); + } } private static void ConfigureWithoutUi(ProjectBuildContext context, List steps) @@ -156,7 +161,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.App private static void CleanupFolderHierarchy(ProjectBuildContext context, List steps) { - if (context.BuildArgs.UiFramework == UiFramework.Mvc) + if (context.BuildArgs.UiFramework == UiFramework.Mvc && context.BuildArgs.MobileApp == MobileApp.None) { steps.Add(new MoveFolderStep("/aspnet-core/", "/")); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/TemplateRandomSslPortStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/TemplateRandomSslPortStep.cs index 8b30ca2519..12bc34221a 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/TemplateRandomSslPortStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/TemplateRandomSslPortStep.cs @@ -44,6 +44,12 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates ) .ToList(); + var reactNativeEnvironments = context.Files.Where(x => + !x.IsDirectory && + x.Name.EndsWith($"{MobileApp.ReactNative.GetFolderName()}/Environment.js", StringComparison.InvariantCultureIgnoreCase) + ) + .ToList(); + if (AppTemplateBase.IsAppTemplate(context.Template.Name)) { // no tiered @@ -127,6 +133,23 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates environment.SetLines(environmentLines); } + foreach (var environment in reactNativeEnvironments) + { + environment.NormalizeLineEndings(); + + var buildInUrlHttp = buildInUrl.Replace("https://", "http://"); + var buildInUrlWithoutPortHttp = buildInUrlWithoutPort.Replace("https://", "http://"); + + var environmentLines = environment.GetLines(); + for (var i = 0; i < environmentLines.Length; i++) + { + if (environmentLines[i].Contains(buildInUrlHttp)) + { + environmentLines[i] = environmentLines[i].Replace(buildInUrlHttp, $"{buildInUrlWithoutPortHttp}:{newPort}"); + } + } + environment.SetLines(environmentLines); + } } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs new file mode 100644 index 0000000000..d8f8404c46 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs @@ -0,0 +1,204 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class AngularModuleSourceCodeAdder : ITransientDependency + { + public async Task AddAsync(string solutionFilePath, string angularPath) + { + var angularProjectsPath = Path.Combine(angularPath, "projects"); + + var projects = await CopyAndGetNamesOfAngularProjectsAsync(solutionFilePath, angularProjectsPath); + + if (!projects.Any()) + { + return; + } + + await ReplaceProjectNamesAndCopySourCodeRequirementsToProjectsAsync(angularProjectsPath, projects); + + await RemoveRedundantFilesAsync(angularProjectsPath, projects); + + await AddPathsToTsConfigAsync(angularPath, angularProjectsPath, projects); + + await AddProjectToAngularJsonAsync(angularPath, projects); + } + + private async Task AddProjectToAngularJsonAsync(string angularPath, List projects) + { + var angularJsonFilePath = Path.Combine(angularPath, "angular.json"); + var fileContent = File.ReadAllText(angularJsonFilePath); + + var json = JObject.Parse(fileContent); + + var projectsJobject = (JObject)json["projects"]; + + foreach (var project in projects) + { + projectsJobject.Add(project, new JObject( + new JProperty("projectType", "library"), + new JProperty("root", $"projects/{project}"), + new JProperty("sourceRoot", $"projects/{project}/src"), + new JProperty("prefix", "lib"), + new JProperty("architect", new JObject( + new JProperty("build", new JObject( + new JProperty("builder", "@angular-devkit/build-ng-package:build"), + new JProperty("options", new JObject( + new JProperty("tsConfig", $"projects/{project}/tsconfig.lib.json"), + new JProperty("project", $"projects/{project}/ng-package.json") + )), + new JProperty("configurations", new JObject( + new JProperty("production", new JObject( + new JProperty("tsConfig", $"projects/{project}/tsconfig.lib.prod.json"))) + )))), + new JProperty("test", new JObject( + new JProperty("builder", "@angular-devkit/build-angular:karma"), + new JProperty("options", new JObject( + new JProperty("main", $"projects/{project}/src/test.ts"), + new JProperty("tsConfig", $"projects/{project}/tsconfig.spec.json"), + new JProperty("karmaConfig", $"projects/{project}/karma.conf.js") + ) + ))), + new JProperty("lint", new JObject( + new JProperty("builder", "@angular-devkit/build-angular:tslint"), + new JProperty("options", new JObject( + new JProperty("tsConfig", new JArray(new JValue($"projects/{project}/tsconfig.lib.json"), new JValue($"projects/{project}/tsconfig.spec.json"))), + new JProperty("exclude", new JArray(new JValue("**/node_modules/**"))) + )) + )) + )) + )); + } + + File.WriteAllText(angularJsonFilePath, json.ToString(Formatting.Indented)); + } + + private async Task AddPathsToTsConfigAsync(string angularPath, string angularProjectsPath, List projects) + { + var tsConfigPath = Path.Combine(angularPath, "tsconfig.json"); + var fileContent = File.ReadAllText(tsConfigPath); + + var tsConfigAsJson = JObject.Parse(fileContent); + + var compilerOptions = (JObject)tsConfigAsJson["compilerOptions"]; + + foreach (var project in projects) + { + var projectPackageName = await GetProjectPackageNameAsync(angularProjectsPath, project); + + var property = new JProperty($"{projectPackageName}", + new JArray(new object[] { $"projects/{project}/src/public-api.ts" }) + ); + var property2 = new JProperty($"{projectPackageName}/*", + new JArray(new object[] { $"projects/{project}/src/lib/*" }) + ); + + if (compilerOptions["paths"] == null) + { + + compilerOptions.Add("paths", new JObject()); + } + + ((JObject)compilerOptions["paths"]).Add(property); + ((JObject)compilerOptions["paths"]).Add(property2); + } + + File.WriteAllText(tsConfigPath, tsConfigAsJson.ToString(Formatting.Indented)); + } + + private async Task GetProjectPackageNameAsync(string angularProjectsPath, string project) + { + var packageJsonPath = Path.Combine(angularProjectsPath, project, "package.json"); + + var fileContent = File.ReadAllText(packageJsonPath); + + return (string)JObject.Parse(fileContent)["name"]; + } + + private async Task RemoveRedundantFilesAsync(string angularProjectsPath, List projects) + { + foreach (var project in projects) + { + var jestConfigPath = Path.Combine(angularProjectsPath, project, "jest.config.js"); + + if (File.Exists(jestConfigPath)) + { + File.Delete(jestConfigPath); + } + + var testPath = Path.Combine(angularProjectsPath, project, "src", "test.ts"); + + if (File.Exists(testPath)) + { + File.Delete(testPath); + } + } + } + + private async Task ReplaceProjectNamesAndCopySourCodeRequirementsToProjectsAsync(string angularProjectsPath, List projects) + { + foreach (var project in projects) + { + var sourceCodeReqFolder = Path.Combine(angularProjectsPath, project, "source-code-requirements"); + + Directory.CreateDirectory(sourceCodeReqFolder); + + var filesUnderSourceCodeReqFolder = Directory.GetFiles(sourceCodeReqFolder); + + foreach (var fileUnderSourceCodeReqFolder in filesUnderSourceCodeReqFolder) + { + var newDest = Path.Combine(angularProjectsPath, project, Path.GetFileName(fileUnderSourceCodeReqFolder)); + File.Move(fileUnderSourceCodeReqFolder, newDest); + + var fileContent = File.ReadAllText(newDest); + fileContent = fileContent.Replace("{{project-name}}", project); + fileContent = fileContent.Replace("{{library-name-kebab-case}}", project); + File.WriteAllText(newDest, fileContent); + } + } + } + + private async Task> CopyAndGetNamesOfAngularProjectsAsync(string solutionFilePath, string angularProjectsPath) + { + var projects = new List(); + + if (!Directory.Exists(angularProjectsPath)) + { + Directory.CreateDirectory(angularProjectsPath); + } + + var angularPathsInDownloadedSourceCode = Directory.GetDirectories(Path.Combine(Path.Combine(Path.GetDirectoryName(solutionFilePath), "modules"))).Select(p => Path.Combine(p, "angular")) + .Where(Directory.Exists); + + + foreach (var folder in angularPathsInDownloadedSourceCode) + { + var projectsInFolder = Directory.GetDirectories(folder); + foreach (var projectInFolder in projectsInFolder) + { + var projectName = Path.GetFileName(projectInFolder.TrimEnd('\\').TrimEnd('/')); + + var destDirName = Path.Combine(angularProjectsPath, projectName); + if (Directory.Exists(destDirName)) + { + Directory.Delete(projectInFolder, true); + continue; + } + + + projects.Add(projectName); + + Directory.Move(projectInFolder, destDirName); + } + } + + return projects; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs index 0211a42243..d369a9af43 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DbContextFileBuilderConfigureAdder.cs @@ -18,7 +18,7 @@ namespace Volo.Abp.Cli.ProjectModification Logger = NullLogger.Instance; } - public void Add(string path, string moduleConfiguration) + public bool Add(string path, string moduleConfiguration) { var file = File.ReadAllText(path); @@ -32,13 +32,18 @@ namespace Volo.Abp.Cli.ProjectModification if (indexToInsert <= 0 || indexToInsert >= file.Length) { Logger.LogWarning($"\"OnModelCreating(ModelBuilder builder)\" method couldn't be found in {path}"); - return; + return false; } file = file.Insert(indexToInsert, " " + stringToAdd + Environment.NewLine + " "); } + else + { + return false; + } File.WriteAllText(path, file); + return true; } protected int FindIndexToInsert(string file) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs index 4cf3209955..8da03763c6 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs @@ -53,7 +53,6 @@ namespace Volo.Abp.Cli.ProjectModification protected bool IsDerived(string csFile, string baseClass) { - Logger.LogDebug(csFile); var root = CSharpSyntaxTree.ParseText(File.ReadAllText(csFile)).GetRoot(); var namespaceSyntax = root.DescendantNodes().OfType().First(); var classDeclaration = (namespaceSyntax.DescendantNodes().OfType()).First(); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleWithMastersInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleWithMastersInfo.cs new file mode 100644 index 0000000000..07d6e12349 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ModuleWithMastersInfo.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class ModuleWithMastersInfo : ModuleInfo + { + public List MasterModuleInfos { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetApiResponse.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetApiResponse.cs new file mode 100644 index 0000000000..9a32d8d6fc --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetApiResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class MyGetApiResponse + { + public string _date { get; set; } + + public List Packages { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackage.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackage.cs new file mode 100644 index 0000000000..e5ef40ca17 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackage.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class MyGetPackage + { + public string PackageType { get; set; } + + public string Id { get; set; } + + public List Versions { get; set; } + + public List Dates { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs new file mode 100644 index 0000000000..41907276f4 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs @@ -0,0 +1,51 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; +using Volo.Abp.Cli.Http; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class MyGetPackageListFinder : ISingletonDependency + { + private MyGetApiResponse _response; + + public ILogger Logger { get; set; } + + public MyGetPackageListFinder() + { + Logger = NullLogger.Instance; + } + + public async Task GetPackages() + { + if (_response != null) + { + return _response; + } + + try + { + using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + { + var responseMessage = await client.GetAsync( + $"{CliUrls.WwwAbpIo}api/myget/packages/" + ); + + _response = JsonConvert.DeserializeObject(Encoding.Default.GetString(await responseMessage.Content.ReadAsByteArrayAsync())); + } + } + catch (Exception) + { + Logger.LogError("Unable to get latest preview version."); + throw; + } + + return _response; + } + + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs index 8140b73b64..59c262e065 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmGlobalPackagesChecker.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.Cli.ProjectModification { public ILogger Logger { get; set; } - public NpmGlobalPackagesChecker(PackageJsonFileFinder packageJsonFileFinder) + public NpmGlobalPackagesChecker() { Logger = NullLogger.Instance; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index 41fd33b4fb..c3434e6e27 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs @@ -2,11 +2,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Volo.Abp.Cli.Http; using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; @@ -18,18 +21,20 @@ namespace Volo.Abp.Cli.ProjectModification private readonly PackageJsonFileFinder _packageJsonFileFinder; private readonly NpmGlobalPackagesChecker _npmGlobalPackagesChecker; + private readonly MyGetPackageListFinder _myGetPackageListFinder; private readonly Dictionary _fileVersionStorage = new Dictionary(); - public NpmPackagesUpdater(PackageJsonFileFinder packageJsonFileFinder, NpmGlobalPackagesChecker npmGlobalPackagesChecker) + public NpmPackagesUpdater(PackageJsonFileFinder packageJsonFileFinder, NpmGlobalPackagesChecker npmGlobalPackagesChecker, MyGetPackageListFinder myGetPackageListFinder) { _packageJsonFileFinder = packageJsonFileFinder; _npmGlobalPackagesChecker = npmGlobalPackagesChecker; + _myGetPackageListFinder = myGetPackageListFinder; Logger = NullLogger.Instance; } - public void Update(string rootDirectory) + public async Task Update(string rootDirectory, bool includePreviews = false, bool switchToStable = false) { var fileList = _packageJsonFileFinder.Find(rootDirectory); @@ -42,15 +47,20 @@ namespace Volo.Abp.Cli.ProjectModification foreach (var file in fileList) { - UpdatePackagesInFile(file, out var packagesUpdated); + var packagesUpdated = await UpdatePackagesInFile(file, includePreviews, switchToStable); if (packagesUpdated) { var fileDirectory = Path.GetDirectoryName(file).EnsureEndsWith(Path.DirectorySeparatorChar); + if (IsAngularProject(fileDirectory)) + { + await CreateNpmrcFileAsync(Path.GetDirectoryName(file)); + } + RunYarn(fileDirectory); - if (IsAngularProject(fileDirectory) == false) + if (!IsAngularProject(fileDirectory)) { Thread.Sleep(500); RunGulp(fileDirectory); @@ -59,26 +69,90 @@ namespace Volo.Abp.Cli.ProjectModification } } + private async Task CreateNpmrcFileAsync(string directoryName) + { + var fileName = Path.Combine(directoryName, ".npmrc"); + + var abpRegistry = "@abp:registry:https://www.myget.org/F/abp-nightly/npm"; + var voloRegistry = await GetVoloRegistryAsync(); + + if (File.Exists(fileName)) + { + var fileContent = File.ReadAllText(fileName); + + if (!fileContent.Contains(abpRegistry)) + { + fileContent += Environment.NewLine + abpRegistry; + } + + if (!fileContent.Contains(voloRegistry)) + { + fileContent += Environment.NewLine + voloRegistry; + } + + File.WriteAllText(fileName, fileContent); + + return; + } + + using var fs = File.Create(fileName); + + var content = new UTF8Encoding(true) + .GetBytes(abpRegistry + Environment.NewLine + voloRegistry); + fs.Write(content, 0, content.Length); + } + + private async Task GetVoloRegistryAsync() + { + var apikey = await GetApiKeyAsync(); + + if (string.IsNullOrWhiteSpace(apikey)) + { + return ""; + } + + return "@volo:registry=https://www.myget.org/F/abp-commercial/auth/" + apikey + "/npm/"; + } + + public async Task GetApiKeyAsync() + { + try + { + using (var client = new CliHttpClient(TimeSpan.FromMinutes(1))) + { + var responseMessage = await client.GetAsync( + $"{CliUrls.WwwAbpIo}api/myget/apikey/" + ); + + return Encoding.Default.GetString(await responseMessage.Content.ReadAsByteArrayAsync()); + } + } + catch (Exception) + { + return ""; + } + } + private bool IsAngularProject(string fileDirectory) { return File.Exists(Path.Combine(fileDirectory, "angular.json")); } - protected virtual void UpdatePackagesInFile(string file, out bool packagesUpdated) + protected virtual async Task UpdatePackagesInFile(string file, bool includePreviews = false, bool switchToStable = false) { - packagesUpdated = false; + var packagesUpdated = false; var fileContent = File.ReadAllText(file); var packageJson = JObject.Parse(fileContent); var abpPackages = GetAbpPackagesFromPackageJson(packageJson); if (!abpPackages.Any()) { - return; + return packagesUpdated; } foreach (var abpPackage in abpPackages) { - TryUpdatePackage(file, abpPackage, out var updated); + var updated = await TryUpdatePackage(file, abpPackage, includePreviews, switchToStable); if (updated) { @@ -89,46 +163,62 @@ namespace Volo.Abp.Cli.ProjectModification var modifiedFileContent = packageJson.ToString(Formatting.Indented); File.WriteAllText(file, modifiedFileContent); + + return packagesUpdated; } - protected virtual void TryUpdatePackage(string file, JProperty package, out bool updated) + protected virtual async Task TryUpdatePackage(string file, JProperty package, + bool includePreviews = false, bool switchToStable = false) { - var version = GetLatestVersion(package); + var currentVersion = (string)package.Value; - var versionWithPrefix = $"^{version}"; + var version = await GetLatestVersion(package, currentVersion, includePreviews, switchToStable); - if (versionWithPrefix == (string)package.Value) - { - updated = false; - return; - } - else + var versionWithPrefix = $"~{version}"; + + if (versionWithPrefix == currentVersion) { - updated = true; + return false; } package.Value.Replace(versionWithPrefix); Logger.LogInformation($"Updated {package.Name} to {version} in {file.Replace(Directory.GetCurrentDirectory(), "")}."); + return true; } - protected virtual string GetLatestVersion(JProperty package) + protected virtual async Task GetLatestVersion(JProperty package, string currentVersion, + bool includePreviews = false, bool switchToStable = false) { if (_fileVersionStorage.ContainsKey(package.Name)) { return _fileVersionStorage[package.Name]; } - var version = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version"); + string newVersion = currentVersion; + + if (includePreviews || (!switchToStable && currentVersion.Contains("-preview"))) + { + var mygetPackage = (await _myGetPackageListFinder.GetPackages()).Packages.FirstOrDefault(p => p.Id == package.Name); + if (mygetPackage != null) + { + newVersion = mygetPackage.Versions.Last(); + } + } + else + { + newVersion = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version"); + } + - _fileVersionStorage[package.Name] = version; + _fileVersionStorage[package.Name] = newVersion; - return version; + return newVersion; } protected virtual List GetAbpPackagesFromPackageJson(JObject fileObject) { - var dependencyList = new [] { "dependencies", "devDependencies", "peerDependencies" }; + var dependencyList = new[] { "dependencies", "devDependencies", "peerDependencies" }; var abpPackages = new List(); foreach (var dependencyListName in dependencyList) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageInfo.cs index 03c5c5493b..6bd6297384 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageInfo.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageInfo.cs @@ -7,5 +7,7 @@ public string ModuleClass { get; set; } public NuGetPackageTarget Target { get; set; } + + public NuGetPackageTarget TieredTarget { get; set; } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs new file mode 100644 index 0000000000..d342e993d7 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Volo.Abp.Cli.Utils; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class NugetPackageToLocalReferenceConverter : ITransientDependency + { + public async Task Convert(ModuleWithMastersInfo module, string solutionFile) + { + var nugetPackageList = GetNugetPackages(module); + var modulesFolder = Path.Combine(Path.GetDirectoryName(solutionFile), "modules"); + var srcFolder = Path.Combine(Path.GetDirectoryName(solutionFile), "src"); + var testFolder = Path.Combine(Path.GetDirectoryName(solutionFile), "test"); + + ConvertToLocalReference(modulesFolder, nugetPackageList, "..\\..\\..\\"); + ConvertToLocalReference(srcFolder, nugetPackageList, "..\\..\\modules\\"); + ConvertToLocalReference(testFolder, nugetPackageList, "..\\..\\modules\\", "test"); + } + + private void ConvertToLocalReference(string folder, List nugetPackageList, string localPathPrefix, string sourceFile = "src") + { + var projectFiles = GetProjectFilesUnder(folder); + + foreach (var projectFile in projectFiles) + { + var content = File.ReadAllText(projectFile); + var doc = new XmlDocument() { PreserveWhitespace = true }; + + doc.Load(StreamHelper.GenerateStreamFromString(content)); + + var convertedProject = ProcessReferenceNodes(folder, doc, nugetPackageList, localPathPrefix, sourceFile); + + File.WriteAllText(projectFile, convertedProject); + } + } + + private string ProcessReferenceNodes(string folder, XmlDocument doc, List nugetPackageList, string localPathPrefix, string sourceFile = "src") + { + var nodes = doc.SelectNodes("/Project/ItemGroup/PackageReference[starts-with(@Include, 'Volo.')]"); + + if (nodes == null) + { + return doc.OuterXml; + } + + foreach (XmlNode oldNode in nodes) + { + var tempSourceFile = sourceFile; + var oldNodeIncludeValue = oldNode?.Attributes?["Include"]?.Value; + + var moduleName = nugetPackageList.FirstOrDefault(n => n.NugetPackage.Name == oldNodeIncludeValue)?.ModuleName; + + if (moduleName == null) + { + var localProject = GetProjectFilesUnder(folder).FirstOrDefault(f=> f.EndsWith($"{oldNodeIncludeValue}.csproj")); + + if (localProject != null) + { + moduleName = Directory.GetParent(Directory.GetParent(Path.GetDirectoryName(localProject)).FullName).Name; + + if (oldNodeIncludeValue.EndsWith(".test", StringComparison.InvariantCultureIgnoreCase) || + oldNodeIncludeValue.EndsWith(".tests", StringComparison.InvariantCultureIgnoreCase) || + oldNodeIncludeValue.EndsWith(".testbase", StringComparison.InvariantCultureIgnoreCase)) + { + tempSourceFile = "test"; + } + else + { + tempSourceFile = "src"; + } + } + else + { + continue; + } + } + + var referenceProjectPath = + $"{localPathPrefix}{moduleName}\\{tempSourceFile}\\{oldNodeIncludeValue}\\{oldNodeIncludeValue}.csproj"; + + XmlNode newNode = GetNewReferenceNode(doc, referenceProjectPath); + + oldNode?.ParentNode?.ReplaceChild(newNode, oldNode); + } + + return doc.OuterXml; + } + + protected XmlElement GetNewReferenceNode(XmlDocument doc, string newValue) + { + var newNode = doc.CreateElement("ProjectReference"); + + var includeAttr = doc.CreateAttribute("Include"); + includeAttr.Value = newValue; + newNode.Attributes.Append(includeAttr); + + return newNode; + } + + public List GetNugetPackages(ModuleWithMastersInfo module) + { + var list = new List(); + + list.AddRange(module.NugetPackages.Select(n => new NugetPackageInfoWithModuleName + { + ModuleName = module.Name, + NugetPackage = n + })); + + if (module.MasterModuleInfos != null) + { + foreach (var masterModule in module.MasterModuleInfos) + { + list.AddRange(GetNugetPackages(masterModule)); + } + } + + return list; + } + + private static string[] GetProjectFilesUnder(string path) + { + return Directory.GetFiles(path, + "*.csproj", + SearchOption.AllDirectories); + } + + public class NugetPackageInfoWithModuleName + { + public NugetPackageInfo NugetPackage { get; set; } + + public string ModuleName { get; set; } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceAdder.cs new file mode 100644 index 0000000000..3e651b3bf1 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceAdder.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.Xml; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class PackageSourceAdder: ITransientDependency + { + public ILogger Logger { get; set; } + + public PackageSourceAdder() + { + Logger = NullLogger.Instance; + } + + public void Add(string sourceKey, string sourceValue) + { + var nugetConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "NuGet", "NuGet.Config"); + + if (!File.Exists(nugetConfigPath)) + { + return; + } + + var fileContent = File.ReadAllText(nugetConfigPath); + + if (fileContent.Contains($"\"{sourceValue}\"")) + { + return; + } + + Logger.LogInformation($"Adding \"{sourceValue}\" ({sourceKey}) to nuget sources..."); + + try + { + var doc = new XmlDocument() { PreserveWhitespace = true }; + + doc.Load(GenerateStreamFromString(fileContent)); + + var sourceNodes = doc.SelectNodes("/configuration/packageSources"); + + var newNode = doc.CreateElement("add"); + + var includeAttr = doc.CreateAttribute("key"); + includeAttr.Value = sourceKey; + newNode.Attributes.Append(includeAttr); + + var versionAttr = doc.CreateAttribute("value"); + versionAttr.Value = sourceValue; + newNode.Attributes.Append(versionAttr); + + sourceNodes?[0]?.AppendChild(newNode); + + File.WriteAllText(nugetConfigPath, doc.OuterXml); + } + catch + { + Logger.LogWarning($"Adding \"{sourceValue}\" ({sourceKey}) to nuget sources FAILED."); + } + } + + private static Stream GenerateStreamFromString(string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs new file mode 100644 index 0000000000..f6c89be039 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.Cli.Args; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class PackageSourceSwitcher : ITransientDependency + { + private readonly PackageSourceAdder _packageSourceAdder; + private readonly NpmPackagesUpdater _npmPackagesUpdater; + private readonly VoloNugetPackagesVersionUpdater _nugetPackagesVersionUpdater; + + public ILogger Logger { get; set; } + + public PackageSourceSwitcher(PackageSourceAdder packageSourceAdder, + NpmPackagesUpdater npmPackagesUpdater, + VoloNugetPackagesVersionUpdater nugetPackagesVersionUpdater) + { + _packageSourceAdder = packageSourceAdder; + _npmPackagesUpdater = npmPackagesUpdater; + _nugetPackagesVersionUpdater = nugetPackagesVersionUpdater; + Logger = NullLogger.Instance; + } + + public async Task SwitchToPreview(CommandLineArgs commandLineArgs) + { + _packageSourceAdder.Add("ABP Nightly", "https://www.myget.org/F/abp-nightly/api/v3/index.json"); + + var solutionPath = GetSolutionPath(commandLineArgs); + var solutionFolder = GetSolutionFolder(commandLineArgs); + + await _nugetPackagesVersionUpdater.UpdateSolutionAsync( + solutionPath, + true); + + await _npmPackagesUpdater.Update( + solutionFolder, + true); + } + + public async Task SwitchToStable(CommandLineArgs commandLineArgs) + { + var solutionPath = GetSolutionPath(commandLineArgs); + var solutionFolder = GetSolutionFolder(commandLineArgs); + + await _nugetPackagesVersionUpdater.UpdateSolutionAsync( + solutionPath, + false, + true); + + await _npmPackagesUpdater.Update( + solutionFolder, + false, + true); + } + + private string GetSolutionPath(CommandLineArgs commandLineArgs) + { + var directory = commandLineArgs.Options.GetOrNull(Options.SolutionDirectory.Short, Options.SolutionDirectory.Long) + ?? Directory.GetCurrentDirectory(); + + var solutionPath = Directory.GetFiles(directory, "*.sln").FirstOrDefault(); + + if (solutionPath == null) + { + var subDirectories = Directory.GetDirectories(directory); + + foreach (var subDirectory in subDirectories) + { + var slnInSubDirectory = Directory.GetFiles(subDirectory, "*.sln").FirstOrDefault(); + + if (slnInSubDirectory != null) + { + return Path.Combine(subDirectory, slnInSubDirectory); + } + } + + Logger.LogError("There is no solution or more that one solution in current directory."); + return null; + } + + return solutionPath; + } + + private string GetSolutionFolder(CommandLineArgs commandLineArgs) + { + return commandLineArgs.Options.GetOrNull(Options.SolutionDirectory.Short, Options.SolutionDirectory.Long) + ?? Directory.GetCurrentDirectory(); + } + + public static class Options + { + public static class SolutionDirectory + { + public const string Short = "sd"; + public const string Long = "solution-directory"; + } + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFileNameHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFileNameHelper.cs new file mode 100644 index 0000000000..2c86fe54f5 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFileNameHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Volo.Abp.Cli.ProjectModification +{ + public static class ProjectFileNameHelper + { + + public static string GetAssemblyNameFromProjectPath(string projectFile) + { + return projectFile + .Substring(projectFile.LastIndexOf(Path.DirectorySeparatorChar) + 1) + .RemovePostFix(StringComparison.OrdinalIgnoreCase, ".csproj"); + } + } +} 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 9d155ba079..b2f5421e6e 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 @@ -94,14 +94,7 @@ namespace Volo.Abp.Cli.ProjectModification public static string[] GetAssemblyNames(string[] projectFiles) { - return projectFiles.Select(GetAssemblyName).ToArray(); - } - - public static string GetAssemblyName(string projectFile) - { - return projectFile - .Substring(projectFile.LastIndexOf(Path.DirectorySeparatorChar) + 1) - .RemovePostFix(StringComparison.OrdinalIgnoreCase, ".csproj"); + return projectFiles.Select(ProjectFileNameHelper.GetAssemblyNameFromProjectPath).ToArray(); } private static string FindProjectEndsWith( 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 63e0eb0393..eeb19e8621 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 @@ -20,7 +20,7 @@ namespace Volo.Abp.Cli.ProjectModification public Task AddAsync(string directory, NpmPackageInfo npmPackage) { var packageJsonFilePath = Path.Combine(directory, "package.json"); - if (!File.Exists(packageJsonFilePath)) + if (!File.Exists(packageJsonFilePath) || File.ReadAllText(packageJsonFilePath).Contains($"\"{npmPackage.Name}\"")) { return Task.CompletedTask; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNugetPackageAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNugetPackageAdder.cs index 62dee30344..a69957a497 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNugetPackageAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNugetPackageAdder.cs @@ -50,6 +50,11 @@ namespace Volo.Abp.Cli.ProjectModification public Task AddAsync(string projectFile, NugetPackageInfo package) { + if (File.ReadAllText(projectFile).Contains($"\"{package.Name}\"")) + { + return Task.CompletedTask; + } + using (DirectoryHelper.ChangeCurrentDirectory(Path.GetDirectoryName(projectFile))) { Logger.LogInformation($"Installing '{package.Name}' package to the project '{Path.GetFileNameWithoutExtension(projectFile)}'..."); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionAbpVersionFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionAbpVersionFinder.cs new file mode 100644 index 0000000000..a86d17fee2 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionAbpVersionFinder.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.Xml; +using Volo.Abp.Cli.Utils; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class SolutionAbpVersionFinder : ITransientDependency + { + public string Find(string solutionFile) + { + var projectFilesUnderSrc = Directory.GetFiles(Path.GetDirectoryName(solutionFile), + "*.csproj", + SearchOption.AllDirectories); + + foreach (var projectFile in projectFilesUnderSrc) + { + var content = File.ReadAllText(projectFile); + var doc = new XmlDocument() { PreserveWhitespace = true }; + + doc.Load(StreamHelper.GenerateStreamFromString(content)); + + var nodes = doc.SelectNodes("/Project/ItemGroup/PackageReference[starts-with(@Include, 'Volo.Abp')]"); + + var value = nodes?[0]?.Attributes?["Version"]?.Value; + + if (value != null) + { + return value; + } + } + + return null; + } + } +} 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 new file mode 100644 index 0000000000..4eabeaa846 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.ProjectModification +{ + public class SolutionFileModifier : ITransientDependency + { + public async Task AddModuleToSolutionFileAsync(ModuleWithMastersInfo module, string solutionFile) + { + await AddModule(module, solutionFile); + } + + private async Task AddModule(ModuleWithMastersInfo module, string solutionFile) + { + 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(); + + + var projectsUnderModule = Directory.GetFiles( + Path.Combine(Path.GetDirectoryName(solutionFile), "modules", module.Name), + "*.csproj", + SearchOption.AllDirectories); + + var projectsUnderTest = Directory.GetFiles( + Path.Combine(Path.GetDirectoryName(solutionFile), "modules", module.Name, "test"), + "*.csproj", + SearchOption.AllDirectories); + + foreach (var projectPath in projectsUnderModule) + { + var parentFolderId = projectsUnderTest.Contains(projectPath) ? testModuleFolderId : srcModuleFolderId; + var projectId = Path.GetFileName(projectPath).Replace(".csproj",""); + var projectParentFolderInModule = projectsUnderTest.Contains(projectPath) ? "test" : "src"; + + if (lines.Any(l => l.Contains($"\"{projectId}\""))) + { + continue; + } + + var projectGuid = Guid.NewGuid().ToString(); + + var newProjectLine = "Project(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"" + projectId + "\"," + + " \"modules\\" + module.Name + "\\"+ projectParentFolderInModule + "\\" + projectId + "\\" + projectId + ".csproj\", \"{" + projectGuid + "}\"" + + 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 + + " {" + projectGuid + "}.Release|Any CPU.ActiveCfg = Release|Any CPU" + Environment.NewLine + + " {" + projectGuid + "}.Release|Any CPU.Build.0 = Release|Any CPU"; + + lines.InsertAfter(l=>l.Contains("GlobalSection") && l.Contains("ProjectConfigurationPlatforms"), newPostSolutionLine); + + var newPreSolutionLine = + " {"+ projectGuid + "} = {"+ parentFolderId + "}"; + + lines.InsertAfter(l=>l.Contains("GlobalSection") && l.Contains("NestedProjects"), newPreSolutionLine); + } + + File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines)); + + if (module.MasterModuleInfos != null) + { + foreach (var masterModule in module.MasterModuleInfos) + { + await AddModule(masterModule, solutionFile); + } + } + } + + private async Task AddNewFolderAndGetIdOrGetExistingId(string solutionFile, string folderName, string parentFolderId = null) + { + var file = File.ReadAllText(solutionFile); + var lines = file.Split(Environment.NewLine).ToList(); + string folderId; + + var folderLineIndex = lines.FindIndex(l => + l.Contains("2150E333-8FDC-42A3-9474-1A3956D46DE8") && l.Contains("\""+ folderName + "\"")); + + if (folderLineIndex < 0) + { + folderId = Guid.NewGuid().ToString(); + var newFolderLine = "Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \""+ folderName + "\", \""+ folderName + "\", \"{" + folderId + "}\"" + + Environment.NewLine + "EndProject"; + + lines.InsertAfter(l => l.Trim().Equals("EndProject"), newFolderLine); + + if (parentFolderId != null) + { + var newPreSolutionLine = + " {" + folderId + "} = {" + parentFolderId + "}"; + + lines.InsertAfter(l => l.Contains("GlobalSection") && l.Contains("NestedProjects"), newPreSolutionLine); + } + } + else + { + folderId = lines[folderLineIndex].Replace("\"", " ").Replace("{", " ").Replace("}", " ").TrimEnd() + .Split(" ").Last(); + } + + File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines)); + + return folderId; + } + } +} 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 d8a376eb02..51a8ee40df 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 @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; +using Volo.Abp.Cli.Commands.Services; using Volo.Abp.Cli.Http; using Volo.Abp.Cli.ProjectBuilding; using Volo.Abp.DependencyInjection; @@ -26,6 +27,10 @@ namespace Volo.Abp.Cli.ProjectModification protected ProjectNpmPackageAdder ProjectNpmPackageAdder { get; } protected NpmGlobalPackagesChecker NpmGlobalPackagesChecker { get; } protected IRemoteServiceExceptionHandler RemoteServiceExceptionHandler { get; } + public SourceCodeDownloadService SourceCodeDownloadService { get; } + public SolutionFileModifier SolutionFileModifier { get; } + public NugetPackageToLocalReferenceConverter NugetPackageToLocalReferenceConverter { get; } + public AngularModuleSourceCodeAdder AngularModuleSourceCodeAdder { get; } public SolutionModuleAdder( IJsonSerializer jsonSerializer, @@ -35,7 +40,11 @@ namespace Volo.Abp.Cli.ProjectModification DerivedClassFinder derivedClassFinder, ProjectNpmPackageAdder projectNpmPackageAdder, NpmGlobalPackagesChecker npmGlobalPackagesChecker, - IRemoteServiceExceptionHandler remoteServiceExceptionHandler) + IRemoteServiceExceptionHandler remoteServiceExceptionHandler, + SourceCodeDownloadService sourceCodeDownloadService, + SolutionFileModifier solutionFileModifier, + NugetPackageToLocalReferenceConverter nugetPackageToLocalReferenceConverter, + AngularModuleSourceCodeAdder angularModuleSourceCodeAdder) { JsonSerializer = jsonSerializer; ProjectNugetPackageAdder = projectNugetPackageAdder; @@ -45,6 +54,10 @@ namespace Volo.Abp.Cli.ProjectModification ProjectNpmPackageAdder = projectNpmPackageAdder; NpmGlobalPackagesChecker = npmGlobalPackagesChecker; RemoteServiceExceptionHandler = remoteServiceExceptionHandler; + SourceCodeDownloadService = sourceCodeDownloadService; + SolutionFileModifier = solutionFileModifier; + NugetPackageToLocalReferenceConverter = nugetPackageToLocalReferenceConverter; + AngularModuleSourceCodeAdder = angularModuleSourceCodeAdder; Logger = NullLogger.Instance; } @@ -52,7 +65,9 @@ namespace Volo.Abp.Cli.ProjectModification [NotNull] string solutionFile, [NotNull] string moduleName, string startupProject, - bool skipDbMigrations = false) + string version, + bool skipDbMigrations = false, + bool withSourceCode = false) { Check.NotNull(solutionFile, nameof(solutionFile)); Check.NotNull(moduleName, nameof(moduleName)); @@ -63,9 +78,98 @@ namespace Volo.Abp.Cli.ProjectModification var projectFiles = ProjectFinder.GetProjectFiles(solutionFile); + await AddNugetAndNpmReferences(module, projectFiles); + + if (withSourceCode) + { + var modulesFolderInSolution = Path.Combine(Path.GetDirectoryName(solutionFile), "modules"); + await DownloadSourceCodesToSolutionFolder(module, modulesFolderInSolution, version); + await SolutionFileModifier.AddModuleToSolutionFileAsync(module, solutionFile); + await NugetPackageToLocalReferenceConverter.Convert(module, solutionFile); + + await HandleAngularProject(modulesFolderInSolution, solutionFile); + } + + ModifyDbContext(projectFiles, module, startupProject, skipDbMigrations); + } + + private async Task HandleAngularProject(string modulesFolderInSolution, string solutionFilePath) + { + var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular"); + + if (!Directory.Exists(angularPath)) + { + DeleteAngularDirectoriesInModulesFolder(modulesFolderInSolution); + return; + } + + await AngularModuleSourceCodeAdder.AddAsync(solutionFilePath, angularPath); + } + + private static void DeleteAngularDirectoriesInModulesFolder(string modulesFolderInSolution) + { + var moduleFolders = Directory.GetDirectories(modulesFolderInSolution); + + foreach (var moduleFolder in moduleFolders) + { + var angDir = Path.Combine(moduleFolder, "angular"); + if (Directory.Exists(angDir)) + { + Directory.Delete(angDir, true); + } + } + } + + private async Task DownloadSourceCodesToSolutionFolder(ModuleWithMastersInfo module, string modulesFolderInSolution, string version = null) + { + var targetModuleFolder = Path.Combine(modulesFolderInSolution, module.Name); + + await SourceCodeDownloadService.DownloadAsync( + module.Name, + targetModuleFolder, + version, + null, + null + ); + + await DeleteAppAndDemoFolderAsync(targetModuleFolder); + + if (module.MasterModuleInfos == null) + { + return; + } + + foreach (var masterModule in module.MasterModuleInfos) + { + await DownloadSourceCodesToSolutionFolder(masterModule, modulesFolderInSolution, version); + } + } + + private async Task DeleteAppAndDemoFolderAsync(string targetModuleFolder) + { + var appFolder = Path.Combine(targetModuleFolder, "app"); + if (Directory.Exists(appFolder)) + { + Directory.Delete(appFolder, true); + } + + var demoFolder = Path.Combine(targetModuleFolder, "demo"); + if (Directory.Exists(demoFolder)) + { + Directory.Delete(demoFolder, true); + } + } + + private async Task AddNugetAndNpmReferences(ModuleWithMastersInfo module, string[] projectFiles) + { foreach (var nugetPackage in module.NugetPackages) { - var targetProjectFile = ProjectFinder.FindNuGetTargetProjectFile(projectFiles, nugetPackage.Target); + var nugetTarget = + await IsProjectTiered(projectFiles) && nugetPackage.TieredTarget != NuGetPackageTarget.Undefined + ? nugetPackage.TieredTarget + : nugetPackage.Target; + + var targetProjectFile = ProjectFinder.FindNuGetTargetProjectFile(projectFiles, nugetTarget); if (targetProjectFile == null) { Logger.LogDebug($"Target project is not available for this NuGet package '{nugetPackage.Name}'"); @@ -84,7 +188,8 @@ namespace Volo.Abp.Cli.ProjectModification foreach (var targetProject in targetProjects) { - foreach (var npmPackage in module.NpmPackages.Where(p => p.ApplicationType.HasFlag(NpmApplicationType.Mvc))) + foreach (var npmPackage in module.NpmPackages.Where(p => + p.ApplicationType.HasFlag(NpmApplicationType.Mvc))) { await ProjectNpmPackageAdder.AddAsync(Path.GetDirectoryName(targetProject), npmPackage); } @@ -95,8 +200,6 @@ namespace Volo.Abp.Cli.ProjectModification Logger.LogDebug("Target project is not available for NPM packages."); } } - - ModifyDbContext(projectFiles, module, startupProject, skipDbMigrations); } protected void ModifyDbContext(string[] projectFiles, ModuleInfo module, string startupProject, bool skipDbMigrations = false) @@ -127,20 +230,19 @@ namespace Volo.Abp.Cli.ProjectModification return; } - DbContextFileBuilderConfigureAdder.Add(dbContextFile, module.EfCoreConfigureMethodName); - + var addedNewBuilder = DbContextFileBuilderConfigureAdder.Add(dbContextFile, module.EfCoreConfigureMethodName); - if (!skipDbMigrations) + if (addedNewBuilder && !skipDbMigrations) { EfCoreMigrationAdder.AddMigration(dbMigrationsProject, module.Name, startupProject); } } - protected virtual async Task FindModuleInfoAsync(string moduleName) + protected virtual async Task FindModuleInfoAsync(string moduleName) { using (var client = new CliHttpClient()) { - var url = $"{CliUrls.WwwAbpIo}api/app/module/byName/?name=" + moduleName; + var url = $"{CliUrls.WwwAbpIo}api/app/module/byNameWithDetails/?name=" + moduleName; var response = await client.GetAsync(url); @@ -155,8 +257,14 @@ namespace Volo.Abp.Cli.ProjectModification } var responseContent = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(responseContent); + return JsonSerializer.Deserialize(responseContent); } } + + protected virtual async Task IsProjectTiered(string[] projectFiles) + { + return projectFiles.Select(ProjectFileNameHelper.GetAssemblyNameFromProjectPath) + .Any(p => p.EndsWith(".IdentityServer") || p.EndsWith(".HttpApi.Host")); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs index d5756a4566..587492804b 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs @@ -1,6 +1,7 @@ using System; using NuGet.Versioning; using System.IO; +using System.Linq; using System.Threading.Tasks; using System.Xml; using Volo.Abp.Cli.NuGet; @@ -13,37 +14,39 @@ namespace Volo.Abp.Cli.ProjectModification public class VoloNugetPackagesVersionUpdater : ITransientDependency { private readonly NuGetService _nuGetService; + private readonly MyGetPackageListFinder _myGetPackageListFinder; public ILogger Logger { get; set; } - public VoloNugetPackagesVersionUpdater(NuGetService nuGetService) + public VoloNugetPackagesVersionUpdater(NuGetService nuGetService, MyGetPackageListFinder myGetPackageListFinder) { _nuGetService = nuGetService; + _myGetPackageListFinder = myGetPackageListFinder; Logger = NullLogger.Instance; } - public async Task UpdateSolutionAsync(string solutionPath, bool includePreviews) + public async Task UpdateSolutionAsync(string solutionPath, bool includePreviews = false, bool switchToStable = false) { var projectPaths = ProjectFinder.GetProjectFiles(solutionPath); foreach (var filePath in projectPaths) { - await UpdateInternalAsync(filePath, includePreviews); + await UpdateInternalAsync(filePath, includePreviews, switchToStable); } } - public async Task UpdateProjectAsync(string projectPath, bool includePreviews) + public async Task UpdateProjectAsync(string projectPath, bool includePreviews = false, bool switchToStable = false) { - await UpdateInternalAsync(projectPath, includePreviews); + await UpdateInternalAsync(projectPath, includePreviews, switchToStable); } - protected virtual async Task UpdateInternalAsync(string projectPath, bool includePreviews) + protected virtual async Task UpdateInternalAsync(string projectPath, bool includePreviews = false, bool switchToStable = false) { var fileContent = File.ReadAllText(projectPath); - File.WriteAllText(projectPath, await UpdateVoloPackagesAsync(fileContent, includePreviews)); + File.WriteAllText(projectPath, await UpdateVoloPackagesAsync(fileContent, includePreviews, switchToStable)); } - private async Task UpdateVoloPackagesAsync(string content, bool includePreviews) + private async Task UpdateVoloPackagesAsync(string content, bool includePreviews = false, bool switchToStable = false) { string packageId = null; @@ -67,23 +70,44 @@ namespace Volo.Abp.Cli.ProjectModification continue; } - var versionAttribute = package.Attributes["Version"]; - packageId = package.Attributes["Include"].Value; - var packageVersion = SemanticVersion.Parse(versionAttribute.Value); + + var versionAttribute = package.Attributes["Version"]; + var currentVersion = versionAttribute.Value; + var packageVersion = SemanticVersion.Parse(currentVersion); Logger.LogDebug("Checking package: \"{0}\" - Current version: {1}", packageId, packageVersion); - var latestVersion = await _nuGetService.GetLatestVersionOrNullAsync(packageId, includePreviews); - if (latestVersion != null && packageVersion < latestVersion) + if (includePreviews || (currentVersion.Contains("-preview") && !switchToStable)) { - Logger.LogInformation("Updating package \"{0}\" from v{1} to v{2}.", packageId, packageVersion.ToString(), latestVersion.ToString()); - versionAttribute.Value = latestVersion.ToString(); + var latestVersion = (await _myGetPackageListFinder.GetPackages()).Packages + .FirstOrDefault(p => p.Id == packageId) + ?.Versions.LastOrDefault(); + + if (currentVersion != latestVersion) + { + Logger.LogInformation("Updating package \"{0}\" from v{1} to v{2}.", packageId, currentVersion, latestVersion); + versionAttribute.Value = latestVersion; + } + else + { + Logger.LogDebug("Package: \"{0}-v{1}\" is up to date.", packageId, currentVersion); + } } else { - Logger.LogDebug("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion); + var latestVersion = await _nuGetService.GetLatestVersionOrNullAsync(packageId); + + if (latestVersion != null && (currentVersion.Contains("-preview") || packageVersion < latestVersion)) + { + Logger.LogInformation("Updating package \"{0}\" from v{1} to v{2}.", packageId, packageVersion.ToString(), latestVersion.ToString()); + versionAttribute.Value = latestVersion.ToString(); + } + else + { + Logger.LogInformation("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion); + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs index b871c295f6..07052a8f48 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs @@ -5,23 +5,43 @@ namespace Volo.Abp.Cli.Utils { public static class CmdHelper { + public static int SuccessfulExitCode = 0; + public static void Run(string file, string arguments) { var procStartInfo = new ProcessStartInfo(file, arguments); - Process.Start(procStartInfo).WaitForExit(); + Process.Start(procStartInfo)?.WaitForExit(); } public static int RunCmd(string command) { - var procStartInfo = new ProcessStartInfo(GetFileName(), GetArguments(command)); - var process = Process.Start(procStartInfo); - process?.WaitForExit(); - return process?.ExitCode ?? 0; + var procStartInfo = new ProcessStartInfo( + GetFileName(), + GetArguments(command) + ); + + using (var process = Process.Start(procStartInfo)) + { + process?.WaitForExit(); + return process?.ExitCode ?? 0; + } } public static string RunCmdAndGetOutput(string command) { - var output = ""; + return RunCmdAndGetOutput(command, out int _); + } + + public static string RunCmdAndGetOutput(string command, out bool isExitCodeSuccessful) + { + var output = RunCmdAndGetOutput(command, out int exitCode); + isExitCodeSuccessful = exitCode == SuccessfulExitCode; + return output; + } + + public static string RunCmdAndGetOutput(string command, out int exitCode) + { + string output; using (var process = new Process()) { @@ -33,16 +53,21 @@ namespace Volo.Abp.Cli.Utils RedirectStandardOutput = true, RedirectStandardError = true }; + process.Start(); - using (var stdOut = process.StandardOutput) + using (var standardOutput = process.StandardOutput) { - using (var stdErr = process.StandardError) + using (var standardError = process.StandardError) { - output = stdOut.ReadToEnd(); - output += stdErr.ReadToEnd(); + output = standardOutput.ReadToEnd(); + output += standardError.ReadToEnd(); } } + + process.WaitForExit(); + + exitCode = process.ExitCode; } return output.Trim(); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/StreamHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/StreamHelper.cs new file mode 100644 index 0000000000..1f8aefe070 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/StreamHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Volo.Abp.Cli.Utils +{ + public static class StreamHelper + { + public static Stream GenerateStreamFromString(string s) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(s); + writer.Flush(); + stream.Position = 0; + return stream; + } + } +} diff --git a/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj b/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj index f6bcbd5def..2dd1772165 100644 --- a/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj +++ b/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj @@ -12,6 +12,7 @@ + diff --git a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs index 8eefc78f9b..2ccf9bb9a1 100644 --- a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs +++ b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs @@ -14,6 +14,7 @@ namespace Volo.Abp.Cli .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) + .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) #if DEBUG .MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Debug) #else diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs index 44b75d1adb..82d393eef3 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs @@ -6,7 +6,8 @@ namespace Microsoft.Extensions.Configuration public static class ConfigurationHelper { public static IConfigurationRoot BuildConfiguration( - AbpConfigurationBuilderOptions options = null) + AbpConfigurationBuilderOptions options = null, + Action builderAction = null) { options = options ?? new AbpConfigurationBuilderOptions(); @@ -43,6 +44,8 @@ namespace Microsoft.Extensions.Configuration builder = builder.AddCommandLine(options.CommandLineArgs); } + builderAction?.Invoke(builder); + return builder.Build(); } } diff --git a/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs b/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs index a930096404..e7c600eff6 100644 --- a/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/AbpTypeExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using JetBrains.Annotations; +using Volo.Abp; namespace System { @@ -17,8 +18,10 @@ namespace System /// Internally uses . /// /// Target type (as reverse). - public static bool IsAssignableTo(this Type type) + public static bool IsAssignableTo([NotNull] this Type type) { + Check.NotNull(type, nameof(type)); + return type.IsAssignableTo(typeof(TTarget)); } @@ -30,8 +33,11 @@ namespace System /// /// this type /// Target type - public static bool IsAssignableTo(this Type type, Type targetType) + public static bool IsAssignableTo([NotNull] this Type type, [NotNull] Type targetType) { + Check.NotNull(type, nameof(type)); + Check.NotNull(targetType, nameof(targetType)); + return targetType.IsAssignableFrom(type); } @@ -40,8 +46,10 @@ namespace System /// /// The type to get its base classes. /// True, to include the standard type in the returned array. - public static Type[] GetBaseClasses(this Type type, bool includeObject = true) + public static Type[] GetBaseClasses([NotNull] this Type type, bool includeObject = true) { + Check.NotNull(type, nameof(type)); + var types = new List(); AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject); return types.ToArray(); @@ -52,6 +60,8 @@ namespace System [CanBeNull] Type type, bool includeObject) { + Check.NotNull(types, nameof(types)); + if (type == null) { return; diff --git a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj index 5ffc15d19f..50fb328387 100644 --- a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj +++ b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj @@ -1,6 +1,8 @@ + + netstandard2.0 Volo.Abp.Core @@ -14,15 +16,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs index f942f44b0f..7001925745 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs @@ -121,6 +121,21 @@ namespace Volo.Abp return value; } + [ContractAnnotation("type:null => halt")] + public static Type AssignableTo( + Type type, + [InvokerParameterName] [NotNull] string parameterName) + { + NotNull(type, parameterName); + + if (!type.IsAssignableTo()) + { + throw new ArgumentException($"{parameterName} (type of {type.AssemblyQualifiedName}) should be assignable to the {typeof(TBaseType).GetFullNameWithAssemblyName()}!"); + } + + return type; + } + public static string Length( [CanBeNull] string value, [InvokerParameterName] [NotNull] string parameterName, diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs index cb0d727795..cf4befae5b 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Volo.Abp.DependencyInjection { @@ -17,10 +16,11 @@ namespace Volo.Abp.DependencyInjection public static List GetExposedServices(Type type) { return type - .GetCustomAttributes() + .GetCustomAttributes(true) .OfType() .DefaultIfEmpty(DefaultExposeServicesAttribute) .SelectMany(p => p.GetExposedServiceTypes(type)) + .Distinct() .ToList(); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs new file mode 100644 index 0000000000..6f7fbda0af --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotificationContext.cs @@ -0,0 +1,32 @@ +using System; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.ExceptionHandling +{ + public class ExceptionNotificationContext + { + /// + /// The exception object. + /// + [NotNull] + public Exception Exception { get; } + + public LogLevel LogLevel { get; } + + /// + /// True, if it is handled. + /// + public bool Handled { get; } + + public ExceptionNotificationContext( + [NotNull] Exception exception, + LogLevel? logLevel = null, + bool handled = true) + { + Exception = Check.NotNull(exception, nameof(exception)); + LogLevel = logLevel ?? exception.GetLogLevel(); + Handled = handled; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs new file mode 100644 index 0000000000..232b9a2ecd --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifier.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ExceptionHandling +{ + public class ExceptionNotifier : IExceptionNotifier, ITransientDependency + { + public ILogger Logger { get; set; } + + protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + + public ExceptionNotifier(IHybridServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + Logger = NullLogger.Instance; + } + + public virtual async Task NotifyAsync([NotNull] ExceptionNotificationContext context) + { + Check.NotNull(context, nameof(context)); + + using (var scope = ServiceScopeFactory.CreateScope()) + { + var exceptionSubscribers = scope.ServiceProvider + .GetServices(); + + foreach (var exceptionSubscriber in exceptionSubscribers) + { + try + { + await exceptionSubscriber.HandleAsync(context); + } + catch (Exception e) + { + Logger.LogWarning($"Exception subscriber of type {exceptionSubscriber.GetType().AssemblyQualifiedName} has thrown an exception!"); + Logger.LogException(e, LogLevel.Warning); + } + } + } + } + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs new file mode 100644 index 0000000000..493c0cf1f3 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionNotifierExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.ExceptionHandling +{ + public static class ExceptionNotifierExtensions + { + public static Task NotifyAsync( + [NotNull] this IExceptionNotifier exceptionNotifier, + [NotNull] Exception exception, + LogLevel? logLevel = null, + bool handled = true) + { + Check.NotNull(exceptionNotifier, nameof(exceptionNotifier)); + + return exceptionNotifier.NotifyAsync( + new ExceptionNotificationContext( + exception, + logLevel, + handled + ) + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs new file mode 100644 index 0000000000..5f2971c8c4 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/ExceptionSubscriber.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ExceptionHandling +{ + [ExposeServices(typeof(IExceptionSubscriber))] + public abstract class ExceptionSubscriber : IExceptionSubscriber, ITransientDependency + { + public abstract Task HandleAsync(ExceptionNotificationContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs new file mode 100644 index 0000000000..5646c28e5e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionNotifier.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.ExceptionHandling +{ + public interface IExceptionNotifier + { + Task NotifyAsync([NotNull] ExceptionNotificationContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs new file mode 100644 index 0000000000..dc7f336970 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/IExceptionSubscriber.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.ExceptionHandling +{ + public interface IExceptionSubscriber + { + Task HandleAsync([NotNull] ExceptionNotificationContext context); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/NullExceptionNotifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/NullExceptionNotifier.cs new file mode 100644 index 0000000000..77043cba48 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/ExceptionHandling/NullExceptionNotifier.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.ExceptionHandling +{ + public class NullExceptionNotifier : IExceptionNotifier + { + public static NullExceptionNotifier Instance { get; } = new NullExceptionNotifier(); + + private NullExceptionNotifier() + { + + } + + public Task NotifyAsync(ExceptionNotificationContext context) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs index 21ca7c2086..a945784352 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/IO/FileHelper.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; +using Volo.Abp.Text; namespace Volo.Abp.IO { @@ -73,6 +75,63 @@ namespace Volo.Abp.IO } } - //TODO: ReadAllLinesAsync + /// + /// Opens a text file, reads all lines of the file, and then closes the file. + /// + /// The file to open for reading. + /// Encoding of the file. Default is UTF8 + /// Specifies how the operating system should open a file. Default is Open + /// Defines constants for read, write, or read/write access to a file. Default is Read + /// Contains constants for controlling the kind of access other FileStream objects can have to the same file. Default is Read + /// Length of StreamReader buffer. Default is 4096. + /// Indicates FileStream options. Default is Asynchronous (The file is to be used for asynchronous reading.) and SequentialScan (The file is to be accessed sequentially from beginning to end.) + /// A string containing all lines of the file. + public static async Task ReadAllLinesAsync(string path, + Encoding encoding = null, + FileMode fileMode = FileMode.Open, + FileAccess fileAccess = FileAccess.Read, + FileShare fileShare = FileShare.Read, + int bufferSize = 4096, + FileOptions fileOptions = FileOptions.Asynchronous | FileOptions.SequentialScan) + { + if (encoding == null) + { + encoding = Encoding.UTF8; + } + + var lines = new List(); + + using (var stream = new FileStream( + path, + fileMode, + fileAccess, + fileShare, + bufferSize, + fileOptions)) + { + using (var reader = new StreamReader(stream, encoding)) + { + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + lines.Add(line); + } + } + } + + return lines.ToArray(); + } + + /// + /// Opens a text file, reads content without BOM + /// + /// The file to open for reading. + /// A string containing all lines of the file. + public static async Task ReadFileWithoutBomAsync(string path) + { + var content = await ReadAllBytesAsync(path); + + return StringHelper.ConvertFromBytesWithoutBom(content); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/HasLogLevelExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/HasLogLevelExtensions.cs new file mode 100644 index 0000000000..e22f2002b0 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/HasLogLevelExtensions.cs @@ -0,0 +1,18 @@ +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.Logging +{ + public static class HasLogLevelExtensions + { + public static TException WithLogLevel([NotNull] this TException exception, LogLevel logLevel) + where TException : IHasLogLevel + { + Check.NotNull(exception, nameof(exception)); + + exception.LogLevel = logLevel; + + return exception; + } + } +} 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 b314ca6b84..8d6e6a42cf 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs @@ -53,14 +53,14 @@ namespace Volo.Abp.Reflection if (givenTypeInfo.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) { - result.Add(givenType); + result.AddIfNotContains(givenType); } foreach (var interfaceType in givenTypeInfo.GetInterfaces()) { if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == genericType) { - result.Add(interfaceType); + result.AddIfNotContains(interfaceType); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index a708ecc10d..5f66f4788e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -12,7 +14,7 @@ namespace Volo.Abp.Reflection { return false; } - + var type = obj.GetType(); if (!type.GetTypeInfo().IsGenericType) { @@ -34,9 +36,7 @@ namespace Volo.Abp.Reflection return true; } - if (includeNullables && - type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Nullable<>)) + if (includeNullables && IsNullable(type)) { return IsPrimitiveExtendedInternal(type.GenericTypeArguments[0], includeEnums); } @@ -44,6 +44,11 @@ namespace Volo.Abp.Reflection return false; } + public static bool IsNullable(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + public static Type GetFirstGenericArgumentIfNullable(this Type t) { if (t.GetGenericArguments().Length > 0 && t.GetGenericTypeDefinition() == typeof(Nullable<>)) @@ -54,6 +59,59 @@ namespace Volo.Abp.Reflection return t; } + public static bool IsEnumerable(Type type, out Type itemType, bool includePrimitives = true) + { + if (!includePrimitives && IsPrimitiveExtended(type)) + { + itemType = null; + return false; + } + + var enumerableTypes = ReflectionHelper.GetImplementedGenericTypes(type, typeof(IEnumerable<>)); + if (enumerableTypes.Count == 1) + { + itemType = enumerableTypes[0].GenericTypeArguments[0]; + return true; + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + itemType = typeof(object); + return true; + } + + itemType = null; + return false; + } + + public static bool IsDictionary(Type type, out Type keyType, out Type valueType) + { + var dictionaryTypes = ReflectionHelper + .GetImplementedGenericTypes( + type, + typeof(IDictionary<,>) + ); + + if (dictionaryTypes.Count == 1) + { + keyType = dictionaryTypes[0].GenericTypeArguments[0]; + valueType = dictionaryTypes[0].GenericTypeArguments[1]; + return true; + } + + if (typeof(IDictionary).IsAssignableFrom(type)) + { + keyType = typeof(object); + valueType = typeof(object); + return true; + } + + keyType = null; + valueType = null; + + return false; + } + private static bool IsPrimitiveExtendedInternal(Type type, bool includeEnums) { if (type.IsPrimitive) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/RemoteServiceAttribute.cs b/framework/src/Volo.Abp.Core/Volo/Abp/RemoteServiceAttribute.cs index e866022437..84e46e2709 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/RemoteServiceAttribute.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/RemoteServiceAttribute.cs @@ -17,6 +17,13 @@ namespace Volo.Abp /// public bool IsMetadataEnabled { get; set; } + /// + /// Group name of the remote service. + /// Group names of all services of a module expected to be the same. + /// This name is also used to distinguish the service endpoint of this group. + /// + public string Name { get; set; } + public RemoteServiceAttribute(bool isEnabled = true) { IsEnabled = isEnabled; diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Text/StringHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Text/StringHelper.cs new file mode 100644 index 0000000000..c6be5db02b --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Text/StringHelper.cs @@ -0,0 +1,37 @@ +using System.Text; + +namespace Volo.Abp.Text +{ + public class StringHelper + { + /// + /// Converts a byte[] to string without BOM (byte order mark). + /// + /// The byte[] to be converted to string + /// The encoding to get string. Default is UTF8 + /// + public static string ConvertFromBytesWithoutBom(byte[] bytes, Encoding encoding = null) + { + if (bytes == null) + { + return null; + } + + if (encoding == null) + { + encoding = Encoding.UTF8; + } + + var hasBom = bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF; + + if (hasBom) + { + return encoding.GetString(bytes, 3, bytes.Length - 3); + } + else + { + return encoding.GetString(bytes); + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/AsyncHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/AsyncHelper.cs index dc678b8fd4..65e411d038 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/AsyncHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/AsyncHelper.cs @@ -27,6 +27,11 @@ namespace Volo.Abp.Threading return type == typeof(Task) || (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)); } + public static bool IsTaskOfT([NotNull] this Type type) + { + return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>); + } + /// /// Returns void if given type is Task. /// Return T, if given type is Task{T}. @@ -41,7 +46,7 @@ namespace Volo.Abp.Threading return typeof(void); } - if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)) + if (type.IsTaskOfT()) { return type.GenericTypeArguments[0]; } diff --git a/framework/src/Volo.Abp.Data/Volo.Abp.Data.csproj b/framework/src/Volo.Abp.Data/Volo.Abp.Data.csproj index 7a53782ae1..cde2718a56 100644 --- a/framework/src/Volo.Abp.Data/Volo.Abp.Data.csproj +++ b/framework/src/Volo.Abp.Data/Volo.Abp.Data.csproj @@ -16,6 +16,7 @@ + diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpDataModule.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpDataModule.cs index 2414803970..8c9643012e 100644 --- a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpDataModule.cs +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpDataModule.cs @@ -2,13 +2,15 @@ using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; using Volo.Abp.Uow; namespace Volo.Abp.Data { [DependsOn( + typeof(AbpObjectExtendingModule), typeof(AbpUnitOfWorkModule) - )] + )] public class AbpDataModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/AuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/AuditedEntityWithUserDto.cs index c5dd43de14..8a8e99d869 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/AuditedEntityWithUserDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/AuditedEntityWithUserDto.cs @@ -5,6 +5,7 @@ namespace Volo.Abp.Application.Dtos { /// /// This class can be inherited by DTO classes to implement interface. + /// It has the and objects as a DTOs represent the related user. /// /// Type of the User DTO [Serializable] @@ -19,6 +20,7 @@ namespace Volo.Abp.Application.Dtos /// /// This class can be inherited by DTO classes to implement interface. + /// It has the and objects as a DTOs represent the related user. /// /// Type of primary key /// Type of the User DTO diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/CreationAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/CreationAuditedEntityWithUserDto.cs index 51e3c9018a..5d6260ebcd 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/CreationAuditedEntityWithUserDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/CreationAuditedEntityWithUserDto.cs @@ -5,6 +5,7 @@ namespace Volo.Abp.Application.Dtos { /// /// This class can be inherited by DTO classes to implement interface. + /// It also has the object as a DTO represents the user. /// /// Type of the User DTO [Serializable] @@ -14,7 +15,8 @@ namespace Volo.Abp.Application.Dtos } /// - /// This class can be inherited by DTO classes to implement interface. + /// This class can be inherited by DTO classes to implement interface. + /// It also has the object as a DTO represents the user. /// /// Type of primary key /// Type of the User DTO diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityDto.cs new file mode 100644 index 0000000000..6be7d38070 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityDto.cs @@ -0,0 +1,35 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Data; + +namespace Volo.Abp.Application.Dtos +{ + /// + /// This class can be inherited by DTO classes to implement interface. + /// It also implements the interface. + /// + /// Type of primary key + [Serializable] + public abstract class ExtensibleAuditedEntityDto : ExtensibleCreationAuditedEntityDto, IAuditedObject + { + /// + public DateTime? LastModificationTime { get; set; } + + /// + public Guid? LastModifierId { get; set; } + } + + /// + /// This class can be inherited by DTO classes to implement interface. + /// It also implements the interface. + /// + [Serializable] + public abstract class ExtensibleAuditedEntityDto : ExtensibleCreationAuditedEntityDto, IAuditedObject + { + /// + public DateTime? LastModificationTime { get; set; } + + /// + public Guid? LastModifierId { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs new file mode 100644 index 0000000000..94086c119b --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs @@ -0,0 +1,40 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Data; + +namespace Volo.Abp.Application.Dtos +{ + /// + /// This class can be inherited by DTO classes to implement interface. + /// It has the and objects as a DTOs represent the related user. + /// It also implements the interface. + /// + /// Type of primary key + /// Type of the User DTO + [Serializable] + public abstract class ExtensibleAuditedEntityWithUserDto : ExtensibleAuditedEntityDto, IAuditedObject + { + /// + public TUserDto Creator { get; set; } + + /// + public TUserDto LastModifier { get; set; } + } + + /// + /// This class can be inherited by DTO classes to implement interface. + /// It has the and objects as a DTOs represent the related user. + /// It also implements the interface. + /// + /// Type of the User DTO + [Serializable] + public abstract class ExtensibleAuditedEntityWithUserDto : ExtensibleAuditedEntityDto, + IAuditedObject + { + /// + public TUserDto Creator { get; set; } + + /// + public TUserDto LastModifier { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs new file mode 100644 index 0000000000..1de40ff4d5 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs @@ -0,0 +1,35 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Data; + +namespace Volo.Abp.Application.Dtos +{ + /// + /// This class can be inherited by DTO classes to implement interface. + /// It also implements the interface. + /// + /// Type of primary key + [Serializable] + public abstract class ExtensibleCreationAuditedEntityDto : ExtensibleEntityDto, ICreationAuditedObject + { + /// + public DateTime CreationTime { get; set; } + + /// + public Guid? CreatorId { get; set; } + } + + /// + /// This class can be inherited by DTO classes to implement interface. + /// It also implements the interface. + /// + [Serializable] + public abstract class ExtensibleCreationAuditedEntityDto : ExtensibleEntityDto, ICreationAuditedObject + { + /// + public DateTime CreationTime { get; set; } + + /// + public Guid? CreatorId { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs new file mode 100644 index 0000000000..d5352d5b03 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs @@ -0,0 +1,32 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Data; + +namespace Volo.Abp.Application.Dtos +{ + /// + /// This class can be inherited by DTO classes to implement interface. + /// It has the object as a DTO represents the user. + /// It also implements the interface. + /// + /// Type of primary key + /// Type of the User DTO + [Serializable] + public abstract class ExtensibleCreationAuditedEntityWithUserDto : ExtensibleCreationAuditedEntityDto, ICreationAuditedObject + { + public TUserDto Creator { get; set; } + } + + /// + /// This class can be inherited by DTO classes to implement interface. + /// It has the object as a DTO represents the user. + /// It also implements the interface. + /// + /// Type of the User DTO + [Serializable] + public abstract class ExtensibleCreationAuditedEntityWithUserDto : ExtensibleCreationAuditedEntityDto, + ICreationAuditedObject + { + public TUserDto Creator { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs new file mode 100644 index 0000000000..63d07bcdcf --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs @@ -0,0 +1,28 @@ +using System; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Application.Dtos +{ + [Serializable] + public abstract class ExtensibleEntityDto : ExtensibleObject, IEntityDto + { + /// + /// Id of the entity. + /// + public TKey Id { get; set; } + + public override string ToString() + { + return $"[DTO: {GetType().Name}] Id = {Id}"; + } + } + + [Serializable] + public abstract class ExtensibleEntityDto : ExtensibleObject, IEntityDto + { + public override string ToString() + { + return $"[DTO: {GetType().Name}]"; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityDto.cs new file mode 100644 index 0000000000..d72bf8135d --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityDto.cs @@ -0,0 +1,41 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Data; + +namespace Volo.Abp.Application.Dtos +{ + /// + /// This class can be inherited by DTO classes to implement interface. + /// It also implements the interface. + /// + /// Type of primary key + [Serializable] + public abstract class ExtensibleFullAuditedEntityDto : ExtensibleAuditedEntityDto, IFullAuditedObject + { + /// + public bool IsDeleted { get; set; } + + /// + public Guid? DeleterId { get; set; } + + /// + public DateTime? DeletionTime { get; set; } + } + + /// + /// This class can be inherited by DTO classes to implement interface. + /// It also implements the interface. + /// + [Serializable] + public abstract class ExtensibleFullAuditedEntityDto : ExtensibleAuditedEntityDto, IFullAuditedObject + { + /// + public bool IsDeleted { get; set; } + + /// + public Guid? DeleterId { get; set; } + + /// + public DateTime? DeletionTime { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs new file mode 100644 index 0000000000..b5e68e7cb7 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs @@ -0,0 +1,46 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Data; + +namespace Volo.Abp.Application.Dtos +{ + /// + /// This class can be inherited by DTO classes to implement interface. + /// It has the , and objects as a DTOs represent the related user. + /// It also implements the interface. + /// + /// Type of primary key + /// Type of the User + [Serializable] + public abstract class ExtensibleFullAuditedEntityWithUserDto : ExtensibleFullAuditedEntityDto, IFullAuditedObject + { + /// + public TUserDto Creator { get; set; } + + /// + public TUserDto LastModifier { get; set; } + + /// + public TUserDto Deleter { get; set; } + } + + /// + /// This class can be inherited by DTO classes to implement interface. + /// It has the , and objects as a DTOs represent the related user. + /// It also implements the interface. + /// + /// Type of the User + [Serializable] + public abstract class ExtensibleFullAuditedEntityWithUserDto : ExtensibleFullAuditedEntityDto, + IFullAuditedObject + { + /// + public TUserDto Creator { get; set; } + + /// + public TUserDto LastModifier { get; set; } + + /// + public TUserDto Deleter { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/FullAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/FullAuditedEntityWithUserDto.cs index 91e31250fb..30b1f0b2ba 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/FullAuditedEntityWithUserDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/FullAuditedEntityWithUserDto.cs @@ -4,7 +4,8 @@ using Volo.Abp.Auditing; namespace Volo.Abp.Application.Dtos { /// - /// This class can be inherited by DTO classes to implement interface. + /// This class can be inherited by DTO classes to implement interface. + /// It has the , and objects as a DTOs represent the related user. /// /// Type of the User [Serializable] @@ -21,7 +22,8 @@ namespace Volo.Abp.Application.Dtos } /// - /// This class can be inherited by DTO classes to implement interface. + /// This class can be inherited by DTO classes to implement interface. + /// It has the , and objects as a DTOs represent the related user. /// /// Type of primary key /// Type of the User diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs index 7816bd20eb..12f3b51d11 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs @@ -5,36 +5,30 @@ namespace Volo.Abp.Application.Services { public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : ICrudAppService - where TEntityDto : IEntityDto { } public interface ICrudAppService : IApplicationService - where TGetOutputDto : IEntityDto - where TGetListOutputDto : IEntityDto { Task GetAsync(TKey id); diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs new file mode 100644 index 0000000000..45915826e4 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs @@ -0,0 +1,341 @@ +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Linq; +using Volo.Abp.MultiTenancy; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services +{ + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + } + + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + } + + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + } + + public abstract class AbstractKeyCrudAppService + : AbstractKeyCrudAppService + where TEntity : class, IEntity + { + protected AbstractKeyCrudAppService(IRepository repository) + : base(repository) + { + + } + + protected override TEntityDto MapToGetListOutputDto(TEntity entity) + { + return MapToGetOutputDto(entity); + } + } + + public abstract class AbstractKeyCrudAppService + : ApplicationService, + ICrudAppService + where TEntity : class, IEntity + { + public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } + + protected IRepository Repository { get; } + + protected virtual string GetPolicyName { get; set; } + + protected virtual string GetListPolicyName { get; set; } + + protected virtual string CreatePolicyName { get; set; } + + protected virtual string UpdatePolicyName { get; set; } + + protected virtual string DeletePolicyName { get; set; } + + protected AbstractKeyCrudAppService(IRepository repository) + { + Repository = repository; + AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance; + } + + public virtual async Task GetAsync(TKey id) + { + await CheckGetPolicyAsync(); + + var entity = await GetEntityByIdAsync(id); + return MapToGetOutputDto(entity); + } + + public virtual async Task> GetListAsync(TGetListInput input) + { + await CheckGetListPolicyAsync(); + + var query = CreateFilteredQuery(input); + + var totalCount = await AsyncQueryableExecuter.CountAsync(query); + + query = ApplySorting(query, input); + query = ApplyPaging(query, input); + + var entities = await AsyncQueryableExecuter.ToListAsync(query); + + return new PagedResultDto( + totalCount, + entities.Select(MapToGetListOutputDto).ToList() + ); + } + + public virtual async Task CreateAsync(TCreateInput input) + { + await CheckCreatePolicyAsync(); + + var entity = MapToEntity(input); + + TryToSetTenantId(entity); + + await Repository.InsertAsync(entity, autoSave: true); + + return MapToGetOutputDto(entity); + } + + public virtual async Task UpdateAsync(TKey id, TUpdateInput input) + { + await CheckUpdatePolicyAsync(); + + var entity = await GetEntityByIdAsync(id); + //TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise + MapToEntity(input, entity); + await Repository.UpdateAsync(entity, autoSave: true); + + return MapToGetOutputDto(entity); + } + + public virtual async Task DeleteAsync(TKey id) + { + await CheckDeletePolicyAsync(); + + await DeleteByIdAsync(id); + } + + protected abstract Task DeleteByIdAsync(TKey id); + + protected abstract Task GetEntityByIdAsync(TKey id); + + protected virtual async Task CheckGetPolicyAsync() + { + await CheckPolicyAsync(GetPolicyName); + } + + protected virtual async Task CheckGetListPolicyAsync() + { + await CheckPolicyAsync(GetListPolicyName); + } + + protected virtual async Task CheckCreatePolicyAsync() + { + await CheckPolicyAsync(CreatePolicyName); + } + + protected virtual async Task CheckUpdatePolicyAsync() + { + await CheckPolicyAsync(UpdatePolicyName); + } + + protected virtual async Task CheckDeletePolicyAsync() + { + await CheckPolicyAsync(DeletePolicyName); + } + + /// + /// Should apply sorting if needed. + /// + /// The query. + /// The input. + protected virtual IQueryable ApplySorting(IQueryable query, TGetListInput input) + { + //Try to sort query if available + if (input is ISortedResultRequest sortInput) + { + if (!sortInput.Sorting.IsNullOrWhiteSpace()) + { + return query.OrderBy(sortInput.Sorting); + } + } + + //IQueryable.Task requires sorting, so we should sort if Take will be used. + if (input is ILimitedResultRequest) + { + return ApplyDefaultSorting(query); + } + + //No sorting + return query; + } + + /// + /// Applies sorting if no sorting specified but a limited result requested. + /// + /// The query. + protected virtual IQueryable ApplyDefaultSorting(IQueryable query) + { + if (typeof(TEntity).IsAssignableTo()) + { + return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); + } + + throw new AbpException("No sorting specified but this query requires sorting. Override the ApplyDefaultSorting method for your application service derived from AbstractKeyCrudAppService!"); + } + + /// + /// Should apply paging if needed. + /// + /// The query. + /// The input. + protected virtual IQueryable ApplyPaging(IQueryable query, TGetListInput input) + { + //Try to use paging if available + if (input is IPagedResultRequest pagedInput) + { + return query.PageBy(pagedInput); + } + + //Try to limit query result if available + if (input is ILimitedResultRequest limitedInput) + { + return query.Take(limitedInput.MaxResultCount); + } + + //No paging + return query; + } + + /// + /// This method should create based on given input. + /// It should filter query if needed, but should not do sorting or paging. + /// Sorting should be done in and paging should be done in + /// methods. + /// + /// The input. + protected virtual IQueryable CreateFilteredQuery(TGetListInput input) + { + return Repository; + } + + /// + /// Maps to . + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity) + { + return ObjectMapper.Map(entity); + } + + /// + /// Maps to . + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity) + { + return ObjectMapper.Map(entity); + } + + /// + /// Maps to to create a new entity. + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual TEntity MapToEntity(TCreateInput createInput) + { + var entity = ObjectMapper.Map(createInput); + SetIdForGuids(entity); + return entity; + } + + /// + /// Sets Id value for the entity if is . + /// It's used while creating a new entity. + /// + protected virtual void SetIdForGuids(TEntity entity) + { + var entityWithGuidId = entity as IEntity; + + if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty) + { + return; + } + + EntityHelper.TrySetId( + entityWithGuidId, + () => GuidGenerator.Create(), + true + ); + } + + /// + /// Maps to to update the entity. + /// It uses by default. + /// It can be overriden for custom mapping. + /// + protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity) + { + ObjectMapper.Map(updateInput, entity); + } + + protected virtual void TryToSetTenantId(TEntity entity) + { + if (entity is IMultiTenant && HasTenantIdProperty(entity)) + { + var tenantId = CurrentTenant.Id; + + if (!tenantId.HasValue) + { + return; + } + + var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)); + + if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null) + { + return; + } + + propertyInfo.SetValue(entity, tenantId); + } + } + + protected virtual bool HasTenantIdProperty(TEntity entity) + { + return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null; + } + } +} \ 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 4134b59a4c..a2af0592a4 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 @@ -1,13 +1,10 @@ using System; using System.Linq; -using System.Linq.Dynamic.Core; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; +using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; -using Volo.Abp.Linq; -using Volo.Abp.MultiTenancy; -using Volo.Abp.ObjectMapping; namespace Volo.Abp.Application.Services { @@ -65,274 +62,49 @@ namespace Volo.Abp.Application.Services } public abstract class CrudAppService - : ApplicationService, - ICrudAppService - where TEntity : class, IEntity + : AbstractKeyCrudAppService + where TEntity : class, IEntity where TGetOutputDto : IEntityDto where TGetListOutputDto : IEntityDto { - public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } - - protected IRepository Repository { get; } - - protected virtual string GetPolicyName { get; set; } - - protected virtual string GetListPolicyName { get; set; } - - protected virtual string CreatePolicyName { get; set; } - - protected virtual string UpdatePolicyName { get; set; } - - protected virtual string DeletePolicyName { get; set; } + protected new IRepository Repository { get; } protected CrudAppService(IRepository repository) + : base(repository) { Repository = repository; - AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance; } - public virtual async Task GetAsync(TKey id) + protected override async Task DeleteByIdAsync(TKey id) { - await CheckGetPolicyAsync(); - - var entity = await GetEntityByIdAsync(id); - return MapToGetOutputDto(entity); - } - - public virtual async Task> GetListAsync(TGetListInput input) - { - await CheckGetListPolicyAsync(); - - var query = CreateFilteredQuery(input); - - var totalCount = await AsyncQueryableExecuter.CountAsync(query); - - query = ApplySorting(query, input); - query = ApplyPaging(query, input); - - var entities = await AsyncQueryableExecuter.ToListAsync(query); - - return new PagedResultDto( - totalCount, - entities.Select(MapToGetListOutputDto).ToList() - ); - } - - public virtual async Task CreateAsync(TCreateInput input) - { - await CheckCreatePolicyAsync(); - - var entity = MapToEntity(input); - - TryToSetTenantId(entity); - - await Repository.InsertAsync(entity, autoSave: true); - - return MapToGetOutputDto(entity); - } - - public virtual async Task UpdateAsync(TKey id, TUpdateInput input) - { - await CheckUpdatePolicyAsync(); - - var entity = await GetEntityByIdAsync(id); - //TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise - MapToEntity(input, entity); - await Repository.UpdateAsync(entity, autoSave: true); - - return MapToGetOutputDto(entity); - } - - public virtual async Task DeleteAsync(TKey id) - { - await CheckDeletePolicyAsync(); - await Repository.DeleteAsync(id); } - protected virtual Task GetEntityByIdAsync(TKey id) - { - return Repository.GetAsync(id); - } - - protected virtual async Task CheckGetPolicyAsync() - { - await CheckPolicyAsync(GetPolicyName); - } - - protected virtual async Task CheckGetListPolicyAsync() - { - await CheckPolicyAsync(GetListPolicyName); - } - - protected virtual async Task CheckCreatePolicyAsync() - { - await CheckPolicyAsync(CreatePolicyName); - } - - protected virtual async Task CheckUpdatePolicyAsync() - { - await CheckPolicyAsync(UpdatePolicyName); - } - - protected virtual async Task CheckDeletePolicyAsync() + protected override async Task GetEntityByIdAsync(TKey id) { - await CheckPolicyAsync(DeletePolicyName); + return await Repository.GetAsync(id); } - /// - /// Should apply sorting if needed. - /// - /// The query. - /// The input. - protected virtual IQueryable ApplySorting(IQueryable query, TGetListInput input) - { - //Try to sort query if available - if (input is ISortedResultRequest sortInput) - { - if (!sortInput.Sorting.IsNullOrWhiteSpace()) - { - return query.OrderBy(sortInput.Sorting); - } - } - - //IQueryable.Task requires sorting, so we should sort if Take will be used. - if (input is ILimitedResultRequest) - { - return query.OrderByDescending(e => e.Id); - } - - //No sorting - return query; - } - - /// - /// Should apply paging if needed. - /// - /// The query. - /// The input. - protected virtual IQueryable ApplyPaging(IQueryable query, TGetListInput input) - { - //Try to use paging if available - if (input is IPagedResultRequest pagedInput) - { - return query.PageBy(pagedInput); - } - - //Try to limit query result if available - if (input is ILimitedResultRequest limitedInput) - { - return query.Take(limitedInput.MaxResultCount); - } - - //No paging - return query; - } - - /// - /// This method should create based on given input. - /// It should filter query if needed, but should not do sorting or paging. - /// Sorting should be done in and paging should be done in - /// methods. - /// - /// The input. - protected virtual IQueryable CreateFilteredQuery(TGetListInput input) - { - return Repository; - } - - /// - /// Maps to . - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity) - { - return ObjectMapper.Map(entity); - } - - /// - /// Maps to . - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity) - { - return ObjectMapper.Map(entity); - } - - /// - /// Maps to to create a new entity. - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual TEntity MapToEntity(TCreateInput createInput) - { - var entity = ObjectMapper.Map(createInput); - SetIdForGuids(entity); - return entity; - } - - /// - /// Sets Id value for the entity if is . - /// It's used while creating a new entity. - /// - protected virtual void SetIdForGuids(TEntity entity) - { - var entityWithGuidId = entity as IEntity; - - if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty) - { - return; - } - - EntityHelper.TrySetId( - entityWithGuidId, - () => GuidGenerator.Create(), - true - ); - } - - /// - /// Maps to to update the entity. - /// It uses by default. - /// It can be overriden for custom mapping. - /// - protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity) + protected override void MapToEntity(TUpdateInput updateInput, TEntity entity) { if (updateInput is IEntityDto entityDto) { entityDto.Id = entity.Id; } - ObjectMapper.Map(updateInput, entity); + base.MapToEntity(updateInput, entity); } - protected virtual void TryToSetTenantId(TEntity entity) + protected override IQueryable ApplyDefaultSorting(IQueryable query) { - if (entity is IMultiTenant && HasTenantIdProperty(entity)) + if (typeof(TEntity).IsAssignableTo()) { - var tenantId = CurrentTenant.Id; - - if (!tenantId.HasValue) - { - return; - } - - var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)); - - if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null) - { - return; - } - - propertyInfo.SetValue(entity, tenantId); + return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); + } + else + { + return query.OrderByDescending(e => e.Id); } - } - - protected virtual bool HasTenantIdProperty(TEntity entity) - { - return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null; } } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedAggregateRootWithUser.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedAggregateRootWithUser.cs index ec490fbb50..477f8750fd 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedAggregateRootWithUser.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedAggregateRootWithUser.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.Domain.Entities.Auditing /// Type of the user [Serializable] public abstract class AuditedAggregateRootWithUser : AuditedAggregateRoot, IAuditedObject - where TUser : IEntity + where TUser : IEntity { /// public virtual TUser Creator { get; set; } @@ -25,7 +25,7 @@ namespace Volo.Abp.Domain.Entities.Auditing /// Type of the user [Serializable] public abstract class AuditedAggregateRootWithUser : AuditedAggregateRoot, IAuditedObject - where TUser : IEntity + where TUser : IEntity { /// public virtual TUser Creator { get; set; } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedEntityWithUser.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedEntityWithUser.cs index fd6aeb9dd9..cc54209595 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedEntityWithUser.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/AuditedEntityWithUser.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.Domain.Entities.Auditing /// Type of the user [Serializable] public abstract class AuditedEntityWithUser : AuditedEntity, IAuditedObject - where TUser : IEntity + where TUser : IEntity { /// public virtual TUser Creator { get; set; } @@ -25,7 +25,7 @@ namespace Volo.Abp.Domain.Entities.Auditing /// Type of the user [Serializable] public abstract class AuditedEntityWithUser : AuditedEntity, IAuditedObject - where TUser : IEntity + where TUser : IEntity { /// public virtual TUser Creator { get; set; } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedAggregateRootWithUser.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedAggregateRootWithUser.cs index b2dbb8c280..c6bfefbb23 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedAggregateRootWithUser.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedAggregateRootWithUser.cs @@ -15,10 +15,10 @@ namespace Volo.Abp.Domain.Entities.Auditing public virtual TUser Deleter { get; set; } /// - public TUser Creator { get; set; } + public virtual TUser Creator { get; set; } /// - public TUser LastModifier { get; set; } + public virtual TUser LastModifier { get; set; } } /// @@ -34,10 +34,10 @@ namespace Volo.Abp.Domain.Entities.Auditing public virtual TUser Deleter { get; set; } /// - public TUser Creator { get; set; } + public virtual TUser Creator { get; set; } /// - public TUser LastModifier { get; set; } + public virtual TUser LastModifier { get; set; } protected FullAuditedAggregateRootWithUser() { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedEntityWithUser.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedEntityWithUser.cs index 12458ebf99..f7ec2b672f 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedEntityWithUser.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Auditing/FullAuditedEntityWithUser.cs @@ -9,16 +9,16 @@ namespace Volo.Abp.Domain.Entities.Auditing /// Type of the user [Serializable] public abstract class FullAuditedEntityWithUser : FullAuditedEntity, IFullAuditedObject - where TUser : IEntity + where TUser : IEntity { /// public virtual TUser Deleter { get; set; } /// - public TUser Creator { get; set; } + public virtual TUser Creator { get; set; } /// - public TUser LastModifier { get; set; } + public virtual TUser LastModifier { get; set; } } /// @@ -28,16 +28,16 @@ namespace Volo.Abp.Domain.Entities.Auditing /// Type of the user [Serializable] public abstract class FullAuditedEntityWithUser : FullAuditedEntity, IFullAuditedObject - where TUser : IEntity + where TUser : IEntity { /// public virtual TUser Deleter { get; set; } /// - public TUser Creator { get; set; } + public virtual TUser Creator { get; set; } /// - public TUser LastModifier { get; set; } + public virtual TUser LastModifier { get; set; } protected FullAuditedEntityWithUser() { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs index a33c5f56ec..2b38059ef8 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Reflection; namespace Volo.Abp.Domain.Entities { @@ -13,12 +11,24 @@ namespace Volo.Abp.Domain.Entities /// public static class EntityHelper { - public static bool IsEntity([NotNull] Type type) { return typeof(IEntity).IsAssignableFrom(type); } + public static bool IsEntityWithId([NotNull] Type type) + { + foreach (var interfaceType in type.GetInterfaces()) + { + if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>)) + { + return true; + } + } + + return false; + } + public static bool HasDefaultId(IEntity entity) { if (EqualityComparer.Default.Equals(entity.Id, default)) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs index 510d0384c2..85466ccf0e 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs @@ -47,7 +47,11 @@ namespace Volo.Abp.Domain.Entities /// Creates a new object. /// public EntityNotFoundException(Type entityType, object id, Exception innerException) - : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException) + : base( + id == null + ? $"There is no such an entity given given id. Entity type: {entityType.FullName}" + : $"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", + innerException) { EntityType = entityType; Id = id; diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/Temp.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/Temp.cs deleted file mode 100644 index 0ac5b40a47..0000000000 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/Distributed/Temp.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.EventBus; -using Volo.Abp.EventBus.Distributed; - -namespace Volo.Abp.Domain.Entities.Events.Distributed -{ - /* - [EventName("Volo.Abp.Identity.Users.IdentityUser")] - public class UserEto : EtoBase - { - public Guid Id { get; set; } - - public string UserName { get; set; } - } - - //Event name (default): [Volo.Abp.Identity.Users.IdentityUser].Created/Updated/Deleted - - public class MyEventHandler : IDistributedEventHandler> - { - //Event name: Volo.Abp.Identity.Users.IdentityUser.Created - public Task HandleEventAsync(EntityCreatedEventInfo eventData) - { - throw new NotImplementedException(); - } - } - - */ -} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs index 2ac16ec229..3bda51bd2e 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs @@ -18,6 +18,34 @@ namespace Volo.Abp.Domain.Repositories public interface IRepository : IReadOnlyRepository, IBasicRepository where TEntity : class, IEntity { + /// + /// Get a single entity by the given . + /// It returns null if no entity with the given . + /// It throws if there are multiple entities with the given . + /// + /// A condition to find the entity + /// Set true to include all children of this entity + /// A to observe while waiting for the task to complete. + Task FindAsync( + [NotNull] Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default + ); + + /// + /// Get a single entity by the given . + /// It throws if there is no entity with the given . + /// It throws if there are multiple entities with the given . + /// + /// A condition to filter entities + /// Set true to include all children of this entity + /// A to observe while waiting for the task to complete. + Task GetAsync( + [NotNull] Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default + ); + /// /// Deletes many entities by function. /// Notice that: All entities fits to given predicate are retrieved and deleted. @@ -30,7 +58,11 @@ namespace Volo.Abp.Domain.Repositories /// This is useful for ORMs / database APIs those only save changes with an explicit method call, but you need to immediately save changes to the database. /// /// A to observe while waiting for the task to complete. - Task DeleteAsync([NotNull] Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default); + Task DeleteAsync( + [NotNull] Expression> predicate, + bool autoSave = false, + CancellationToken cancellationToken = default + ); } public interface IRepository : IRepository, IReadOnlyRepository, IBasicRepository 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 3ade403017..bd7e486d29 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 @@ -49,6 +49,26 @@ namespace Volo.Abp.Domain.Repositories protected abstract IQueryable GetQueryable(); + public abstract Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default); + + public async Task GetAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var entity = await FindAsync(predicate, includeDetails, cancellationToken); + + if (entity == null) + { + throw new EntityNotFoundException(typeof(TEntity)); + } + + return entity; + } + public abstract Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default); protected virtual TQueryable ApplyDataFilters(TQueryable query) diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/sl.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/sl.json new file mode 100644 index 0000000000..c8c6045476 --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/sl.json @@ -0,0 +1,23 @@ +{ + "culture": "sl", + "texts": { + "DisplayName:Abp.Mailing.DefaultFromAddress": "Privzeti naslov za pošiljanje", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Privzeto ime za prikaz pri pošiljanju", + "DisplayName:Abp.Mailing.Smtp.Host": "Gostitelj", + "DisplayName:Abp.Mailing.Smtp.Port": "Vrata", + "DisplayName:Abp.Mailing.Smtp.UserName": "Uporabniško ime", + "DisplayName:Abp.Mailing.Smtp.Password": "Geslo", + "DisplayName:Abp.Mailing.Smtp.Domain": "Domena", + "DisplayName:Abp.Mailing.Smtp.EnableSsl": "Omogoči SSL", + "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Uporabi privzete poverilnice", + "Description:Abp.Mailing.DefaultFromAddress": "Privzeti naslov za pošiljanje", + "Description:Abp.Mailing.DefaultFromDisplayName": "Privzeto ime za prikaz pri pošiljanju", + "Description:Abp.Mailing.Smtp.Host": "Naziv ali IP naslov gostitelja za uporabo SMTP transakcij.", + "Description:Abp.Mailing.Smtp.Port": "Vrata za uporabo SMTP transakcij.", + "Description:Abp.Mailing.Smtp.UserName": "Uporabniško ime, povezano s poverilnicami.", + "Description:Abp.Mailing.Smtp.Password": "The password for the user name associated with the credentials.", + "Description:Abp.Mailing.Smtp.Domain": "Geslo za uporabniško ime, povezano s poverilnicami.", + "Description:Abp.Mailing.Smtp.EnableSsl": "Ali SmtpClient uporablja sloj varnih vtičnic (SSL) za šifriranje povezave.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Ali so privzete poverilnice poslane skupaj z zahtevki." + } +} 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 ebad209a50..d565f6c114 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 @@ -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 bc88ef83d1..74d9e15537 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj index 7747d7faf2..385ad9068a 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 @@ -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 d0d0c58601..a1418c84d3 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj @@ -20,8 +20,8 @@ - - + + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 106e00f77a..a48e5d7da5 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -93,6 +93,20 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore return DbSet.AsQueryable(); } + public override async Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return includeDetails + ? await WithDetails() + .Where(predicate) + .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)) + : await DbSet + .Where(predicate) + .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + public override async Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { var entities = await GetQueryable() @@ -173,18 +187,6 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore } - public virtual TEntity Get(TKey id, bool includeDetails = true) - { - var entity = Find(id, includeDetails); - - if (entity == null) - { - throw new EntityNotFoundException(typeof(TEntity), id); - } - - return entity; - } - public virtual async Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { var entity = await FindAsync(id, includeDetails, GetCancellationToken(cancellationToken)); @@ -197,13 +199,6 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore return entity; } - public virtual TEntity Find(TKey id, bool includeDetails = true) - { - return includeDetails - ? WithDetails().FirstOrDefault(e => e.Id.Equals(id)) - : DbSet.Find(id); - } - public virtual async Task FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { return includeDetails 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 ba9cee70ee..3ba7092395 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -22,13 +22,14 @@ using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; +using Volo.Abp.ObjectExtending; using Volo.Abp.Reflection; using Volo.Abp.Timing; using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore { - public abstract class AbpDbContext : DbContext, IEfCoreDbContext, ITransientDependency + public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, ITransientDependency where TDbContext : DbContext { protected virtual Guid? CurrentTenantId => CurrentTenant?.Id; @@ -144,6 +145,71 @@ namespace Volo.Abp.EntityFrameworkCore } } + public virtual void Initialize(AbpEfCoreDbContextInitializationContext initializationContext) + { + if (initializationContext.UnitOfWork.Options.Timeout.HasValue && + Database.IsRelational() && + !Database.GetCommandTimeout().HasValue) + { + Database.SetCommandTimeout(initializationContext.UnitOfWork.Options.Timeout.Value.TotalSeconds.To()); + } + + ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges; + ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges; + + ChangeTracker.Tracked += ChangeTracker_Tracked; + } + + protected virtual void ChangeTracker_Tracked(object sender, EntityTrackedEventArgs e) + { + FillExtraPropertiesForTrackedEntities(e); + } + + protected virtual void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e) + { + var entityType = e.Entry.Metadata.ClrType; + if (entityType == null) + { + return; + } + + if (!(e.Entry.Entity is IHasExtraProperties entity)) + { + return; + } + + if (!e.FromQuery) + { + return; + } + + var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType); + if (objectExtension == null) + { + return; + } + + foreach (var property in objectExtension.GetProperties()) + { + if (!property.IsMappedToFieldForEfCore()) + { + continue; + } + /* Checking "currentValue != null" has a good advantage: + * Assume that you we already using a named extra property, + * then decided to create a field (entity extension) for it. + * In this way, it prevents to delete old value in the JSON and + * updates the field on the next save! + */ + + var currentValue = e.Entry.CurrentValues[property.Name]; + if (currentValue != null) + { + entity.SetProperty(property.Name, currentValue); + } + } + } + protected virtual EntityChangeReport ApplyAbpConcepts() { var changeReport = new EntityChangeReport(); @@ -171,9 +237,46 @@ namespace Volo.Abp.EntityFrameworkCore break; } + HandleExtraPropertiesOnSave(entry); + AddDomainEvents(changeReport, entry.Entity); } + protected virtual void HandleExtraPropertiesOnSave(EntityEntry entry) + { + if (entry.State.IsIn(EntityState.Deleted, EntityState.Unchanged)) + { + return; + } + + var entityType = entry.Metadata.ClrType; + if (entityType == null) + { + return; + } + + if (!(entry.Entity is IHasExtraProperties entity)) + { + return; + } + + var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType); + if (objectExtension == null) + { + return; + } + + foreach (var property in objectExtension.GetProperties()) + { + if (!entity.HasProperty(property.Name)) + { + continue; + } + + entry.Property(property.Name).CurrentValue = entity.GetProperty(property.Name); + } + } + protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, EntityChangeReport changeReport) { CheckAndSetId(entry); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEfCoreDbContextInitializationContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEfCoreDbContextInitializationContext.cs new file mode 100644 index 0000000000..d5e7a456ec --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEfCoreDbContextInitializationContext.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Uow; + +namespace Volo.Abp.EntityFrameworkCore +{ + public class AbpEfCoreDbContextInitializationContext + { + public IUnitOfWork UnitOfWork { get; } + + public AbpEfCoreDbContextInitializationContext(IUnitOfWork unitOfWork) + { + UnitOfWork = unitOfWork; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index 155e639bff..af728686fd 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -15,7 +14,6 @@ using Volo.Abp.Json; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Timing; -using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore.EntityHistory { @@ -26,6 +24,7 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory protected IAuditingStore AuditingStore { get; } protected IJsonSerializer JsonSerializer { get; } protected AbpAuditingOptions Options { get; } + protected IAuditingHelper AuditingHelper { get; } private readonly IClock _clock; @@ -33,11 +32,13 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory IAuditingStore auditingStore, IOptions options, IClock clock, - IJsonSerializer jsonSerializer) + IJsonSerializer jsonSerializer, + IAuditingHelper auditingHelper) { _clock = clock; AuditingStore = auditingStore; JsonSerializer = jsonSerializer; + AuditingHelper = auditingHelper; Options = options.Value; Logger = NullLogger.Instance; @@ -203,39 +204,14 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory return false; } - if (Options.IgnoredTypes.Any(t => t.IsInstanceOfType(entityEntry.Entity))) - { - return false; - } + var entityType = entityEntry.Metadata.ClrType; - var entityType = entityEntry.Entity.GetType(); if (!EntityHelper.IsEntity(entityType)) { return false; } - if (!entityType.IsPublic) - { - return false; - } - - if (entityType.IsDefined(typeof(AuditedAttribute), true)) - { - return true; - } - - if (entityEntry.Metadata.GetProperties() - .Any(p => p.PropertyInfo?.IsDefined(typeof(AuditedAttribute)) ?? false)) - { - return true; - } - - if (entityType.IsDefined(typeof(DisableAuditingAttribute), true)) - { - return false; - } - - if (Options.EntityHistorySelectors.Any(selector => selector.Predicate(entityType))) + if (AuditingHelper.IsEntityHistoryEnabled(entityType)) { return true; } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IAbpEfCoreDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IAbpEfCoreDbContext.cs new file mode 100644 index 0000000000..a1eba1290f --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IAbpEfCoreDbContext.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.EntityFrameworkCore +{ + public interface IAbpEfCoreDbContext : IEfCoreDbContext + { + void Initialize(AbpEfCoreDbContextInitializationContext initializationContext); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs index 6db09d4d17..0a84b6871d 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs @@ -8,6 +8,7 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.EntityFrameworkCore.ValueComparers; using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.MultiTenancy; +using Volo.Abp.ObjectExtending; namespace Volo.Abp.EntityFrameworkCore.Modeling { @@ -17,6 +18,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling { b.TryConfigureConcurrencyStamp(); b.TryConfigureExtraProperties(); + b.TryConfigureObjectExtensions(); b.TryConfigureMayHaveCreator(); b.TryConfigureMustHaveCreator(); b.TryConfigureSoftDelete(); @@ -25,7 +27,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling b.TryConfigureCreationTime(); b.TryConfigureLastModificationTime(); b.TryConfigureModificationAudited(); - b.TryConfigureMultiTenant(); + b.TryConfigureMultiTenant(); } public static void ConfigureConcurrencyStamp(this EntityTypeBuilder b) @@ -53,13 +55,30 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling public static void TryConfigureExtraProperties(this EntityTypeBuilder b) { - //TODO: Max length? + if (!b.Metadata.ClrType.IsAssignableTo()) + { + return; + } + + b.Property>(nameof(IHasExtraProperties.ExtraProperties)) + .HasColumnName(nameof(IHasExtraProperties.ExtraProperties)) + .HasConversion(new ExtraPropertiesValueConverter(b.Metadata.ClrType)) + .Metadata.SetValueComparer(new AbpDictionaryValueComparer()); + + b.TryConfigureObjectExtensions(); + } + + public static void ConfigureObjectExtensions(this EntityTypeBuilder b) + where T : class, IHasExtraProperties + { + b.As().TryConfigureObjectExtensions(); + } + + public static void TryConfigureObjectExtensions(this EntityTypeBuilder b) + { if (b.Metadata.ClrType.IsAssignableTo()) { - b.Property>(nameof(IHasExtraProperties.ExtraProperties)) - .HasColumnName(nameof(IHasExtraProperties.ExtraProperties)) - .HasConversion(new AbpJsonValueConverter>()) - .Metadata.SetValueComparer(new AbpDictionaryValueComparer()); + ObjectExtensionManager.Instance.ConfigureEfCoreEntity(b); } } @@ -276,14 +295,13 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling } public static void ConfigureFullAuditedAggregateRoot(this EntityTypeBuilder b) - where T : class + where T : class { b.As().TryConfigureFullAudited(); b.As().TryConfigureExtraProperties(); b.As().TryConfigureConcurrencyStamp(); } - //TODO: Add other interfaces (IAuditedObject...) } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs index e77b2842a8..6ecf19fa98 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpJsonValueConverter.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; +using Volo.Abp.Data; namespace Volo.Abp.EntityFrameworkCore.ValueConverters { @@ -7,9 +8,20 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters { public AbpJsonValueConverter() : base( - d => JsonConvert.SerializeObject(d, Formatting.None), - s => JsonConvert.DeserializeObject(s)) + d => SerializeObject(d), + s => DeserializeObject(s)) { + + } + + private static string SerializeObject(TPropertyType d) + { + return JsonConvert.SerializeObject(d, Formatting.None); + } + + private static TPropertyType DeserializeObject(string s) + { + return JsonConvert.DeserializeObject(s); } } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs new file mode 100644 index 0000000000..ddcdb942ba --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/ExtraPropertiesValueConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.EntityFrameworkCore.ValueConverters +{ + public class ExtraPropertiesValueConverter : ValueConverter, string> + { + public ExtraPropertiesValueConverter(Type entityType) + : base( + d => SerializeObject(d, entityType), + s => DeserializeObject(s)) + { + + } + + private static string SerializeObject(Dictionary extraProperties, Type entityType) + { + var copyDictionary = new Dictionary(extraProperties); + + if (entityType != null) + { + var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType); + if (objectExtension != null) + { + foreach (var property in objectExtension.GetProperties()) + { + if (property.IsMappedToFieldForEfCore()) + { + copyDictionary.Remove(property.Name); + } + } + } + } + + return JsonConvert.SerializeObject(copyDictionary, Formatting.None); + } + + private static Dictionary DeserializeObject(string extraPropertiesAsJson) + { + return JsonConvert.DeserializeObject>(extraPropertiesAsJson); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionInfoExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionInfoExtensions.cs new file mode 100644 index 0000000000..ef3d87119a --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionInfoExtensions.cs @@ -0,0 +1,41 @@ +using System; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Volo.Abp.ObjectExtending +{ + public static class EfCoreObjectExtensionInfoExtensions + { + public static ObjectExtensionInfo MapEfCoreProperty( + [NotNull] this ObjectExtensionInfo objectExtensionInfo, + [NotNull] string propertyName, + [CanBeNull] Action propertyBuildAction) + { + return objectExtensionInfo.MapEfCoreProperty( + typeof(TProperty), + propertyName, + propertyBuildAction + ); + } + + public static ObjectExtensionInfo MapEfCoreProperty( + [NotNull] this ObjectExtensionInfo objectExtensionInfo, + [NotNull] Type propertyType, + [NotNull] string propertyName, + [CanBeNull] Action propertyBuildAction) + { + Check.NotNull(objectExtensionInfo, nameof(objectExtensionInfo)); + + return objectExtensionInfo.AddOrUpdateProperty( + propertyType, + propertyName, + options => + { + options.MapEfCore( + propertyBuildAction + ); + } + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionManagerExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionManagerExtensions.cs new file mode 100644 index 0000000000..52dca0c105 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionManagerExtensions.cs @@ -0,0 +1,80 @@ +using System; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.ObjectExtending +{ + public static class EfCoreObjectExtensionManagerExtensions + { + public static ObjectExtensionManager MapEfCoreProperty( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] string propertyName, + [CanBeNull] Action propertyBuildAction = null) + where TEntity : IHasExtraProperties, IEntity + { + return objectExtensionManager.MapEfCoreProperty( + typeof(TEntity), + typeof(TProperty), + propertyName, + propertyBuildAction + ); + } + + public static ObjectExtensionManager MapEfCoreProperty( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] Type entityType, + [NotNull] Type propertyType, + [NotNull] string propertyName, + [CanBeNull] Action propertyBuildAction = null) + { + Check.NotNull(objectExtensionManager, nameof(objectExtensionManager)); + + return objectExtensionManager.AddOrUpdateProperty( + entityType, + propertyType, + propertyName, + options => + { + options.MapEfCore( + propertyBuildAction + ); + } + ); + } + + public static void ConfigureEfCoreEntity( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] EntityTypeBuilder typeBuilder) + { + Check.NotNull(objectExtensionManager, nameof(objectExtensionManager)); + Check.NotNull(typeBuilder, nameof(typeBuilder)); + + var objectExtension = objectExtensionManager.GetOrNull(typeBuilder.Metadata.ClrType); + if (objectExtension == null) + { + return; + } + + foreach (var property in objectExtension.GetProperties()) + { + var efCoreMapping = property.GetEfCoreMappingOrNull(); + if (efCoreMapping == null) + { + continue; + } + + /* Prevent multiple calls to the entityTypeBuilder.Property(...) method */ + if (typeBuilder.Metadata.FindProperty(property.Name) != null) + { + continue; + } + + var propertyBuilder = typeBuilder.Property(property.Type, property.Name); + + efCoreMapping.PropertyBuildAction?.Invoke(propertyBuilder); + } + } + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionPropertyInfoExtensions.cs new file mode 100644 index 0000000000..fda5d88ceb --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionPropertyInfoExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Volo.Abp.ObjectExtending +{ + public static class EfCoreObjectExtensionPropertyInfoExtensions + { + public const string EfCorePropertyConfigurationName = "EfCoreMapping"; + + [NotNull] + public static ObjectExtensionPropertyInfo MapEfCore( + [NotNull] this ObjectExtensionPropertyInfo propertyExtension, + [CanBeNull] Action propertyBuildAction = null) + { + Check.NotNull(propertyExtension, nameof(propertyExtension)); + + propertyExtension.Configuration[EfCorePropertyConfigurationName] = + new ObjectExtensionPropertyInfoEfCoreMappingOptions( + propertyExtension, + propertyBuildAction + ); + + return propertyExtension; + } + + [CanBeNull] + public static ObjectExtensionPropertyInfoEfCoreMappingOptions GetEfCoreMappingOrNull( + [NotNull] this ObjectExtensionPropertyInfo propertyExtension) + { + Check.NotNull(propertyExtension, nameof(propertyExtension)); + + return propertyExtension + .Configuration + .GetOrDefault(EfCorePropertyConfigurationName) + as ObjectExtensionPropertyInfoEfCoreMappingOptions; + } + + public static bool IsMappedToFieldForEfCore( + [NotNull] this ObjectExtensionPropertyInfo propertyExtension) + { + Check.NotNull(propertyExtension, nameof(propertyExtension)); + + return propertyExtension + .Configuration + .ContainsKey(EfCorePropertyConfigurationName); + } + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfoEfCoreMappingOptions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfoEfCoreMappingOptions.cs new file mode 100644 index 0000000000..4189c87962 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfoEfCoreMappingOptions.cs @@ -0,0 +1,27 @@ +using System; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Volo.Abp.ObjectExtending +{ + public class ObjectExtensionPropertyInfoEfCoreMappingOptions + { + [NotNull] + public ObjectExtensionPropertyInfo ExtensionProperty { get; } + + [NotNull] + public ObjectExtensionInfo ObjectExtension => ExtensionProperty.ObjectExtension; + + [CanBeNull] + public Action PropertyBuildAction { get; set; } + + public ObjectExtensionPropertyInfoEfCoreMappingOptions( + [NotNull] ObjectExtensionPropertyInfo extensionProperty, + [CanBeNull] Action propertyBuildAction = null) + { + ExtensionProperty = Check.NotNull(extensionProperty, nameof(extensionProperty)); + + PropertyBuildAction = propertyBuildAction; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs index a09d756cd2..91ed2f8126 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs @@ -1,5 +1,6 @@ using System; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Data; @@ -53,11 +54,13 @@ namespace Volo.Abp.Uow.EntityFrameworkCore { var dbContext = CreateDbContext(unitOfWork); - if (unitOfWork.Options.Timeout.HasValue && - dbContext.Database.IsRelational() && - !dbContext.Database.GetCommandTimeout().HasValue) + if (dbContext is IAbpEfCoreDbContext abpEfCoreDbContext) { - dbContext.Database.SetCommandTimeout(unitOfWork.Options.Timeout.Value.TotalSeconds.To()); + abpEfCoreDbContext.Initialize( + new AbpEfCoreDbContextInitializationContext( + unitOfWork + ) + ); } return dbContext; diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xml b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xsd b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/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.Http.Client.IdentityModel.Web/Properties/launchSettings.json b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Properties/launchSettings.json new file mode 100644 index 0000000000..d56e5c65b7 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52306/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Volo.Abp.Http.Client.IdentityModel": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:52307/" + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..1518da9045 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj @@ -0,0 +1,23 @@ + + + + + + + netcoreapp3.1 + Volo.Abp.Http.Client.IdentityModel.Web + Volo.Abp.Http.Client.IdentityModel.Web + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + true + Library + + + + + + + + diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/AbpHttpClientIdentityModelWebModule.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/AbpHttpClientIdentityModelWebModule.cs new file mode 100644 index 0000000000..bdb216e41c --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/AbpHttpClientIdentityModelWebModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Http.Client.IdentityModel.Web +{ + [DependsOn( + typeof(AbpHttpClientIdentityModelModule) + )] + public class AbpHttpClientIdentityModelWebModule : AbpModule + { + + } +} diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs new file mode 100644 index 0000000000..a829a630d6 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using IdentityModel.Client; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.Authentication; +using Volo.Abp.IdentityModel; + +namespace Volo.Abp.Http.Client.IdentityModel.Web +{ + [Dependency(ReplaceServices = true)] + public class HttpContextIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator + { + public IHttpContextAccessor HttpContextAccessor { get; set; } + + public HttpContextIdentityModelRemoteServiceHttpClientAuthenticator( + IIdentityModelAuthenticationService identityModelAuthenticationService) + : base(identityModelAuthenticationService) + { + } + + public override async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context) + { + if (context.RemoteService.GetUseCurrentAccessToken() != false) + { + var accessToken = await GetAccessTokenFromHttpContextOrNullAsync(); + if (accessToken != null) + { + context.Request.SetBearerToken(accessToken); + return; + } + } + + await base.Authenticate(context); + } + + protected virtual async Task GetAccessTokenFromHttpContextOrNullAsync() + { + var httpContext = HttpContextAccessor?.HttpContext; + if (httpContext == null) + { + return null; + } + + return await httpContext.GetTokenAsync("access_token"); + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo.Abp.Http.Client.IdentityModel.csproj b/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo.Abp.Http.Client.IdentityModel.csproj index e62293964a..d8eef1ba5d 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo.Abp.Http.Client.IdentityModel.csproj +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo.Abp.Http.Client.IdentityModel.csproj @@ -1,18 +1,16 @@ - + - netcoreapp3.1 + netstandard2.0 Volo.Abp.Http.Client.IdentityModel Volo.Abp.Http.Client.IdentityModel $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; false false false - true - Library diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo/Abp/Http/Client/IdentityModel/IdentityModelRemoteServiceHttpClientAuthenticator.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo/Abp/Http/Client/IdentityModel/IdentityModelRemoteServiceHttpClientAuthenticator.cs index 25763efbd5..0b1749f445 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo/Abp/Http/Client/IdentityModel/IdentityModelRemoteServiceHttpClientAuthenticator.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel/Volo/Abp/Http/Client/IdentityModel/IdentityModelRemoteServiceHttpClientAuthenticator.cs @@ -1,7 +1,4 @@ using System.Threading.Tasks; -using IdentityModel.Client; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; using Volo.Abp.IdentityModel; @@ -11,8 +8,6 @@ namespace Volo.Abp.Http.Client.IdentityModel [Dependency(ReplaceServices = true)] public class IdentityModelRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency { - public IHttpContextAccessor HttpContextAccessor { get; set; } - protected IIdentityModelAuthenticationService IdentityModelAuthenticationService { get; } public IdentityModelRemoteServiceHttpClientAuthenticator( @@ -21,33 +16,12 @@ namespace Volo.Abp.Http.Client.IdentityModel IdentityModelAuthenticationService = identityModelAuthenticationService; } - public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context) + public virtual async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context) { - if (context.RemoteService.GetUseCurrentAccessToken() != false) - { - var accessToken = await GetAccessTokenFromHttpContextOrNullAsync(); - if (accessToken != null) - { - context.Request.SetBearerToken(accessToken); - return; - } - } - await IdentityModelAuthenticationService.TryAuthenticateAsync( context.Client, context.RemoteService.GetIdentityClient() ); } - - protected virtual async Task GetAccessTokenFromHttpContextOrNullAsync() - { - var httpContext = HttpContextAccessor?.HttpContext; - if (httpContext == null) - { - return null; - } - - return await httpContext.GetTokenAsync("access_token"); - } } } 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 461a0c35f5..8607f32a35 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,23 +1,21 @@ - + - netcoreapp3.1 + netstandard2.0 Volo.Abp.Http.Client Volo.Abp.Http.Client $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; false false false - true - Library - + diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApiDescriptionFinder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApiDescriptionFinder.cs index 9b9c8e804c..2e8eb41844 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApiDescriptionFinder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApiDescriptionFinder.cs @@ -24,7 +24,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying }; public ApiDescriptionFinder( - IApiDescriptionCache cache, + IApiDescriptionCache cache, IDynamicProxyHttpClientFactory httpClientFactory) { Cache = cache; @@ -56,8 +56,8 @@ namespace Volo.Abp.Http.Client.DynamicProxying var found = true; for (int i = 0; i < methodParameters.Length; i++) - { - if (action.ParametersOnMethod[i].TypeAsString != methodParameters[i].ParameterType.GetFullNameWithAssemblyName()) + { + if (!TypeMatches(action.ParametersOnMethod[i], methodParameters[i])) { found = false; break; @@ -104,5 +104,20 @@ namespace Volo.Abp.Http.Client.DynamicProxying return (ApplicationApiDescriptionModel)result; } } + + protected virtual bool TypeMatches(MethodParameterApiDescriptionModel actionParameter, ParameterInfo methodParameter) + { + return NormalizeTypeName(actionParameter.TypeAsString) == + NormalizeTypeName(methodParameter.ParameterType.GetFullNameWithAssemblyName()); + } + + protected virtual string NormalizeTypeName(string typeName) + { + const string placeholder = "%COREFX%"; + const string netCoreLib = "System.Private.CoreLib"; + const string netFxLib = "mscorlib"; + + return typeName.Replace(netCoreLib, placeholder).Replace(netFxLib, placeholder); + } } } 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 3e0124237f..d5d90ed182 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 @@ -56,7 +56,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying IJsonSerializer jsonSerializer, IRemoteServiceHttpClientAuthenticator clientAuthenticator, ICancellationTokenProvider cancellationTokenProvider, - ICorrelationIdProvider correlationIdProvider, + ICorrelationIdProvider correlationIdProvider, IOptions correlationIdOptions, ICurrentTenant currentTenant) { @@ -110,7 +110,14 @@ namespace Volo.Abp.Http.Client.DynamicProxying //TODO: Think on that if (TypeHelper.IsPrimitiveExtended(typeof(T), true)) { - return (T)Convert.ChangeType(responseAsString, typeof(T)); + if (typeof(DateTime).IsAssignableFrom(typeof(T))) + { + return (T)(object)DateTime.Parse(responseAsString.Trim('\"'), CultureInfo.InvariantCulture); + } + else + { + return (T)Convert.ChangeType(responseAsString, typeof(T)); + } } return JsonSerializer.Deserialize(responseAsString); @@ -151,8 +158,8 @@ namespace Volo.Abp.Http.Client.DynamicProxying } return await response.Content.ReadAsStringAsync(); - } - + } + private ApiVersionInfo GetApiVersionInfo(ActionApiDescriptionModel action) { var apiVersion = FindBestApiVersion(action); diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs index dcb9868b7f..dae7853e08 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; +using JetBrains.Annotations; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; +using Volo.Abp.Reflection; namespace Volo.Abp.Http.Client.DynamicProxying { @@ -88,10 +91,25 @@ namespace Volo.Abp.Http.Client.DynamicProxying } } - private static void AddQueryStringParameter(StringBuilder urlBuilder, bool isFirstParam, string name, object value) + private static void AddQueryStringParameter( + StringBuilder urlBuilder, + bool isFirstParam, + string name, + [NotNull] object value) { urlBuilder.Append(isFirstParam ? "?" : "&"); - urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(value.ToString())); + + urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(value))); + } + + private static string ConvertValueToString([NotNull] object value) + { + if (value is DateTime dateTimeValue) + { + return dateTimeValue.ToUniversalTime().ToString("u"); + } + + return value.ToString(); } } } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs index a7def10625..cc09b7a153 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs @@ -10,6 +10,8 @@ namespace Volo.Abp.Http.Modeling { public IDictionary Modules { get; set; } + public IDictionary Types { get; set; } + private ApplicationApiDescriptionModel() { @@ -19,8 +21,8 @@ namespace Volo.Abp.Http.Modeling { return new ApplicationApiDescriptionModel { - //TODO: Why ConcurrentDictionary? - Modules = new ConcurrentDictionary() + Modules = new ConcurrentDictionary(), //TODO: Why ConcurrentDictionary? + Types = new Dictionary() }; } @@ -34,9 +36,9 @@ namespace Volo.Abp.Http.Modeling return Modules[module.RootPath] = module; } - public ModuleApiDescriptionModel GetOrAddModule(string rootPath) + public ModuleApiDescriptionModel GetOrAddModule(string rootPath, string remoteServiceName) { - return Modules.GetOrAdd(rootPath, () => ModuleApiDescriptionModel.Create(rootPath)); + return Modules.GetOrAdd(rootPath, () => ModuleApiDescriptionModel.Create(rootPath, remoteServiceName)); } public ApplicationApiDescriptionModel CreateSubModel(string[] modules = null, string[] controllers = null, string[] actions = null) diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs new file mode 100644 index 0000000000..bb50fc0d96 --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Http.Modeling +{ + public class ApplicationApiDescriptionModelRequestDto + { + public bool IncludeTypes { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs index 2bd9a301ab..db5a5eb37a 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs @@ -10,7 +10,7 @@ namespace Volo.Abp.Http.Modeling { public string ControllerName { get; set; } - public string TypeAsString { get; set; } + public string Type { get; set; } public List Interfaces { get; set; } @@ -26,7 +26,7 @@ namespace Volo.Abp.Http.Modeling return new ControllerApiDescriptionModel { ControllerName = controllerName, - TypeAsString = type.GetFullNameWithAssemblyName(), + Type = type.FullName, Actions = new Dictionary(), Interfaces = type .GetInterfaces() @@ -52,7 +52,7 @@ namespace Volo.Abp.Http.Modeling { var subModel = new ControllerApiDescriptionModel { - TypeAsString = TypeAsString, + Type = Type, Interfaces = Interfaces, ControllerName = ControllerName, Actions = new Dictionary() @@ -71,7 +71,7 @@ namespace Volo.Abp.Http.Modeling public bool Implements(Type interfaceType) { - return Interfaces.Any(i => i.TypeAsString == interfaceType.GetFullNameWithAssemblyName()); + return Interfaces.Any(i => i.Type == interfaceType.FullName); } public override string ToString() diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs index 3605cf675b..04b5ea1bc9 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs @@ -5,7 +5,7 @@ namespace Volo.Abp.Http.Modeling [Serializable] public class ControllerInterfaceApiDescriptionModel { - public string TypeAsString { get; set; } + public string Type { get; set; } private ControllerInterfaceApiDescriptionModel() { @@ -16,7 +16,7 @@ namespace Volo.Abp.Http.Modeling { return new ControllerInterfaceApiDescriptionModel { - TypeAsString = type.GetFullNameWithAssemblyName() + Type = type.FullName }; } } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs index 800dc504db..6f8803ddb6 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs @@ -2,6 +2,6 @@ namespace Volo.Abp.Http.Modeling { public interface IApiDescriptionModelProvider { - ApplicationApiDescriptionModel CreateApiModel(); + ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs index 61ffc516f6..4199e29ec0 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs @@ -10,13 +10,17 @@ namespace Volo.Abp.Http.Modeling public string TypeAsString { get; set; } + public string Type { get; set; } + + public string TypeSimple { get; set; } + public bool IsOptional { get; set; } public object DefaultValue { get; set; } private MethodParameterApiDescriptionModel() { - + } public static MethodParameterApiDescriptionModel Create(ParameterInfo parameterInfo) @@ -25,6 +29,8 @@ namespace Volo.Abp.Http.Modeling { Name = parameterInfo.Name, TypeAsString = parameterInfo.ParameterType.GetFullNameWithAssemblyName(), + Type = parameterInfo.ParameterType != null ? ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(parameterInfo.ParameterType) : null, + TypeSimple = parameterInfo.ParameterType != null ? ModelingTypeHelper.GetSimplifiedName(parameterInfo.ParameterType) : null, IsOptional = parameterInfo.IsOptional, DefaultValue = parameterInfo.HasDefaultValue ? parameterInfo.DefaultValue : null }; diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModelingTypeHelper.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModelingTypeHelper.cs new file mode 100644 index 0000000000..2d4b2bac85 --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModelingTypeHelper.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +namespace Volo.Abp.Http.Modeling +{ + public static class ModelingTypeHelper + { + public static string GetFullNameHandlingNullableAndGenerics([NotNull] Type type) + { + Check.NotNull(type, nameof(type)); + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return type.GenericTypeArguments[0].FullName + "?"; + } + + if (type.IsGenericType) + { + var genericType = type.GetGenericTypeDefinition(); + return $"{genericType.FullName.Left(genericType.FullName.IndexOf('`'))}<{type.GenericTypeArguments.Select(GetFullNameHandlingNullableAndGenerics).JoinAsString(",")}>"; + } + + return type.FullName; + } + + public static string GetSimplifiedName([NotNull] Type type) + { + Check.NotNull(type, nameof(type)); + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return GetSimplifiedName(type.GenericTypeArguments[0]) + "?"; + } + + if (type.IsGenericType) + { + var genericType = type.GetGenericTypeDefinition(); + return $"{genericType.FullName.Left(genericType.FullName.IndexOf('`'))}<{type.GenericTypeArguments.Select(GetSimplifiedName).JoinAsString(",")}>"; + } + + if (type == typeof(string)) + { + return "string"; + } + else if (type == typeof(int)) + { + return "number"; + } + else if (type == typeof(long)) + { + return "number"; + } + else if (type == typeof(bool)) + { + return "boolean"; + } + else if (type == typeof(char)) + { + return "string"; + } + else if (type == typeof(double)) + { + return "number"; + } + else if (type == typeof(float)) + { + return "number"; + } + else if (type == typeof(decimal)) + { + return "number"; + } + else if (type == typeof(DateTime)) + { + return "string"; + } + else if (type == typeof(DateTimeOffset)) + { + return "string"; + } + else if (type == typeof(TimeSpan)) + { + return "string"; + } + else if (type == typeof(Guid)) + { + return "string"; + } + else if (type == typeof(byte)) + { + return "number"; + } + else if (type == typeof(sbyte)) + { + return "number"; + } + else if (type == typeof(short)) + { + return "number"; + } + else if (type == typeof(ushort)) + { + return "number"; + } + else if (type == typeof(uint)) + { + return "number"; + } + else if (type == typeof(ulong)) + { + return "number"; + } + else if (type == typeof(IntPtr)) + { + return "number"; + } + else if (type == typeof(UIntPtr)) + { + return "number"; + } + + return type.FullName; + } + } +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs index 44967fa50c..b879b90135 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs @@ -11,10 +11,17 @@ namespace Volo.Abp.Http.Modeling /// /// "app". /// - public const string DefaultRootPath = "app"; + public const string DefaultRootPath = "app"; + /// + /// "Default". + /// + public const string DefaultRemoteServiceName = "Default"; + public string RootPath { get; set; } + public string RemoteServiceName { get; set; } + public IDictionary Controllers { get; set; } private ModuleApiDescriptionModel() @@ -22,11 +29,12 @@ namespace Volo.Abp.Http.Modeling } - public static ModuleApiDescriptionModel Create(string rootPath) + public static ModuleApiDescriptionModel Create(string rootPath, string remoteServiceName) { return new ModuleApiDescriptionModel { RootPath = rootPath, + RemoteServiceName = remoteServiceName, Controllers = new Dictionary() }; } @@ -48,7 +56,7 @@ namespace Volo.Abp.Http.Modeling public ModuleApiDescriptionModel CreateSubModel(string[] controllers, string[] actions) { - var subModel = Create(RootPath); + var subModel = Create(RootPath, RemoteServiceName); foreach (var controller in Controllers.Values) { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs index 257f06d3db..0be791053c 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs @@ -9,7 +9,9 @@ namespace Volo.Abp.Http.Modeling public string Name { get; set; } - public string TypeAsString { get; set; } + public string Type { get; set; } + + public string TypeSimple { get; set; } public bool IsOptional { get; set; } @@ -32,7 +34,8 @@ namespace Volo.Abp.Http.Modeling { Name = name, NameOnMethod = nameOnMethod, - TypeAsString = type?.GetFullNameWithAssemblyName(), + Type = type != null ? ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(type) : null, + TypeSimple = type != null ? ModelingTypeHelper.GetSimplifiedName(type) : null, IsOptional = isOptional, DefaultValue = defaultValue, ConstraintTypes = constraintTypes, diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs new file mode 100644 index 0000000000..ca8af7f22a --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs @@ -0,0 +1,46 @@ +using System; +using System.Reflection; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class PropertyApiDescriptionModel + { + public string Name { get; set; } + + public string Type { get; set; } + + public string TypeSimple { get; set; } + + //TODO: Validation rules for this property + public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo) + { + string typeName; + string simpleTypeName; + + if (TypeHelper.IsEnumerable(propertyInfo.PropertyType, out var itemType, includePrimitives: false)) + { + typeName = $"[{ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(itemType)}]"; + simpleTypeName = $"[{ModelingTypeHelper.GetSimplifiedName(itemType)}]"; + } + else if (TypeHelper.IsDictionary(propertyInfo.PropertyType, out var keyType, out var valueType)) + { + typeName = $"{{{ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(keyType)}:{ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(valueType)}}}"; + simpleTypeName = $"{{{ModelingTypeHelper.GetSimplifiedName(keyType)}:{ModelingTypeHelper.GetSimplifiedName(valueType)}}}"; + } + else + { + typeName = ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(propertyInfo.PropertyType); + simpleTypeName = ModelingTypeHelper.GetSimplifiedName(propertyInfo.PropertyType); + } + + return new PropertyApiDescriptionModel + { + Name = propertyInfo.Name, + Type = typeName, + TypeSimple = simpleTypeName + }; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs index 001bd1b9c9..d6532be6d4 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs @@ -6,7 +6,9 @@ namespace Volo.Abp.Http.Modeling [Serializable] public class ReturnValueApiDescriptionModel { - public string TypeAsString { get; set; } + public string Type { get; set; } + + public string TypeSimple { get; set; } private ReturnValueApiDescriptionModel() { @@ -15,9 +17,12 @@ namespace Volo.Abp.Http.Modeling public static ReturnValueApiDescriptionModel Create(Type type) { + var unwrappedType = AsyncHelper.UnwrapTask(type); + return new ReturnValueApiDescriptionModel { - TypeAsString = AsyncHelper.UnwrapTask(type).GetFullNameWithAssemblyName() + Type = ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(unwrappedType), + TypeSimple = ModelingTypeHelper.GetSimplifiedName(unwrappedType) }; } } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs new file mode 100644 index 0000000000..fcc4b034ea --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class TypeApiDescriptionModel + { + public string BaseType { get; set; } + + public bool IsEnum { get; set; } + + public string[] EnumNames { get; set; } + + public object[] EnumValues { get; set; } + + public PropertyApiDescriptionModel[] Properties { get; set; } + + private TypeApiDescriptionModel() + { + + } + + public static TypeApiDescriptionModel Create(Type type) + { + var baseType = type.BaseType; + if (baseType == typeof(object)) + { + baseType = null; + } + + var typeModel = new TypeApiDescriptionModel + { + IsEnum = type.IsEnum, + BaseType = baseType != null ? ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(baseType) : null + }; + + if (typeModel.IsEnum) + { + typeModel.EnumNames = type.GetEnumNames(); + typeModel.EnumValues = type.GetEnumValues().Cast().ToArray(); + } + else + { + typeModel.Properties = type + .GetProperties() + .Select(PropertyApiDescriptionModel.Create) + .ToArray(); + } + + return typeModel; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs index b267c26296..c9a10c5432 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs @@ -56,7 +56,7 @@ namespace Volo.Abp.Http.ProxyScripting.Generators.JQuery private static void AddControllerScript(StringBuilder script, ControllerApiDescriptionModel controller) { - var controllerName = GetNormalizedTypeName(controller.TypeAsString); + var controllerName = GetNormalizedTypeName(controller.Type); script.AppendLine($" // controller {controllerName}"); script.AppendLine(); @@ -129,7 +129,7 @@ namespace Volo.Abp.Http.ProxyScripting.Generators.JQuery script.AppendLine(" url: abp.appPath + '" + ProxyScriptingHelper.GenerateUrlWithParameters(action) + "',"); script.Append(" type: '" + httpMethod + "'"); - if (action.ReturnValue.TypeAsString == typeof(void).GetFullNameWithAssemblyName()) + if (action.ReturnValue.Type == typeof(void).FullName) { script.AppendLine(","); script.Append(" dataType: null"); diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs index a3df2c70a2..7ac3651ad9 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs @@ -49,7 +49,7 @@ namespace Volo.Abp.Http.ProxyScripting private string CreateScript(ProxyScriptingModel scriptingModel) { - var apiModel = _modelProvider.CreateApiModel(); + var apiModel = _modelProvider.CreateApiModel(new ApplicationApiDescriptionModelRequestDto {IncludeTypes = false}); if (scriptingModel.IsPartialRequest()) { diff --git a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj index 3e4ae78b91..ce26fe7bea 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj +++ b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj @@ -16,6 +16,7 @@ + diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs index b26b545fec..d0a08707b5 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs +++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/AbpIdentityModelModule.cs @@ -13,6 +13,8 @@ namespace Volo.Abp.IdentityModel { var configuration = context.Services.GetConfiguration(); + context.Services.AddHttpClient(); + Configure(configuration); } } diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs index ca6e41d60d..b78a05c684 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs +++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs @@ -21,13 +21,16 @@ namespace Volo.Abp.IdentityModel public ILogger Logger { get; set; } protected AbpIdentityClientOptions ClientOptions { get; } protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IHttpClientFactory HttpClientFactory { get; } public IdentityModelAuthenticationService( IOptions options, - ICancellationTokenProvider cancellationTokenProvider) + ICancellationTokenProvider cancellationTokenProvider, + IHttpClientFactory httpClientFactory) { - CancellationTokenProvider = cancellationTokenProvider; ClientOptions = options.Value; + CancellationTokenProvider = cancellationTokenProvider; + HttpClientFactory = httpClientFactory; Logger = NullLogger.Instance; } @@ -67,9 +70,17 @@ namespace Volo.Abp.IdentityModel } var tokenResponse = await GetTokenResponse(discoveryResponse, configuration); + if (tokenResponse.IsError) { - throw new AbpException($"Could not get token from the OpenId Connect server! ErrorType: {tokenResponse.ErrorType}. Error: {tokenResponse.Error}. ErrorDescription: {tokenResponse.ErrorDescription}. HttpStatusCode: {tokenResponse.HttpStatusCode}"); + if (tokenResponse.ErrorDescription != null) + { + throw new AbpException($"Could not get token from the OpenId Connect server! ErrorType: {tokenResponse.ErrorType}. Error: {tokenResponse.Error}. ErrorDescription: {tokenResponse.ErrorDescription}. HttpStatusCode: {tokenResponse.HttpStatusCode}"); + } + + var rawError = tokenResponse.Raw; + var withoutInnerException = rawError.Split(new string[] { "" }, StringSplitOptions.RemoveEmptyEntries); + throw new AbpException(withoutInnerException[0]); } return tokenResponse.AccessToken; @@ -95,7 +106,7 @@ namespace Volo.Abp.IdentityModel protected virtual async Task GetDiscoveryResponse( IdentityClientConfiguration configuration) { - using (var httpClient = new HttpClient()) + using (var httpClient = HttpClientFactory.CreateClient()) { return await httpClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { @@ -109,10 +120,10 @@ namespace Volo.Abp.IdentityModel } protected virtual async Task GetTokenResponse( - DiscoveryDocumentResponse discoveryResponse, + DiscoveryDocumentResponse discoveryResponse, IdentityClientConfiguration configuration) { - using (var httpClient = new HttpClient()) + using (var httpClient = HttpClientFactory.CreateClient()) { switch (configuration.GrantType) { @@ -134,7 +145,7 @@ namespace Volo.Abp.IdentityModel protected virtual Task CreatePasswordTokenRequestAsync(DiscoveryDocumentResponse discoveryResponse, IdentityClientConfiguration configuration) { - var request = new PasswordTokenRequest + var request = new PasswordTokenRequest { Address = discoveryResponse.TokenEndpoint, Scope = configuration.Scope, @@ -149,11 +160,11 @@ namespace Volo.Abp.IdentityModel return Task.FromResult(request); } - protected virtual Task CreateClientCredentialsTokenRequestAsync( - DiscoveryDocumentResponse discoveryResponse, + protected virtual Task CreateClientCredentialsTokenRequestAsync( + DiscoveryDocumentResponse discoveryResponse, IdentityClientConfiguration configuration) { - var request = new ClientCredentialsTokenRequest + var request = new ClientCredentialsTokenRequest { Address = discoveryResponse.TokenEndpoint, Scope = configuration.Scope, diff --git a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs index d54e7d82d0..9e0b8f04a7 100644 --- a/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs +++ b/framework/src/Volo.Abp.Ldap/Volo/Abp/Ldap/LdapManager.cs @@ -3,7 +3,9 @@ using Microsoft.Extensions.Options; using Novell.Directory.Ldap; using System.Collections.Generic; using System.Text; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Ldap.Exceptions; using Volo.Abp.Ldap.Modeling; @@ -13,6 +15,7 @@ namespace Volo.Abp.Ldap { private readonly string _searchBase; private readonly AbpLdapOptions _ldapOptions; + private readonly IHybridServiceScopeFactory _hybridServiceScopeFactory; private readonly string[] _attributes = { @@ -21,8 +24,9 @@ namespace Volo.Abp.Ldap "sAMAccountName", "userPrincipalName", "telephoneNumber", "mail" }; - public LdapManager(IOptions ldapSettingsOptions) + public LdapManager(IOptions ldapSettingsOptions, IHybridServiceScopeFactory hybridServiceScopeFactory) { + _hybridServiceScopeFactory = hybridServiceScopeFactory; _ldapOptions = ldapSettingsOptions.Value; _searchBase = _ldapOptions.SearchBase; } @@ -231,8 +235,15 @@ namespace Volo.Abp.Ldap return true; } } - catch (Exception ) + catch (Exception ex) { + using (var scope = _hybridServiceScopeFactory.CreateScope()) + { + scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(ex); + } + return false; } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs index edb1eaae77..103567248f 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs @@ -38,10 +38,18 @@ namespace Volo.Abp.Localization return _innerFactory.Create(resourceType); } - return _localizerCache.GetOrAdd( - resourceType, - _ => CreateStringLocalizerCacheItem(resource) - ).Localizer; + if (_localizerCache.TryGetValue(resourceType, out var cacheItem)) + { + return cacheItem.Localizer; + } + + lock (_localizerCache) + { + return _localizerCache.GetOrAdd( + resourceType, + _ => CreateStringLocalizerCacheItem(resource) + ).Localizer; + } } private StringLocalizerCacheItem CreateStringLocalizerCacheItem(LocalizationResource resource) diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/sl.json b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/sl.json new file mode 100644 index 0000000000..8f5073108f --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/sl.json @@ -0,0 +1,7 @@ +{ + "culture": "sl", + "texts": { + "DisplayName:Abp.Localization.DefaultLanguage": "Privzeti jezik", + "Description:Abp.Localization.DefaultLanguage": "Privzeti jezik aplikacije." + } +} diff --git a/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj b/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj index 8aa654922a..d282ce5269 100644 --- a/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj +++ b/framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs index b81aa7ae87..2457c3c453 100644 --- a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs @@ -31,6 +31,14 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb return ApplyDataFilters(Collection.AsQueryable()); } + public override Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return Task.FromResult(Collection.AsQueryable().Where(predicate).SingleOrDefault()); + } + public override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { var entities = Collection.AsQueryable().Where(predicate).ToList(); diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs index 3333f1d505..956bc7853f 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs @@ -169,6 +169,16 @@ namespace Volo.Abp.Domain.Repositories.MongoDB return GetMongoQueryable(); } + public override async Task FindAsync( + Expression> predicate, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .Where(predicate) + .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); + } + public virtual IMongoQueryable GetMongoQueryable() { return ApplyDataFilters( @@ -359,10 +369,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB bool autoSave = false, CancellationToken cancellationToken = default) { - return Collection.DeleteOneAsync( - CreateEntityFilter(id), - GetCancellationToken(cancellationToken) - ); + return DeleteAsync(x => x.Id.Equals(id), autoSave, cancellationToken); } protected override FilterDefinition CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null) diff --git a/framework/src/Volo.Abp.ObjectExtending/FodyWeavers.xml b/framework/src/Volo.Abp.ObjectExtending/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/FodyWeavers.xsd b/framework/src/Volo.Abp.ObjectExtending/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/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.ObjectExtending/Properties/AssemblyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fe0b656805 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Volo.Abp.ObjectExtending.Tests")] diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj b/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj new file mode 100644 index 0000000000..43da943f8b --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo.Abp.ObjectExtending.csproj @@ -0,0 +1,21 @@ + + + + + + + netstandard2.0 + Volo.Abp.ObjectExtending + Volo.Abp.ObjectExtending + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs similarity index 100% rename from framework/src/Volo.Abp.Data/Volo/Abp/Data/HasExtraPropertiesExtensions.cs rename to framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/IHasExtraProperties.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/IHasExtraProperties.cs similarity index 100% rename from framework/src/Volo.Abp.Data/Volo/Abp/Data/IHasExtraProperties.cs rename to framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/IHasExtraProperties.cs diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/AbpObjectExtendingModule.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/AbpObjectExtendingModule.cs new file mode 100644 index 0000000000..9392429fd3 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/AbpObjectExtendingModule.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.ObjectExtending +{ + public class AbpObjectExtendingModule : AbpModule + { + + } +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs new file mode 100644 index 0000000000..f2b7aeecdb --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Volo.Abp.Data; + +namespace Volo.Abp.ObjectExtending +{ + public static class ExtensibleObjectMapper + { + /// + /// Copies extra properties from the object + /// to the object. + /// + /// Checks property definitions (over the ) + /// based on the preference. + /// + /// Source class type + /// Destination class type + /// The source object + /// The destination object + /// + /// Controls which properties to map. + /// + /// Used to ignore some properties + public static void MapExtraPropertiesTo( + [NotNull] TSource source, + [NotNull] TDestination destination, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + where TSource : IHasExtraProperties + where TDestination : IHasExtraProperties + { + Check.NotNull(source, nameof(source)); + Check.NotNull(destination, nameof(destination)); + + ExtensibleObjectMapper.MapExtraPropertiesTo( + typeof(TSource), + typeof(TDestination), + source.ExtraProperties, + destination.ExtraProperties, + definitionChecks, + ignoredProperties + ); + } + + /// + /// Copies extra properties from the object + /// to the object. + /// + /// Checks property definitions (over the ) + /// based on the preference. + /// + /// Source class type (for definition check) + /// Destination class type (for definition check) + /// The source dictionary object + /// The destination dictionary object + /// + /// Controls which properties to map. + /// + /// Used to ignore some properties + public static void MapExtraPropertiesTo( + [NotNull] Dictionary sourceDictionary, + [NotNull] Dictionary destinationDictionary, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + where TSource : IHasExtraProperties + where TDestination : IHasExtraProperties + { + MapExtraPropertiesTo( + typeof(TSource), + typeof(TDestination), + sourceDictionary, + destinationDictionary, + definitionChecks, + ignoredProperties + ); + } + + /// + /// Copies extra properties from the object + /// to the object. + /// + /// Checks property definitions (over the ) + /// based on the preference. + /// + /// Source type (for definition check) + /// Destination class type (for definition check) + /// The source dictionary object + /// The destination dictionary object + /// + /// Controls which properties to map. + /// + /// Used to ignore some properties + public static void MapExtraPropertiesTo( + [NotNull] Type sourceType, + [NotNull] Type destinationType, + [NotNull] Dictionary sourceDictionary, + [NotNull] Dictionary destinationDictionary, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + { + Check.AssignableTo(sourceType, nameof(sourceType)); + Check.AssignableTo(destinationType, nameof(destinationType)); + Check.NotNull(sourceDictionary, nameof(sourceDictionary)); + Check.NotNull(destinationDictionary, nameof(destinationDictionary)); + + var sourceObjectExtension = ObjectExtensionManager.Instance.GetOrNull(sourceType); + var destinationObjectExtension = ObjectExtensionManager.Instance.GetOrNull(destinationType); + + foreach (var keyValue in sourceDictionary) + { + if (CanMapProperty( + keyValue.Key, + sourceObjectExtension, + destinationObjectExtension, + definitionChecks, + ignoredProperties)) + { + destinationDictionary[keyValue.Key] = keyValue.Value; + } + } + } + + //TODO: Move these methods to a class like ObjectExtensionHelper + + public static bool CanMapProperty( + [NotNull] string propertyName, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + { + return CanMapProperty( + typeof(TSource), + typeof(TDestination), + propertyName, + definitionChecks, + ignoredProperties + ); + } + + public static bool CanMapProperty( + [NotNull] Type sourceType, + [NotNull] Type destinationType, + [NotNull] string propertyName, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + { + Check.AssignableTo(sourceType, nameof(sourceType)); + Check.AssignableTo(destinationType, nameof(destinationType)); + Check.NotNull(propertyName, nameof(propertyName)); + + var sourceObjectExtension = ObjectExtensionManager.Instance.GetOrNull(sourceType); + var destinationObjectExtension = ObjectExtensionManager.Instance.GetOrNull(destinationType); + + return CanMapProperty( + propertyName, + sourceObjectExtension, + destinationObjectExtension, + definitionChecks, + ignoredProperties); + } + + private static bool CanMapProperty( + [NotNull] string propertyName, + [CanBeNull] ObjectExtensionInfo sourceObjectExtension, + [CanBeNull] ObjectExtensionInfo destinationObjectExtension, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + { + Check.NotNull(propertyName, nameof(propertyName)); + + if (ignoredProperties != null && + ignoredProperties.Contains(propertyName)) + { + return false; + } + + if (definitionChecks != null) + { + if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Source)) + { + if (sourceObjectExtension == null) + { + return false; + } + + if (!sourceObjectExtension.HasProperty(propertyName)) + { + return false; + } + } + + if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Destination)) + { + if (destinationObjectExtension == null) + { + return false; + } + + if (!destinationObjectExtension.HasProperty(propertyName)) + { + return false; + } + } + + return true; + } + else + { + var sourcePropertyDefinition = sourceObjectExtension?.GetPropertyOrNull(propertyName); + var destinationPropertyDefinition = destinationObjectExtension?.GetPropertyOrNull(propertyName); + + if (sourcePropertyDefinition != null) + { + if (destinationPropertyDefinition != null) + { + return true; + } + + if (sourcePropertyDefinition.CheckPairDefinitionOnMapping == false) + { + return true; + } + } + else if (destinationPropertyDefinition != null) + { + if (destinationPropertyDefinition.CheckPairDefinitionOnMapping == false) + { + return true; + } + } + + return false; + } + } + } +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObject.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObject.cs new file mode 100644 index 0000000000..07243c9dca --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObject.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Data; + +namespace Volo.Abp.ObjectExtending +{ + [Serializable] + public class ExtensibleObject : IHasExtraProperties + { + public Dictionary ExtraProperties { get; protected set; } + + public ExtensibleObject() + { + ExtraProperties = new Dictionary(); + } + } +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions.cs new file mode 100644 index 0000000000..051b1fd0ad --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/HasExtraPropertiesObjectExtendingExtensions.cs @@ -0,0 +1,39 @@ +using JetBrains.Annotations; +using Volo.Abp.Data; + +namespace Volo.Abp.ObjectExtending +{ + public static class HasExtraPropertiesObjectExtendingExtensions + { + /// + /// Copies extra properties from the object + /// to the object. + /// + /// Checks property definitions (over the ) + /// based on the preference. + /// + /// Source class type + /// Destination class type + /// The source object + /// The destination object + /// + /// Controls which properties to map. + /// + /// Used to ignore some properties + public static void MapExtraPropertiesTo( + [NotNull] this TSource source, + [NotNull] TDestination destination, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[] ignoredProperties = null) + where TSource : IHasExtraProperties + where TDestination : IHasExtraProperties + { + ExtensibleObjectMapper.MapExtraPropertiesTo( + source, + destination, + definitionChecks, + ignoredProperties + ); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs new file mode 100644 index 0000000000..b33638fa7c --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs @@ -0,0 +1,28 @@ +using System; + +namespace Volo.Abp.ObjectExtending +{ + [Flags] + public enum MappingPropertyDefinitionChecks : byte + { + /// + /// No check. Copy all extra properties from the source to the destination. + /// + None = 0, + + /// + /// Copy the extra properties defined for the source class. + /// + Source = 1, + + /// + /// Copy the extra properties defined for the destination class. + /// + Destination = 2, + + /// + /// Copy extra properties defined for both of the source and destination classes. + /// + Both = Source | Destination + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionInfo.cs new file mode 100644 index 0000000000..89ecbab574 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionInfo.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using JetBrains.Annotations; +using Volo.Abp.Data; + +namespace Volo.Abp.ObjectExtending +{ + public class ObjectExtensionInfo + { + [NotNull] + public Type Type { get; } + + [NotNull] + protected Dictionary Properties { get; } + + [NotNull] + public Dictionary Configuration { get; } + + public ObjectExtensionInfo([NotNull] Type type) + { + Type = Check.AssignableTo(type, nameof(type)); + Properties = new Dictionary(); + Configuration = new Dictionary(); + } + + public virtual bool HasProperty(string propertyName) + { + return Properties.ContainsKey(propertyName); + } + + [NotNull] + public virtual ObjectExtensionInfo AddOrUpdateProperty( + [NotNull] string propertyName, + [CanBeNull] Action configureAction = null) + { + return AddOrUpdateProperty( + typeof(TProperty), + propertyName, + configureAction + ); + } + + [NotNull] + public virtual ObjectExtensionInfo AddOrUpdateProperty( + [NotNull] Type propertyType, + [NotNull] string propertyName, + [CanBeNull] Action configureAction = null) + { + Check.NotNull(propertyType, nameof(propertyType)); + Check.NotNull(propertyName, nameof(propertyName)); + + var propertyInfo = Properties.GetOrAdd( + propertyName, + () => new ObjectExtensionPropertyInfo(this, propertyType, propertyName) + ); + + configureAction?.Invoke(propertyInfo); + + return this; + } + + [NotNull] + public virtual ImmutableList GetProperties() + { + return Properties.Values.ToImmutableList(); + } + + [CanBeNull] + public virtual ObjectExtensionPropertyInfo GetPropertyOrNull( + [NotNull] string propertyName) + { + Check.NotNullOrEmpty(propertyName, nameof(propertyName)); + + return Properties.GetOrDefault(propertyName); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs new file mode 100644 index 0000000000..9f97b45261 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using JetBrains.Annotations; +using Volo.Abp.Data; + +namespace Volo.Abp.ObjectExtending +{ + public class ObjectExtensionManager + { + public static ObjectExtensionManager Instance { get; set; } = new ObjectExtensionManager(); + + protected Dictionary ObjectsExtensions { get; } + + protected internal ObjectExtensionManager() + { + ObjectsExtensions = new Dictionary(); + } + + [NotNull] + public virtual ObjectExtensionManager AddOrUpdate( + [CanBeNull] Action configureAction = null) + where TObject : IHasExtraProperties + { + return AddOrUpdate(typeof(TObject), configureAction); + } + + [NotNull] + public virtual ObjectExtensionManager AddOrUpdate( + [NotNull] Type[] types, + [CanBeNull] Action configureAction = null) + { + Check.NotNull(types, nameof(types)); + + foreach (var type in types) + { + AddOrUpdate(type, configureAction); + } + + return this; + } + + [NotNull] + public virtual ObjectExtensionManager AddOrUpdate( + [NotNull] Type type, + [CanBeNull] Action configureAction = null) + { + Check.AssignableTo(type, nameof(type)); + + var extensionInfo = ObjectsExtensions.GetOrAdd( + type, + () => new ObjectExtensionInfo(type) + ); + + configureAction?.Invoke(extensionInfo); + + return this; + } + + [CanBeNull] + public virtual ObjectExtensionInfo GetOrNull() + where TObject : IHasExtraProperties + { + return GetOrNull(typeof(TObject)); + } + + [CanBeNull] + public virtual ObjectExtensionInfo GetOrNull([NotNull] Type type) + { + Check.AssignableTo(type, nameof(type)); + + return ObjectsExtensions.GetOrDefault(type); + } + + [NotNull] + public virtual ImmutableList GetExtendedObjects() + { + return ObjectsExtensions.Values.ToImmutableList(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManagerExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManagerExtensions.cs new file mode 100644 index 0000000000..21d139913b --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionManagerExtensions.cs @@ -0,0 +1,83 @@ +using System; +using JetBrains.Annotations; +using Volo.Abp.Data; + +namespace Volo.Abp.ObjectExtending +{ + public static class ObjectExtensionManagerExtensions + { + [NotNull] + public static ObjectExtensionManager AddOrUpdateProperty( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] Type[] objectTypes, + [NotNull] string propertyName, + [CanBeNull] Action configureAction = null) + { + return objectExtensionManager.AddOrUpdateProperty( + objectTypes, + typeof(TProperty), + propertyName, configureAction + ); + } + + [NotNull] + public static ObjectExtensionManager AddOrUpdateProperty( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] string propertyName, + [CanBeNull] Action configureAction = null) + where TObject : IHasExtraProperties + { + return objectExtensionManager.AddOrUpdateProperty( + typeof(TObject), + typeof(TProperty), + propertyName, + configureAction + ); + } + + [NotNull] + public static ObjectExtensionManager AddOrUpdateProperty( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] Type[] objectTypes, + [NotNull] Type propertyType, + [NotNull] string propertyName, + [CanBeNull] Action configureAction = null) + { + Check.NotNull(objectTypes, nameof(objectTypes)); + + foreach (var objectType in objectTypes) + { + objectExtensionManager.AddOrUpdateProperty( + objectType, + propertyType, + propertyName, + configureAction + ); + } + + return objectExtensionManager; + } + + [NotNull] + public static ObjectExtensionManager AddOrUpdateProperty( + [NotNull] this ObjectExtensionManager objectExtensionManager, + [NotNull] Type objectType, + [NotNull] Type propertyType, + [NotNull] string propertyName, + [CanBeNull] Action configureAction = null) + { + Check.NotNull(objectExtensionManager, nameof(objectExtensionManager)); + + return objectExtensionManager.AddOrUpdate( + objectType, + options => + { + options.AddOrUpdateProperty( + propertyType, + propertyName, + configureAction + ); + }); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs new file mode 100644 index 0000000000..2edae9a3f8 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ObjectExtensionPropertyInfo.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.ObjectExtending +{ + public class ObjectExtensionPropertyInfo + { + [NotNull] + public ObjectExtensionInfo ObjectExtension { get; } + + [NotNull] + public string Name { get; } + + [NotNull] + public Type Type { get; } + + /// + /// Indicates whether to check the other side of the object mapping + /// if it explicitly defines the property. This property is used in; + /// + /// * .MapExtraPropertiesTo() extension method. + /// * .MapExtraProperties() configuration for the AutoMapper. + /// + /// It this is true, these methods check if the mapping object + /// has defined the property using the . + /// + /// Default: null (unspecified, uses the default logic). + /// + public bool? CheckPairDefinitionOnMapping { get; set; } + + [NotNull] + public Dictionary Configuration { get; } + + public ObjectExtensionPropertyInfo( + [NotNull] ObjectExtensionInfo objectExtension, + [NotNull] Type type, + [NotNull] string name) + { + ObjectExtension = Check.NotNull(objectExtension, nameof(objectExtension)); + Type = Check.NotNull(type, nameof(type)); + Name = Check.NotNull(name, nameof(name)); + + Configuration = new Dictionary(); + } + } +} diff --git a/framework/src/Volo.Abp.Quartz/FodyWeavers.xml b/framework/src/Volo.Abp.Quartz/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.Quartz/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Quartz/FodyWeavers.xsd b/framework/src/Volo.Abp.Quartz/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Quartz/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.Quartz/Volo.Abp.Quartz.csproj b/framework/src/Volo.Abp.Quartz/Volo.Abp.Quartz.csproj new file mode 100644 index 0000000000..8e29b1f803 --- /dev/null +++ b/framework/src/Volo.Abp.Quartz/Volo.Abp.Quartz.csproj @@ -0,0 +1,25 @@ + + + + + + + netstandard2.0 + Volo.Abp.Quartz + Volo.Abp.Quartz + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzJobFactory.cs b/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzJobFactory.cs new file mode 100644 index 0000000000..428dbe03af --- /dev/null +++ b/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzJobFactory.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Quartz; +using Quartz.Spi; + +namespace Volo.Abp.Quartz +{ + /// + /// Get the job from the dependency injection + /// + public class AbpQuartzJobFactory : IJobFactory + { + private readonly IServiceProvider _serviceProvider; + + private readonly ConcurrentDictionary _scopes = new ConcurrentDictionary(); + + public AbpQuartzJobFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) + { + var scope = _serviceProvider.CreateScope(); + var job = scope.ServiceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob; + if (job == null) + { + throw new ArgumentException("Given job does not implement IJob"); + } + _scopes.TryAdd(job, scope); + return job; + } + + public void ReturnJob(IJob job) + { + _scopes.TryRemove(job, out var serviceScope); + serviceScope?.Dispose(); + } + } +} diff --git a/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs b/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs new file mode 100644 index 0000000000..0c6c65b1ac --- /dev/null +++ b/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using Quartz; +using Quartz.Impl; +using Quartz.Spi; +using Volo.Abp.Modularity; +using Volo.Abp.Threading; + +namespace Volo.Abp.Quartz +{ + public class AbpQuartzModule : AbpModule + { + private IScheduler _scheduler; + + public override void ConfigureServices(ServiceConfigurationContext context) + { + var options = context.Services.ExecutePreConfiguredActions(); + context.Services.AddSingleton(AsyncHelper.RunSync(() => new StdSchedulerFactory(options.Properties).GetScheduler())); + context.Services.AddSingleton(typeof(IJobFactory), typeof(AbpQuartzJobFactory)); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + _scheduler = context.ServiceProvider.GetService(); + _scheduler.JobFactory = context.ServiceProvider.GetService(); + AsyncHelper.RunSync(() => _scheduler.Start()); + } + + public override void OnApplicationShutdown(ApplicationShutdownContext context) + { + //TODO: ABP may provide two methods for application shutdown: OnPreApplicationShutdown & OnApplicationShutdown + AsyncHelper.RunSync(() => _scheduler.Shutdown()); + } + } +} diff --git a/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzPreOptions.cs b/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzPreOptions.cs new file mode 100644 index 0000000000..e86f993d1d --- /dev/null +++ b/framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzPreOptions.cs @@ -0,0 +1,14 @@ +using System.Collections.Specialized; + +namespace Volo.Abp.Quartz +{ + public class AbpQuartzPreOptions + { + public NameValueCollection Properties { get; set; } + + public AbpQuartzPreOptions() + { + Properties = new NameValueCollection(); + } + } +} diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs index 1214a82a99..135156f7a8 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; namespace Volo.Abp.RabbitMQ @@ -17,6 +18,8 @@ namespace Volo.Abp.RabbitMQ protected IConnectionPool ConnectionPool { get; } + protected IExceptionNotifier ExceptionNotifier { get; } + protected AbpTimer Timer { get; } protected ExchangeDeclareConfiguration Exchange { get; private set; } @@ -35,10 +38,12 @@ namespace Volo.Abp.RabbitMQ public RabbitMqMessageConsumer( IConnectionPool connectionPool, - AbpTimer timer) + AbpTimer timer, + IExceptionNotifier exceptionNotifier) { ConnectionPool = connectionPool; Timer = timer; + ExceptionNotifier = exceptionNotifier; Logger = NullLogger.Instance; QueueBindCommands = new ConcurrentQueue(); @@ -114,6 +119,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } @@ -180,6 +186,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } @@ -197,6 +204,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex); + await ExceptionNotifier.NotifyAsync(ex); } } @@ -214,6 +222,7 @@ namespace Volo.Abp.RabbitMQ catch (Exception ex) { Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); } } diff --git a/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs b/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs index 694fb7baa6..f4e4bba1a0 100644 --- a/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs +++ b/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs @@ -17,8 +17,11 @@ namespace System.Security.Principal { return null; } - - return Guid.Parse(userIdOrNull.Value); + if (Guid.TryParse(userIdOrNull.Value, out Guid result)) + { + return result; + } + return null; } public static Guid? FindUserId([NotNull] this IIdentity identity) diff --git a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs index cae8ac984f..5c5041cdf6 100644 --- a/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs +++ b/framework/src/Volo.Abp.Threading/Volo/Abp/Threading/AbpTimer.cs @@ -3,11 +3,12 @@ using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; namespace Volo.Abp.Threading { /// - /// A roboust timer implementation that ensures no overlapping occurs. It waits exactly specified between ticks. + /// A robust timer implementation that ensures no overlapping occurs. It waits exactly specified between ticks. /// public class AbpTimer : ITransientDependency { @@ -29,15 +30,23 @@ namespace Volo.Abp.Threading public ILogger Logger { get; set; } + public IExceptionNotifier ExceptionNotifier { get; set; } + private readonly Timer _taskTimer; private volatile bool _performingTasks; private volatile bool _isRunning; public AbpTimer() { + ExceptionNotifier = NullExceptionNotifier.Instance; Logger = NullLogger.Instance; - _taskTimer = new Timer(TimerCallBack, null, Timeout.Infinite, Timeout.Infinite); + _taskTimer = new Timer( + TimerCallBack, + null, + Timeout.Infinite, + Timeout.Infinite + ); } public void Start(CancellationToken cancellationToken = default) @@ -89,9 +98,10 @@ namespace Volo.Abp.Threading { Elapsed.InvokeSafely(this, new EventArgs()); } - catch + catch(Exception ex) { - + Logger.LogException(ex); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex)); } finally { diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/sl.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/sl.json new file mode 100644 index 0000000000..149a3295e9 --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/sl.json @@ -0,0 +1,6 @@ +{ + "culture": "sl", + "texts": { + "Menu:Administration": "Administracija" + } +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json new file mode 100644 index 0000000000..3fb22139b7 --- /dev/null +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json @@ -0,0 +1,62 @@ +{ + "culture": "ar", + "texts": { + "InternalServerErrorMessage": "حدث خطأ داخلي أثناء طلبك!", + "ValidationErrorMessage": "طلبك غير صحيح!", + "ValidationNarrativeErrorMessageTitle": "تم الكشف عن الأخطاء التالية أثناء التحقق .", + "DefaultErrorMessage": "حدث خطأ!", + "DefaultErrorMessageDetail": "لم يتم إرسال تفاصيل الخطأ بواسطة الخادم.", + "DefaultErrorMessage401": "أنت غير مصدق!", + "DefaultErrorMessage401Detail": "يجب عليك تسجيل الدخول لأداء هذه العملية.", + "DefaultErrorMessage403": "أنك غير مخول!", + "DefaultErrorMessage403Detail": "لا يسمح لك بإجراء هذه العملية!", + "DefaultErrorMessage404": "المورد غير موجود!", + "DefaultErrorMessage404Detail": "لم يتم العثور على المورد المطلوب على الخادم!", + "EntityNotFoundErrorMessage": "لا يوجد كيان {0} بالمعرف = {1}!", + "Languages": "اللغات", + "Error": "خطأ", + "AreYouSure": "هل أنت متأكد؟", + "Cancel": "إلغاء", + "Yes": "نعم", + "No": "لا", + "Ok": "حسنا", + "Close": "أغلق", + "Save": "حفظ", + "SavingWithThreeDot": "حفظ...", + "Actions": "أجراءات", + "Delete": "حذف", + "Edit": "تعديل", + "Refresh": "تحديث", + "Language": "لغة", + "LoadMore": "تحميل المزيد", + "ProcessingWithThreeDot": "معالجة...", + "LoadingWithThreeDot": "جار التحميل...", + "Welcome": "أهلا بك", + "Login": "تسجيل الدخول", + "Register": "تسجيل", + "Logout": "تسجيل خروج", + "Submit": "إرسال", + "Back": "عودة", + "PagerSearch": "بحث", + "PagerNext": "التالى", + "PagerPrevious": "السابق", + "PagerFirst": "الأول", + "PagerLast": "الاخير", + "PagerInfo": "عرض _START_ الي _END_ من _TOTAL_ إدخالات", + "PagerInfoEmpty": "عرض 0 الي 0 من 0 إدخالات", + "PagerInfoFiltered": "(تمت تصفيته من _MAX_ مجموع الإدخالات)", + "NoDataAvailableInDatatable": "لا توجد بيانات متاحة في الجدول", + "PagerShowMenuEntries": "عرض _MENU_ إدخالات", + "DatatableActionDropdownDefaultText": "أجراءات", + "ChangePassword": "تغيير كلمة السر", + "PersonalInfo": "ملفي الشخصي", + "AreYouSureYouWantToCancelEditingWarningMessage": "لم تحفظ التغييرات.", + "UnhandledException": "استثناء غير معالج!", + "401Message": "غير مصرح", + "403Message": "ممنوع", + "404Message": "الصفحة غير موجودة", + "500Message": "خطأ في الخادم الداخلي", + "GoHomePage": "اذهب الى الصفحة الرئيسية", + "GoBack": "عد" + } +} diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs index 8ef50ce9f7..fb3be504c5 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/AbpValidationException.cs @@ -58,6 +58,16 @@ namespace Volo.Abp.Validation LogLevel = LogLevel.Warning; } + /// + /// Constructor. + /// + /// Validation errors + public AbpValidationException(IList validationErrors) + { + ValidationErrors = validationErrors; + LogLevel = LogLevel.Warning; + } + /// /// Constructor. /// diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/HasValidationErrorsExtensions.cs b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/HasValidationErrorsExtensions.cs new file mode 100644 index 0000000000..8db6898276 --- /dev/null +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/HasValidationErrorsExtensions.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace Volo.Abp.Validation +{ + public static class HasValidationErrorsExtensions + { + public static TException WithValidationError([NotNull] this TException exception, [NotNull] ValidationResult validationError) + where TException : IHasValidationErrors + { + Check.NotNull(exception, nameof(exception)); + Check.NotNull(validationError, nameof(validationError)); + + exception.ValidationErrors.Add(validationError); + + return exception; + } + + public static TException WithValidationError([NotNull] this TException exception, string errorMessage, params string[] memberNames) + where TException : IHasValidationErrors + { + var validationResult = memberNames.IsNullOrEmpty() + ? new ValidationResult(errorMessage) + : new ValidationResult(errorMessage, memberNames); + + return exception.WithValidationError(validationResult); + } + } +} \ 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 2ba15cae3d..46995e4eb4 100644 --- a/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj +++ b/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs index 44b206e462..8dbbf3eac2 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using Shouldly; using Volo.Abp.Http.Modeling; using Xunit; @@ -12,6 +13,15 @@ namespace Volo.Abp.AspNetCore.Mvc.ApiExploring { var model = await GetResponseAsObjectAsync("/api/abp/api-definition"); model.ShouldNotBeNull(); + model.Types.IsNullOrEmpty().ShouldBeTrue(); + } + + [Fact] + public async Task GetAsync_IncludeTypes() + { + var model = await GetResponseAsObjectAsync("/api/abp/api-definition?includeTypes=true"); + model.ShouldNotBeNull(); + model.Types.IsNullOrEmpty().ShouldBeFalse(); } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController.cs index 3c8d5dc4db..49dfef9db8 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController.cs @@ -5,7 +5,6 @@ using Volo.Abp.Auditing; namespace Volo.Abp.AspNetCore.Mvc.Auditing { [Route("api/audit-test")] - [Audited] public class AuditTestController : AbpController { private readonly AbpAuditingOptions _options; diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs index 5243307116..0de46fbe55 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs @@ -1,6 +1,10 @@ using System.Net; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NSubstitute; using Shouldly; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Xunit; @@ -8,12 +12,27 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling { public class ExceptionTestController_Tests : AspNetCoreMvcTestBase { + private IExceptionSubscriber _fakeExceptionSubscriber; + + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + base.ConfigureServices(context, services); + + _fakeExceptionSubscriber = Substitute.For(); + + services.AddSingleton(_fakeExceptionSubscriber); + } + [Fact] public async Task Should_Return_RemoteServiceErrorResponse_For_UserFriendlyException_For_Void_Return_Value() { var result = await GetResponseAsObjectAsync("/api/exception-test/UserFriendlyException1", HttpStatusCode.Forbidden); result.Error.ShouldNotBeNull(); result.Error.Message.ShouldBe("This is a sample exception!"); + + _fakeExceptionSubscriber + .Received() + .HandleAsync(Arg.Any()); } [Fact] @@ -24,6 +43,10 @@ namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling "/api/exception-test/UserFriendlyException2" ) ); + + _fakeExceptionSubscriber + .DidNotReceive() + .HandleAsync(Arg.Any()); } } } 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 0274cd4d69..e58817d91c 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 @@ -31,16 +31,16 @@
- - - Secondary - Success - Danger - Warning - Info - Light - Dark - Link + + +Secondary +Success +Danger +Warning +Info +Light +Dark +Link
@@ -116,14 +116,14 @@
- Primary - Secondary - Success - Danger - Warning - Info - Light - Dark +Primary +Secondary +Success +Danger +Warning +Info +Light +Dark
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Modals.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Modals.cshtml index 74116488ad..ad2f4a602c 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Modals.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Modals.cshtml @@ -45,16 +45,16 @@
-

-<abp-button button-type="Primary" data-toggle="modal" data-target="#myModal">Launch modal</abp-button>
-
-<abp-modal centered="true" size="Large" id="myModal">
-   <abp-modal-header title="Modal title"></abp-modal-header>
-   <abp-modal-body>
-       Woohoo, you're reading this text in a modal!
-   </abp-modal-body>
-   <abp-modal-footer buttons="(AbpModalButtons.Save|AbpModalButtons.Close)"></abp-modal-footer>
-</abp-modal>
+                

+    <abp-button button-type="Primary" data-toggle="modal" data-target="#myModal">Launch modal</abp-button>
+
+    <abp-modal centered="true" size="Large" id="myModal">
+    <abp-modal-header title="Modal title"></abp-modal-header>
+    <abp-modal-body>
+    Woohoo, you're reading this text in a modal!
+    </abp-modal-body>
+    <abp-modal-footer buttons="@(AbpModalButtons.Save|AbpModalButtons.Close)"></abp-modal-footer>
+    </abp-modal>
 
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml index 7f43407831..779494d70f 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml @@ -25,26 +25,29 @@

Popovers

-

Based on Bootstrap Popovers.

+

Based on Bootstrap Popovers.

Example

-
- - - Popover Default - - - Popover With Title - - - Dismissible Popover - - - Disabled Popover - +
+ + + Popover Default + + + Popover With Title + + + Dismissible Popover + + + Disabled Popover + + + Disabled Popover +
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs index 086c20c365..5b303d4489 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Startup.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs new file mode 100644 index 0000000000..88327941eb --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/AbpAspNetCoreMvcUiThemeBasicDemoModule.cs @@ -0,0 +1,63 @@ +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; +using Volo.Abp.UI; +using Volo.Abp.UI.Navigation; +using Volo.Abp.VirtualFileSystem; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo +{ + [DependsOn( + typeof(AbpAspNetCoreMvcUiBasicThemeModule), + typeof(AbpAspNetCoreMvcUiThemeSharedDemoModule), + typeof(AbpAutofacModule) + )] + public class AbpAspNetCoreMvcUiThemeBasicDemoModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var env = context.Services.GetHostingEnvironment(); + + if (env.IsDevelopment()) + { + Configure(options => + { + options.FileSets.ReplaceEmbeddedByPhysical(Path.Combine(env.ContentRootPath, string.Format("..{0}..{0}src{0}Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo", Path.DirectorySeparatorChar))); + }); + } + + Configure(options => + { + options.StyleBundles + .Get(StandardBundles.Styles.Global) + .AddFiles("/demo/styles/main.css"); + }); + + Configure(options => + { + options.MenuContributors.Add(new BasicThemeDemoMenuContributor()); + }); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseVirtualFiles(); + app.UseRouting(); + app.UseMvcWithDefaultRouteAndArea(); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/BasicThemeDemoMenuContributor.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/BasicThemeDemoMenuContributor.cs new file mode 100644 index 0000000000..20f0332c8b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/BasicThemeDemoMenuContributor.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo +{ + public class BasicThemeDemoMenuContributor : IMenuContributor + { + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if(context.Menu.Name == StandardMenus.Main) + { + AddMainMenuItems(context); + } + + return Task.CompletedTask; + } + + private void AddMainMenuItems(MenuConfigurationContext context) + { + var menuItem = new ApplicationMenuItem("BasicThemeDemo.Components", "Components"); + + var items = new List() + { + new ApplicationMenuItem("BasicThemeDemo.Components.Alerts", "Alerts", url: "/Components/Alerts"), + new ApplicationMenuItem("BasicThemeDemo.Components.Badges", "Badges", url: "/Components/Badges"), + new ApplicationMenuItem("BasicThemeDemo.Components.Borders", "Borders", url: "/Components/Borders"), + new ApplicationMenuItem("BasicThemeDemo.Components.Breadcrumbs", "Breadcrumbs", url: "/Components/Breadcrumbs"), + new ApplicationMenuItem("BasicThemeDemo.Components.Buttons", "Buttons", url: "/Components/Buttons"), + new ApplicationMenuItem("BasicThemeDemo.Components.ButtonGroups", "ButtonGroups", url: "/Components/ButtonGroups"), + new ApplicationMenuItem("BasicThemeDemo.Components.Cards", "Cards", url: "/Components/Cards"), + new ApplicationMenuItem("BasicThemeDemo.Components.Carousel", "Carousel", url: "/Components/Carousel"), + new ApplicationMenuItem("BasicThemeDemo.Components.Collapse", "Collapse", url: "/Components/Collapse"), + new ApplicationMenuItem("BasicThemeDemo.Components.Dropdowns", "Dropdowns", url: "/Components/Dropdowns"), + new ApplicationMenuItem("BasicThemeDemo.Components.DynamicForms", "DynamicForms", url: "/Components/DynamicForms"), + new ApplicationMenuItem("BasicThemeDemo.Components.FormElements", "FormElements", url: "/Components/FormElements"), + new ApplicationMenuItem("BasicThemeDemo.Components.Grids", "Grids", url: "/Components/Grids"), + new ApplicationMenuItem("BasicThemeDemo.Components.ListGroups", "List Groups", url: "/Components/ListGroups"), + new ApplicationMenuItem("BasicThemeDemo.Components.Modals", "Modals", url: "/Components/Modals"), + new ApplicationMenuItem("BasicThemeDemo.Components.Navs", "Navs", url: "/Components/Navs"), + new ApplicationMenuItem("BasicThemeDemo.Components.Navbars", "Navbars", url: "/Components/Navbars"), + new ApplicationMenuItem("BasicThemeDemo.Components.Paginator", "Paginator", url: "/Components/Paginator"), + new ApplicationMenuItem("BasicThemeDemo.Components.Popovers", "Popovers", url: "/Components/Popovers"), + new ApplicationMenuItem("BasicThemeDemo.Components.ProgressBars", "Progress Bars", url: "/Components/ProgressBars"), + new ApplicationMenuItem("BasicThemeDemo.Components.Tables", "Tables", url: "/Components/Tables"), + new ApplicationMenuItem("BasicThemeDemo.Components.Tabs", "Tabs", url: "/Components/Tabs"), + new ApplicationMenuItem("BasicThemeDemo.Components.Tooltips", "Tooltips", url: "/Components/Tooltips") + }; + + items.OrderBy(x => x.Name) + .ToList() + .ForEach(x => menuItem.AddItem(x)); + + context.Menu.AddItem(menuItem); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Alerts/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Alerts/Index.cshtml new file mode 100644 index 0000000000..245cc0cacd --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Alerts/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.AlertsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Alerts.IndexModel +

Alerts

+

Based on Bootstrap Alert.

+ +@await Component.InvokeAsync(typeof(AlertsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Alerts/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Alerts/Index.cshtml.cs new file mode 100644 index 0000000000..1187dc021a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Alerts/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Alerts +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Badges/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Badges/Index.cshtml new file mode 100644 index 0000000000..31a03ec2a9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Badges/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BadgesDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Badges.IndexModel +

Badges

+

Based on Bootstrap Badge.

+ +@await Component.InvokeAsync(typeof(BadgesDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Badges/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Badges/Index.cshtml.cs new file mode 100644 index 0000000000..75067fcc35 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Badges/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Badges +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Borders/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Borders/Index.cshtml new file mode 100644 index 0000000000..ce4d20f635 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Borders/Index.cshtml @@ -0,0 +1,15 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BordersDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Borders.IndexModel +

Borders

+

Based on Bootstrap Border.

+ +@await Component.InvokeAsync(typeof(BordersDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Borders/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Borders/Index.cshtml.cs new file mode 100644 index 0000000000..65ec1dc8dd --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Borders/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Borders +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Breadcrumbs/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Breadcrumbs/Index.cshtml new file mode 100644 index 0000000000..380c3b1e66 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Breadcrumbs/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BreadcrumbsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Breadcrumbs.IndexModel +

Breadcrumbs

+

Based on Bootstrap Breadcrumb.

+ +@await Component.InvokeAsync(typeof(BreadcrumbsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Breadcrumbs/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Breadcrumbs/Index.cshtml.cs new file mode 100644 index 0000000000..e4f35e84a6 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Breadcrumbs/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Breadcrumbs +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ButtonGroups/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ButtonGroups/Index.cshtml new file mode 100644 index 0000000000..d9f803a1bd --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ButtonGroups/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonGroupsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.ButtonGroups.IndexModel +

Button Groups

+

Based on Bootstrap Button group.

+ +@await Component.InvokeAsync(typeof(ButtonGroupsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ButtonGroups/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ButtonGroups/Index.cshtml.cs new file mode 100644 index 0000000000..fcc91c386b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ButtonGroups/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.ButtonGroups +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Buttons/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Buttons/Index.cshtml new file mode 100644 index 0000000000..317a6d2510 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Buttons/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Buttons.IndexModel +

Buttons

+

Based on Bootstrap Buttons.

+ +@await Component.InvokeAsync(typeof(ButtonsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Buttons/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Buttons/Index.cshtml.cs new file mode 100644 index 0000000000..e9417a5eee --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Buttons/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Buttons +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Cards/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Cards/Index.cshtml new file mode 100644 index 0000000000..d0a2be5f44 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Cards/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CardsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Cards.IndexModel +

Cards

+

Based on Bootstrap card.

+ +@await Component.InvokeAsync(typeof(CardsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Cards/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Cards/Index.cshtml.cs new file mode 100644 index 0000000000..94761d1828 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Cards/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Cards +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Carousel/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Carousel/Index.cshtml new file mode 100644 index 0000000000..3c25da2b0c --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Carousel/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CarouselDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Carousel.IndexModel +

Carousel

+

Based on Bootstrap carousel.

+ +@await Component.InvokeAsync(typeof(CarouselDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Carousel/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Carousel/Index.cshtml.cs new file mode 100644 index 0000000000..e4926f5ac0 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Carousel/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Carousel +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Collapse/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Collapse/Index.cshtml new file mode 100644 index 0000000000..5dde43970d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Collapse/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CollapseDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Collapse.IndexModel +

Collapse

+

Based on Bootstrap Collapse.

+ +@await Component.InvokeAsync(typeof(CollapseDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Collapse/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Collapse/Index.cshtml.cs new file mode 100644 index 0000000000..afaaf65d10 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Collapse/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Collapse +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Dropdowns/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Dropdowns/Index.cshtml new file mode 100644 index 0000000000..2aa853bac9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Dropdowns/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DropdownsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Dropdowns.IndexModel +

Cards

+

Based on Bootstrap button.

+ +@await Component.InvokeAsync(typeof(DropdownsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Dropdowns/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Dropdowns/Index.cshtml.cs new file mode 100644 index 0000000000..6d27a631d4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Dropdowns/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Dropdowns +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/DynamicForms/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/DynamicForms/Index.cshtml new file mode 100644 index 0000000000..7b7092980a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/DynamicForms/Index.cshtml @@ -0,0 +1,6 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.DynamicFormsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.DynamicForms.IndexModel +

Dynamic Forms

+ +@await Component.InvokeAsync(typeof(DynamicFormsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/DynamicForms/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/DynamicForms/Index.cshtml.cs new file mode 100644 index 0000000000..a9413ce767 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/DynamicForms/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.DynamicForms +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/FormElements/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/FormElements/Index.cshtml new file mode 100644 index 0000000000..033bb4210d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/FormElements/Index.cshtml @@ -0,0 +1,6 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.FormElementsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.FormElements.IndexModel +

Form Elements

+ +@await Component.InvokeAsync(typeof(FormElementsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/FormElements/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/FormElements/Index.cshtml.cs new file mode 100644 index 0000000000..a2864fed98 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/FormElements/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.FormElements +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Grids/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Grids/Index.cshtml new file mode 100644 index 0000000000..de80040d47 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Grids/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.GridsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Grids.IndexModel +

Grids

+

Based on Bootstrap grid.

+ +@await Component.InvokeAsync(typeof(GridsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Grids/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Grids/Index.cshtml.cs new file mode 100644 index 0000000000..e8d829cf52 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Grids/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Grids +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ListGroups/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ListGroups/Index.cshtml new file mode 100644 index 0000000000..64eadfa161 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ListGroups/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ListGroupsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.ListGroups.IndexModel +

List Groups

+

Based on Bootstrap List Group.

+ +@await Component.InvokeAsync(typeof(ListGroupsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ListGroups/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ListGroups/Index.cshtml.cs new file mode 100644 index 0000000000..9f324dcbb0 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ListGroups/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.ListGroups +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Modals/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Modals/Index.cshtml new file mode 100644 index 0000000000..cec4610e76 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Modals/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ModalsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Modals.IndexModel +

Modals

+

Based on Bootstrap Modal.

+ +@await Component.InvokeAsync(typeof(ModalsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Modals/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Modals/Index.cshtml.cs new file mode 100644 index 0000000000..35749b0832 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Modals/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Modals +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navbars/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navbars/Index.cshtml new file mode 100644 index 0000000000..52c8ccb41f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navbars/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.NavbarsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Modals.IndexModel +

Navbars

+

Based on Bootstrap Navbar.

+ +@await Component.InvokeAsync(typeof(NavbarsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navbars/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navbars/Index.cshtml.cs new file mode 100644 index 0000000000..c2814e6686 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navbars/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navs/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navs/Index.cshtml new file mode 100644 index 0000000000..a22f1cd47d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navs/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.NavsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Navs.IndexModel +

Navs

+

Based on Bootstrap Navs.

+ +@await Component.InvokeAsync(typeof(NavsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navs/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navs/Index.cshtml.cs new file mode 100644 index 0000000000..e9f901052f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Navs/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Navs +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml new file mode 100644 index 0000000000..c43a2afbc9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml @@ -0,0 +1,14 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.PaginatorDemo +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Paginator +@model IndexModel + +@section scripts { + + + +} + +

Paginator

+ +@await Component.InvokeAsync(typeof(PaginatorDemoViewComponent), new { pagerModel = Model.PagerModel }) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs new file mode 100644 index 0000000000..50659a0ab4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Paginator +{ + public class IndexModel : PageModel + { + public PagerModel PagerModel { get; set; } + + public void OnGet(int currentPage = 1, string sort = null) + { + PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js new file mode 100644 index 0000000000..843e0dea00 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js @@ -0,0 +1,9 @@ +$(function () { + var links = $("a.page-link"); + + $.each(links, function (key, value) { + var oldUrl = links[key].getAttribute("href"); + var value = Number(oldUrl.match(/currentPage=(\d+)&page/)[1]); + links[key].setAttribute("href", "/Components/Paginator?currentPage=" + value); + }) +}); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Popovers/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Popovers/Index.cshtml new file mode 100644 index 0000000000..ebc878c511 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Popovers/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.PopoversDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Popovers.IndexModel +

Popovers

+

Based on Bootstrap Popovers.

+ +@await Component.InvokeAsync(typeof(PopoversDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Popovers/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Popovers/Index.cshtml.cs new file mode 100644 index 0000000000..6bb055517b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Popovers/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Popovers +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ProgressBars/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ProgressBars/Index.cshtml new file mode 100644 index 0000000000..b2c51f412e --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ProgressBars/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ProgressBarsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.ProgressBars.IndexModel +

Progress Bars

+

Based on Bootstrap Progress Bars.

+ +@await Component.InvokeAsync(typeof(ProgressBarsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ProgressBars/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ProgressBars/Index.cshtml.cs new file mode 100644 index 0000000000..e2be5ad928 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/ProgressBars/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.ProgressBars +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tables/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tables/Index.cshtml new file mode 100644 index 0000000000..a2048c7384 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tables/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TablesDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Tables.IndexModel +

Tables

+

Based on Bootstrap Tables.

+ +@await Component.InvokeAsync(typeof(TablesDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tables/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tables/Index.cshtml.cs new file mode 100644 index 0000000000..84289beeff --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tables/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Tables +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tabs/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tabs/Index.cshtml new file mode 100644 index 0000000000..7c5cff5970 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tabs/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TabsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Tabs.IndexModel +

Tabs

+

Based on Bootstrap tab.

+ +@await Component.InvokeAsync(typeof(TabsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tabs/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tabs/Index.cshtml.cs new file mode 100644 index 0000000000..62f727d427 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tabs/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Tabs +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tooltips/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tooltips/Index.cshtml new file mode 100644 index 0000000000..617bb37582 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tooltips/Index.cshtml @@ -0,0 +1,7 @@ +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.TooltipsDemo +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Tooltips.IndexModel +

Tooltips

+

Based on Bootstrap Tooltips.

+ +@await Component.InvokeAsync(typeof(TooltipsDemoViewComponent)) \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tooltips/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tooltips/Index.cshtml.cs new file mode 100644 index 0000000000..8f5170c171 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Tooltips/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Tooltips +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Index.cshtml new file mode 100644 index 0000000000..233d0f7c6a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Index.cshtml @@ -0,0 +1,3 @@ +@page +@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.IndexModel +

Basic Theme Demo

\ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Index.cshtml.cs new file mode 100644 index 0000000000..8d1eec93d4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/_ViewImports.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/_ViewImports.cshtml new file mode 100644 index 0000000000..229004aa93 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using System.Globalization +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Program.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Program.cs new file mode 100644 index 0000000000..e0964df9c3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Program.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo +{ + public class Program + { + public static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.File("Logs/logs.txt") + .CreateLogger(); + + try + { + Log.Information("Starting web host."); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly!"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + + internal static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .UseAutofac() + .UseSerilog(); + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Properties/launchSettings.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Properties/launchSettings.json new file mode 100644 index 0000000000..e169e2d054 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:61659", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Startup.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Startup.cs new file mode 100644 index 0000000000..8c6a9c08de --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Startup.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddApplication(); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + app.InitializeApplication(); + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..7a95c77246 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj @@ -0,0 +1,29 @@ + + + + + + netcoreapp3.1 + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.Development.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/BasicAspNetCoreApplication/BasicAspNetCoreApplication/appsettings.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.json similarity index 100% rename from samples/BasicAspNetCoreApplication/BasicAspNetCoreApplication/appsettings.json rename to framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.json diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/gulpfile.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/gulpfile.js new file mode 100644 index 0000000000..f7ebc78f23 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/gulpfile.js @@ -0,0 +1,9 @@ +"use strict"; + +var gulp = require("gulp"), + path = require('path'), + copyResources = require('./node_modules/@abp/aspnetcore.mvc.ui/gulp/copy-resources.js'); + +exports.default = function(){ + return copyResources(path.resolve('./')); +}; \ No newline at end of file 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 new file mode 100644 index 0000000000..d09283b646 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json @@ -0,0 +1,9 @@ +{ + "version": "1.0.0", + "name": "asp.net", + "private": true, + "dependencies": { + "@abp/aspnetcore.mvc.ui.theme.basic": "^2.1.0" + }, + "devDependencies": {} +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/demo/styles/main.css b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/demo/styles/main.css new file mode 100644 index 0000000000..781074c39f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/demo/styles/main.css @@ -0,0 +1,29 @@ +.abp-component-demo-section { + border: 1px solid #999; + padding: 10px; +} + +.abp-component-demo-section-body { + padding-bottom: 10px; +} + +.abp-component-demo-section-raw-source { + background-color: #eee; + padding: 5px; +} + + .abp-component-demo-section-raw-source pre { + border: 1px solid #999; + margin: 5px; + } + + +.abp-component-demo-section-bs-source { + background-color: #ddd; + padding: 5px; +} + + .abp-component-demo-section-bs-source pre { + border: 1px solid #999; + margin: 5px; + } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/css/all.css b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/css/all.css new file mode 100644 index 0000000000..ba15e192e2 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/css/all.css @@ -0,0 +1,4463 @@ +/*! + * Font Awesome Free 5.12.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa, +.fas, +.far, +.fal, +.fad, +.fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; } + +.fa-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -.0667em; } + +.fa-xs { + font-size: .75em; } + +.fa-sm { + font-size: .875em; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: 2.5em; + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: -2em; + position: absolute; + text-align: center; + width: 2em; + line-height: inherit; } + +.fa-border { + border: solid 0.08em #eee; + border-radius: .1em; + padding: .2em .25em .15em; } + +.fa-pull-left { + float: left; } + +.fa-pull-right { + float: right; } + +.fa.fa-pull-left, +.fas.fa-pull-left, +.far.fa-pull-left, +.fal.fa-pull-left, +.fab.fa-pull-left { + margin-right: .3em; } + +.fa.fa-pull-right, +.fas.fa-pull-right, +.far.fa-pull-right, +.fal.fa-pull-right, +.fab.fa-pull-right { + margin-left: .3em; } + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical, +:root .fa-flip-both { + -webkit-filter: none; + filter: none; } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: #fff; } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ +.fa-500px:before { + content: "\f26e"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-acquisitions-incorporated:before { + content: "\f6af"; } + +.fa-ad:before { + content: "\f641"; } + +.fa-address-book:before { + content: "\f2b9"; } + +.fa-address-card:before { + content: "\f2bb"; } + +.fa-adjust:before { + content: "\f042"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-adobe:before { + content: "\f778"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-air-freshener:before { + content: "\f5d0"; } + +.fa-airbnb:before { + content: "\f834"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-align-center:before { + content: "\f037"; } + +.fa-align-justify:before { + content: "\f039"; } + +.fa-align-left:before { + content: "\f036"; } + +.fa-align-right:before { + content: "\f038"; } + +.fa-alipay:before { + content: "\f642"; } + +.fa-allergies:before { + content: "\f461"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-ambulance:before { + content: "\f0f9"; } + +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-anchor:before { + content: "\f13d"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-angle-double-down:before { + content: "\f103"; } + +.fa-angle-double-left:before { + content: "\f100"; } + +.fa-angle-double-right:before { + content: "\f101"; } + +.fa-angle-double-up:before { + content: "\f102"; } + +.fa-angle-down:before { + content: "\f107"; } + +.fa-angle-left:before { + content: "\f104"; } + +.fa-angle-right:before { + content: "\f105"; } + +.fa-angle-up:before { + content: "\f106"; } + +.fa-angry:before { + content: "\f556"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-ankh:before { + content: "\f644"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-apple-alt:before { + content: "\f5d1"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-archive:before { + content: "\f187"; } + +.fa-archway:before { + content: "\f557"; } + +.fa-arrow-alt-circle-down:before { + content: "\f358"; } + +.fa-arrow-alt-circle-left:before { + content: "\f359"; } + +.fa-arrow-alt-circle-right:before { + content: "\f35a"; } + +.fa-arrow-alt-circle-up:before { + content: "\f35b"; } + +.fa-arrow-circle-down:before { + content: "\f0ab"; } + +.fa-arrow-circle-left:before { + content: "\f0a8"; } + +.fa-arrow-circle-right:before { + content: "\f0a9"; } + +.fa-arrow-circle-up:before { + content: "\f0aa"; } + +.fa-arrow-down:before { + content: "\f063"; } + +.fa-arrow-left:before { + content: "\f060"; } + +.fa-arrow-right:before { + content: "\f061"; } + +.fa-arrow-up:before { + content: "\f062"; } + +.fa-arrows-alt:before { + content: "\f0b2"; } + +.fa-arrows-alt-h:before { + content: "\f337"; } + +.fa-arrows-alt-v:before { + content: "\f338"; } + +.fa-artstation:before { + content: "\f77a"; } + +.fa-assistive-listening-systems:before { + content: "\f2a2"; } + +.fa-asterisk:before { + content: "\f069"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-at:before { + content: "\f1fa"; } + +.fa-atlas:before { + content: "\f558"; } + +.fa-atlassian:before { + content: "\f77b"; } + +.fa-atom:before { + content: "\f5d2"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-audio-description:before { + content: "\f29e"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-award:before { + content: "\f559"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-baby:before { + content: "\f77c"; } + +.fa-baby-carriage:before { + content: "\f77d"; } + +.fa-backspace:before { + content: "\f55a"; } + +.fa-backward:before { + content: "\f04a"; } + +.fa-bacon:before { + content: "\f7e5"; } + +.fa-bahai:before { + content: "\f666"; } + +.fa-balance-scale:before { + content: "\f24e"; } + +.fa-balance-scale-left:before { + content: "\f515"; } + +.fa-balance-scale-right:before { + content: "\f516"; } + +.fa-ban:before { + content: "\f05e"; } + +.fa-band-aid:before { + content: "\f462"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-barcode:before { + content: "\f02a"; } + +.fa-bars:before { + content: "\f0c9"; } + +.fa-baseball-ball:before { + content: "\f433"; } + +.fa-basketball-ball:before { + content: "\f434"; } + +.fa-bath:before { + content: "\f2cd"; } + +.fa-battery-empty:before { + content: "\f244"; } + +.fa-battery-full:before { + content: "\f240"; } + +.fa-battery-half:before { + content: "\f242"; } + +.fa-battery-quarter:before { + content: "\f243"; } + +.fa-battery-three-quarters:before { + content: "\f241"; } + +.fa-battle-net:before { + content: "\f835"; } + +.fa-bed:before { + content: "\f236"; } + +.fa-beer:before { + content: "\f0fc"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-bell:before { + content: "\f0f3"; } + +.fa-bell-slash:before { + content: "\f1f6"; } + +.fa-bezier-curve:before { + content: "\f55b"; } + +.fa-bible:before { + content: "\f647"; } + +.fa-bicycle:before { + content: "\f206"; } + +.fa-biking:before { + content: "\f84a"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-binoculars:before { + content: "\f1e5"; } + +.fa-biohazard:before { + content: "\f780"; } + +.fa-birthday-cake:before { + content: "\f1fd"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-blender:before { + content: "\f517"; } + +.fa-blender-phone:before { + content: "\f6b6"; } + +.fa-blind:before { + content: "\f29d"; } + +.fa-blog:before { + content: "\f781"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-bold:before { + content: "\f032"; } + +.fa-bolt:before { + content: "\f0e7"; } + +.fa-bomb:before { + content: "\f1e2"; } + +.fa-bone:before { + content: "\f5d7"; } + +.fa-bong:before { + content: "\f55c"; } + +.fa-book:before { + content: "\f02d"; } + +.fa-book-dead:before { + content: "\f6b7"; } + +.fa-book-medical:before { + content: "\f7e6"; } + +.fa-book-open:before { + content: "\f518"; } + +.fa-book-reader:before { + content: "\f5da"; } + +.fa-bookmark:before { + content: "\f02e"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-border-all:before { + content: "\f84c"; } + +.fa-border-none:before { + content: "\f850"; } + +.fa-border-style:before { + content: "\f853"; } + +.fa-bowling-ball:before { + content: "\f436"; } + +.fa-box:before { + content: "\f466"; } + +.fa-box-open:before { + content: "\f49e"; } + +.fa-boxes:before { + content: "\f468"; } + +.fa-braille:before { + content: "\f2a1"; } + +.fa-brain:before { + content: "\f5dc"; } + +.fa-bread-slice:before { + content: "\f7ec"; } + +.fa-briefcase:before { + content: "\f0b1"; } + +.fa-briefcase-medical:before { + content: "\f469"; } + +.fa-broadcast-tower:before { + content: "\f519"; } + +.fa-broom:before { + content: "\f51a"; } + +.fa-brush:before { + content: "\f55d"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-buffer:before { + content: "\f837"; } + +.fa-bug:before { + content: "\f188"; } + +.fa-building:before { + content: "\f1ad"; } + +.fa-bullhorn:before { + content: "\f0a1"; } + +.fa-bullseye:before { + content: "\f140"; } + +.fa-burn:before { + content: "\f46a"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-bus:before { + content: "\f207"; } + +.fa-bus-alt:before { + content: "\f55e"; } + +.fa-business-time:before { + content: "\f64a"; } + +.fa-buy-n-large:before { + content: "\f8a6"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-calculator:before { + content: "\f1ec"; } + +.fa-calendar:before { + content: "\f133"; } + +.fa-calendar-alt:before { + content: "\f073"; } + +.fa-calendar-check:before { + content: "\f274"; } + +.fa-calendar-day:before { + content: "\f783"; } + +.fa-calendar-minus:before { + content: "\f272"; } + +.fa-calendar-plus:before { + content: "\f271"; } + +.fa-calendar-times:before { + content: "\f273"; } + +.fa-calendar-week:before { + content: "\f784"; } + +.fa-camera:before { + content: "\f030"; } + +.fa-camera-retro:before { + content: "\f083"; } + +.fa-campground:before { + content: "\f6bb"; } + +.fa-canadian-maple-leaf:before { + content: "\f785"; } + +.fa-candy-cane:before { + content: "\f786"; } + +.fa-cannabis:before { + content: "\f55f"; } + +.fa-capsules:before { + content: "\f46b"; } + +.fa-car:before { + content: "\f1b9"; } + +.fa-car-alt:before { + content: "\f5de"; } + +.fa-car-battery:before { + content: "\f5df"; } + +.fa-car-crash:before { + content: "\f5e1"; } + +.fa-car-side:before { + content: "\f5e4"; } + +.fa-caravan:before { + content: "\f8ff"; } + +.fa-caret-down:before { + content: "\f0d7"; } + +.fa-caret-left:before { + content: "\f0d9"; } + +.fa-caret-right:before { + content: "\f0da"; } + +.fa-caret-square-down:before { + content: "\f150"; } + +.fa-caret-square-left:before { + content: "\f191"; } + +.fa-caret-square-right:before { + content: "\f152"; } + +.fa-caret-square-up:before { + content: "\f151"; } + +.fa-caret-up:before { + content: "\f0d8"; } + +.fa-carrot:before { + content: "\f787"; } + +.fa-cart-arrow-down:before { + content: "\f218"; } + +.fa-cart-plus:before { + content: "\f217"; } + +.fa-cash-register:before { + content: "\f788"; } + +.fa-cat:before { + content: "\f6be"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-centos:before { + content: "\f789"; } + +.fa-certificate:before { + content: "\f0a3"; } + +.fa-chair:before { + content: "\f6c0"; } + +.fa-chalkboard:before { + content: "\f51b"; } + +.fa-chalkboard-teacher:before { + content: "\f51c"; } + +.fa-charging-station:before { + content: "\f5e7"; } + +.fa-chart-area:before { + content: "\f1fe"; } + +.fa-chart-bar:before { + content: "\f080"; } + +.fa-chart-line:before { + content: "\f201"; } + +.fa-chart-pie:before { + content: "\f200"; } + +.fa-check:before { + content: "\f00c"; } + +.fa-check-circle:before { + content: "\f058"; } + +.fa-check-double:before { + content: "\f560"; } + +.fa-check-square:before { + content: "\f14a"; } + +.fa-cheese:before { + content: "\f7ef"; } + +.fa-chess:before { + content: "\f439"; } + +.fa-chess-bishop:before { + content: "\f43a"; } + +.fa-chess-board:before { + content: "\f43c"; } + +.fa-chess-king:before { + content: "\f43f"; } + +.fa-chess-knight:before { + content: "\f441"; } + +.fa-chess-pawn:before { + content: "\f443"; } + +.fa-chess-queen:before { + content: "\f445"; } + +.fa-chess-rook:before { + content: "\f447"; } + +.fa-chevron-circle-down:before { + content: "\f13a"; } + +.fa-chevron-circle-left:before { + content: "\f137"; } + +.fa-chevron-circle-right:before { + content: "\f138"; } + +.fa-chevron-circle-up:before { + content: "\f139"; } + +.fa-chevron-down:before { + content: "\f078"; } + +.fa-chevron-left:before { + content: "\f053"; } + +.fa-chevron-right:before { + content: "\f054"; } + +.fa-chevron-up:before { + content: "\f077"; } + +.fa-child:before { + content: "\f1ae"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-chromecast:before { + content: "\f838"; } + +.fa-church:before { + content: "\f51d"; } + +.fa-circle:before { + content: "\f111"; } + +.fa-circle-notch:before { + content: "\f1ce"; } + +.fa-city:before { + content: "\f64f"; } + +.fa-clinic-medical:before { + content: "\f7f2"; } + +.fa-clipboard:before { + content: "\f328"; } + +.fa-clipboard-check:before { + content: "\f46c"; } + +.fa-clipboard-list:before { + content: "\f46d"; } + +.fa-clock:before { + content: "\f017"; } + +.fa-clone:before { + content: "\f24d"; } + +.fa-closed-captioning:before { + content: "\f20a"; } + +.fa-cloud:before { + content: "\f0c2"; } + +.fa-cloud-download-alt:before { + content: "\f381"; } + +.fa-cloud-meatball:before { + content: "\f73b"; } + +.fa-cloud-moon:before { + content: "\f6c3"; } + +.fa-cloud-moon-rain:before { + content: "\f73c"; } + +.fa-cloud-rain:before { + content: "\f73d"; } + +.fa-cloud-showers-heavy:before { + content: "\f740"; } + +.fa-cloud-sun:before { + content: "\f6c4"; } + +.fa-cloud-sun-rain:before { + content: "\f743"; } + +.fa-cloud-upload-alt:before { + content: "\f382"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-cocktail:before { + content: "\f561"; } + +.fa-code:before { + content: "\f121"; } + +.fa-code-branch:before { + content: "\f126"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-coffee:before { + content: "\f0f4"; } + +.fa-cog:before { + content: "\f013"; } + +.fa-cogs:before { + content: "\f085"; } + +.fa-coins:before { + content: "\f51e"; } + +.fa-columns:before { + content: "\f0db"; } + +.fa-comment:before { + content: "\f075"; } + +.fa-comment-alt:before { + content: "\f27a"; } + +.fa-comment-dollar:before { + content: "\f651"; } + +.fa-comment-dots:before { + content: "\f4ad"; } + +.fa-comment-medical:before { + content: "\f7f5"; } + +.fa-comment-slash:before { + content: "\f4b3"; } + +.fa-comments:before { + content: "\f086"; } + +.fa-comments-dollar:before { + content: "\f653"; } + +.fa-compact-disc:before { + content: "\f51f"; } + +.fa-compass:before { + content: "\f14e"; } + +.fa-compress:before { + content: "\f066"; } + +.fa-compress-alt:before { + content: "\f422"; } + +.fa-compress-arrows-alt:before { + content: "\f78c"; } + +.fa-concierge-bell:before { + content: "\f562"; } + +.fa-confluence:before { + content: "\f78d"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-cookie:before { + content: "\f563"; } + +.fa-cookie-bite:before { + content: "\f564"; } + +.fa-copy:before { + content: "\f0c5"; } + +.fa-copyright:before { + content: "\f1f9"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + +.fa-couch:before { + content: "\f4b8"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-creative-commons-by:before { + content: "\f4e7"; } + +.fa-creative-commons-nc:before { + content: "\f4e8"; } + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; } + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; } + +.fa-creative-commons-nd:before { + content: "\f4eb"; } + +.fa-creative-commons-pd:before { + content: "\f4ec"; } + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; } + +.fa-creative-commons-remix:before { + content: "\f4ee"; } + +.fa-creative-commons-sa:before { + content: "\f4ef"; } + +.fa-creative-commons-sampling:before { + content: "\f4f0"; } + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; } + +.fa-creative-commons-share:before { + content: "\f4f2"; } + +.fa-creative-commons-zero:before { + content: "\f4f3"; } + +.fa-credit-card:before { + content: "\f09d"; } + +.fa-critical-role:before { + content: "\f6c9"; } + +.fa-crop:before { + content: "\f125"; } + +.fa-crop-alt:before { + content: "\f565"; } + +.fa-cross:before { + content: "\f654"; } + +.fa-crosshairs:before { + content: "\f05b"; } + +.fa-crow:before { + content: "\f520"; } + +.fa-crown:before { + content: "\f521"; } + +.fa-crutch:before { + content: "\f7f7"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-cube:before { + content: "\f1b2"; } + +.fa-cubes:before { + content: "\f1b3"; } + +.fa-cut:before { + content: "\f0c4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-d-and-d-beyond:before { + content: "\f6ca"; } + +.fa-dailymotion:before { + content: "\f952"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-database:before { + content: "\f1c0"; } + +.fa-deaf:before { + content: "\f2a4"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-democrat:before { + content: "\f747"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-desktop:before { + content: "\f108"; } + +.fa-dev:before { + content: "\f6cc"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-dharmachakra:before { + content: "\f655"; } + +.fa-dhl:before { + content: "\f790"; } + +.fa-diagnoses:before { + content: "\f470"; } + +.fa-diaspora:before { + content: "\f791"; } + +.fa-dice:before { + content: "\f522"; } + +.fa-dice-d20:before { + content: "\f6cf"; } + +.fa-dice-d6:before { + content: "\f6d1"; } + +.fa-dice-five:before { + content: "\f523"; } + +.fa-dice-four:before { + content: "\f524"; } + +.fa-dice-one:before { + content: "\f525"; } + +.fa-dice-six:before { + content: "\f526"; } + +.fa-dice-three:before { + content: "\f527"; } + +.fa-dice-two:before { + content: "\f528"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-digital-tachograph:before { + content: "\f566"; } + +.fa-directions:before { + content: "\f5eb"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-divide:before { + content: "\f529"; } + +.fa-dizzy:before { + content: "\f567"; } + +.fa-dna:before { + content: "\f471"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-dog:before { + content: "\f6d3"; } + +.fa-dollar-sign:before { + content: "\f155"; } + +.fa-dolly:before { + content: "\f472"; } + +.fa-dolly-flatbed:before { + content: "\f474"; } + +.fa-donate:before { + content: "\f4b9"; } + +.fa-door-closed:before { + content: "\f52a"; } + +.fa-door-open:before { + content: "\f52b"; } + +.fa-dot-circle:before { + content: "\f192"; } + +.fa-dove:before { + content: "\f4ba"; } + +.fa-download:before { + content: "\f019"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-drafting-compass:before { + content: "\f568"; } + +.fa-dragon:before { + content: "\f6d5"; } + +.fa-draw-polygon:before { + content: "\f5ee"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-drum:before { + content: "\f569"; } + +.fa-drum-steelpan:before { + content: "\f56a"; } + +.fa-drumstick-bite:before { + content: "\f6d7"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-dumbbell:before { + content: "\f44b"; } + +.fa-dumpster:before { + content: "\f793"; } + +.fa-dumpster-fire:before { + content: "\f794"; } + +.fa-dungeon:before { + content: "\f6d9"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-ebay:before { + content: "\f4f4"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-edit:before { + content: "\f044"; } + +.fa-egg:before { + content: "\f7fb"; } + +.fa-eject:before { + content: "\f052"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-ellipsis-h:before { + content: "\f141"; } + +.fa-ellipsis-v:before { + content: "\f142"; } + +.fa-ello:before { + content: "\f5f1"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envelope:before { + content: "\f0e0"; } + +.fa-envelope-open:before { + content: "\f2b6"; } + +.fa-envelope-open-text:before { + content: "\f658"; } + +.fa-envelope-square:before { + content: "\f199"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-equals:before { + content: "\f52c"; } + +.fa-eraser:before { + content: "\f12d"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-ethernet:before { + content: "\f796"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-euro-sign:before { + content: "\f153"; } + +.fa-evernote:before { + content: "\f839"; } + +.fa-exchange-alt:before { + content: "\f362"; } + +.fa-exclamation:before { + content: "\f12a"; } + +.fa-exclamation-circle:before { + content: "\f06a"; } + +.fa-exclamation-triangle:before { + content: "\f071"; } + +.fa-expand:before { + content: "\f065"; } + +.fa-expand-alt:before { + content: "\f424"; } + +.fa-expand-arrows-alt:before { + content: "\f31e"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-external-link-alt:before { + content: "\f35d"; } + +.fa-external-link-square-alt:before { + content: "\f360"; } + +.fa-eye:before { + content: "\f06e"; } + +.fa-eye-dropper:before { + content: "\f1fb"; } + +.fa-eye-slash:before { + content: "\f070"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-fan:before { + content: "\f863"; } + +.fa-fantasy-flight-games:before { + content: "\f6dc"; } + +.fa-fast-backward:before { + content: "\f049"; } + +.fa-fast-forward:before { + content: "\f050"; } + +.fa-fax:before { + content: "\f1ac"; } + +.fa-feather:before { + content: "\f52d"; } + +.fa-feather-alt:before { + content: "\f56b"; } + +.fa-fedex:before { + content: "\f797"; } + +.fa-fedora:before { + content: "\f798"; } + +.fa-female:before { + content: "\f182"; } + +.fa-fighter-jet:before { + content: "\f0fb"; } + +.fa-figma:before { + content: "\f799"; } + +.fa-file:before { + content: "\f15b"; } + +.fa-file-alt:before { + content: "\f15c"; } + +.fa-file-archive:before { + content: "\f1c6"; } + +.fa-file-audio:before { + content: "\f1c7"; } + +.fa-file-code:before { + content: "\f1c9"; } + +.fa-file-contract:before { + content: "\f56c"; } + +.fa-file-csv:before { + content: "\f6dd"; } + +.fa-file-download:before { + content: "\f56d"; } + +.fa-file-excel:before { + content: "\f1c3"; } + +.fa-file-export:before { + content: "\f56e"; } + +.fa-file-image:before { + content: "\f1c5"; } + +.fa-file-import:before { + content: "\f56f"; } + +.fa-file-invoice:before { + content: "\f570"; } + +.fa-file-invoice-dollar:before { + content: "\f571"; } + +.fa-file-medical:before { + content: "\f477"; } + +.fa-file-medical-alt:before { + content: "\f478"; } + +.fa-file-pdf:before { + content: "\f1c1"; } + +.fa-file-powerpoint:before { + content: "\f1c4"; } + +.fa-file-prescription:before { + content: "\f572"; } + +.fa-file-signature:before { + content: "\f573"; } + +.fa-file-upload:before { + content: "\f574"; } + +.fa-file-video:before { + content: "\f1c8"; } + +.fa-file-word:before { + content: "\f1c2"; } + +.fa-fill:before { + content: "\f575"; } + +.fa-fill-drip:before { + content: "\f576"; } + +.fa-film:before { + content: "\f008"; } + +.fa-filter:before { + content: "\f0b0"; } + +.fa-fingerprint:before { + content: "\f577"; } + +.fa-fire:before { + content: "\f06d"; } + +.fa-fire-alt:before { + content: "\f7e4"; } + +.fa-fire-extinguisher:before { + content: "\f134"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-firefox-browser:before { + content: "\f907"; } + +.fa-first-aid:before { + content: "\f479"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-first-order-alt:before { + content: "\f50a"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-fish:before { + content: "\f578"; } + +.fa-fist-raised:before { + content: "\f6de"; } + +.fa-flag:before { + content: "\f024"; } + +.fa-flag-checkered:before { + content: "\f11e"; } + +.fa-flag-usa:before { + content: "\f74d"; } + +.fa-flask:before { + content: "\f0c3"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-flipboard:before { + content: "\f44d"; } + +.fa-flushed:before { + content: "\f579"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-folder:before { + content: "\f07b"; } + +.fa-folder-minus:before { + content: "\f65d"; } + +.fa-folder-open:before { + content: "\f07c"; } + +.fa-folder-plus:before { + content: "\f65e"; } + +.fa-font:before { + content: "\f031"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-font-awesome-flag:before { + content: "\f425"; } + +.fa-font-awesome-logo-full:before { + content: "\f4e6"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-football-ball:before { + content: "\f44e"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-forward:before { + content: "\f04e"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-frog:before { + content: "\f52e"; } + +.fa-frown:before { + content: "\f119"; } + +.fa-frown-open:before { + content: "\f57a"; } + +.fa-fulcrum:before { + content: "\f50b"; } + +.fa-funnel-dollar:before { + content: "\f662"; } + +.fa-futbol:before { + content: "\f1e3"; } + +.fa-galactic-republic:before { + content: "\f50c"; } + +.fa-galactic-senate:before { + content: "\f50d"; } + +.fa-gamepad:before { + content: "\f11b"; } + +.fa-gas-pump:before { + content: "\f52f"; } + +.fa-gavel:before { + content: "\f0e3"; } + +.fa-gem:before { + content: "\f3a5"; } + +.fa-genderless:before { + content: "\f22d"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-ghost:before { + content: "\f6e2"; } + +.fa-gift:before { + content: "\f06b"; } + +.fa-gifts:before { + content: "\f79c"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-git-alt:before { + content: "\f841"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-glass-cheers:before { + content: "\f79f"; } + +.fa-glass-martini:before { + content: "\f000"; } + +.fa-glass-martini-alt:before { + content: "\f57b"; } + +.fa-glass-whiskey:before { + content: "\f7a0"; } + +.fa-glasses:before { + content: "\f530"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-globe:before { + content: "\f0ac"; } + +.fa-globe-africa:before { + content: "\f57c"; } + +.fa-globe-americas:before { + content: "\f57d"; } + +.fa-globe-asia:before { + content: "\f57e"; } + +.fa-globe-europe:before { + content: "\f7a2"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-golf-ball:before { + content: "\f450"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-gopuram:before { + content: "\f664"; } + +.fa-graduation-cap:before { + content: "\f19d"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-greater-than:before { + content: "\f531"; } + +.fa-greater-than-equal:before { + content: "\f532"; } + +.fa-grimace:before { + content: "\f57f"; } + +.fa-grin:before { + content: "\f580"; } + +.fa-grin-alt:before { + content: "\f581"; } + +.fa-grin-beam:before { + content: "\f582"; } + +.fa-grin-beam-sweat:before { + content: "\f583"; } + +.fa-grin-hearts:before { + content: "\f584"; } + +.fa-grin-squint:before { + content: "\f585"; } + +.fa-grin-squint-tears:before { + content: "\f586"; } + +.fa-grin-stars:before { + content: "\f587"; } + +.fa-grin-tears:before { + content: "\f588"; } + +.fa-grin-tongue:before { + content: "\f589"; } + +.fa-grin-tongue-squint:before { + content: "\f58a"; } + +.fa-grin-tongue-wink:before { + content: "\f58b"; } + +.fa-grin-wink:before { + content: "\f58c"; } + +.fa-grip-horizontal:before { + content: "\f58d"; } + +.fa-grip-lines:before { + content: "\f7a4"; } + +.fa-grip-lines-vertical:before { + content: "\f7a5"; } + +.fa-grip-vertical:before { + content: "\f58e"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-guitar:before { + content: "\f7a6"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-h-square:before { + content: "\f0fd"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-hackerrank:before { + content: "\f5f7"; } + +.fa-hamburger:before { + content: "\f805"; } + +.fa-hammer:before { + content: "\f6e3"; } + +.fa-hamsa:before { + content: "\f665"; } + +.fa-hand-holding:before { + content: "\f4bd"; } + +.fa-hand-holding-heart:before { + content: "\f4be"; } + +.fa-hand-holding-usd:before { + content: "\f4c0"; } + +.fa-hand-lizard:before { + content: "\f258"; } + +.fa-hand-middle-finger:before { + content: "\f806"; } + +.fa-hand-paper:before { + content: "\f256"; } + +.fa-hand-peace:before { + content: "\f25b"; } + +.fa-hand-point-down:before { + content: "\f0a7"; } + +.fa-hand-point-left:before { + content: "\f0a5"; } + +.fa-hand-point-right:before { + content: "\f0a4"; } + +.fa-hand-point-up:before { + content: "\f0a6"; } + +.fa-hand-pointer:before { + content: "\f25a"; } + +.fa-hand-rock:before { + content: "\f255"; } + +.fa-hand-scissors:before { + content: "\f257"; } + +.fa-hand-spock:before { + content: "\f259"; } + +.fa-hands:before { + content: "\f4c2"; } + +.fa-hands-helping:before { + content: "\f4c4"; } + +.fa-handshake:before { + content: "\f2b5"; } + +.fa-hanukiah:before { + content: "\f6e6"; } + +.fa-hard-hat:before { + content: "\f807"; } + +.fa-hashtag:before { + content: "\f292"; } + +.fa-hat-cowboy:before { + content: "\f8c0"; } + +.fa-hat-cowboy-side:before { + content: "\f8c1"; } + +.fa-hat-wizard:before { + content: "\f6e8"; } + +.fa-hdd:before { + content: "\f0a0"; } + +.fa-heading:before { + content: "\f1dc"; } + +.fa-headphones:before { + content: "\f025"; } + +.fa-headphones-alt:before { + content: "\f58f"; } + +.fa-headset:before { + content: "\f590"; } + +.fa-heart:before { + content: "\f004"; } + +.fa-heart-broken:before { + content: "\f7a9"; } + +.fa-heartbeat:before { + content: "\f21e"; } + +.fa-helicopter:before { + content: "\f533"; } + +.fa-highlighter:before { + content: "\f591"; } + +.fa-hiking:before { + content: "\f6ec"; } + +.fa-hippo:before { + content: "\f6ed"; } + +.fa-hips:before { + content: "\f452"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-history:before { + content: "\f1da"; } + +.fa-hockey-puck:before { + content: "\f453"; } + +.fa-holly-berry:before { + content: "\f7aa"; } + +.fa-home:before { + content: "\f015"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-hornbill:before { + content: "\f592"; } + +.fa-horse:before { + content: "\f6f0"; } + +.fa-horse-head:before { + content: "\f7ab"; } + +.fa-hospital:before { + content: "\f0f8"; } + +.fa-hospital-alt:before { + content: "\f47d"; } + +.fa-hospital-symbol:before { + content: "\f47e"; } + +.fa-hot-tub:before { + content: "\f593"; } + +.fa-hotdog:before { + content: "\f80f"; } + +.fa-hotel:before { + content: "\f594"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-hourglass:before { + content: "\f254"; } + +.fa-hourglass-end:before { + content: "\f253"; } + +.fa-hourglass-half:before { + content: "\f252"; } + +.fa-hourglass-start:before { + content: "\f251"; } + +.fa-house-damage:before { + content: "\f6f1"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-hryvnia:before { + content: "\f6f2"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-i-cursor:before { + content: "\f246"; } + +.fa-ice-cream:before { + content: "\f810"; } + +.fa-icicles:before { + content: "\f7ad"; } + +.fa-icons:before { + content: "\f86d"; } + +.fa-id-badge:before { + content: "\f2c1"; } + +.fa-id-card:before { + content: "\f2c2"; } + +.fa-id-card-alt:before { + content: "\f47f"; } + +.fa-ideal:before { + content: "\f913"; } + +.fa-igloo:before { + content: "\f7ae"; } + +.fa-image:before { + content: "\f03e"; } + +.fa-images:before { + content: "\f302"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-inbox:before { + content: "\f01c"; } + +.fa-indent:before { + content: "\f03c"; } + +.fa-industry:before { + content: "\f275"; } + +.fa-infinity:before { + content: "\f534"; } + +.fa-info:before { + content: "\f129"; } + +.fa-info-circle:before { + content: "\f05a"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-instagram-square:before { + content: "\f955"; } + +.fa-intercom:before { + content: "\f7af"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-invision:before { + content: "\f7b0"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-italic:before { + content: "\f033"; } + +.fa-itch-io:before { + content: "\f83a"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-java:before { + content: "\f4e4"; } + +.fa-jedi:before { + content: "\f669"; } + +.fa-jedi-order:before { + content: "\f50e"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-jira:before { + content: "\f7b1"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-joint:before { + content: "\f595"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-journal-whills:before { + content: "\f66a"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-kaaba:before { + content: "\f66b"; } + +.fa-kaggle:before { + content: "\f5fa"; } + +.fa-key:before { + content: "\f084"; } + +.fa-keybase:before { + content: "\f4f5"; } + +.fa-keyboard:before { + content: "\f11c"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-khanda:before { + content: "\f66d"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-kiss:before { + content: "\f596"; } + +.fa-kiss-beam:before { + content: "\f597"; } + +.fa-kiss-wink-heart:before { + content: "\f598"; } + +.fa-kiwi-bird:before { + content: "\f535"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-landmark:before { + content: "\f66f"; } + +.fa-language:before { + content: "\f1ab"; } + +.fa-laptop:before { + content: "\f109"; } + +.fa-laptop-code:before { + content: "\f5fc"; } + +.fa-laptop-medical:before { + content: "\f812"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-laugh:before { + content: "\f599"; } + +.fa-laugh-beam:before { + content: "\f59a"; } + +.fa-laugh-squint:before { + content: "\f59b"; } + +.fa-laugh-wink:before { + content: "\f59c"; } + +.fa-layer-group:before { + content: "\f5fd"; } + +.fa-leaf:before { + content: "\f06c"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-lemon:before { + content: "\f094"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-less-than:before { + content: "\f536"; } + +.fa-less-than-equal:before { + content: "\f537"; } + +.fa-level-down-alt:before { + content: "\f3be"; } + +.fa-level-up-alt:before { + content: "\f3bf"; } + +.fa-life-ring:before { + content: "\f1cd"; } + +.fa-lightbulb:before { + content: "\f0eb"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-link:before { + content: "\f0c1"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-lira-sign:before { + content: "\f195"; } + +.fa-list:before { + content: "\f03a"; } + +.fa-list-alt:before { + content: "\f022"; } + +.fa-list-ol:before { + content: "\f0cb"; } + +.fa-list-ul:before { + content: "\f0ca"; } + +.fa-location-arrow:before { + content: "\f124"; } + +.fa-lock:before { + content: "\f023"; } + +.fa-lock-open:before { + content: "\f3c1"; } + +.fa-long-arrow-alt-down:before { + content: "\f309"; } + +.fa-long-arrow-alt-left:before { + content: "\f30a"; } + +.fa-long-arrow-alt-right:before { + content: "\f30b"; } + +.fa-long-arrow-alt-up:before { + content: "\f30c"; } + +.fa-low-vision:before { + content: "\f2a8"; } + +.fa-luggage-cart:before { + content: "\f59d"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-magic:before { + content: "\f0d0"; } + +.fa-magnet:before { + content: "\f076"; } + +.fa-mail-bulk:before { + content: "\f674"; } + +.fa-mailchimp:before { + content: "\f59e"; } + +.fa-male:before { + content: "\f183"; } + +.fa-mandalorian:before { + content: "\f50f"; } + +.fa-map:before { + content: "\f279"; } + +.fa-map-marked:before { + content: "\f59f"; } + +.fa-map-marked-alt:before { + content: "\f5a0"; } + +.fa-map-marker:before { + content: "\f041"; } + +.fa-map-marker-alt:before { + content: "\f3c5"; } + +.fa-map-pin:before { + content: "\f276"; } + +.fa-map-signs:before { + content: "\f277"; } + +.fa-markdown:before { + content: "\f60f"; } + +.fa-marker:before { + content: "\f5a1"; } + +.fa-mars:before { + content: "\f222"; } + +.fa-mars-double:before { + content: "\f227"; } + +.fa-mars-stroke:before { + content: "\f229"; } + +.fa-mars-stroke-h:before { + content: "\f22b"; } + +.fa-mars-stroke-v:before { + content: "\f22a"; } + +.fa-mask:before { + content: "\f6fa"; } + +.fa-mastodon:before { + content: "\f4f6"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-mdb:before { + content: "\f8ca"; } + +.fa-medal:before { + content: "\f5a2"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f3c7"; } + +.fa-medkit:before { + content: "\f0fa"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-megaport:before { + content: "\f5a3"; } + +.fa-meh:before { + content: "\f11a"; } + +.fa-meh-blank:before { + content: "\f5a4"; } + +.fa-meh-rolling-eyes:before { + content: "\f5a5"; } + +.fa-memory:before { + content: "\f538"; } + +.fa-mendeley:before { + content: "\f7b3"; } + +.fa-menorah:before { + content: "\f676"; } + +.fa-mercury:before { + content: "\f223"; } + +.fa-meteor:before { + content: "\f753"; } + +.fa-microblog:before { + content: "\f91a"; } + +.fa-microchip:before { + content: "\f2db"; } + +.fa-microphone:before { + content: "\f130"; } + +.fa-microphone-alt:before { + content: "\f3c9"; } + +.fa-microphone-alt-slash:before { + content: "\f539"; } + +.fa-microphone-slash:before { + content: "\f131"; } + +.fa-microscope:before { + content: "\f610"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-minus:before { + content: "\f068"; } + +.fa-minus-circle:before { + content: "\f056"; } + +.fa-minus-square:before { + content: "\f146"; } + +.fa-mitten:before { + content: "\f7b5"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-mixer:before { + content: "\f956"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-mobile:before { + content: "\f10b"; } + +.fa-mobile-alt:before { + content: "\f3cd"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-money-bill:before { + content: "\f0d6"; } + +.fa-money-bill-alt:before { + content: "\f3d1"; } + +.fa-money-bill-wave:before { + content: "\f53a"; } + +.fa-money-bill-wave-alt:before { + content: "\f53b"; } + +.fa-money-check:before { + content: "\f53c"; } + +.fa-money-check-alt:before { + content: "\f53d"; } + +.fa-monument:before { + content: "\f5a6"; } + +.fa-moon:before { + content: "\f186"; } + +.fa-mortar-pestle:before { + content: "\f5a7"; } + +.fa-mosque:before { + content: "\f678"; } + +.fa-motorcycle:before { + content: "\f21c"; } + +.fa-mountain:before { + content: "\f6fc"; } + +.fa-mouse:before { + content: "\f8cc"; } + +.fa-mouse-pointer:before { + content: "\f245"; } + +.fa-mug-hot:before { + content: "\f7b6"; } + +.fa-music:before { + content: "\f001"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-neos:before { + content: "\f612"; } + +.fa-network-wired:before { + content: "\f6ff"; } + +.fa-neuter:before { + content: "\f22c"; } + +.fa-newspaper:before { + content: "\f1ea"; } + +.fa-nimblr:before { + content: "\f5a8"; } + +.fa-node:before { + content: "\f419"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-not-equal:before { + content: "\f53e"; } + +.fa-notes-medical:before { + content: "\f481"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-object-group:before { + content: "\f247"; } + +.fa-object-ungroup:before { + content: "\f248"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-oil-can:before { + content: "\f613"; } + +.fa-old-republic:before { + content: "\f510"; } + +.fa-om:before { + content: "\f679"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-orcid:before { + content: "\f8d2"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-otter:before { + content: "\f700"; } + +.fa-outdent:before { + content: "\f03b"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-pager:before { + content: "\f815"; } + +.fa-paint-brush:before { + content: "\f1fc"; } + +.fa-paint-roller:before { + content: "\f5aa"; } + +.fa-palette:before { + content: "\f53f"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-pallet:before { + content: "\f482"; } + +.fa-paper-plane:before { + content: "\f1d8"; } + +.fa-paperclip:before { + content: "\f0c6"; } + +.fa-parachute-box:before { + content: "\f4cd"; } + +.fa-paragraph:before { + content: "\f1dd"; } + +.fa-parking:before { + content: "\f540"; } + +.fa-passport:before { + content: "\f5ab"; } + +.fa-pastafarianism:before { + content: "\f67b"; } + +.fa-paste:before { + content: "\f0ea"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-pause:before { + content: "\f04c"; } + +.fa-pause-circle:before { + content: "\f28b"; } + +.fa-paw:before { + content: "\f1b0"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-peace:before { + content: "\f67c"; } + +.fa-pen:before { + content: "\f304"; } + +.fa-pen-alt:before { + content: "\f305"; } + +.fa-pen-fancy:before { + content: "\f5ac"; } + +.fa-pen-nib:before { + content: "\f5ad"; } + +.fa-pen-square:before { + content: "\f14b"; } + +.fa-pencil-alt:before { + content: "\f303"; } + +.fa-pencil-ruler:before { + content: "\f5ae"; } + +.fa-penny-arcade:before { + content: "\f704"; } + +.fa-people-carry:before { + content: "\f4ce"; } + +.fa-pepper-hot:before { + content: "\f816"; } + +.fa-percent:before { + content: "\f295"; } + +.fa-percentage:before { + content: "\f541"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-person-booth:before { + content: "\f756"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-phoenix-squadron:before { + content: "\f511"; } + +.fa-phone:before { + content: "\f095"; } + +.fa-phone-alt:before { + content: "\f879"; } + +.fa-phone-slash:before { + content: "\f3dd"; } + +.fa-phone-square:before { + content: "\f098"; } + +.fa-phone-square-alt:before { + content: "\f87b"; } + +.fa-phone-volume:before { + content: "\f2a0"; } + +.fa-photo-video:before { + content: "\f87c"; } + +.fa-php:before { + content: "\f457"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-pied-piper-hat:before { + content: "\f4e5"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-pied-piper-square:before { + content: "\f91e"; } + +.fa-piggy-bank:before { + content: "\f4d3"; } + +.fa-pills:before { + content: "\f484"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-pizza-slice:before { + content: "\f818"; } + +.fa-place-of-worship:before { + content: "\f67f"; } + +.fa-plane:before { + content: "\f072"; } + +.fa-plane-arrival:before { + content: "\f5af"; } + +.fa-plane-departure:before { + content: "\f5b0"; } + +.fa-play:before { + content: "\f04b"; } + +.fa-play-circle:before { + content: "\f144"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-plug:before { + content: "\f1e6"; } + +.fa-plus:before { + content: "\f067"; } + +.fa-plus-circle:before { + content: "\f055"; } + +.fa-plus-square:before { + content: "\f0fe"; } + +.fa-podcast:before { + content: "\f2ce"; } + +.fa-poll:before { + content: "\f681"; } + +.fa-poll-h:before { + content: "\f682"; } + +.fa-poo:before { + content: "\f2fe"; } + +.fa-poo-storm:before { + content: "\f75a"; } + +.fa-poop:before { + content: "\f619"; } + +.fa-portrait:before { + content: "\f3e0"; } + +.fa-pound-sign:before { + content: "\f154"; } + +.fa-power-off:before { + content: "\f011"; } + +.fa-pray:before { + content: "\f683"; } + +.fa-praying-hands:before { + content: "\f684"; } + +.fa-prescription:before { + content: "\f5b1"; } + +.fa-prescription-bottle:before { + content: "\f485"; } + +.fa-prescription-bottle-alt:before { + content: "\f486"; } + +.fa-print:before { + content: "\f02f"; } + +.fa-procedures:before { + content: "\f487"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-project-diagram:before { + content: "\f542"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-puzzle-piece:before { + content: "\f12e"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-qrcode:before { + content: "\f029"; } + +.fa-question:before { + content: "\f128"; } + +.fa-question-circle:before { + content: "\f059"; } + +.fa-quidditch:before { + content: "\f458"; } + +.fa-quinscape:before { + content: "\f459"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-quote-left:before { + content: "\f10d"; } + +.fa-quote-right:before { + content: "\f10e"; } + +.fa-quran:before { + content: "\f687"; } + +.fa-r-project:before { + content: "\f4f7"; } + +.fa-radiation:before { + content: "\f7b9"; } + +.fa-radiation-alt:before { + content: "\f7ba"; } + +.fa-rainbow:before { + content: "\f75b"; } + +.fa-random:before { + content: "\f074"; } + +.fa-raspberry-pi:before { + content: "\f7bb"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-reacteurope:before { + content: "\f75d"; } + +.fa-readme:before { + content: "\f4d5"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-receipt:before { + content: "\f543"; } + +.fa-record-vinyl:before { + content: "\f8d9"; } + +.fa-recycle:before { + content: "\f1b8"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-redhat:before { + content: "\f7bc"; } + +.fa-redo:before { + content: "\f01e"; } + +.fa-redo-alt:before { + content: "\f2f9"; } + +.fa-registered:before { + content: "\f25d"; } + +.fa-remove-format:before { + content: "\f87d"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-reply:before { + content: "\f3e5"; } + +.fa-reply-all:before { + content: "\f122"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-republican:before { + content: "\f75e"; } + +.fa-researchgate:before { + content: "\f4f8"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-restroom:before { + content: "\f7bd"; } + +.fa-retweet:before { + content: "\f079"; } + +.fa-rev:before { + content: "\f5b2"; } + +.fa-ribbon:before { + content: "\f4d6"; } + +.fa-ring:before { + content: "\f70b"; } + +.fa-road:before { + content: "\f018"; } + +.fa-robot:before { + content: "\f544"; } + +.fa-rocket:before { + content: "\f135"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-route:before { + content: "\f4d7"; } + +.fa-rss:before { + content: "\f09e"; } + +.fa-rss-square:before { + content: "\f143"; } + +.fa-ruble-sign:before { + content: "\f158"; } + +.fa-ruler:before { + content: "\f545"; } + +.fa-ruler-combined:before { + content: "\f546"; } + +.fa-ruler-horizontal:before { + content: "\f547"; } + +.fa-ruler-vertical:before { + content: "\f548"; } + +.fa-running:before { + content: "\f70c"; } + +.fa-rupee-sign:before { + content: "\f156"; } + +.fa-sad-cry:before { + content: "\f5b3"; } + +.fa-sad-tear:before { + content: "\f5b4"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-satellite:before { + content: "\f7bf"; } + +.fa-satellite-dish:before { + content: "\f7c0"; } + +.fa-save:before { + content: "\f0c7"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-school:before { + content: "\f549"; } + +.fa-screwdriver:before { + content: "\f54a"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-scroll:before { + content: "\f70e"; } + +.fa-sd-card:before { + content: "\f7c2"; } + +.fa-search:before { + content: "\f002"; } + +.fa-search-dollar:before { + content: "\f688"; } + +.fa-search-location:before { + content: "\f689"; } + +.fa-search-minus:before { + content: "\f010"; } + +.fa-search-plus:before { + content: "\f00e"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-seedling:before { + content: "\f4d8"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-server:before { + content: "\f233"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-shapes:before { + content: "\f61f"; } + +.fa-share:before { + content: "\f064"; } + +.fa-share-alt:before { + content: "\f1e0"; } + +.fa-share-alt-square:before { + content: "\f1e1"; } + +.fa-share-square:before { + content: "\f14d"; } + +.fa-shekel-sign:before { + content: "\f20b"; } + +.fa-shield-alt:before { + content: "\f3ed"; } + +.fa-ship:before { + content: "\f21a"; } + +.fa-shipping-fast:before { + content: "\f48b"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-shoe-prints:before { + content: "\f54b"; } + +.fa-shopify:before { + content: "\f957"; } + +.fa-shopping-bag:before { + content: "\f290"; } + +.fa-shopping-basket:before { + content: "\f291"; } + +.fa-shopping-cart:before { + content: "\f07a"; } + +.fa-shopware:before { + content: "\f5b5"; } + +.fa-shower:before { + content: "\f2cc"; } + +.fa-shuttle-van:before { + content: "\f5b6"; } + +.fa-sign:before { + content: "\f4d9"; } + +.fa-sign-in-alt:before { + content: "\f2f6"; } + +.fa-sign-language:before { + content: "\f2a7"; } + +.fa-sign-out-alt:before { + content: "\f2f5"; } + +.fa-signal:before { + content: "\f012"; } + +.fa-signature:before { + content: "\f5b7"; } + +.fa-sim-card:before { + content: "\f7c4"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-sitemap:before { + content: "\f0e8"; } + +.fa-sith:before { + content: "\f512"; } + +.fa-skating:before { + content: "\f7c5"; } + +.fa-sketch:before { + content: "\f7c6"; } + +.fa-skiing:before { + content: "\f7c9"; } + +.fa-skiing-nordic:before { + content: "\f7ca"; } + +.fa-skull:before { + content: "\f54c"; } + +.fa-skull-crossbones:before { + content: "\f714"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f3ef"; } + +.fa-slash:before { + content: "\f715"; } + +.fa-sleigh:before { + content: "\f7cc"; } + +.fa-sliders-h:before { + content: "\f1de"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-smile:before { + content: "\f118"; } + +.fa-smile-beam:before { + content: "\f5b8"; } + +.fa-smile-wink:before { + content: "\f4da"; } + +.fa-smog:before { + content: "\f75f"; } + +.fa-smoking:before { + content: "\f48d"; } + +.fa-smoking-ban:before { + content: "\f54d"; } + +.fa-sms:before { + content: "\f7cd"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ac"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-snowboarding:before { + content: "\f7ce"; } + +.fa-snowflake:before { + content: "\f2dc"; } + +.fa-snowman:before { + content: "\f7d0"; } + +.fa-snowplow:before { + content: "\f7d2"; } + +.fa-socks:before { + content: "\f696"; } + +.fa-solar-panel:before { + content: "\f5ba"; } + +.fa-sort:before { + content: "\f0dc"; } + +.fa-sort-alpha-down:before { + content: "\f15d"; } + +.fa-sort-alpha-down-alt:before { + content: "\f881"; } + +.fa-sort-alpha-up:before { + content: "\f15e"; } + +.fa-sort-alpha-up-alt:before { + content: "\f882"; } + +.fa-sort-amount-down:before { + content: "\f160"; } + +.fa-sort-amount-down-alt:before { + content: "\f884"; } + +.fa-sort-amount-up:before { + content: "\f161"; } + +.fa-sort-amount-up-alt:before { + content: "\f885"; } + +.fa-sort-down:before { + content: "\f0dd"; } + +.fa-sort-numeric-down:before { + content: "\f162"; } + +.fa-sort-numeric-down-alt:before { + content: "\f886"; } + +.fa-sort-numeric-up:before { + content: "\f163"; } + +.fa-sort-numeric-up-alt:before { + content: "\f887"; } + +.fa-sort-up:before { + content: "\f0de"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-sourcetree:before { + content: "\f7d3"; } + +.fa-spa:before { + content: "\f5bb"; } + +.fa-space-shuttle:before { + content: "\f197"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-spell-check:before { + content: "\f891"; } + +.fa-spider:before { + content: "\f717"; } + +.fa-spinner:before { + content: "\f110"; } + +.fa-splotch:before { + content: "\f5bc"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-spray-can:before { + content: "\f5bd"; } + +.fa-square:before { + content: "\f0c8"; } + +.fa-square-full:before { + content: "\f45c"; } + +.fa-square-root-alt:before { + content: "\f698"; } + +.fa-squarespace:before { + content: "\f5be"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-stackpath:before { + content: "\f842"; } + +.fa-stamp:before { + content: "\f5bf"; } + +.fa-star:before { + content: "\f005"; } + +.fa-star-and-crescent:before { + content: "\f699"; } + +.fa-star-half:before { + content: "\f089"; } + +.fa-star-half-alt:before { + content: "\f5c0"; } + +.fa-star-of-david:before { + content: "\f69a"; } + +.fa-star-of-life:before { + content: "\f621"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } + +.fa-step-backward:before { + content: "\f048"; } + +.fa-step-forward:before { + content: "\f051"; } + +.fa-stethoscope:before { + content: "\f0f1"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-sticky-note:before { + content: "\f249"; } + +.fa-stop:before { + content: "\f04d"; } + +.fa-stop-circle:before { + content: "\f28d"; } + +.fa-stopwatch:before { + content: "\f2f2"; } + +.fa-store:before { + content: "\f54e"; } + +.fa-store-alt:before { + content: "\f54f"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-stream:before { + content: "\f550"; } + +.fa-street-view:before { + content: "\f21d"; } + +.fa-strikethrough:before { + content: "\f0cc"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-stroopwafel:before { + content: "\f551"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-subscript:before { + content: "\f12c"; } + +.fa-subway:before { + content: "\f239"; } + +.fa-suitcase:before { + content: "\f0f2"; } + +.fa-suitcase-rolling:before { + content: "\f5c1"; } + +.fa-sun:before { + content: "\f185"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-superscript:before { + content: "\f12b"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-surprise:before { + content: "\f5c2"; } + +.fa-suse:before { + content: "\f7d6"; } + +.fa-swatchbook:before { + content: "\f5c3"; } + +.fa-swift:before { + content: "\f8e1"; } + +.fa-swimmer:before { + content: "\f5c4"; } + +.fa-swimming-pool:before { + content: "\f5c5"; } + +.fa-symfony:before { + content: "\f83d"; } + +.fa-synagogue:before { + content: "\f69b"; } + +.fa-sync:before { + content: "\f021"; } + +.fa-sync-alt:before { + content: "\f2f1"; } + +.fa-syringe:before { + content: "\f48e"; } + +.fa-table:before { + content: "\f0ce"; } + +.fa-table-tennis:before { + content: "\f45d"; } + +.fa-tablet:before { + content: "\f10a"; } + +.fa-tablet-alt:before { + content: "\f3fa"; } + +.fa-tablets:before { + content: "\f490"; } + +.fa-tachometer-alt:before { + content: "\f3fd"; } + +.fa-tag:before { + content: "\f02b"; } + +.fa-tags:before { + content: "\f02c"; } + +.fa-tape:before { + content: "\f4db"; } + +.fa-tasks:before { + content: "\f0ae"; } + +.fa-taxi:before { + content: "\f1ba"; } + +.fa-teamspeak:before { + content: "\f4f9"; } + +.fa-teeth:before { + content: "\f62e"; } + +.fa-teeth-open:before { + content: "\f62f"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f3fe"; } + +.fa-temperature-high:before { + content: "\f769"; } + +.fa-temperature-low:before { + content: "\f76b"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-tenge:before { + content: "\f7d7"; } + +.fa-terminal:before { + content: "\f120"; } + +.fa-text-height:before { + content: "\f034"; } + +.fa-text-width:before { + content: "\f035"; } + +.fa-th:before { + content: "\f00a"; } + +.fa-th-large:before { + content: "\f009"; } + +.fa-th-list:before { + content: "\f00b"; } + +.fa-the-red-yeti:before { + content: "\f69d"; } + +.fa-theater-masks:before { + content: "\f630"; } + +.fa-themeco:before { + content: "\f5c6"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-thermometer:before { + content: "\f491"; } + +.fa-thermometer-empty:before { + content: "\f2cb"; } + +.fa-thermometer-full:before { + content: "\f2c7"; } + +.fa-thermometer-half:before { + content: "\f2c9"; } + +.fa-thermometer-quarter:before { + content: "\f2ca"; } + +.fa-thermometer-three-quarters:before { + content: "\f2c8"; } + +.fa-think-peaks:before { + content: "\f731"; } + +.fa-thumbs-down:before { + content: "\f165"; } + +.fa-thumbs-up:before { + content: "\f164"; } + +.fa-thumbtack:before { + content: "\f08d"; } + +.fa-ticket-alt:before { + content: "\f3ff"; } + +.fa-times:before { + content: "\f00d"; } + +.fa-times-circle:before { + content: "\f057"; } + +.fa-tint:before { + content: "\f043"; } + +.fa-tint-slash:before { + content: "\f5c7"; } + +.fa-tired:before { + content: "\f5c8"; } + +.fa-toggle-off:before { + content: "\f204"; } + +.fa-toggle-on:before { + content: "\f205"; } + +.fa-toilet:before { + content: "\f7d8"; } + +.fa-toilet-paper:before { + content: "\f71e"; } + +.fa-toolbox:before { + content: "\f552"; } + +.fa-tools:before { + content: "\f7d9"; } + +.fa-tooth:before { + content: "\f5c9"; } + +.fa-torah:before { + content: "\f6a0"; } + +.fa-torii-gate:before { + content: "\f6a1"; } + +.fa-tractor:before { + content: "\f722"; } + +.fa-trade-federation:before { + content: "\f513"; } + +.fa-trademark:before { + content: "\f25c"; } + +.fa-traffic-light:before { + content: "\f637"; } + +.fa-trailer:before { + content: "\f941"; } + +.fa-train:before { + content: "\f238"; } + +.fa-tram:before { + content: "\f7da"; } + +.fa-transgender:before { + content: "\f224"; } + +.fa-transgender-alt:before { + content: "\f225"; } + +.fa-trash:before { + content: "\f1f8"; } + +.fa-trash-alt:before { + content: "\f2ed"; } + +.fa-trash-restore:before { + content: "\f829"; } + +.fa-trash-restore-alt:before { + content: "\f82a"; } + +.fa-tree:before { + content: "\f1bb"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-tripadvisor:before { + content: "\f262"; } + +.fa-trophy:before { + content: "\f091"; } + +.fa-truck:before { + content: "\f0d1"; } + +.fa-truck-loading:before { + content: "\f4de"; } + +.fa-truck-monster:before { + content: "\f63b"; } + +.fa-truck-moving:before { + content: "\f4df"; } + +.fa-truck-pickup:before { + content: "\f63c"; } + +.fa-tshirt:before { + content: "\f553"; } + +.fa-tty:before { + content: "\f1e4"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-tv:before { + content: "\f26c"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-ubuntu:before { + content: "\f7df"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-umbraco:before { + content: "\f8e8"; } + +.fa-umbrella:before { + content: "\f0e9"; } + +.fa-umbrella-beach:before { + content: "\f5ca"; } + +.fa-underline:before { + content: "\f0cd"; } + +.fa-undo:before { + content: "\f0e2"; } + +.fa-undo-alt:before { + content: "\f2ea"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-unity:before { + content: "\f949"; } + +.fa-universal-access:before { + content: "\f29a"; } + +.fa-university:before { + content: "\f19c"; } + +.fa-unlink:before { + content: "\f127"; } + +.fa-unlock:before { + content: "\f09c"; } + +.fa-unlock-alt:before { + content: "\f13e"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-upload:before { + content: "\f093"; } + +.fa-ups:before { + content: "\f7e0"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-user:before { + content: "\f007"; } + +.fa-user-alt:before { + content: "\f406"; } + +.fa-user-alt-slash:before { + content: "\f4fa"; } + +.fa-user-astronaut:before { + content: "\f4fb"; } + +.fa-user-check:before { + content: "\f4fc"; } + +.fa-user-circle:before { + content: "\f2bd"; } + +.fa-user-clock:before { + content: "\f4fd"; } + +.fa-user-cog:before { + content: "\f4fe"; } + +.fa-user-edit:before { + content: "\f4ff"; } + +.fa-user-friends:before { + content: "\f500"; } + +.fa-user-graduate:before { + content: "\f501"; } + +.fa-user-injured:before { + content: "\f728"; } + +.fa-user-lock:before { + content: "\f502"; } + +.fa-user-md:before { + content: "\f0f0"; } + +.fa-user-minus:before { + content: "\f503"; } + +.fa-user-ninja:before { + content: "\f504"; } + +.fa-user-nurse:before { + content: "\f82f"; } + +.fa-user-plus:before { + content: "\f234"; } + +.fa-user-secret:before { + content: "\f21b"; } + +.fa-user-shield:before { + content: "\f505"; } + +.fa-user-slash:before { + content: "\f506"; } + +.fa-user-tag:before { + content: "\f507"; } + +.fa-user-tie:before { + content: "\f508"; } + +.fa-user-times:before { + content: "\f235"; } + +.fa-users:before { + content: "\f0c0"; } + +.fa-users-cog:before { + content: "\f509"; } + +.fa-usps:before { + content: "\f7e1"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-utensil-spoon:before { + content: "\f2e5"; } + +.fa-utensils:before { + content: "\f2e7"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-vector-square:before { + content: "\f5cb"; } + +.fa-venus:before { + content: "\f221"; } + +.fa-venus-double:before { + content: "\f226"; } + +.fa-venus-mars:before { + content: "\f228"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-vial:before { + content: "\f492"; } + +.fa-vials:before { + content: "\f493"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-video:before { + content: "\f03d"; } + +.fa-video-slash:before { + content: "\f4e2"; } + +.fa-vihara:before { + content: "\f6a7"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-voicemail:before { + content: "\f897"; } + +.fa-volleyball-ball:before { + content: "\f45f"; } + +.fa-volume-down:before { + content: "\f027"; } + +.fa-volume-mute:before { + content: "\f6a9"; } + +.fa-volume-off:before { + content: "\f026"; } + +.fa-volume-up:before { + content: "\f028"; } + +.fa-vote-yea:before { + content: "\f772"; } + +.fa-vr-cardboard:before { + content: "\f729"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-walking:before { + content: "\f554"; } + +.fa-wallet:before { + content: "\f555"; } + +.fa-warehouse:before { + content: "\f494"; } + +.fa-water:before { + content: "\f773"; } + +.fa-wave-square:before { + content: "\f83e"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-weebly:before { + content: "\f5cc"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-weight:before { + content: "\f496"; } + +.fa-weight-hanging:before { + content: "\f5cd"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-wheelchair:before { + content: "\f193"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-wifi:before { + content: "\f1eb"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-wind:before { + content: "\f72e"; } + +.fa-window-close:before { + content: "\f410"; } + +.fa-window-maximize:before { + content: "\f2d0"; } + +.fa-window-minimize:before { + content: "\f2d1"; } + +.fa-window-restore:before { + content: "\f2d2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-wine-bottle:before { + content: "\f72f"; } + +.fa-wine-glass:before { + content: "\f4e3"; } + +.fa-wine-glass-alt:before { + content: "\f5ce"; } + +.fa-wix:before { + content: "\f5cf"; } + +.fa-wizards-of-the-coast:before { + content: "\f730"; } + +.fa-wolf-pack-battalion:before { + content: "\f514"; } + +.fa-won-sign:before { + content: "\f159"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-wpressr:before { + content: "\f3e4"; } + +.fa-wrench:before { + content: "\f0ad"; } + +.fa-x-ray:before { + content: "\f497"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-yammer:before { + content: "\f840"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-yarn:before { + content: "\f7e3"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-yen-sign:before { + content: "\f157"; } + +.fa-yin-yang:before { + content: "\f6ad"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.fa-zhihu:before { + content: "\f63f"; } + +.sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +.sr-only-focusable:active, .sr-only-focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; } +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-style: normal; + font-weight: 400; + font-display: auto; + src: url("../webfonts/fa-brands-400.eot"); + src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } + +.fab { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 400; + font-display: auto; + src: url("../webfonts/fa-regular-400.eot"); + src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } + +.far { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 900; + font-display: auto; + src: url("../webfonts/fa-solid-900.eot"); + src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } + +.fa, +.fas { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/css/v4-shims.css b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/css/v4-shims.css new file mode 100644 index 0000000000..9e0774271a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/css/v4-shims.css @@ -0,0 +1,2172 @@ +/*! + * Font Awesome Free 5.12.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa.fa-glass:before { + content: "\f000"; } + +.fa.fa-meetup { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-star-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-star-o:before { + content: "\f005"; } + +.fa.fa-remove:before { + content: "\f00d"; } + +.fa.fa-close:before { + content: "\f00d"; } + +.fa.fa-gear:before { + content: "\f013"; } + +.fa.fa-trash-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-trash-o:before { + content: "\f2ed"; } + +.fa.fa-file-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-o:before { + content: "\f15b"; } + +.fa.fa-clock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-clock-o:before { + content: "\f017"; } + +.fa.fa-arrow-circle-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-arrow-circle-o-down:before { + content: "\f358"; } + +.fa.fa-arrow-circle-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-arrow-circle-o-up:before { + content: "\f35b"; } + +.fa.fa-play-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-play-circle-o:before { + content: "\f144"; } + +.fa.fa-repeat:before { + content: "\f01e"; } + +.fa.fa-rotate-right:before { + content: "\f01e"; } + +.fa.fa-refresh:before { + content: "\f021"; } + +.fa.fa-list-alt { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-dedent:before { + content: "\f03b"; } + +.fa.fa-video-camera:before { + content: "\f03d"; } + +.fa.fa-picture-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-picture-o:before { + content: "\f03e"; } + +.fa.fa-photo { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-photo:before { + content: "\f03e"; } + +.fa.fa-image { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-image:before { + content: "\f03e"; } + +.fa.fa-pencil:before { + content: "\f303"; } + +.fa.fa-map-marker:before { + content: "\f3c5"; } + +.fa.fa-pencil-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-pencil-square-o:before { + content: "\f044"; } + +.fa.fa-share-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-share-square-o:before { + content: "\f14d"; } + +.fa.fa-check-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-check-square-o:before { + content: "\f14a"; } + +.fa.fa-arrows:before { + content: "\f0b2"; } + +.fa.fa-times-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-times-circle-o:before { + content: "\f057"; } + +.fa.fa-check-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-check-circle-o:before { + content: "\f058"; } + +.fa.fa-mail-forward:before { + content: "\f064"; } + +.fa.fa-expand:before { + content: "\f424"; } + +.fa.fa-compress:before { + content: "\f422"; } + +.fa.fa-eye { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-eye-slash { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-warning:before { + content: "\f071"; } + +.fa.fa-calendar:before { + content: "\f073"; } + +.fa.fa-arrows-v:before { + content: "\f338"; } + +.fa.fa-arrows-h:before { + content: "\f337"; } + +.fa.fa-bar-chart { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-bar-chart:before { + content: "\f080"; } + +.fa.fa-bar-chart-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-bar-chart-o:before { + content: "\f080"; } + +.fa.fa-twitter-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-facebook-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gears:before { + content: "\f085"; } + +.fa.fa-thumbs-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-thumbs-o-up:before { + content: "\f164"; } + +.fa.fa-thumbs-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-thumbs-o-down:before { + content: "\f165"; } + +.fa.fa-heart-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-heart-o:before { + content: "\f004"; } + +.fa.fa-sign-out:before { + content: "\f2f5"; } + +.fa.fa-linkedin-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-linkedin-square:before { + content: "\f08c"; } + +.fa.fa-thumb-tack:before { + content: "\f08d"; } + +.fa.fa-external-link:before { + content: "\f35d"; } + +.fa.fa-sign-in:before { + content: "\f2f6"; } + +.fa.fa-github-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-lemon-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-lemon-o:before { + content: "\f094"; } + +.fa.fa-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-square-o:before { + content: "\f0c8"; } + +.fa.fa-bookmark-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-bookmark-o:before { + content: "\f02e"; } + +.fa.fa-twitter { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-facebook { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-facebook:before { + content: "\f39e"; } + +.fa.fa-facebook-f { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-facebook-f:before { + content: "\f39e"; } + +.fa.fa-github { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-credit-card { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-feed:before { + content: "\f09e"; } + +.fa.fa-hdd-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hdd-o:before { + content: "\f0a0"; } + +.fa.fa-hand-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-o-right:before { + content: "\f0a4"; } + +.fa.fa-hand-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-o-left:before { + content: "\f0a5"; } + +.fa.fa-hand-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-o-up:before { + content: "\f0a6"; } + +.fa.fa-hand-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-o-down:before { + content: "\f0a7"; } + +.fa.fa-arrows-alt:before { + content: "\f31e"; } + +.fa.fa-group:before { + content: "\f0c0"; } + +.fa.fa-chain:before { + content: "\f0c1"; } + +.fa.fa-scissors:before { + content: "\f0c4"; } + +.fa.fa-files-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-files-o:before { + content: "\f0c5"; } + +.fa.fa-floppy-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-floppy-o:before { + content: "\f0c7"; } + +.fa.fa-navicon:before { + content: "\f0c9"; } + +.fa.fa-reorder:before { + content: "\f0c9"; } + +.fa.fa-pinterest { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-pinterest-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-plus-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-plus { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-plus:before { + content: "\f0d5"; } + +.fa.fa-money { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-money:before { + content: "\f3d1"; } + +.fa.fa-unsorted:before { + content: "\f0dc"; } + +.fa.fa-sort-desc:before { + content: "\f0dd"; } + +.fa.fa-sort-asc:before { + content: "\f0de"; } + +.fa.fa-linkedin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-linkedin:before { + content: "\f0e1"; } + +.fa.fa-rotate-left:before { + content: "\f0e2"; } + +.fa.fa-legal:before { + content: "\f0e3"; } + +.fa.fa-tachometer:before { + content: "\f3fd"; } + +.fa.fa-dashboard:before { + content: "\f3fd"; } + +.fa.fa-comment-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-comment-o:before { + content: "\f075"; } + +.fa.fa-comments-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-comments-o:before { + content: "\f086"; } + +.fa.fa-flash:before { + content: "\f0e7"; } + +.fa.fa-clipboard { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-paste { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-paste:before { + content: "\f328"; } + +.fa.fa-lightbulb-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-lightbulb-o:before { + content: "\f0eb"; } + +.fa.fa-exchange:before { + content: "\f362"; } + +.fa.fa-cloud-download:before { + content: "\f381"; } + +.fa.fa-cloud-upload:before { + content: "\f382"; } + +.fa.fa-bell-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-bell-o:before { + content: "\f0f3"; } + +.fa.fa-cutlery:before { + content: "\f2e7"; } + +.fa.fa-file-text-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-text-o:before { + content: "\f15c"; } + +.fa.fa-building-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-building-o:before { + content: "\f1ad"; } + +.fa.fa-hospital-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hospital-o:before { + content: "\f0f8"; } + +.fa.fa-tablet:before { + content: "\f3fa"; } + +.fa.fa-mobile:before { + content: "\f3cd"; } + +.fa.fa-mobile-phone:before { + content: "\f3cd"; } + +.fa.fa-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-circle-o:before { + content: "\f111"; } + +.fa.fa-mail-reply:before { + content: "\f3e5"; } + +.fa.fa-github-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-folder-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-folder-o:before { + content: "\f07b"; } + +.fa.fa-folder-open-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-folder-open-o:before { + content: "\f07c"; } + +.fa.fa-smile-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-smile-o:before { + content: "\f118"; } + +.fa.fa-frown-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-frown-o:before { + content: "\f119"; } + +.fa.fa-meh-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-meh-o:before { + content: "\f11a"; } + +.fa.fa-keyboard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-keyboard-o:before { + content: "\f11c"; } + +.fa.fa-flag-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-flag-o:before { + content: "\f024"; } + +.fa.fa-mail-reply-all:before { + content: "\f122"; } + +.fa.fa-star-half-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-star-half-o:before { + content: "\f089"; } + +.fa.fa-star-half-empty { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-star-half-empty:before { + content: "\f089"; } + +.fa.fa-star-half-full { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-star-half-full:before { + content: "\f089"; } + +.fa.fa-code-fork:before { + content: "\f126"; } + +.fa.fa-chain-broken:before { + content: "\f127"; } + +.fa.fa-shield:before { + content: "\f3ed"; } + +.fa.fa-calendar-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-calendar-o:before { + content: "\f133"; } + +.fa.fa-maxcdn { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-html5 { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-css3 { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ticket:before { + content: "\f3ff"; } + +.fa.fa-minus-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-minus-square-o:before { + content: "\f146"; } + +.fa.fa-level-up:before { + content: "\f3bf"; } + +.fa.fa-level-down:before { + content: "\f3be"; } + +.fa.fa-pencil-square:before { + content: "\f14b"; } + +.fa.fa-external-link-square:before { + content: "\f360"; } + +.fa.fa-compass { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-caret-square-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-caret-square-o-down:before { + content: "\f150"; } + +.fa.fa-toggle-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-toggle-down:before { + content: "\f150"; } + +.fa.fa-caret-square-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-caret-square-o-up:before { + content: "\f151"; } + +.fa.fa-toggle-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-toggle-up:before { + content: "\f151"; } + +.fa.fa-caret-square-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-caret-square-o-right:before { + content: "\f152"; } + +.fa.fa-toggle-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-toggle-right:before { + content: "\f152"; } + +.fa.fa-eur:before { + content: "\f153"; } + +.fa.fa-euro:before { + content: "\f153"; } + +.fa.fa-gbp:before { + content: "\f154"; } + +.fa.fa-usd:before { + content: "\f155"; } + +.fa.fa-dollar:before { + content: "\f155"; } + +.fa.fa-inr:before { + content: "\f156"; } + +.fa.fa-rupee:before { + content: "\f156"; } + +.fa.fa-jpy:before { + content: "\f157"; } + +.fa.fa-cny:before { + content: "\f157"; } + +.fa.fa-rmb:before { + content: "\f157"; } + +.fa.fa-yen:before { + content: "\f157"; } + +.fa.fa-rub:before { + content: "\f158"; } + +.fa.fa-ruble:before { + content: "\f158"; } + +.fa.fa-rouble:before { + content: "\f158"; } + +.fa.fa-krw:before { + content: "\f159"; } + +.fa.fa-won:before { + content: "\f159"; } + +.fa.fa-btc { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bitcoin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bitcoin:before { + content: "\f15a"; } + +.fa.fa-file-text:before { + content: "\f15c"; } + +.fa.fa-sort-alpha-asc:before { + content: "\f15d"; } + +.fa.fa-sort-alpha-desc:before { + content: "\f881"; } + +.fa.fa-sort-amount-asc:before { + content: "\f160"; } + +.fa.fa-sort-amount-desc:before { + content: "\f884"; } + +.fa.fa-sort-numeric-asc:before { + content: "\f162"; } + +.fa.fa-sort-numeric-desc:before { + content: "\f886"; } + +.fa.fa-youtube-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-youtube { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-xing { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-xing-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-youtube-play { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-youtube-play:before { + content: "\f167"; } + +.fa.fa-dropbox { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-stack-overflow { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-instagram { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-flickr { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-adn { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bitbucket { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bitbucket-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bitbucket-square:before { + content: "\f171"; } + +.fa.fa-tumblr { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-tumblr-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-long-arrow-down:before { + content: "\f309"; } + +.fa.fa-long-arrow-up:before { + content: "\f30c"; } + +.fa.fa-long-arrow-left:before { + content: "\f30a"; } + +.fa.fa-long-arrow-right:before { + content: "\f30b"; } + +.fa.fa-apple { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-windows { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-android { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-linux { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-dribbble { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-skype { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-foursquare { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-trello { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gratipay { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gittip { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gittip:before { + content: "\f184"; } + +.fa.fa-sun-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-sun-o:before { + content: "\f185"; } + +.fa.fa-moon-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-moon-o:before { + content: "\f186"; } + +.fa.fa-vk { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-weibo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-renren { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-pagelines { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-stack-exchange { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-arrow-circle-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-arrow-circle-o-right:before { + content: "\f35a"; } + +.fa.fa-arrow-circle-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-arrow-circle-o-left:before { + content: "\f359"; } + +.fa.fa-caret-square-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-caret-square-o-left:before { + content: "\f191"; } + +.fa.fa-toggle-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-toggle-left:before { + content: "\f191"; } + +.fa.fa-dot-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-dot-circle-o:before { + content: "\f192"; } + +.fa.fa-vimeo-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-try:before { + content: "\f195"; } + +.fa.fa-turkish-lira:before { + content: "\f195"; } + +.fa.fa-plus-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-plus-square-o:before { + content: "\f0fe"; } + +.fa.fa-slack { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wordpress { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-openid { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-institution:before { + content: "\f19c"; } + +.fa.fa-bank:before { + content: "\f19c"; } + +.fa.fa-mortar-board:before { + content: "\f19d"; } + +.fa.fa-yahoo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-reddit { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-reddit-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-stumbleupon-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-stumbleupon { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-delicious { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-digg { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-pied-piper-pp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-pied-piper-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-drupal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-joomla { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-spoon:before { + content: "\f2e5"; } + +.fa.fa-behance { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-behance-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-steam { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-steam-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-automobile:before { + content: "\f1b9"; } + +.fa.fa-envelope-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-envelope-o:before { + content: "\f0e0"; } + +.fa.fa-spotify { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-deviantart { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-soundcloud { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-file-pdf-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-pdf-o:before { + content: "\f1c1"; } + +.fa.fa-file-word-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-word-o:before { + content: "\f1c2"; } + +.fa.fa-file-excel-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-excel-o:before { + content: "\f1c3"; } + +.fa.fa-file-powerpoint-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-powerpoint-o:before { + content: "\f1c4"; } + +.fa.fa-file-image-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-image-o:before { + content: "\f1c5"; } + +.fa.fa-file-photo-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-photo-o:before { + content: "\f1c5"; } + +.fa.fa-file-picture-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-picture-o:before { + content: "\f1c5"; } + +.fa.fa-file-archive-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-archive-o:before { + content: "\f1c6"; } + +.fa.fa-file-zip-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-zip-o:before { + content: "\f1c6"; } + +.fa.fa-file-audio-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-audio-o:before { + content: "\f1c7"; } + +.fa.fa-file-sound-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-sound-o:before { + content: "\f1c7"; } + +.fa.fa-file-video-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-video-o:before { + content: "\f1c8"; } + +.fa.fa-file-movie-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-movie-o:before { + content: "\f1c8"; } + +.fa.fa-file-code-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-file-code-o:before { + content: "\f1c9"; } + +.fa.fa-vine { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-codepen { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-jsfiddle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-life-ring { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-life-bouy { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-life-bouy:before { + content: "\f1cd"; } + +.fa.fa-life-buoy { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-life-buoy:before { + content: "\f1cd"; } + +.fa.fa-life-saver { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-life-saver:before { + content: "\f1cd"; } + +.fa.fa-support { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-support:before { + content: "\f1cd"; } + +.fa.fa-circle-o-notch:before { + content: "\f1ce"; } + +.fa.fa-rebel { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ra { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ra:before { + content: "\f1d0"; } + +.fa.fa-resistance { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-resistance:before { + content: "\f1d0"; } + +.fa.fa-empire { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ge { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ge:before { + content: "\f1d1"; } + +.fa.fa-git-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-git { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-hacker-news { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-y-combinator-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-y-combinator-square:before { + content: "\f1d4"; } + +.fa.fa-yc-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-yc-square:before { + content: "\f1d4"; } + +.fa.fa-tencent-weibo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-qq { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-weixin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wechat { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wechat:before { + content: "\f1d7"; } + +.fa.fa-send:before { + content: "\f1d8"; } + +.fa.fa-paper-plane-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-paper-plane-o:before { + content: "\f1d8"; } + +.fa.fa-send-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-send-o:before { + content: "\f1d8"; } + +.fa.fa-circle-thin { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-circle-thin:before { + content: "\f111"; } + +.fa.fa-header:before { + content: "\f1dc"; } + +.fa.fa-sliders:before { + content: "\f1de"; } + +.fa.fa-futbol-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-futbol-o:before { + content: "\f1e3"; } + +.fa.fa-soccer-ball-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-soccer-ball-o:before { + content: "\f1e3"; } + +.fa.fa-slideshare { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-twitch { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-yelp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-newspaper-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-newspaper-o:before { + content: "\f1ea"; } + +.fa.fa-paypal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-wallet { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-visa { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-mastercard { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-discover { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-amex { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-paypal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-stripe { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bell-slash-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-bell-slash-o:before { + content: "\f1f6"; } + +.fa.fa-trash:before { + content: "\f2ed"; } + +.fa.fa-copyright { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-eyedropper:before { + content: "\f1fb"; } + +.fa.fa-area-chart:before { + content: "\f1fe"; } + +.fa.fa-pie-chart:before { + content: "\f200"; } + +.fa.fa-line-chart:before { + content: "\f201"; } + +.fa.fa-lastfm { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-lastfm-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ioxhost { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-angellist { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-cc:before { + content: "\f20a"; } + +.fa.fa-ils:before { + content: "\f20b"; } + +.fa.fa-shekel:before { + content: "\f20b"; } + +.fa.fa-sheqel:before { + content: "\f20b"; } + +.fa.fa-meanpath { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-meanpath:before { + content: "\f2b4"; } + +.fa.fa-buysellads { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-connectdevelop { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-dashcube { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-forumbee { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-leanpub { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-sellsy { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-shirtsinbulk { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-simplybuilt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-skyatlas { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-diamond { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-diamond:before { + content: "\f3a5"; } + +.fa.fa-intersex:before { + content: "\f224"; } + +.fa.fa-facebook-official { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-facebook-official:before { + content: "\f09a"; } + +.fa.fa-pinterest-p { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-whatsapp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-hotel:before { + content: "\f236"; } + +.fa.fa-viacoin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-medium { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-y-combinator { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-yc { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-yc:before { + content: "\f23b"; } + +.fa.fa-optin-monster { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-opencart { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-expeditedssl { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-battery-4:before { + content: "\f240"; } + +.fa.fa-battery:before { + content: "\f240"; } + +.fa.fa-battery-3:before { + content: "\f241"; } + +.fa.fa-battery-2:before { + content: "\f242"; } + +.fa.fa-battery-1:before { + content: "\f243"; } + +.fa.fa-battery-0:before { + content: "\f244"; } + +.fa.fa-object-group { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-object-ungroup { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-sticky-note-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-sticky-note-o:before { + content: "\f249"; } + +.fa.fa-cc-jcb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cc-diners-club { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-clone { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hourglass-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hourglass-o:before { + content: "\f254"; } + +.fa.fa-hourglass-1:before { + content: "\f251"; } + +.fa.fa-hourglass-2:before { + content: "\f252"; } + +.fa.fa-hourglass-3:before { + content: "\f253"; } + +.fa.fa-hand-rock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-rock-o:before { + content: "\f255"; } + +.fa.fa-hand-grab-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-grab-o:before { + content: "\f255"; } + +.fa.fa-hand-paper-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-paper-o:before { + content: "\f256"; } + +.fa.fa-hand-stop-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-stop-o:before { + content: "\f256"; } + +.fa.fa-hand-scissors-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-scissors-o:before { + content: "\f257"; } + +.fa.fa-hand-lizard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-lizard-o:before { + content: "\f258"; } + +.fa.fa-hand-spock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-spock-o:before { + content: "\f259"; } + +.fa.fa-hand-pointer-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-pointer-o:before { + content: "\f25a"; } + +.fa.fa-hand-peace-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-hand-peace-o:before { + content: "\f25b"; } + +.fa.fa-registered { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-creative-commons { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gg { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gg-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-tripadvisor { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-odnoklassniki { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-odnoklassniki-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-get-pocket { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wikipedia-w { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-safari { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-chrome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-firefox { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-opera { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-internet-explorer { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-television:before { + content: "\f26c"; } + +.fa.fa-contao { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-500px { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-amazon { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-calendar-plus-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-calendar-plus-o:before { + content: "\f271"; } + +.fa.fa-calendar-minus-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-calendar-minus-o:before { + content: "\f272"; } + +.fa.fa-calendar-times-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-calendar-times-o:before { + content: "\f273"; } + +.fa.fa-calendar-check-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-calendar-check-o:before { + content: "\f274"; } + +.fa.fa-map-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-map-o:before { + content: "\f279"; } + +.fa.fa-commenting:before { + content: "\f4ad"; } + +.fa.fa-commenting-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-commenting-o:before { + content: "\f4ad"; } + +.fa.fa-houzz { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-vimeo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-vimeo:before { + content: "\f27d"; } + +.fa.fa-black-tie { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-fonticons { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-reddit-alien { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-edge { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-credit-card-alt:before { + content: "\f09d"; } + +.fa.fa-codiepie { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-modx { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-fort-awesome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-usb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-product-hunt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-mixcloud { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-scribd { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-pause-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-pause-circle-o:before { + content: "\f28b"; } + +.fa.fa-stop-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-stop-circle-o:before { + content: "\f28d"; } + +.fa.fa-bluetooth { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-bluetooth-b { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-gitlab { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wpbeginner { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wpforms { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-envira { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wheelchair-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wheelchair-alt:before { + content: "\f368"; } + +.fa.fa-question-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-question-circle-o:before { + content: "\f059"; } + +.fa.fa-volume-control-phone:before { + content: "\f2a0"; } + +.fa.fa-asl-interpreting:before { + content: "\f2a3"; } + +.fa.fa-deafness:before { + content: "\f2a4"; } + +.fa.fa-hard-of-hearing:before { + content: "\f2a4"; } + +.fa.fa-glide { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-glide-g { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-signing:before { + content: "\f2a7"; } + +.fa.fa-viadeo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-viadeo-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-snapchat { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-snapchat-ghost { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-snapchat-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-pied-piper { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-first-order { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-yoast { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-themeisle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-plus-official { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-plus-official:before { + content: "\f2b3"; } + +.fa.fa-google-plus-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-google-plus-circle:before { + content: "\f2b3"; } + +.fa.fa-font-awesome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-fa { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-fa:before { + content: "\f2b4"; } + +.fa.fa-handshake-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-handshake-o:before { + content: "\f2b5"; } + +.fa.fa-envelope-open-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-envelope-open-o:before { + content: "\f2b6"; } + +.fa.fa-linode { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-address-book-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-address-book-o:before { + content: "\f2b9"; } + +.fa.fa-vcard:before { + content: "\f2bb"; } + +.fa.fa-address-card-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-address-card-o:before { + content: "\f2bb"; } + +.fa.fa-vcard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-vcard-o:before { + content: "\f2bb"; } + +.fa.fa-user-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-user-circle-o:before { + content: "\f2bd"; } + +.fa.fa-user-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-user-o:before { + content: "\f007"; } + +.fa.fa-id-badge { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-drivers-license:before { + content: "\f2c2"; } + +.fa.fa-id-card-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-id-card-o:before { + content: "\f2c2"; } + +.fa.fa-drivers-license-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-drivers-license-o:before { + content: "\f2c2"; } + +.fa.fa-quora { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-free-code-camp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-telegram { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-thermometer-4:before { + content: "\f2c7"; } + +.fa.fa-thermometer:before { + content: "\f2c7"; } + +.fa.fa-thermometer-3:before { + content: "\f2c8"; } + +.fa.fa-thermometer-2:before { + content: "\f2c9"; } + +.fa.fa-thermometer-1:before { + content: "\f2ca"; } + +.fa.fa-thermometer-0:before { + content: "\f2cb"; } + +.fa.fa-bathtub:before { + content: "\f2cd"; } + +.fa.fa-s15:before { + content: "\f2cd"; } + +.fa.fa-window-maximize { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-window-restore { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-times-rectangle:before { + content: "\f410"; } + +.fa.fa-window-close-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-window-close-o:before { + content: "\f410"; } + +.fa.fa-times-rectangle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-times-rectangle-o:before { + content: "\f410"; } + +.fa.fa-bandcamp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-grav { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-etsy { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-imdb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-ravelry { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-eercast { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-eercast:before { + content: "\f2da"; } + +.fa.fa-snowflake-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } + +.fa.fa-snowflake-o:before { + content: "\f2dc"; } + +.fa.fa-superpowers { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-wpexplorer { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; } + +.fa.fa-cab:before { + content: "\f1ba"; } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot new file mode 100644 index 0000000000..e4ccce2d5c Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg new file mode 100644 index 0000000000..eb0f26fcbc --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Tue Feb 4 18:05:39 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000..08622a3b0f Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff new file mode 100644 index 0000000000..a43870c27f Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000000..3c5189de12 Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot new file mode 100644 index 0000000000..dbc06a4a33 Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg new file mode 100644 index 0000000000..273a984113 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Tue Feb 4 18:05:39 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000000..4b6fdef613 Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff new file mode 100644 index 0000000000..f17953a79f Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000..1f796c7505 Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot new file mode 100644 index 0000000000..550f8f021f Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg new file mode 100644 index 0000000000..6933b2b52b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg @@ -0,0 +1,4700 @@ + + + + + +Created by FontForge 20190801 at Tue Feb 4 18:05:39 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000..9dd8c7fd78 Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff new file mode 100644 index 0000000000..3c9ef93429 Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000..ba7507b18d Binary files /dev/null and b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 differ diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.css b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.css new file mode 100644 index 0000000000..ddf9cae5b2 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.css @@ -0,0 +1,56 @@ +@keyframes spin { + 0% { + transform: translateZ(0) rotate(0deg); + } + + 100% { + transform: translateZ(0) rotate(360deg); + } +} + +.abp-block-area { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 102; + background-color: #fff; + opacity: .8; + transition: opacity .25s; +} + + .abp-block-area.abp-block-area-disappearing { + opacity: 0; + } + + .abp-block-area.abp-block-area-busy:after { + content: attr(data-text); + display: block; + max-width: 125px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 20px; + font-family: sans-serif; + color: #343a40; + text-align: center; + text-transform: uppercase; + } + + .abp-block-area.abp-block-area-busy:before { + content: ""; + display: block; + width: 150px; + height: 150px; + border-radius: 50%; + border-width: 2px; + border-style: solid; + border-color: transparent #228ae6 #228ae6 #228ae6; + position: absolute; + top: calc(50% - 75px); + left: calc(50% - 75px); + will-change: transform; + animation: spin .75s infinite ease-in-out; + } 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 new file mode 100644 index 0000000000..730db8bc97 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js @@ -0,0 +1,636 @@ +var abp = abp || {}; +(function () { + + /* Application paths *****************************************/ + + //Current application root path (including virtual directory if exists). + abp.appPath = abp.appPath || '/'; + + abp.pageLoadTime = new Date(); + + //Converts given path to absolute path using abp.appPath variable. + abp.toAbsAppPath = function (path) { + if (path.indexOf('/') == 0) { + path = path.substring(1); + } + + return abp.appPath + path; + }; + + /* LOGGING ***************************************************/ + //Implements Logging API that provides secure & controlled usage of console.log + + abp.log = abp.log || {}; + + abp.log.levels = { + DEBUG: 1, + INFO: 2, + WARN: 3, + ERROR: 4, + FATAL: 5 + }; + + abp.log.level = abp.log.levels.DEBUG; + + abp.log.log = function (logObject, logLevel) { + if (!window.console || !window.console.log) { + return; + } + + if (logLevel != undefined && logLevel < abp.log.level) { + return; + } + + console.log(logObject); + }; + + abp.log.debug = function (logObject) { + abp.log.log("DEBUG: ", abp.log.levels.DEBUG); + abp.log.log(logObject, abp.log.levels.DEBUG); + }; + + abp.log.info = function (logObject) { + abp.log.log("INFO: ", abp.log.levels.INFO); + abp.log.log(logObject, abp.log.levels.INFO); + }; + + abp.log.warn = function (logObject) { + abp.log.log("WARN: ", abp.log.levels.WARN); + abp.log.log(logObject, abp.log.levels.WARN); + }; + + abp.log.error = function (logObject) { + abp.log.log("ERROR: ", abp.log.levels.ERROR); + abp.log.log(logObject, abp.log.levels.ERROR); + }; + + abp.log.fatal = function (logObject) { + abp.log.log("FATAL: ", abp.log.levels.FATAL); + abp.log.log(logObject, abp.log.levels.FATAL); + }; + + /* LOCALIZATION ***********************************************/ + + abp.localization = abp.localization || {}; + + abp.localization.values = {}; + + abp.localization.localize = function (key, sourceName) { + sourceName = sourceName || abp.localization.defaultResourceName; + + var source = abp.localization.values[sourceName]; + + if (!source) { + abp.log.warn('Could not find localization source: ' + sourceName); + return key; + } + + var value = source[key]; + if (value == undefined) { + return key; + } + + var copiedArguments = Array.prototype.slice.call(arguments, 0); + copiedArguments.splice(1, 1); + copiedArguments[0] = value; + + return abp.utils.formatString.apply(this, copiedArguments); + }; + + abp.localization.getResource = function (name) { + return function () { + var copiedArguments = Array.prototype.slice.call(arguments, 0); + copiedArguments.splice(1, 0, name); + return abp.localization.localize.apply(this, copiedArguments); + }; + }; + + abp.localization.defaultResourceName = undefined; + + /* AUTHORIZATION **********************************************/ + + abp.auth = abp.auth || {}; + + abp.auth.policies = abp.auth.policies || {}; + + abp.auth.grantedPolicies = abp.auth.grantedPolicies || {}; + + abp.auth.isGranted = function (policyName) { + return abp.auth.policies[policyName] != undefined && abp.auth.grantedPolicies[policyName] != undefined; + }; + + abp.auth.isAnyGranted = function () { + if (!arguments || arguments.length <= 0) { + return true; + } + + for (var i = 0; i < arguments.length; i++) { + if (abp.auth.isGranted(arguments[i])) { + return true; + } + } + + return false; + }; + + abp.auth.areAllGranted = function () { + if (!arguments || arguments.length <= 0) { + return true; + } + + for (var i = 0; i < arguments.length; i++) { + if (!abp.auth.isGranted(arguments[i])) { + return false; + } + } + + return true; + }; + + abp.auth.tokenCookieName = 'Abp.AuthToken'; + + abp.auth.setToken = function (authToken, expireDate) { + abp.utils.setCookieValue(abp.auth.tokenCookieName, authToken, expireDate, abp.appPath, abp.domain); + }; + + abp.auth.getToken = function () { + return abp.utils.getCookieValue(abp.auth.tokenCookieName); + } + + abp.auth.clearToken = function () { + abp.auth.setToken(); + } + + /* SETTINGS *************************************************/ + + abp.setting = abp.setting || {}; + + abp.setting.values = abp.setting.values || {}; + + abp.setting.get = function (name) { + return abp.setting.values[name]; + }; + + abp.setting.getBoolean = function (name) { + var value = abp.setting.get(name); + return value == 'true' || value == 'True'; + }; + + abp.setting.getInt = function (name) { + return parseInt(abp.setting.values[name]); + }; + + /* NOTIFICATION *********************************************/ + //Defines Notification API, not implements it + + abp.notify = abp.notify || {}; + + abp.notify.success = function (message, title, options) { + abp.log.warn('abp.notify.success is not implemented!'); + }; + + abp.notify.info = function (message, title, options) { + abp.log.warn('abp.notify.info is not implemented!'); + }; + + abp.notify.warn = function (message, title, options) { + abp.log.warn('abp.notify.warn is not implemented!'); + }; + + abp.notify.error = function (message, title, options) { + abp.log.warn('abp.notify.error is not implemented!'); + }; + + /* MESSAGE **************************************************/ + //Defines Message API, not implements it + + abp.message = abp.message || {}; + + abp.message._showMessage = function (message, title) { + alert((title || '') + ' ' + message); + }; + + abp.message.info = function (message, title) { + abp.log.warn('abp.message.info is not implemented!'); + return abp.message._showMessage(message, title); + }; + + abp.message.success = function (message, title) { + abp.log.warn('abp.message.success is not implemented!'); + return abp.message._showMessage(message, title); + }; + + abp.message.warn = function (message, title) { + abp.log.warn('abp.message.warn is not implemented!'); + return abp.message._showMessage(message, title); + }; + + abp.message.error = function (message, title) { + abp.log.warn('abp.message.error is not implemented!'); + return abp.message._showMessage(message, title); + }; + + abp.message.confirm = function (message, titleOrCallback, callback) { + abp.log.warn('abp.message.confirm is not properly implemented!'); + + if (titleOrCallback && !(typeof titleOrCallback == 'string')) { + callback = titleOrCallback; + } + + var result = confirm(message); + callback && callback(result); + }; + + /* UI *******************************************************/ + + abp.ui = abp.ui || {}; + + /* UI BLOCK */ + //Defines UI Block API and implements basically + + var $abpBlockArea = document.createElement('div'); + $abpBlockArea.classList.add('abp-block-area'); + + /* opts: { //Can be an object with options or a string for query a selector + * elm: a query selector (optional - default: document.body) + * busy: boolean (optional - default: false) + * promise: A promise with always or finally handler (optional - auto unblocks the ui if provided) + * } + */ + abp.ui.block = function (opts) { + if (!opts) { + opts = {}; + } else if (typeof opts == 'string') { + opts = { + elm: opts + }; + } + + var $elm = document.querySelector(opts.elm) || document.body; + + if (opts.busy) { + $abpBlockArea.classList.add('abp-block-area-busy'); + } else { + $abpBlockArea.classList.remove('abp-block-area-busy'); + } + + if (document.querySelector(opts.elm)) { + $abpBlockArea.style.position = 'absolute'; + } else { + $abpBlockArea.style.position = 'fixed'; + } + + $elm.appendChild($abpBlockArea); + + if (opts.promise) { + if (opts.promise.always) { //jQuery.Deferred style + opts.promise.always(function () { + abp.ui.unblock({ + $elm: opts.elm + }); + }); + } else if (opts.promise['finally']) { //Q style + opts.promise['finally'](function () { + abp.ui.unblock({ + $elm: opts.elm + }); + }); + } + } + }; + + /* opts: { + * + * } + */ + abp.ui.unblock = function (opts) { + var element = document.querySelector('.abp-block-area'); + if (element) { + element.classList.add('abp-block-area-disappearing'); + setTimeout(function () { + if (element) { + element.classList.remove('abp-block-area-disappearing'); + element.parentElement.removeChild(element); + } + }, 250); + } + }; + + /* UI BUSY */ + //Defines UI Busy API, not implements it + + abp.ui.setBusy = function (opts) { + if (!opts) { + opts = { + busy: true + }; + } else if (typeof opts == 'string') { + opts = { + elm: opts, + busy: true + }; + } + + abp.ui.block(opts); + }; + + abp.ui.clearBusy = function (opts) { + abp.ui.unblock(opts); + }; + + /* SIMPLE EVENT BUS *****************************************/ + + abp.event = (function () { + + var _callbacks = {}; + + var on = function (eventName, callback) { + if (!_callbacks[eventName]) { + _callbacks[eventName] = []; + } + + _callbacks[eventName].push(callback); + }; + + var off = function (eventName, callback) { + var callbacks = _callbacks[eventName]; + if (!callbacks) { + return; + } + + var index = -1; + for (var i = 0; i < callbacks.length; i++) { + if (callbacks[i] === callback) { + index = i; + break; + } + } + + if (index < 0) { + return; + } + + _callbacks[eventName].splice(index, 1); + }; + + var trigger = function (eventName) { + var callbacks = _callbacks[eventName]; + if (!callbacks || !callbacks.length) { + return; + } + + var args = Array.prototype.slice.call(arguments, 1); + for (var i = 0; i < callbacks.length; i++) { + callbacks[i].apply(this, args); + } + }; + + // Public interface /////////////////////////////////////////////////// + + return { + on: on, + off: off, + trigger: trigger + }; + })(); + + + /* UTILS ***************************************************/ + + abp.utils = abp.utils || {}; + + /* Creates a name namespace. + * Example: + * var taskService = abp.utils.createNamespace(abp, 'services.task'); + * taskService will be equal to abp.services.task + * first argument (root) must be defined first + ************************************************************/ + abp.utils.createNamespace = function (root, ns) { + var parts = ns.split('.'); + for (var i = 0; i < parts.length; i++) { + if (typeof root[parts[i]] == 'undefined') { + root[parts[i]] = {}; + } + + root = root[parts[i]]; + } + + return root; + }; + + /* Find and replaces a string (search) to another string (replacement) in + * given string (str). + * Example: + * abp.utils.replaceAll('This is a test string', 'is', 'X') = 'ThX X a test string' + ************************************************************/ + abp.utils.replaceAll = function (str, search, replacement) { + var fix = search.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return str.replace(new RegExp(fix, 'g'), replacement); + }; + + /* Formats a string just like string.format in C#. + * Example: + * abp.utils.formatString('Hello {0}','Tuana') = 'Hello Tuana' + ************************************************************/ + abp.utils.formatString = function () { + if (arguments.length < 1) { + return null; + } + + var str = arguments[0]; + + for (var i = 1; i < arguments.length; i++) { + var placeHolder = '{' + (i - 1) + '}'; + str = abp.utils.replaceAll(str, placeHolder, arguments[i]); + } + + return str; + }; + + abp.utils.toPascalCase = function (str) { + if (!str || !str.length) { + return str; + } + + if (str.length === 1) { + return str.charAt(0).toUpperCase(); + } + + return str.charAt(0).toUpperCase() + str.substr(1); + } + + abp.utils.toCamelCase = function (str) { + if (!str || !str.length) { + return str; + } + + if (str.length === 1) { + return str.charAt(0).toLowerCase(); + } + + return str.charAt(0).toLowerCase() + str.substr(1); + } + + abp.utils.truncateString = function (str, maxLength) { + if (!str || !str.length || str.length <= maxLength) { + return str; + } + + return str.substr(0, maxLength); + }; + + abp.utils.truncateStringWithPostfix = function (str, maxLength, postfix) { + postfix = postfix || '...'; + + if (!str || !str.length || str.length <= maxLength) { + return str; + } + + if (maxLength <= postfix.length) { + return postfix.substr(0, maxLength); + } + + return str.substr(0, maxLength - postfix.length) + postfix; + }; + + abp.utils.isFunction = function (obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }; + + /** + * parameterInfos should be an array of { name, value } objects + * where name is query string parameter name and value is it's value. + * includeQuestionMark is true by default. + */ + abp.utils.buildQueryString = function (parameterInfos, includeQuestionMark) { + if (includeQuestionMark === undefined) { + includeQuestionMark = true; + } + + var qs = ''; + + function addSeperator() { + if (!qs.length) { + if (includeQuestionMark) { + qs = qs + '?'; + } + } else { + qs = qs + '&'; + } + } + + for (var i = 0; i < parameterInfos.length; ++i) { + var parameterInfo = parameterInfos[i]; + if (parameterInfo.value === undefined) { + continue; + } + + if (parameterInfo.value === null) { + parameterInfo.value = ''; + } + + addSeperator(); + + if (parameterInfo.value.toJSON && typeof parameterInfo.value.toJSON === "function") { + qs = qs + parameterInfo.name + '=' + encodeURIComponent(parameterInfo.value.toJSON()); + } else if (Array.isArray(parameterInfo.value) && parameterInfo.value.length) { + for (var j = 0; j < parameterInfo.value.length; j++) { + if (j > 0) { + addSeperator(); + } + + qs = qs + parameterInfo.name + '[' + j + ']=' + encodeURIComponent(parameterInfo.value[j]); + } + } else { + qs = qs + parameterInfo.name + '=' + encodeURIComponent(parameterInfo.value); + } + } + + return qs; + } + + /** + * Sets a cookie value for given key. + * This is a simple implementation created to be used by ABP. + * Please use a complete cookie library if you need. + * @param {string} key + * @param {string} value + * @param {Date} expireDate (optional). If not specified the cookie will expire at the end of session. + * @param {string} path (optional) + */ + abp.utils.setCookieValue = function (key, value, expireDate, path) { + var cookieValue = encodeURIComponent(key) + '='; + + if (value) { + cookieValue = cookieValue + encodeURIComponent(value); + } + + if (expireDate) { + cookieValue = cookieValue + "; expires=" + expireDate.toUTCString(); + } + + if (path) { + cookieValue = cookieValue + "; path=" + path; + } + + document.cookie = cookieValue; + }; + + /** + * Gets a cookie with given key. + * This is a simple implementation created to be used by ABP. + * Please use a complete cookie library if you need. + * @param {string} key + * @returns {string} Cookie value or null + */ + abp.utils.getCookieValue = function (key) { + var equalities = document.cookie.split('; '); + for (var i = 0; i < equalities.length; i++) { + if (!equalities[i]) { + continue; + } + + var splitted = equalities[i].split('='); + if (splitted.length != 2) { + continue; + } + + if (decodeURIComponent(splitted[0]) === key) { + return decodeURIComponent(splitted[1] || ''); + } + } + + return null; + }; + + /** + * Deletes cookie for given key. + * This is a simple implementation created to be used by ABP. + * Please use a complete cookie library if you need. + * @param {string} key + * @param {string} path (optional) + */ + abp.utils.deleteCookie = function (key, path) { + var cookieValue = encodeURIComponent(key) + '='; + + cookieValue = cookieValue + "; expires=" + (new Date(new Date().getTime() - 86400000)).toUTCString(); + + if (path) { + cookieValue = cookieValue + "; path=" + path; + } + + document.cookie = cookieValue; + } + + /* SECURITY ***************************************/ + abp.security = abp.security || {}; + abp.security.antiForgery = abp.security.antiForgery || {}; + + abp.security.antiForgery.tokenCookieName = 'XSRF-TOKEN'; + abp.security.antiForgery.tokenHeaderName = 'X-XSRF-TOKEN'; + + abp.security.antiForgery.getToken = function () { + return abp.utils.getCookieValue(abp.security.antiForgery.tokenCookieName); + }; + +})(); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/jquery/abp.jquery.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/jquery/abp.jquery.js new file mode 100644 index 0000000000..e6aff047f7 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/jquery/abp.jquery.js @@ -0,0 +1,393 @@ +var abp = abp || {}; +(function($) { + + if (!$) { + throw "abp/jquery library requires the jquery library included to the page!"; + } + + // ABP CORE OVERRIDES ///////////////////////////////////////////////////// + + abp.message._showMessage = function (message, title) { + alert((title || '') + ' ' + message); + + return $.Deferred(function ($dfd) { + $dfd.resolve(); + }); + }; + + abp.message.confirm = function (message, titleOrCallback, callback) { + if (titleOrCallback && !(typeof titleOrCallback == 'string')) { + callback = titleOrCallback; + } + + var result = confirm(message); + callback && callback(result); + + return $.Deferred(function ($dfd) { + $dfd.resolve(result); + }); + }; + + abp.utils.isFunction = function (obj) { + return $.isFunction(obj); + }; + + // JQUERY EXTENSIONS ////////////////////////////////////////////////////// + + $.fn.findWithSelf = function (selector) { + return this.filter(selector).add(this.find(selector)); + }; + + // DOM //////////////////////////////////////////////////////////////////// + + abp.dom = abp.dom || {}; + + abp.dom.onNodeAdded = function (callback) { + abp.event.on('abp.dom.nodeAdded', callback); + }; + + abp.dom.onNodeRemoved = function (callback) { + abp.event.on('abp.dom.nodeRemoved', callback); + }; + + var mutationObserverCallback = function (mutationsList) { + for (var i = 0; i < mutationsList.length; i++) { + var mutation = mutationsList[i]; + if (mutation.type === 'childList') { + if (mutation.addedNodes && mutation.removedNodes.length) { + for (var k = 0; k < mutation.removedNodes.length; k++) { + abp.event.trigger( + 'abp.dom.nodeRemoved', + { + $el: $(mutation.removedNodes[k]) + } + ); + } + } + + if (mutation.addedNodes && mutation.addedNodes.length) { + for (var j = 0; j < mutation.addedNodes.length; j++) { + abp.event.trigger( + 'abp.dom.nodeAdded', + { + $el: $(mutation.addedNodes[j]) + } + ); + } + } + } + } + }; + + new MutationObserver(mutationObserverCallback).observe( + $('body')[0], + { + subtree: true, + childList: true + } + ); + + // AJAX /////////////////////////////////////////////////////////////////// + + abp.ajax = function (userOptions) { + userOptions = userOptions || {}; + + var options = $.extend(true, {}, abp.ajax.defaultOpts, userOptions); + + options.success = undefined; + options.error = undefined; + + return $.Deferred(function ($dfd) { + $.ajax(options) + .done(function (data, textStatus, jqXHR) { + $dfd.resolve(data); + userOptions.success && userOptions.success(data); + }).fail(function (jqXHR) { + if (jqXHR.getResponseHeader('_AbpErrorFormat') === 'true') { + abp.ajax.handleAbpErrorResponse(jqXHR, userOptions, $dfd); + } else { + abp.ajax.handleNonAbpErrorResponse(jqXHR, userOptions, $dfd); + } + }); + }); + }; + + $.extend(abp.ajax, { + defaultOpts: { + dataType: 'json', + type: 'POST', + contentType: 'application/json', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }, + + defaultError: { + message: 'An error has occurred!', + details: 'Error detail not sent by server.' + }, + + defaultError401: { + message: 'You are not authenticated!', + details: 'You should be authenticated (sign in) in order to perform this operation.' + }, + + defaultError403: { + message: 'You are not authorized!', + details: 'You are not allowed to perform this operation.' + }, + + defaultError404: { + message: 'Resource not found!', + details: 'The resource requested could not found on the server.' + }, + + logError: function (error) { + abp.log.error(error); + }, + + showError: function (error) { + if (error.details) { + return abp.message.error(error.details, error.message); + } else { + return abp.message.error(error.message || abp.ajax.defaultError.message); + } + }, + + handleTargetUrl: function (targetUrl) { + if (!targetUrl) { + location.href = abp.appPath; + } else { + location.href = targetUrl; + } + }, + + handleErrorStatusCode: function (status) { + switch (status) { + case 401: + abp.ajax.handleUnAuthorizedRequest( + abp.ajax.showError(abp.ajax.defaultError401), + abp.appPath + ); + break; + case 403: + abp.ajax.showError(abp.ajax.defaultError403); + break; + case 404: + abp.ajax.showError(abp.ajax.defaultError404); + break; + default: + abp.ajax.showError(abp.ajax.defaultError); + break; + } + }, + + handleNonAbpErrorResponse: function (jqXHR, userOptions, $dfd) { + if (userOptions.abpHandleError !== false) { + abp.ajax.handleErrorStatusCode(jqXHR.status); + } + + $dfd.reject.apply(this, arguments); + userOptions.error && userOptions.error.apply(this, arguments); + }, + + handleAbpErrorResponse: function (jqXHR, userOptions, $dfd) { + var messagePromise = null; + + if (userOptions.abpHandleError !== false) { + messagePromise = abp.ajax.showError(jqXHR.responseJSON.error); + } + + abp.ajax.logError(jqXHR.responseJSON.error); + + $dfd && $dfd.reject(jqXHR.responseJSON.error, jqXHR); + userOptions.error && userOptions.error(jqXHR.responseJSON.error, jqXHR); + + if (jqXHR.status === 401 && userOptions.abpHandleError !== false) { + abp.ajax.handleUnAuthorizedRequest(messagePromise); + } + }, + + handleUnAuthorizedRequest: function (messagePromise, targetUrl) { + if (messagePromise) { + messagePromise.done(function () { + abp.ajax.handleTargetUrl(targetUrl); + }); + } else { + abp.ajax.handleTargetUrl(targetUrl); + } + }, + + blockUI: function (options) { + if (options.blockUI) { + if (options.blockUI === true) { //block whole page + abp.ui.setBusy(); + } else { //block an element + abp.ui.setBusy(options.blockUI); + } + } + }, + + unblockUI: function (options) { + if (options.blockUI) { + if (options.blockUI === true) { //unblock whole page + abp.ui.clearBusy(); + } else { //unblock an element + abp.ui.clearBusy(options.blockUI); + } + } + }, + + ajaxSendHandler: function (event, request, settings) { + var token = abp.security.antiForgery.getToken(); + if (!token) { + return; + } + + if (!settings.headers || settings.headers[abp.security.antiForgery.tokenHeaderName] === undefined) { + request.setRequestHeader(abp.security.antiForgery.tokenHeaderName, token); + } + } + }); + + $(document).ajaxSend(function (event, request, settings) { + return abp.ajax.ajaxSendHandler(event, request, settings); + }); + + abp.event.on('abp.configurationInitialized', function () { + var l = abp.localization.getResource('AbpUi'); + + abp.ajax.defaultError.message = l('DefaultErrorMessage'); + abp.ajax.defaultError.details = l('DefaultErrorMessageDetail'); + abp.ajax.defaultError401.message = l('DefaultErrorMessage401'); + abp.ajax.defaultError401.details = l('DefaultErrorMessage401Detail'); + abp.ajax.defaultError403.message = l('DefaultErrorMessage403'); + abp.ajax.defaultError403.details = l('DefaultErrorMessage403Detail'); + abp.ajax.defaultError404.message = l('DefaultErrorMessage404'); + abp.ajax.defaultError404.details = l('DefaultErrorMessage404Detail'); + }); + + // RESOURCE LOADER //////////////////////////////////////////////////////// + + /* UrlStates enum */ + var UrlStates = { + LOADING: 'LOADING', + LOADED: 'LOADED', + FAILED: 'FAILED' + }; + + /* UrlInfo class */ + function UrlInfo(url) { + this.url = url; + this.state = UrlStates.LOADING; + this.loadCallbacks = []; + this.failCallbacks = []; + } + + UrlInfo.prototype.succeed = function () { + this.state = UrlStates.LOADED; + for (var i = 0; i < this.loadCallbacks.length; i++) { + this.loadCallbacks[i](); + } + }; + + UrlInfo.prototype.failed = function () { + this.state = UrlStates.FAILED; + for (var i = 0; i < this.failCallbacks.length; i++) { + this.failCallbacks[i](); + } + }; + + UrlInfo.prototype.handleCallbacks = function (loadCallback, failCallback) { + switch (this.state) { + case UrlStates.LOADED: + loadCallback && loadCallback(); + break; + case UrlStates.FAILED: + failCallback && failCallback(); + break; + case UrlStates.LOADING: + this.addCallbacks(loadCallback, failCallback); + break; + } + }; + + UrlInfo.prototype.addCallbacks = function (loadCallback, failCallback) { + loadCallback && this.loadCallbacks.push(loadCallback); + failCallback && this.failCallbacks.push(failCallback); + }; + + /* ResourceLoader API */ + + abp.ResourceLoader = (function () { + + var _urlInfos = {}; + + function getCacheKey(url) { + return url; + } + + function appendTimeToUrl(url) { + + if (url.indexOf('?') < 0) { + url += '?'; + } else { + url += '&'; + } + + url += '_=' + new Date().getTime(); + + return url; + } + + var _loadFromUrl = function (url, loadCallback, failCallback, serverLoader) { + + var cacheKey = getCacheKey(url); + + var urlInfo = _urlInfos[cacheKey]; + + if (urlInfo) { + urlInfo.handleCallbacks(loadCallback, failCallback); + return; + } + + _urlInfos[cacheKey] = urlInfo = new UrlInfo(url); + urlInfo.addCallbacks(loadCallback, failCallback); + + serverLoader(urlInfo); + }; + + var _loadScript = function (url, loadCallback, failCallback) { + _loadFromUrl(url, loadCallback, failCallback, function (urlInfo) { + $.get({ + url: url, + dataType: 'text' + }) + .done(function (script) { + $.globalEval(script); + urlInfo.succeed(); + }) + .fail(function () { + urlInfo.failed(); + }); + }); + }; + + var _loadStyle = function (url) { + _loadFromUrl(url, undefined, undefined, function (urlInfo) { + + $('', { + rel: 'stylesheet', + type: 'text/css', + href: appendTimeToUrl(url) + }).appendTo('head'); + }); + }; + + return { + loadScript: _loadScript, + loadStyle: _loadStyle + } + })(); + +})(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.css.map b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.css.map new file mode 100644 index 0000000000..7e08a21044 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["less/datepicker.less","build/build.less"],"names":[],"mappings":"AAAA;EACC,YAAA;ECsBC,0BAAA;EACG,uBAAA;EACK,kBAAA;EDnBT,cAAA;;AAHA,WAAC;EACA,YAAA;;AAGD,WAAC;EACA,cAAA;;AACA,WAFA,IAEC;EAAiB,UAAA;;AAFnB,WAAC,IAGA,MAAM,GAAG,GAAG;EACX,YAAA;;AAGF,WAAC;EACA,MAAA;EACA,OAAA;;AACA,WAHA,SAGC;EACA,SAAS,EAAT;EACA,qBAAA;EACA,kCAAA;EACA,mCAAA;EACA,6BAAA;EACA,aAAA;EACA,uCAAA;EACA,kBAAA;;AAED,WAbA,SAaC;EACA,SAAS,EAAT;EACA,qBAAA;EACA,kCAAA;EACA,mCAAA;EACA,6BAAA;EACA,aAAA;EACA,kBAAA;;AAED,WAtBA,SAsBC,uBAAuB;EAAY,SAAA;;AACpC,WAvBA,SAuBC,uBAAuB;EAAY,SAAA;;AACpC,WAxBA,SAwBC,wBAAwB;EAAW,UAAA;;AACpC,WAzBA,SAyBC,wBAAwB;EAAW,UAAA;;AACpC,WA1BA,SA0BC,yBAAyB;EAAU,SAAA;;AACpC,WA3BA,SA2BC,yBAAyB;EAAU,SAAA;;AACpC,WA5BA,SA4BC,sBAAsB;EACtB,YAAA;EACA,gBAAA;EACA,0BAAA;;AAED,WAjCA,SAiCC,sBAAsB;EACtB,YAAA;EACA,gBAAA;EACA,0BAAA;;AAlDH,WAqDC;EACC,SAAA;EACA,2BAAA;EACA,yBAAA;EACA,wBAAA;EACA,sBAAA;EACA,qBAAA;EACA,iBAAA;;AA5DF,WA8DC;AA9DD,WA8DK;EACH,kBAAA;EACA,WAAA;EACA,YAAA;EC1CA,0BAAA;EACG,uBAAA;EACK,kBAAA;ED2CR,YAAA;;AAID,cAAe,YAAE,MAAM,GACtB;AADD,cAAe,YAAE,MAAM,GAClB;EACH,6BAAA;;AAID,WADD,MAAM,GAAG,GACP,IAAI;AACL,WAFD,MAAM,GAAG,GAEP,IAAI;EACJ,gBAAA;EACA,eAAA;;AAED,WAND,MAAM,GAAG,GAMP;AACD,WAPD,MAAM,GAAG,GAOP;EACA,WAAA;;AAED,WAVD,MAAM,GAAG,GAUP;AACD,WAXD,MAAM,GAAG,GAWP,SAAS;EACT,gBAAA;EACA,WAAA;EACA,eAAA;;AAED,WAhBD,MAAM,GAAG,GAgBP;EACA,mBAAA;EACA,gBAAA;;AAED,WApBD,MAAM,GAAG,GAoBP;AACD,WArBD,MAAM,GAAG,GAqBP,MAAM;AACP,WAtBD,MAAM,GAAG,GAsBP,MAAM;AACP,WAvBD,MAAM,GAAG,GAuBP,MAAM,SAAS;EC5Cd,yBAAA;EACA,kBAAkB,iDAAlB;EACA,kBAAkB,gDAAlB;EACA,kBAAkB,sCAAsC,eAAmB,YAA3E;EACA,kBAAkB,oDAAlB;EACA,kBAAkB,+CAAlB;EACA,kBAAkB,4CAAlB;EACA,2BAAA;EACA,QAAQ,0GAAR;EAfF,qCAAA;EACA,uEAAA;EAPA,QAAQ,yDAAR;ED4DC,WAAA;;ACvED,WD6CD,MAAM,GAAG,GAoBP,MCjEA;AAAD,WD6CD,MAAM,GAAG,GAqBP,MAAM,MClEN;AAAD,WD6CD,MAAM,GAAG,GAsBP,MAAM,SCnEN;AAAD,WD6CD,MAAM,GAAG,GAuBP,MAAM,SAAS,MCpEf;AAAQ,WD6CV,MAAM,GAAG,GAoBP,MCjES;AAAD,WD6CV,MAAM,GAAG,GAqBP,MAAM,MClEG;AAAD,WD6CV,MAAM,GAAG,GAsBP,MAAM,SCnEG;AAAD,WD6CV,MAAM,GAAG,GAuBP,MAAM,SAAS,MCpEN;AAAS,WD6CpB,MAAM,GAAG,GAoBP,MCjEmB;AAAD,WD6CpB,MAAM,GAAG,GAqBP,MAAM,MClEa;AAAD,WD6CpB,MAAM,GAAG,GAsBP,MAAM,SCnEa;AAAD,WD6CpB,MAAM,GAAG,GAuBP,MAAM,SAAS,MCpEI;AAAS,WD6C9B,MAAM,GAAG,GAoBP,MCjE6B;AAAD,WD6C9B,MAAM,GAAG,GAqBP,MAAM,MClEuB;AAAD,WD6C9B,MAAM,GAAG,GAsBP,MAAM,SCnEuB;AAAD,WD6C9B,MAAM,GAAG,GAuBP,MAAM,SAAS,MCpEc;AAAW,WD6C1C,MAAM,GAAG,GAoBP,MCjEyC;AAAD,WD6C1C,MAAM,GAAG,GAqBP,MAAM,MClEmC;AAAD,WD6C1C,MAAM,GAAG,GAsBP,MAAM,SCnEmC;AAAD,WD6C1C,MAAM,GAAG,GAuBP,MAAM,SAAS,MCpE0B;EACxC,yBAAA;;AAEF,WD0CD,MAAM,GAAG,GAoBP,MC9DA;AAAD,WD0CD,MAAM,GAAG,GAqBP,MAAM,MC/DN;AAAD,WD0CD,MAAM,GAAG,GAsBP,MAAM,SChEN;AAAD,WD0CD,MAAM,GAAG,GAuBP,MAAM,SAAS,MCjEf;AACD,WDyCD,MAAM,GAAG,GAoBP,MC7DA;AAAD,WDyCD,MAAM,GAAG,GAqBP,MAAM,MC9DN;AAAD,WDyCD,MAAM,GAAG,GAsBP,MAAM,SC/DN;AAAD,WDyCD,MAAM,GAAG,GAuBP,MAAM,SAAS,MChEf;EACC,0BAAyC,EAAzC;;ADoEF,WA5BD,MAAM,GAAG,GA4BP,MAAM,MAAM;EAEZ,WAAA;;AAED,WAhCD,MAAM,GAAG,GAgCP,MAAM,OAAO;EACb,WAAA;;AAED,WAnCD,MAAM,GAAG,GAmCP;AACD,WApCD,MAAM,GAAG,GAoCP,MAAM;AACP,WArCD,MAAM,GAAG,GAqCP,MAAM;AACP,WAtCD,MAAM,GAAG,GAsCP,MAAM,SAAS;EACf,gBAAA;EC7FD,wBAAA;EACG,qBAAA;EACK,gBAAA;;AD8FR,WA1CD,MAAM,GAAG,GA0CP,MAAM;AACP,WA3CD,MAAM,GAAG,GA2CP,MAAM,MAAM;AACb,WA5CD,MAAM,GAAG,GA4CP,MAAM,MAAM;AACb,WA7CD,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS;EClEpB,yBAAA;EACA,kBAAkB,iDAAlB;EACA,kBAAkB,gDAAlB;EACA,kBAAkB,sCAAsC,eAAmB,YAA3E;EACA,kBAAkB,oDAAlB;EACA,kBAAkB,+CAAlB;EACA,kBAAkB,4CAAlB;EACA,2BAAA;EACA,QAAQ,0GAAR;EAfF,qCAAA;EACA,uEAAA;EAPA,QAAQ,yDAAR;EApBA,wBAAA;EACG,qBAAA;EACK,gBAAA;;AAOR,WD6CD,MAAM,GAAG,GA0CP,MAAM,MCvFN;AAAD,WD6CD,MAAM,GAAG,GA2CP,MAAM,MAAM,MCxFZ;AAAD,WD6CD,MAAM,GAAG,GA4CP,MAAM,MAAM,SCzFZ;AAAD,WD6CD,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MC1FrB;AAAQ,WD6CV,MAAM,GAAG,GA0CP,MAAM,MCvFG;AAAD,WD6CV,MAAM,GAAG,GA2CP,MAAM,MAAM,MCxFH;AAAD,WD6CV,MAAM,GAAG,GA4CP,MAAM,MAAM,SCzFH;AAAD,WD6CV,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MC1FZ;AAAS,WD6CpB,MAAM,GAAG,GA0CP,MAAM,MCvFa;AAAD,WD6CpB,MAAM,GAAG,GA2CP,MAAM,MAAM,MCxFO;AAAD,WD6CpB,MAAM,GAAG,GA4CP,MAAM,MAAM,SCzFO;AAAD,WD6CpB,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MC1FF;AAAS,WD6C9B,MAAM,GAAG,GA0CP,MAAM,MCvFuB;AAAD,WD6C9B,MAAM,GAAG,GA2CP,MAAM,MAAM,MCxFiB;AAAD,WD6C9B,MAAM,GAAG,GA4CP,MAAM,MAAM,SCzFiB;AAAD,WD6C9B,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MC1FQ;AAAW,WD6C1C,MAAM,GAAG,GA0CP,MAAM,MCvFmC;AAAD,WD6C1C,MAAM,GAAG,GA2CP,MAAM,MAAM,MCxF6B;AAAD,WD6C1C,MAAM,GAAG,GA4CP,MAAM,MAAM,SCzF6B;AAAD,WD6C1C,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MC1FoB;EACxC,yBAAA;;AAEF,WD0CD,MAAM,GAAG,GA0CP,MAAM,MCpFN;AAAD,WD0CD,MAAM,GAAG,GA2CP,MAAM,MAAM,MCrFZ;AAAD,WD0CD,MAAM,GAAG,GA4CP,MAAM,MAAM,SCtFZ;AAAD,WD0CD,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MCvFrB;AACD,WDyCD,MAAM,GAAG,GA0CP,MAAM,MCnFN;AAAD,WDyCD,MAAM,GAAG,GA2CP,MAAM,MAAM,MCpFZ;AAAD,WDyCD,MAAM,GAAG,GA4CP,MAAM,MAAM,SCrFZ;AAAD,WDyCD,MAAM,GAAG,GA6CP,MAAM,MAAM,SAAS,MCtFrB;EACC,0BAAyC,EAAzC;;AD0FF,WAlDD,MAAM,GAAG,GAkDP;AACD,WAnDD,MAAM,GAAG,GAmDP,SAAS;AACV,WApDD,MAAM,GAAG,GAoDP,SAAS;AACV,WArDD,MAAM,GAAG,GAqDP,SAAS,SAAS;EC1EjB,yBAAA;EACA,kBAAkB,iDAAlB;EACA,kBAAkB,gDAAlB;EACA,kBAAkB,sCAAsC,eAAmB,YAA3E;EACA,kBAAkB,oDAAlB;EACA,kBAAkB,+CAAlB;EACA,kBAAkB,4CAAlB;EACA,2BAAA;EACA,QAAQ,0GAAR;EAfF,qCAAA;EACA,uEAAA;EAPA,QAAQ,yDAAR;EDyFC,WAAA;EACA,yCAAA;;ACrGD,WD6CD,MAAM,GAAG,GAkDP,SC/FA;AAAD,WD6CD,MAAM,GAAG,GAmDP,SAAS,MChGT;AAAD,WD6CD,MAAM,GAAG,GAoDP,SAAS,SCjGT;AAAD,WD6CD,MAAM,GAAG,GAqDP,SAAS,SAAS,MClGlB;AAAQ,WD6CV,MAAM,GAAG,GAkDP,SC/FS;AAAD,WD6CV,MAAM,GAAG,GAmDP,SAAS,MChGA;AAAD,WD6CV,MAAM,GAAG,GAoDP,SAAS,SCjGA;AAAD,WD6CV,MAAM,GAAG,GAqDP,SAAS,SAAS,MClGT;AAAS,WD6CpB,MAAM,GAAG,GAkDP,SC/FmB;AAAD,WD6CpB,MAAM,GAAG,GAmDP,SAAS,MChGU;AAAD,WD6CpB,MAAM,GAAG,GAoDP,SAAS,SCjGU;AAAD,WD6CpB,MAAM,GAAG,GAqDP,SAAS,SAAS,MClGC;AAAS,WD6C9B,MAAM,GAAG,GAkDP,SC/F6B;AAAD,WD6C9B,MAAM,GAAG,GAmDP,SAAS,MChGoB;AAAD,WD6C9B,MAAM,GAAG,GAoDP,SAAS,SCjGoB;AAAD,WD6C9B,MAAM,GAAG,GAqDP,SAAS,SAAS,MClGW;AAAW,WD6C1C,MAAM,GAAG,GAkDP,SC/FyC;AAAD,WD6C1C,MAAM,GAAG,GAmDP,SAAS,MChGgC;AAAD,WD6C1C,MAAM,GAAG,GAoDP,SAAS,SCjGgC;AAAD,WD6C1C,MAAM,GAAG,GAqDP,SAAS,SAAS,MClGuB;EACxC,yBAAA;;AAEF,WD0CD,MAAM,GAAG,GAkDP,SC5FA;AAAD,WD0CD,MAAM,GAAG,GAmDP,SAAS,MC7FT;AAAD,WD0CD,MAAM,GAAG,GAoDP,SAAS,SC9FT;AAAD,WD0CD,MAAM,GAAG,GAqDP,SAAS,SAAS,MC/FlB;AACD,WDyCD,MAAM,GAAG,GAkDP,SC3FA;AAAD,WDyCD,MAAM,GAAG,GAmDP,SAAS,MC5FT;AAAD,WDyCD,MAAM,GAAG,GAoDP,SAAS,SC7FT;AAAD,WDyCD,MAAM,GAAG,GAqDP,SAAS,SAAS,MC9FlB;EACC,0BAAyC,EAAzC;;ADkGF,WA1DD,MAAM,GAAG,GA0DP;AACD,WA3DD,MAAM,GAAG,GA2DP,OAAO;AACR,WA5DD,MAAM,GAAG,GA4DP,OAAO;AACR,WA7DD,MAAM,GAAG,GA6DP,OAAO,SAAS;EClFf,yBAAA;EACA,kBAAkB,8CAAlB;EACA,kBAAkB,6CAAlB;EACA,kBAAkB,sCAAsC,YAAmB,YAA3E;EACA,kBAAkB,iDAAlB;EACA,kBAAkB,4CAAlB;EACA,kBAAkB,yCAAlB;EACA,2BAAA;EACA,QAAQ,uGAAR;EAfF,qCAAA;EACA,uEAAA;EAPA,QAAQ,yDAAR;EDiGC,WAAA;EACA,yCAAA;;AC7GD,WD6CD,MAAM,GAAG,GA0DP,OCvGA;AAAD,WD6CD,MAAM,GAAG,GA2DP,OAAO,MCxGP;AAAD,WD6CD,MAAM,GAAG,GA4DP,OAAO,SCzGP;AAAD,WD6CD,MAAM,GAAG,GA6DP,OAAO,SAAS,MC1GhB;AAAQ,WD6CV,MAAM,GAAG,GA0DP,OCvGS;AAAD,WD6CV,MAAM,GAAG,GA2DP,OAAO,MCxGE;AAAD,WD6CV,MAAM,GAAG,GA4DP,OAAO,SCzGE;AAAD,WD6CV,MAAM,GAAG,GA6DP,OAAO,SAAS,MC1GP;AAAS,WD6CpB,MAAM,GAAG,GA0DP,OCvGmB;AAAD,WD6CpB,MAAM,GAAG,GA2DP,OAAO,MCxGY;AAAD,WD6CpB,MAAM,GAAG,GA4DP,OAAO,SCzGY;AAAD,WD6CpB,MAAM,GAAG,GA6DP,OAAO,SAAS,MC1GG;AAAS,WD6C9B,MAAM,GAAG,GA0DP,OCvG6B;AAAD,WD6C9B,MAAM,GAAG,GA2DP,OAAO,MCxGsB;AAAD,WD6C9B,MAAM,GAAG,GA4DP,OAAO,SCzGsB;AAAD,WD6C9B,MAAM,GAAG,GA6DP,OAAO,SAAS,MC1Ga;AAAW,WD6C1C,MAAM,GAAG,GA0DP,OCvGyC;AAAD,WD6C1C,MAAM,GAAG,GA2DP,OAAO,MCxGkC;AAAD,WD6C1C,MAAM,GAAG,GA4DP,OAAO,SCzGkC;AAAD,WD6C1C,MAAM,GAAG,GA6DP,OAAO,SAAS,MC1GyB;EACxC,yBAAA;;AAEF,WD0CD,MAAM,GAAG,GA0DP,OCpGA;AAAD,WD0CD,MAAM,GAAG,GA2DP,OAAO,MCrGP;AAAD,WD0CD,MAAM,GAAG,GA4DP,OAAO,SCtGP;AAAD,WD0CD,MAAM,GAAG,GA6DP,OAAO,SAAS,MCvGhB;AACD,WDyCD,MAAM,GAAG,GA0DP,OCnGA;AAAD,WDyCD,MAAM,GAAG,GA2DP,OAAO,MCpGP;AAAD,WDyCD,MAAM,GAAG,GA4DP,OAAO,SCrGP;AAAD,WDyCD,MAAM,GAAG,GA6DP,OAAO,SAAS,MCtGhB;EACC,0BAAyC,EAAzC;;ADrCJ,WA6EC,MAAM,GAAG,GAkER;EACC,cAAA;EACA,UAAA;EACA,YAAA;EACA,iBAAA;EACA,WAAA;EACA,UAAA;EACA,eAAA;EC/HD,0BAAA;EACG,uBAAA;EACK,kBAAA;;AD+HP,WA3EF,MAAM,GAAG,GAkER,KASE;AACD,WA5EF,MAAM,GAAG,GAkER,KAUE;EACA,gBAAA;;AAED,WA/EF,MAAM,GAAG,GAkER,KAaE;AACD,WAhFF,MAAM,GAAG,GAkER,KAcE,SAAS;EACT,gBAAA;EACA,WAAA;EACA,eAAA;;AAED,WArFF,MAAM,GAAG,GAkER,KAmBE;AACD,WAtFF,MAAM,GAAG,GAkER,KAoBE,OAAO;AACR,WAvFF,MAAM,GAAG,GAkER,KAqBE,OAAO;AACR,WAxFF,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS;EC7GhB,yBAAA;EACA,kBAAkB,8CAAlB;EACA,kBAAkB,6CAAlB;EACA,kBAAkB,sCAAsC,YAAmB,YAA3E;EACA,kBAAkB,iDAAlB;EACA,kBAAkB,4CAAlB;EACA,kBAAkB,yCAAlB;EACA,2BAAA;EACA,QAAQ,uGAAR;EAfF,qCAAA;EACA,uEAAA;EAPA,QAAQ,yDAAR;ED4HE,WAAA;EACA,yCAAA;;ACxIF,WD6CD,MAAM,GAAG,GAkER,KAmBE,OClID;AAAD,WD6CD,MAAM,GAAG,GAkER,KAoBE,OAAO,MCnIR;AAAD,WD6CD,MAAM,GAAG,GAkER,KAqBE,OAAO,SCpIR;AAAD,WD6CD,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MCrIjB;AAAQ,WD6CV,MAAM,GAAG,GAkER,KAmBE,OClIQ;AAAD,WD6CV,MAAM,GAAG,GAkER,KAoBE,OAAO,MCnIC;AAAD,WD6CV,MAAM,GAAG,GAkER,KAqBE,OAAO,SCpIC;AAAD,WD6CV,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MCrIR;AAAS,WD6CpB,MAAM,GAAG,GAkER,KAmBE,OClIkB;AAAD,WD6CpB,MAAM,GAAG,GAkER,KAoBE,OAAO,MCnIW;AAAD,WD6CpB,MAAM,GAAG,GAkER,KAqBE,OAAO,SCpIW;AAAD,WD6CpB,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MCrIE;AAAS,WD6C9B,MAAM,GAAG,GAkER,KAmBE,OClI4B;AAAD,WD6C9B,MAAM,GAAG,GAkER,KAoBE,OAAO,MCnIqB;AAAD,WD6C9B,MAAM,GAAG,GAkER,KAqBE,OAAO,SCpIqB;AAAD,WD6C9B,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MCrIY;AAAW,WD6C1C,MAAM,GAAG,GAkER,KAmBE,OClIwC;AAAD,WD6C1C,MAAM,GAAG,GAkER,KAoBE,OAAO,MCnIiC;AAAD,WD6C1C,MAAM,GAAG,GAkER,KAqBE,OAAO,SCpIiC;AAAD,WD6C1C,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MCrIwB;EACxC,yBAAA;;AAEF,WD0CD,MAAM,GAAG,GAkER,KAmBE,OC/HD;AAAD,WD0CD,MAAM,GAAG,GAkER,KAoBE,OAAO,MChIR;AAAD,WD0CD,MAAM,GAAG,GAkER,KAqBE,OAAO,SCjIR;AAAD,WD0CD,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MClIjB;AACD,WDyCD,MAAM,GAAG,GAkER,KAmBE,OC9HD;AAAD,WDyCD,MAAM,GAAG,GAkER,KAoBE,OAAO,MC/HR;AAAD,WDyCD,MAAM,GAAG,GAkER,KAqBE,OAAO,SChIR;AAAD,WDyCD,MAAM,GAAG,GAkER,KAsBE,OAAO,SAAS,MCjIjB;EACC,0BAAyC,EAAzC;;ADqID,WA7FF,MAAM,GAAG,GAkER,KA2BE;AACD,WA9FF,MAAM,GAAG,GAkER,KA4BE;EACA,WAAA;;AA5KJ,WAiLC;EACC,YAAA;;AAlLF,WAqLC;AArLD,WAsLC;AAtLD,WAuLC;AAvLD,WAwLC,MAAM,GAAG;EACR,eAAA;;AACA,WALD,mBAKE;AAAD,WAJD,MAIE;AAAD,WAHD,MAGE;AAAD,WAFD,MAAM,GAAG,GAEP;EACA,gBAAA;;AAKD,WADD,MACE;AAAD,WADM,MACL;EACA,kBAAA;;AAjMH,WAsMC;EACC,eAAA;EACA,WAAA;EACA,oBAAA;EACA,sBAAA;;AAKD,aAAC,KAAM;AAAP,cAAC,KAAM;EACN,eAAA;;AADD,aAAC,KAAM,QAGN;AAHD,cAAC,KAAM,QAGN;EACC,eAAA;;AAIH,gBACC;EACC,kBAAA;;AAFF,gBAIC,MAAK;ECpMJ,kCAAA;EACG,+BAAA;EACK,0BAAA;;AD8LV,gBAOC,MAAK;ECvMJ,kCAAA;EACG,+BAAA;EACK,0BAAA;;AD8LV,gBAUC;EACC,qBAAA;EACA,WAAA;EACA,eAAA;EACA,YAAA;EACA,gBAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,yBAAA;EACA,sBAAA;EACA,sBAAA;EACA,sBAAA;EACA,iBAAA;EACA,kBAAA","sourcesContent":[".datepicker {\n\tpadding: 4px;\n\t.border-radius(@baseBorderRadius);\n\t&-inline {\n\t\twidth: 220px;\n\t}\n\tdirection: ltr;\n\t&-rtl {\n\t\tdirection: rtl;\n\t\t&.dropdown-menu { left: auto; }\n\t\ttable tr td span {\n\t\t\tfloat: right;\n\t\t}\n\t}\n\t&-dropdown {\n\t\ttop: 0;\n\t\tleft: 0;\n\t\t&:before {\n\t\t\tcontent: '';\n\t\t\tdisplay: inline-block;\n\t\t\tborder-left: 7px solid transparent;\n\t\t\tborder-right: 7px solid transparent;\n\t\t\tborder-bottom: 7px solid @grayLight;\n\t\t\tborder-top: 0;\n\t\t\tborder-bottom-color: rgba(0,0,0,.2);\n\t\t\tposition: absolute;\n\t\t}\n\t\t&:after {\n\t\t\tcontent: '';\n\t\t\tdisplay: inline-block;\n\t\t\tborder-left: 6px solid transparent;\n\t\t\tborder-right: 6px solid transparent;\n\t\t\tborder-bottom: 6px solid @white;\n\t\t\tborder-top: 0;\n\t\t\tposition: absolute;\n\t\t}\n\t\t&.datepicker-orient-left:before { left: 6px; }\n\t\t&.datepicker-orient-left:after { left: 7px; }\n\t\t&.datepicker-orient-right:before { right: 6px; }\n\t\t&.datepicker-orient-right:after { right: 7px; }\n\t\t&.datepicker-orient-bottom:before { top: -7px; }\n\t\t&.datepicker-orient-bottom:after { top: -6px; }\n\t\t&.datepicker-orient-top:before {\n\t\t\tbottom: -7px;\n\t\t\tborder-bottom: 0;\n\t\t\tborder-top: 7px solid @grayLight;\n\t\t}\n\t\t&.datepicker-orient-top:after {\n\t\t\tbottom: -6px;\n\t\t\tborder-bottom: 0;\n\t\t\tborder-top: 6px solid @white;\n\t\t}\n\t}\n\ttable {\n\t\tmargin: 0;\n\t\t-webkit-touch-callout: none;\n\t\t-webkit-user-select: none;\n\t\t-khtml-user-select: none;\n\t\t-moz-user-select: none;\n\t\t-ms-user-select: none;\n\t\tuser-select: none;\n\t}\n\ttd, th {\n\t\ttext-align: center;\n\t\twidth: 20px;\n\t\theight: 20px;\n\t\t.border-radius(4px);\n\n\t\tborder: none;\n\t}\n\t// Inline display inside a table presents some problems with\n\t// border and background colors.\n\t.table-striped & table tr {\n\t\ttd, th {\n\t\t\tbackground-color: transparent;\n\t\t}\n\t}\n\ttable tr td {\n\t\t&.day:hover,\n\t\t&.day.focused {\n\t\t\tbackground: @grayLighter;\n\t\t\tcursor: pointer;\n\t\t}\n\t\t&.old,\n\t\t&.new {\n\t\t\tcolor: @grayLight;\n\t\t}\n\t\t&.disabled,\n\t\t&.disabled:hover {\n\t\t\tbackground: none;\n\t\t\tcolor: @grayLight;\n\t\t\tcursor: default;\n\t\t}\n\t\t&.highlighted {\n\t\t\tbackground: @infoBackground;\n\t\t\tborder-radius: 0;\n\t\t}\n\t\t&.today,\n\t\t&.today:hover,\n\t\t&.today.disabled,\n\t\t&.today.disabled:hover {\n\t\t\t@todayBackground: lighten(@orange, 30%);\n\t\t\t.buttonBackground(@todayBackground, spin(@todayBackground, 20));\n\t\t\tcolor: #000;\n\t\t}\n\t\t&.today:hover:hover { // Thank bootstrap 2.0 for this selector...\n\t\t\t// TODO: Bump min BS to 2.1, use @textColor in buttonBackground above\n\t\t\tcolor: #000;\n\t\t}\n\t\t&.today.active:hover {\n\t\t\tcolor: #fff;\n\t\t}\n\t\t&.range,\n\t\t&.range:hover,\n\t\t&.range.disabled,\n\t\t&.range.disabled:hover {\n\t\t\tbackground: @grayLighter;\n\t\t\t.border-radius(0);\n\t\t}\n\t\t&.range.today,\n\t\t&.range.today:hover,\n\t\t&.range.today.disabled,\n\t\t&.range.today.disabled:hover {\n\t\t\t@todayBackground: mix(@orange, @grayLighter, 50%);\n\t\t\t.buttonBackground(@todayBackground, spin(@todayBackground, 20));\n\t\t\t.border-radius(0);\n\t\t}\n\t\t&.selected,\n\t\t&.selected:hover,\n\t\t&.selected.disabled,\n\t\t&.selected.disabled:hover {\n\t\t\t.buttonBackground(lighten(@grayLight, 10), darken(@grayLight, 10));\n\t\t\tcolor: #fff;\n\t\t\ttext-shadow: 0 -1px 0 rgba(0,0,0,.25);\n\t\t}\n\t\t&.active,\n\t\t&.active:hover,\n\t\t&.active.disabled,\n\t\t&.active.disabled:hover {\n\t\t\t.buttonBackground(@btnPrimaryBackground, spin(@btnPrimaryBackground, 20));\n\t\t\tcolor: #fff;\n\t\t\ttext-shadow: 0 -1px 0 rgba(0,0,0,.25);\n\t\t}\n\t\tspan {\n\t\t\tdisplay: block;\n\t\t\twidth: 23%;\n\t\t\theight: 54px;\n\t\t\tline-height: 54px;\n\t\t\tfloat: left;\n\t\t\tmargin: 1%;\n\t\t\tcursor: pointer;\n\t\t\t.border-radius(4px);\n\t\t\t&:hover,\n\t\t\t&.focused {\n\t\t\t\tbackground: @grayLighter;\n\t\t\t}\n\t\t\t&.disabled,\n\t\t\t&.disabled:hover {\n\t\t\t\tbackground: none;\n\t\t\t\tcolor: @grayLight;\n\t\t\t\tcursor: default;\n\t\t\t}\n\t\t\t&.active,\n\t\t\t&.active:hover,\n\t\t\t&.active.disabled,\n\t\t\t&.active.disabled:hover {\n\t\t\t\t.buttonBackground(@btnPrimaryBackground, spin(@btnPrimaryBackground, 20));\n\t\t\t\tcolor: #fff;\n\t\t\t\ttext-shadow: 0 -1px 0 rgba(0,0,0,.25);\n\t\t\t}\n\t\t\t&.old,\n\t\t\t&.new {\n\t\t\t\tcolor: @grayLight;\n\t\t\t}\n\t\t}\n\t}\n\n\t.datepicker-switch {\n\t\twidth: 145px;\n\t}\n\n\t.datepicker-switch,\n\t.prev,\n\t.next,\n\ttfoot tr th {\n\t\tcursor: pointer;\n\t\t&:hover {\n\t\t\tbackground: @grayLighter;\n\t\t}\n\t}\n\n\t.prev, .next {\n\t\t&.disabled {\n\t\t\tvisibility: hidden;\n\t\t}\n\t}\n\n\t// Basic styling for calendar-week cells\n\t.cw {\n\t\tfont-size: 10px;\n\t\twidth: 12px;\n\t\tpadding: 0 2px 0 5px;\n\t\tvertical-align: middle;\n\t}\n}\n.input-append,\n.input-prepend {\n\t&.date .add-on {\n\t\tcursor: pointer;\n\n\t\ti {\n\t\t\tmargin-top: 3px;\n\t\t}\n\t}\n}\n.input-daterange {\n\tinput {\n\t\ttext-align:center;\n\t}\n\tinput:first-child {\n\t\t.border-radius(3px 0 0 3px);\n\t}\n\tinput:last-child {\n\t\t.border-radius(0 3px 3px 0);\n\t}\n\t.add-on {\n\t\tdisplay: inline-block;\n\t\twidth: auto;\n\t\tmin-width: 16px;\n\t\theight: @baseLineHeight;\n\t\tpadding: 4px 5px;\n\t\tfont-weight: normal;\n\t\tline-height: @baseLineHeight;\n\t\ttext-align: center;\n\t\ttext-shadow: 0 1px 0 @white;\n\t\tvertical-align: middle;\n\t\tbackground-color: @grayLighter;\n\t\tborder: 1px solid #ccc;\n\t\tmargin-left: -5px;\n\t\tmargin-right: -5px;\n\t}\n}\n","// Datepicker .less buildfile. Includes select mixins/variables from bootstrap\n// and imports the included datepicker.less to output a minimal datepicker.css\n//\n// Usage:\n// lessc build.less datepicker.css\n//\n// Variables and mixins copied from bootstrap 2.0.2\n\n// Variables\n@grayLight: #999;\n@grayLighter: #eee;\n@white: #fff;\n@linkColor: #08c;\n@btnPrimaryBackground: @linkColor;\n@orange: #f89406;\n@infoBackground: #d9edf7;\n@baseLineHeight: 18px;\n@baseBorderRadius: 4px;\n\n// Mixins\n\n// Border Radius\n.border-radius(@radius: 5px) {\n -webkit-border-radius: @radius;\n -moz-border-radius: @radius;\n border-radius: @radius;\n}\n\n// Button backgrounds\n.buttonBackground(@startColor, @endColor) {\n .gradientBar(@startColor, @endColor);\n .reset-filter();\n &:hover, &:active, &.active, &.disabled, &[disabled] {\n background-color: @endColor;\n }\n &:active,\n &.active {\n background-color: darken(@endColor, 10%) e(\"\\9\");\n }\n}\n\n// Reset filters for IE\n.reset-filter() {\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n\n// Gradient Bar Colors for buttons and alerts\n.gradientBar(@primaryColor, @secondaryColor) {\n #gradient > .vertical(@primaryColor, @secondaryColor);\n border-color: @secondaryColor @secondaryColor darken(@secondaryColor, 15%);\n border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%);\n}\n\n// Gradients\n#gradient {\n .vertical(@startColor: #555, @endColor: #333) {\n background-color: mix(@startColor, @endColor, 60%);\n background-image: -moz-linear-gradient(to bottom, @startColor, @endColor); // FF 3.6+\n background-image: -ms-linear-gradient(to bottom, @startColor, @endColor); // IE10\n background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+\n background-image: -webkit-linear-gradient(to bottom, @startColor, @endColor); // Safari 5.1+, Chrome 10+\n background-image: -o-linear-gradient(to bottom, @startColor, @endColor); // Opera 11.10\n background-image: linear-gradient(to bottom, @startColor, @endColor); // The standard\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",@startColor,@endColor)); // IE9 and down\n }\n}\n\n@import \"../less/datepicker.less\";\n"]} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.css b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.css new file mode 100644 index 0000000000..eb681513fb --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.css @@ -0,0 +1,7 @@ +/*! + * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker) + * + * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +.datepicker{padding:4px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;direction:ltr}.datepicker-inline{width:220px}.datepicker-rtl{direction:rtl}.datepicker-rtl.dropdown-menu{left:auto}.datepicker-rtl table tr td span{float:right}.datepicker-dropdown{top:0;left:0}.datepicker-dropdown:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #999;border-top:0;border-bottom-color:rgba(0,0,0,.2);position:absolute}.datepicker-dropdown:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;border-top:0;position:absolute}.datepicker-dropdown.datepicker-orient-left:before{left:6px}.datepicker-dropdown.datepicker-orient-left:after{left:7px}.datepicker-dropdown.datepicker-orient-right:before{right:6px}.datepicker-dropdown.datepicker-orient-right:after{right:7px}.datepicker-dropdown.datepicker-orient-bottom:before{top:-7px}.datepicker-dropdown.datepicker-orient-bottom:after{top:-6px}.datepicker-dropdown.datepicker-orient-top:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.datepicker-dropdown.datepicker-orient-top:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.datepicker table{margin:0;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.datepicker td,.datepicker th{text-align:center;width:20px;height:20px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:none}.table-striped .datepicker table tr td,.table-striped .datepicker table tr th{background-color:transparent}.datepicker table tr td.day.focused,.datepicker table tr td.day:hover{background:#eee;cursor:pointer}.datepicker table tr td.new,.datepicker table tr td.old{color:#999}.datepicker table tr td.disabled,.datepicker table tr td.disabled:hover{background:0 0;color:#999;cursor:default}.datepicker table tr td.highlighted{background:#d9edf7;border-radius:0}.datepicker table tr td.today,.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today:hover{background-color:#fde19a;background-image:-moz-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-ms-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fdd49a),to(#fdf59a));background-image:-webkit-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-o-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:linear-gradient(to bottom,#fdd49a,#fdf59a);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);border-color:#fdf59a #fdf59a #fbed50;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#000}.datepicker table tr td.today.active,.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled.active,.datepicker table tr td.today.disabled.disabled,.datepicker table tr td.today.disabled:active,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today.disabled:hover.active,.datepicker table tr td.today.disabled:hover.disabled,.datepicker table tr td.today.disabled:hover:active,.datepicker table tr td.today.disabled:hover:hover,.datepicker table tr td.today.disabled:hover[disabled],.datepicker table tr td.today.disabled[disabled],.datepicker table tr td.today:active,.datepicker table tr td.today:hover,.datepicker table tr td.today:hover.active,.datepicker table tr td.today:hover.disabled,.datepicker table tr td.today:hover:active,.datepicker table tr td.today:hover:hover,.datepicker table tr td.today:hover[disabled],.datepicker table tr td.today[disabled]{background-color:#fdf59a}.datepicker table tr td.today.active,.datepicker table tr td.today.disabled.active,.datepicker table tr td.today.disabled:active,.datepicker table tr td.today.disabled:hover.active,.datepicker table tr td.today.disabled:hover:active,.datepicker table tr td.today:active,.datepicker table tr td.today:hover.active,.datepicker table tr td.today:hover:active{background-color:#fbf069\9}.datepicker table tr td.today:hover:hover{color:#000}.datepicker table tr td.today.active:hover{color:#fff}.datepicker table tr td.range,.datepicker table tr td.range.disabled,.datepicker table tr td.range.disabled:hover,.datepicker table tr td.range:hover{background:#eee;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.datepicker table tr td.range.today,.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today:hover{background-color:#f3d17a;background-image:-moz-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-ms-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f3c17a),to(#f3e97a));background-image:-webkit-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-o-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:linear-gradient(to bottom,#f3c17a,#f3e97a);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);border-color:#f3e97a #f3e97a #edde34;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled.active,.datepicker table tr td.range.today.disabled.disabled,.datepicker table tr td.range.today.disabled:active,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today.disabled:hover.active,.datepicker table tr td.range.today.disabled:hover.disabled,.datepicker table tr td.range.today.disabled:hover:active,.datepicker table tr td.range.today.disabled:hover:hover,.datepicker table tr td.range.today.disabled:hover[disabled],.datepicker table tr td.range.today.disabled[disabled],.datepicker table tr td.range.today:active,.datepicker table tr td.range.today:hover,.datepicker table tr td.range.today:hover.active,.datepicker table tr td.range.today:hover.disabled,.datepicker table tr td.range.today:hover:active,.datepicker table tr td.range.today:hover:hover,.datepicker table tr td.range.today:hover[disabled],.datepicker table tr td.range.today[disabled]{background-color:#f3e97a}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today.disabled.active,.datepicker table tr td.range.today.disabled:active,.datepicker table tr td.range.today.disabled:hover.active,.datepicker table tr td.range.today.disabled:hover:active,.datepicker table tr td.range.today:active,.datepicker table tr td.range.today:hover.active,.datepicker table tr td.range.today:hover:active{background-color:#efe24b\9}.datepicker table tr td.selected,.datepicker table tr td.selected.disabled,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected:hover{background-color:#9e9e9e;background-image:-moz-linear-gradient(to bottom,#b3b3b3,grey);background-image:-ms-linear-gradient(to bottom,#b3b3b3,grey);background-image:-webkit-gradient(linear,0 0,0 100%,from(#b3b3b3),to(grey));background-image:-webkit-linear-gradient(to bottom,#b3b3b3,grey);background-image:-o-linear-gradient(to bottom,#b3b3b3,grey);background-image:linear-gradient(to bottom,#b3b3b3,grey);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);border-color:grey grey #595959;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.selected.active,.datepicker table tr td.selected.disabled,.datepicker table tr td.selected.disabled.active,.datepicker table tr td.selected.disabled.disabled,.datepicker table tr td.selected.disabled:active,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected.disabled:hover.active,.datepicker table tr td.selected.disabled:hover.disabled,.datepicker table tr td.selected.disabled:hover:active,.datepicker table tr td.selected.disabled:hover:hover,.datepicker table tr td.selected.disabled:hover[disabled],.datepicker table tr td.selected.disabled[disabled],.datepicker table tr td.selected:active,.datepicker table tr td.selected:hover,.datepicker table tr td.selected:hover.active,.datepicker table tr td.selected:hover.disabled,.datepicker table tr td.selected:hover:active,.datepicker table tr td.selected:hover:hover,.datepicker table tr td.selected:hover[disabled],.datepicker table tr td.selected[disabled]{background-color:grey}.datepicker table tr td.selected.active,.datepicker table tr td.selected.disabled.active,.datepicker table tr td.selected.disabled:active,.datepicker table tr td.selected.disabled:hover.active,.datepicker table tr td.selected.disabled:hover:active,.datepicker table tr td.selected:active,.datepicker table tr td.selected:hover.active,.datepicker table tr td.selected:hover:active{background-color:#666\9}.datepicker table tr td.active,.datepicker table tr td.active.disabled,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active:hover{background-color:#006dcc;background-image:-moz-linear-gradient(to bottom,#08c,#04c);background-image:-ms-linear-gradient(to bottom,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(to bottom,#08c,#04c);background-image:-o-linear-gradient(to bottom,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.active.active,.datepicker table tr td.active.disabled,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled.disabled,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover.active,.datepicker table tr td.active.disabled:hover.disabled,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active.disabled:hover[disabled],.datepicker table tr td.active.disabled[disabled],.datepicker table tr td.active:active,.datepicker table tr td.active:hover,.datepicker table tr td.active:hover.active,.datepicker table tr td.active:hover.disabled,.datepicker table tr td.active:hover:active,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active:hover[disabled],.datepicker table tr td.active[disabled]{background-color:#04c}.datepicker table tr td.active.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover.active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active:active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active:hover:active{background-color:#039\9}.datepicker table tr td span{display:block;width:23%;height:54px;line-height:54px;float:left;margin:1%;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.datepicker table tr td span.focused,.datepicker table tr td span:hover{background:#eee}.datepicker table tr td span.disabled,.datepicker table tr td span.disabled:hover{background:0 0;color:#999;cursor:default}.datepicker table tr td span.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active:hover{background-color:#006dcc;background-image:-moz-linear-gradient(to bottom,#08c,#04c);background-image:-ms-linear-gradient(to bottom,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(to bottom,#08c,#04c);background-image:-o-linear-gradient(to bottom,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled.disabled,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover.disabled,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active.disabled:hover[disabled],.datepicker table tr td span.active.disabled[disabled],.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover.disabled,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active:hover[disabled],.datepicker table tr td span.active[disabled]{background-color:#04c}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover:active{background-color:#039\9}.datepicker table tr td span.new,.datepicker table tr td span.old{color:#999}.datepicker .datepicker-switch{width:145px}.datepicker .datepicker-switch,.datepicker .next,.datepicker .prev,.datepicker tfoot tr th{cursor:pointer}.datepicker .datepicker-switch:hover,.datepicker .next:hover,.datepicker .prev:hover,.datepicker tfoot tr th:hover{background:#eee}.datepicker .next.disabled,.datepicker .prev.disabled{visibility:hidden}.datepicker .cw{font-size:10px;width:12px;padding:0 2px 0 5px;vertical-align:middle}.input-append.date .add-on,.input-prepend.date .add-on{cursor:pointer}.input-append.date .add-on i,.input-prepend.date .add-on i{margin-top:3px}.input-daterange input{text-align:center}.input-daterange input:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-daterange input:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-daterange .add-on{display:inline-block;width:auto;min-width:16px;height:18px;padding:4px 5px;font-weight:400;line-height:18px;text-align:center;text-shadow:0 1px 0 #fff;vertical-align:middle;background-color:#eee;border:1px solid #ccc;margin-left:-5px;margin-right:-5px} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.js new file mode 100644 index 0000000000..8800106e8f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.js @@ -0,0 +1,8 @@ +/*! + * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker) + * + * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a,b){function c(){return new Date(Date.UTC.apply(Date,arguments))}function d(){var a=new Date;return c(a.getFullYear(),a.getMonth(),a.getDate())}function e(a,b){return a.getUTCFullYear()===b.getUTCFullYear()&&a.getUTCMonth()===b.getUTCMonth()&&a.getUTCDate()===b.getUTCDate()}function f(c,d){return function(){return d!==b&&a.fn.datepicker.deprecated(d),this[c].apply(this,arguments)}}function g(a){return a&&!isNaN(a.getTime())}function h(b,c){function d(a,b){return b.toLowerCase()}var e,f=a(b).data(),g={},h=new RegExp("^"+c.toLowerCase()+"([A-Z])");c=new RegExp("^"+c.toLowerCase());for(var i in f)c.test(i)&&(e=i.replace(h,d),g[e]=f[i]);return g}function i(b){var c={};if(q[b]||(b=b.split("-")[0],q[b])){var d=q[b];return a.each(p,function(a,b){b in d&&(c[b]=d[b])}),c}}var j=function(){var b={get:function(a){return this.slice(a)[0]},contains:function(a){for(var b=a&&a.valueOf(),c=0,d=this.length;c]/g)||[]).length<=0)return!0;return a(c).length>0}catch(a){return!1}},_process_options:function(b){this._o=a.extend({},this._o,b);var e=this.o=a.extend({},this._o),f=e.language;q[f]||(f=f.split("-")[0],q[f]||(f=o.language)),e.language=f,e.startView=this._resolveViewName(e.startView),e.minViewMode=this._resolveViewName(e.minViewMode),e.maxViewMode=this._resolveViewName(e.maxViewMode),e.startView=Math.max(this.o.minViewMode,Math.min(this.o.maxViewMode,e.startView)),!0!==e.multidate&&(e.multidate=Number(e.multidate)||!1,!1!==e.multidate&&(e.multidate=Math.max(0,e.multidate))),e.multidateSeparator=String(e.multidateSeparator),e.weekStart%=7,e.weekEnd=(e.weekStart+6)%7;var g=r.parseFormat(e.format);e.startDate!==-1/0&&(e.startDate?e.startDate instanceof Date?e.startDate=this._local_to_utc(this._zero_time(e.startDate)):e.startDate=r.parseDate(e.startDate,g,e.language,e.assumeNearbyYear):e.startDate=-1/0),e.endDate!==1/0&&(e.endDate?e.endDate instanceof Date?e.endDate=this._local_to_utc(this._zero_time(e.endDate)):e.endDate=r.parseDate(e.endDate,g,e.language,e.assumeNearbyYear):e.endDate=1/0),e.daysOfWeekDisabled=this._resolveDaysOfWeek(e.daysOfWeekDisabled||[]),e.daysOfWeekHighlighted=this._resolveDaysOfWeek(e.daysOfWeekHighlighted||[]),e.datesDisabled=e.datesDisabled||[],a.isArray(e.datesDisabled)||(e.datesDisabled=e.datesDisabled.split(",")),e.datesDisabled=a.map(e.datesDisabled,function(a){return r.parseDate(a,g,e.language,e.assumeNearbyYear)});var h=String(e.orientation).toLowerCase().split(/\s+/g),i=e.orientation.toLowerCase();if(h=a.grep(h,function(a){return/^auto|left|right|top|bottom$/.test(a)}),e.orientation={x:"auto",y:"auto"},i&&"auto"!==i)if(1===h.length)switch(h[0]){case"top":case"bottom":e.orientation.y=h[0];break;case"left":case"right":e.orientation.x=h[0]}else i=a.grep(h,function(a){return/^left|right$/.test(a)}),e.orientation.x=i[0]||"auto",i=a.grep(h,function(a){return/^top|bottom$/.test(a)}),e.orientation.y=i[0]||"auto";else;if(e.defaultViewDate instanceof Date||"string"==typeof e.defaultViewDate)e.defaultViewDate=r.parseDate(e.defaultViewDate,g,e.language,e.assumeNearbyYear);else if(e.defaultViewDate){var j=e.defaultViewDate.year||(new Date).getFullYear(),k=e.defaultViewDate.month||0,l=e.defaultViewDate.day||1;e.defaultViewDate=c(j,k,l)}else e.defaultViewDate=d()},_applyEvents:function(a){for(var c,d,e,f=0;fe?(this.picker.addClass("datepicker-orient-right"),m+=l-b):this.o.rtl?this.picker.addClass("datepicker-orient-right"):this.picker.addClass("datepicker-orient-left");var o,p=this.o.orientation.y;if("auto"===p&&(o=-f+n-c,p=o<0?"bottom":"top"),this.picker.addClass("datepicker-orient-"+p),"top"===p?n-=c+parseInt(this.picker.css("padding-top")):n+=k,this.o.rtl){var q=e-(m+l);this.picker.css({top:n,right:q,zIndex:i})}else this.picker.css({top:n,left:m,zIndex:i});return this},_allow_update:!0,update:function(){if(!this._allow_update)return this;var b=this.dates.copy(),c=[],d=!1;return arguments.length?(a.each(arguments,a.proxy(function(a,b){b instanceof Date&&(b=this._local_to_utc(b)),c.push(b)},this)),d=!0):(c=this.isInput?this.element.val():this.element.data("date")||this.inputField.val(),c=c&&this.o.multidate?c.split(this.o.multidateSeparator):[c],delete this.element.data().date),c=a.map(c,a.proxy(function(a){return r.parseDate(a,this.o.format,this.o.language,this.o.assumeNearbyYear)},this)),c=a.grep(c,a.proxy(function(a){return!this.dateWithinRange(a)||!a},this),!0),this.dates.replace(c),this.o.updateViewDate&&(this.dates.length?this.viewDate=new Date(this.dates.get(-1)):this.viewDatethis.o.endDate?this.viewDate=new Date(this.o.endDate):this.viewDate=this.o.defaultViewDate),d?(this.setValue(),this.element.change()):this.dates.length&&String(b)!==String(this.dates)&&d&&(this._trigger("changeDate"),this.element.change()),!this.dates.length&&b.length&&(this._trigger("clearDate"),this.element.change()),this.fill(),this},fillDow:function(){if(this.o.showWeekDays){var b=this.o.weekStart,c="";for(this.o.calendarWeeks&&(c+=' ');b";c+="",this.picker.find(".datepicker-days thead").append(c)}},fillMonths:function(){for(var a,b=this._utc_to_local(this.viewDate),c="",d=0;d<12;d++)a=b&&b.getMonth()===d?" focused":"",c+=''+q[this.o.language].monthsShort[d]+"";this.picker.find(".datepicker-months td").html(c)},setRange:function(b){b&&b.length?this.range=a.map(b,function(a){return a.valueOf()}):delete this.range,this.fill()},getClassNames:function(b){var c=[],f=this.viewDate.getUTCFullYear(),g=this.viewDate.getUTCMonth(),h=d();return b.getUTCFullYear()f||b.getUTCFullYear()===f&&b.getUTCMonth()>g)&&c.push("new"),this.focusDate&&b.valueOf()===this.focusDate.valueOf()&&c.push("focused"),this.o.todayHighlight&&e(b,h)&&c.push("today"),-1!==this.dates.contains(b)&&c.push("active"),this.dateWithinRange(b)||c.push("disabled"),this.dateIsDisabled(b)&&c.push("disabled","disabled-date"),-1!==a.inArray(b.getUTCDay(),this.o.daysOfWeekHighlighted)&&c.push("highlighted"),this.range&&(b>this.range[0]&&bh)&&j.push("disabled"),t===r&&j.push("focused"),i!==a.noop&&(l=i(new Date(t,0,1)),l===b?l={}:"boolean"==typeof l?l={enabled:l}:"string"==typeof l&&(l={classes:l}),!1===l.enabled&&j.push("disabled"),l.classes&&(j=j.concat(l.classes.split(/\s+/))),l.tooltip&&(k=l.tooltip)),m+='"+t+"";o.find(".datepicker-switch").text(p+"-"+q),o.find("td").html(m)},fill:function(){var e,f,g=new Date(this.viewDate),h=g.getUTCFullYear(),i=g.getUTCMonth(),j=this.o.startDate!==-1/0?this.o.startDate.getUTCFullYear():-1/0,k=this.o.startDate!==-1/0?this.o.startDate.getUTCMonth():-1/0,l=this.o.endDate!==1/0?this.o.endDate.getUTCFullYear():1/0,m=this.o.endDate!==1/0?this.o.endDate.getUTCMonth():1/0,n=q[this.o.language].today||q.en.today||"",o=q[this.o.language].clear||q.en.clear||"",p=q[this.o.language].titleFormat||q.en.titleFormat,s=d(),t=(!0===this.o.todayBtn||"linked"===this.o.todayBtn)&&s>=this.o.startDate&&s<=this.o.endDate&&!this.weekOfDateIsDisabled(s);if(!isNaN(h)&&!isNaN(i)){this.picker.find(".datepicker-days .datepicker-switch").text(r.formatDate(g,p,this.o.language)),this.picker.find("tfoot .today").text(n).css("display",t?"table-cell":"none"),this.picker.find("tfoot .clear").text(o).css("display",!0===this.o.clearBtn?"table-cell":"none"),this.picker.find("thead .datepicker-title").text(this.o.title).css("display","string"==typeof this.o.title&&""!==this.o.title?"table-cell":"none"),this.updateNavArrows(),this.fillMonths();var u=c(h,i,0),v=u.getUTCDate();u.setUTCDate(v-(u.getUTCDay()-this.o.weekStart+7)%7);var w=new Date(u);u.getUTCFullYear()<100&&w.setUTCFullYear(u.getUTCFullYear()),w.setUTCDate(w.getUTCDate()+42),w=w.valueOf();for(var x,y,z=[];u.valueOf()"),this.o.calendarWeeks)){var A=new Date(+u+(this.o.weekStart-x-7)%7*864e5),B=new Date(Number(A)+(11-A.getUTCDay())%7*864e5),C=new Date(Number(C=c(B.getUTCFullYear(),0,1))+(11-C.getUTCDay())%7*864e5),D=(B-C)/864e5/7+1;z.push(''+D+"")}y=this.getClassNames(u),y.push("day");var E=u.getUTCDate();this.o.beforeShowDay!==a.noop&&(f=this.o.beforeShowDay(this._utc_to_local(u)),f===b?f={}:"boolean"==typeof f?f={enabled:f}:"string"==typeof f&&(f={classes:f}),!1===f.enabled&&y.push("disabled"),f.classes&&(y=y.concat(f.classes.split(/\s+/))),f.tooltip&&(e=f.tooltip),f.content&&(E=f.content)),y=a.isFunction(a.uniqueSort)?a.uniqueSort(y):a.unique(y),z.push(''+E+""),e=null,x===this.o.weekEnd&&z.push(""),u.setUTCDate(u.getUTCDate()+1)}this.picker.find(".datepicker-days tbody").html(z.join(""));var F=q[this.o.language].monthsTitle||q.en.monthsTitle||"Months",G=this.picker.find(".datepicker-months").find(".datepicker-switch").text(this.o.maxViewMode<2?F:h).end().find("tbody span").removeClass("active");if(a.each(this.dates,function(a,b){b.getUTCFullYear()===h&&G.eq(b.getUTCMonth()).addClass("active")}),(hl)&&G.addClass("disabled"),h===j&&G.slice(0,k).addClass("disabled"),h===l&&G.slice(m+1).addClass("disabled"),this.o.beforeShowMonth!==a.noop){var H=this;a.each(G,function(c,d){var e=new Date(h,c,1),f=H.o.beforeShowMonth(e);f===b?f={}:"boolean"==typeof f?f={enabled:f}:"string"==typeof f&&(f={classes:f}),!1!==f.enabled||a(d).hasClass("disabled")||a(d).addClass("disabled"),f.classes&&a(d).addClass(f.classes),f.tooltip&&a(d).prop("title",f.tooltip)})}this._fill_yearsView(".datepicker-years","year",10,h,j,l,this.o.beforeShowYear),this._fill_yearsView(".datepicker-decades","decade",100,h,j,l,this.o.beforeShowDecade),this._fill_yearsView(".datepicker-centuries","century",1e3,h,j,l,this.o.beforeShowCentury)}},updateNavArrows:function(){if(this._allow_update){var a,b,c=new Date(this.viewDate),d=c.getUTCFullYear(),e=c.getUTCMonth(),f=this.o.startDate!==-1/0?this.o.startDate.getUTCFullYear():-1/0,g=this.o.startDate!==-1/0?this.o.startDate.getUTCMonth():-1/0,h=this.o.endDate!==1/0?this.o.endDate.getUTCFullYear():1/0,i=this.o.endDate!==1/0?this.o.endDate.getUTCMonth():1/0,j=1;switch(this.viewMode){case 4:j*=10;case 3:j*=10;case 2:j*=10;case 1:a=Math.floor(d/j)*j<=f,b=Math.floor(d/j)*j+j>h;break;case 0:a=d<=f&&e<=g,b=d>=h&&e>=i}this.picker.find(".prev").toggleClass("disabled",a),this.picker.find(".next").toggleClass("disabled",b)}},click:function(b){b.preventDefault(),b.stopPropagation();var e,f,g,h;e=a(b.target),e.hasClass("datepicker-switch")&&this.viewMode!==this.o.maxViewMode&&this.setViewMode(this.viewMode+1),e.hasClass("today")&&!e.hasClass("day")&&(this.setViewMode(0),this._setDate(d(),"linked"===this.o.todayBtn?null:"view")),e.hasClass("clear")&&this.clearDates(),e.hasClass("disabled")||(e.hasClass("month")||e.hasClass("year")||e.hasClass("decade")||e.hasClass("century"))&&(this.viewDate.setUTCDate(1),f=1,1===this.viewMode?(h=e.parent().find("span").index(e),g=this.viewDate.getUTCFullYear(),this.viewDate.setUTCMonth(h)):(h=0,g=Number(e.text()),this.viewDate.setUTCFullYear(g)),this._trigger(r.viewModes[this.viewMode-1].e,this.viewDate),this.viewMode===this.o.minViewMode?this._setDate(c(g,h,f)):(this.setViewMode(this.viewMode-1),this.fill())),this.picker.is(":visible")&&this._focused_from&&this._focused_from.focus(),delete this._focused_from},dayCellClick:function(b){var c=a(b.currentTarget),d=c.data("date"),e=new Date(d);this.o.updateViewDate&&(e.getUTCFullYear()!==this.viewDate.getUTCFullYear()&&this._trigger("changeYear",this.viewDate),e.getUTCMonth()!==this.viewDate.getUTCMonth()&&this._trigger("changeMonth",this.viewDate)),this._setDate(e)},navArrowsClick:function(b){var c=a(b.currentTarget),d=c.hasClass("prev")?-1:1;0!==this.viewMode&&(d*=12*r.viewModes[this.viewMode].navStep),this.viewDate=this.moveMonth(this.viewDate,d),this._trigger(r.viewModes[this.viewMode].e,this.viewDate),this.fill()},_toggle_multidate:function(a){var b=this.dates.contains(a);if(a||this.dates.clear(),-1!==b?(!0===this.o.multidate||this.o.multidate>1||this.o.toggleActive)&&this.dates.remove(b):!1===this.o.multidate?(this.dates.clear(),this.dates.push(a)):this.dates.push(a),"number"==typeof this.o.multidate)for(;this.dates.length>this.o.multidate;)this.dates.remove(0)},_setDate:function(a,b){b&&"date"!==b||this._toggle_multidate(a&&new Date(a)),(!b&&this.o.updateViewDate||"view"===b)&&(this.viewDate=a&&new Date(a)),this.fill(),this.setValue(),b&&"view"===b||this._trigger("changeDate"),this.inputField.trigger("change"),!this.o.autoclose||b&&"date"!==b||this.hide()},moveDay:function(a,b){var c=new Date(a);return c.setUTCDate(a.getUTCDate()+b),c},moveWeek:function(a,b){return this.moveDay(a,7*b)},moveMonth:function(a,b){if(!g(a))return this.o.defaultViewDate;if(!b)return a;var c,d,e=new Date(a.valueOf()),f=e.getUTCDate(),h=e.getUTCMonth(),i=Math.abs(b);if(b=b>0?1:-1,1===i)d=-1===b?function(){return e.getUTCMonth()===h}:function(){return e.getUTCMonth()!==c},c=h+b,e.setUTCMonth(c),c=(c+12)%12;else{for(var j=0;j0},dateWithinRange:function(a){return a>=this.o.startDate&&a<=this.o.endDate},keydown:function(a){if(!this.picker.is(":visible"))return void(40!==a.keyCode&&27!==a.keyCode||(this.show(),a.stopPropagation()));var b,c,d=!1,e=this.focusDate||this.viewDate;switch(a.keyCode){case 27:this.focusDate?(this.focusDate=null,this.viewDate=this.dates.get(-1)||this.viewDate,this.fill()):this.hide(),a.preventDefault(),a.stopPropagation();break;case 37:case 38:case 39:case 40:if(!this.o.keyboardNavigation||7===this.o.daysOfWeekDisabled.length)break;b=37===a.keyCode||38===a.keyCode?-1:1,0===this.viewMode?a.ctrlKey?(c=this.moveAvailableDate(e,b,"moveYear"))&&this._trigger("changeYear",this.viewDate):a.shiftKey?(c=this.moveAvailableDate(e,b,"moveMonth"))&&this._trigger("changeMonth",this.viewDate):37===a.keyCode||39===a.keyCode?c=this.moveAvailableDate(e,b,"moveDay"):this.weekOfDateIsDisabled(e)||(c=this.moveAvailableDate(e,b,"moveWeek")):1===this.viewMode?(38!==a.keyCode&&40!==a.keyCode||(b*=4),c=this.moveAvailableDate(e,b,"moveMonth")):2===this.viewMode&&(38!==a.keyCode&&40!==a.keyCode||(b*=4),c=this.moveAvailableDate(e,b,"moveYear")),c&&(this.focusDate=this.viewDate=c,this.setValue(),this.fill(),a.preventDefault());break;case 13:if(!this.o.forceParse)break;e=this.focusDate||this.dates.get(-1)||this.viewDate,this.o.keyboardNavigation&&(this._toggle_multidate(e),d=!0),this.focusDate=null,this.viewDate=this.dates.get(-1)||this.viewDate,this.setValue(),this.fill(),this.picker.is(":visible")&&(a.preventDefault(),a.stopPropagation(),this.o.autoclose&&this.hide());break;case 9:this.focusDate=null,this.viewDate=this.dates.get(-1)||this.viewDate,this.fill(),this.hide()}d&&(this.dates.length?this._trigger("changeDate"):this._trigger("clearDate"),this.inputField.trigger("change"))},setViewMode:function(a){this.viewMode=a,this.picker.children("div").hide().filter(".datepicker-"+r.viewModes[this.viewMode].clsName).show(),this.updateNavArrows(),this._trigger("changeViewMode",new Date(this.viewDate))}};var l=function(b,c){a.data(b,"datepicker",this),this.element=a(b),this.inputs=a.map(c.inputs,function(a){return a.jquery?a[0]:a}),delete c.inputs,this.keepEmptyValues=c.keepEmptyValues,delete c.keepEmptyValues,n.call(a(this.inputs),c).on("changeDate",a.proxy(this.dateUpdated,this)),this.pickers=a.map(this.inputs,function(b){return a.data(b,"datepicker")}),this.updateDates()};l.prototype={updateDates:function(){this.dates=a.map(this.pickers,function(a){return a.getUTCDate()}),this.updateRanges()},updateRanges:function(){var b=a.map(this.dates,function(a){return a.valueOf()});a.each(this.pickers,function(a,c){c.setRange(b)})},clearDates:function(){a.each(this.pickers,function(a,b){b.clearDates()})},dateUpdated:function(c){if(!this.updating){this.updating=!0;var d=a.data(c.target,"datepicker");if(d!==b){var e=d.getUTCDate(),f=this.keepEmptyValues,g=a.inArray(c.target,this.inputs),h=g-1,i=g+1,j=this.inputs.length;if(-1!==g){if(a.each(this.pickers,function(a,b){b.getUTCDate()||b!==d&&f||b.setUTCDate(e)}),e=0&&ethis.dates[i])for(;ithis.dates[i];)this.pickers[i++].setUTCDate(e);this.updateDates(),delete this.updating}}}},destroy:function(){a.map(this.pickers,function(a){a.destroy()}),a(this.inputs).off("changeDate",this.dateUpdated),delete this.element.data().datepicker},remove:f("destroy","Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead")};var m=a.fn.datepicker,n=function(c){var d=Array.apply(null,arguments);d.shift();var e;if(this.each(function(){var b=a(this),f=b.data("datepicker"),g="object"==typeof c&&c;if(!f){var j=h(this,"date"),m=a.extend({},o,j,g),n=i(m.language),p=a.extend({},o,n,j,g);b.hasClass("input-daterange")||p.inputs?(a.extend(p,{inputs:p.inputs||b.find("input").toArray()}),f=new l(this,p)):f=new k(this,p),b.data("datepicker",f)}"string"==typeof c&&"function"==typeof f[c]&&(e=f[c].apply(f,d))}),e===b||e instanceof k||e instanceof l)return this;if(this.length>1)throw new Error("Using only allowed for the collection of a single element ("+c+" function)");return e};a.fn.datepicker=n;var o=a.fn.datepicker.defaults={assumeNearbyYear:!1,autoclose:!1,beforeShowDay:a.noop,beforeShowMonth:a.noop,beforeShowYear:a.noop,beforeShowDecade:a.noop,beforeShowCentury:a.noop,calendarWeeks:!1,clearBtn:!1,toggleActive:!1,daysOfWeekDisabled:[],daysOfWeekHighlighted:[],datesDisabled:[],endDate:1/0,forceParse:!0,format:"mm/dd/yyyy",keepEmptyValues:!1,keyboardNavigation:!0,language:"en",minViewMode:0,maxViewMode:4,multidate:!1,multidateSeparator:",",orientation:"auto",rtl:!1,startDate:-1/0,startView:0,todayBtn:!1,todayHighlight:!1,updateViewDate:!0,weekStart:0,disableTouchKeyboard:!1,enableOnReadonly:!0,showOnFocus:!0,zIndexOffset:10,container:"body",immediateUpdates:!1,title:"",templates:{leftArrow:"«",rightArrow:"»"},showWeekDays:!0},p=a.fn.datepicker.locale_opts=["format","rtl","weekStart"];a.fn.datepicker.Constructor=k;var q=a.fn.datepicker.dates={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",clear:"Clear",titleFormat:"MM yyyy"}},r={viewModes:[{names:["days","month"],clsName:"days",e:"changeMonth"},{names:["months","year"],clsName:"months",e:"changeYear",navStep:1},{names:["years","decade"],clsName:"years",e:"changeDecade",navStep:10},{names:["decades","century"],clsName:"decades",e:"changeCentury",navStep:100},{names:["centuries","millennium"],clsName:"centuries",e:"changeMillennium",navStep:1e3}],validParts:/dd?|DD?|mm?|MM?|yy(?:yy)?/g,nonpunctuation:/[^ -\/:-@\u5e74\u6708\u65e5\[-`{-~\t\n\r]+/g,parseFormat:function(a){if("function"==typeof a.toValue&&"function"==typeof a.toDisplay)return a;var b=a.replace(this.validParts,"\0").split("\0"),c=a.match(this.validParts);if(!b||!b.length||!c||0===c.length)throw new Error("Invalid date format.");return{separators:b,parts:c}},parseDate:function(c,e,f,g){function h(a,b){return!0===b&&(b=10),a<100&&(a+=2e3)>(new Date).getFullYear()+b&&(a-=100),a}function i(){var a=this.slice(0,j[n].length),b=j[n].slice(0,a.length);return a.toLowerCase()===b.toLowerCase()}if(!c)return b;if(c instanceof Date)return c;if("string"==typeof e&&(e=r.parseFormat(e)),e.toValue)return e.toValue(c,e,f);var j,l,m,n,o,p={d:"moveDay",m:"moveMonth",w:"moveWeek",y:"moveYear"},s={yesterday:"-1d",today:"+0d",tomorrow:"+1d"};if(c in s&&(c=s[c]),/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/i.test(c)){for(j=c.match(/([\-+]\d+)([dmwy])/gi),c=new Date,n=0;n'+o.templates.leftArrow+''+o.templates.rightArrow+"",contTemplate:'',footTemplate:''};r.template='
'+r.headTemplate+""+r.footTemplate+'
'+r.headTemplate+r.contTemplate+r.footTemplate+'
'+r.headTemplate+r.contTemplate+r.footTemplate+'
'+r.headTemplate+r.contTemplate+r.footTemplate+'
'+r.headTemplate+r.contTemplate+r.footTemplate+"
",a.fn.datepicker.DPGlobal=r,a.fn.datepicker.noConflict=function(){return a.fn.datepicker=m,this},a.fn.datepicker.version="1.9.0",a.fn.datepicker.deprecated=function(a){var b=window.console;b&&b.warn&&b.warn("DEPRECATED: "+a)},a(document).on("focus.datepicker.data-api click.datepicker.data-api",'[data-provide="datepicker"]',function(b){var c=a(this);c.data("datepicker")||(b.preventDefault(),n.call(c,"show"))}),a(function(){n.call(a('[data-provide="datepicker-inline"]'))})}); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js new file mode 100644 index 0000000000..0aab38f3a4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"},a.fn.datepicker.deprecated("This filename doesn't follow the convention, use bootstrap-datepicker.en-CA.js instead.")}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js new file mode 100644 index 0000000000..9d70dc2fa3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["ar-tn"]={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js new file mode 100644 index 0000000000..ece41af725 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ar={days:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت","الأحد"],daysShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت","أحد"],daysMin:["ح","ن","ث","ع","خ","ج","س","ح"],months:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthsShort:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],today:"هذا اليوم",rtl:!0}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js new file mode 100644 index 0000000000..aa1edbf4f8 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.az={days:["Bazar","Bazar ertəsi","Çərşənbə axşamı","Çərşənbə","Cümə axşamı","Cümə","Şənbə"],daysShort:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],daysMin:["B.","B.e","Ç.a","Ç.","C.a","C.","Ş."],months:["Yanvar","Fevral","Mart","Aprel","May","İyun","İyul","Avqust","Sentyabr","Oktyabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","İyun","İyul","Avq","Sen","Okt","Noy","Dek"],today:"Bu gün",weekStart:1,clear:"Təmizlə",monthsTitle:"Aylar"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js new file mode 100644 index 0000000000..28e8b22dcf --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.bg={days:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],daysShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],daysMin:["Н","П","В","С","Ч","П","С"],months:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthsShort:["Ян","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Ное","Дек"],today:"днес"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js new file mode 100644 index 0000000000..e0796a3ba7 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.bm={days:["Kari","Ntɛnɛn","Tarata","Araba","Alamisa","Juma","Sibiri"],daysShort:["Kar","Ntɛ","Tar","Ara","Ala","Jum","Sib"],daysMin:["Ka","Nt","Ta","Ar","Al","Ju","Si"],months:["Zanwuyekalo","Fewuruyekalo","Marisikalo","Awirilikalo","Mɛkalo","Zuwɛnkalo","Zuluyekalo","Utikalo","Sɛtanburukalo","ɔkutɔburukalo","Nowanburukalo","Desanburukalo"],monthsShort:["Zan","Few","Mar","Awi","Mɛ","Zuw","Zul","Uti","Sɛt","ɔku","Now","Des"],today:"Bi",monthsTitle:"Kalo",clear:"Ka jɔsi",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js new file mode 100644 index 0000000000..f67b5e26e7 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.bn={days:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],daysShort:["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],daysMin:["রবি","সোম","মঙ্গল","বুধ","বৃহস্পতি","শুক্র","শনি"],months:["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],monthsShort:["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],today:"আজ",monthsTitle:"মাস",clear:"পরিষ্কার",weekStart:0,format:"mm/dd/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js new file mode 100644 index 0000000000..af3e3bd0af --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.br={days:["Sul","Lun","Meurzh","Merc'her","Yaou","Gwener","Sadorn"],daysShort:["Sul","Lun","Meu.","Mer.","Yao.","Gwe.","Sad."],daysMin:["Su","L","Meu","Mer","Y","G","Sa"],months:["Genver","C'hwevrer","Meurzh","Ebrel","Mae","Mezheven","Gouere","Eost","Gwengolo","Here","Du","Kerzu"],monthsShort:["Genv.","C'hw.","Meur.","Ebre.","Mae","Mezh.","Goue.","Eost","Gwen.","Here","Du","Kerz."],today:"Hiziv",monthsTitle:"Miz",clear:"Dilemel",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js new file mode 100644 index 0000000000..cfb06fde79 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.bs={days:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],daysMin:["N","Po","U","Sr","Č","Pe","Su"],months:["Januar","Februar","Mart","April","Maj","Juni","Juli","August","Septembar","Oktobar","Novembar","Decembar"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"Danas",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js new file mode 100644 index 0000000000..ac107894c4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ca={days:["Diumenge","Dilluns","Dimarts","Dimecres","Dijous","Divendres","Dissabte"],daysShort:["Diu","Dil","Dmt","Dmc","Dij","Div","Dis"],daysMin:["dg","dl","dt","dc","dj","dv","ds"],months:["Gener","Febrer","Març","Abril","Maig","Juny","Juliol","Agost","Setembre","Octubre","Novembre","Desembre"],monthsShort:["Gen","Feb","Mar","Abr","Mai","Jun","Jul","Ago","Set","Oct","Nov","Des"],today:"Avui",monthsTitle:"Mesos",clear:"Esborrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js new file mode 100644 index 0000000000..42dfd1a29d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.cs={days:["Neděle","Pondělí","Úterý","Středa","Čtvrtek","Pátek","Sobota"],daysShort:["Ned","Pon","Úte","Stř","Čtv","Pát","Sob"],daysMin:["Ne","Po","Út","St","Čt","Pá","So"],months:["Leden","Únor","Březen","Duben","Květen","Červen","Červenec","Srpen","Září","Říjen","Listopad","Prosinec"],monthsShort:["Led","Úno","Bře","Dub","Kvě","Čer","Čnc","Srp","Zář","Říj","Lis","Pro"],today:"Dnes",clear:"Vymazat",monthsTitle:"Měsíc",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js new file mode 100644 index 0000000000..f85ea031dd --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.cy={days:["Sul","Llun","Mawrth","Mercher","Iau","Gwener","Sadwrn"],daysShort:["Sul","Llu","Maw","Mer","Iau","Gwe","Sad"],daysMin:["Su","Ll","Ma","Me","Ia","Gwe","Sa"],months:["Ionawr","Chewfror","Mawrth","Ebrill","Mai","Mehefin","Gorfennaf","Awst","Medi","Hydref","Tachwedd","Rhagfyr"],monthsShort:["Ion","Chw","Maw","Ebr","Mai","Meh","Gor","Aws","Med","Hyd","Tach","Rha"],today:"Heddiw"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js new file mode 100644 index 0000000000..53c8180528 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.da={days:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],daysShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],daysMin:["Sø","Ma","Ti","On","To","Fr","Lø"],months:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],today:"I Dag",weekStart:1,clear:"Nulstil",format:"dd/mm/yyyy",monthsTitle:"Måneder"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js new file mode 100644 index 0000000000..1b5d6a2474 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.de={days:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],daysShort:["Son","Mon","Die","Mit","Don","Fre","Sam"],daysMin:["So","Mo","Di","Mi","Do","Fr","Sa"],months:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthsShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],today:"Heute",monthsTitle:"Monate",clear:"Löschen",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js new file mode 100644 index 0000000000..046e9eb5ef --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.el={days:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],daysShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],daysMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],months:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthsShort:["Ιαν","Φεβ","Μαρ","Απρ","Μάι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],today:"Σήμερα",clear:"Καθαρισμός",weekStart:1,format:"d/m/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js new file mode 100644 index 0000000000..b8d5f41cfa --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-AU"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"d/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js new file mode 100644 index 0000000000..7b1070f74d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-CA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:0,format:"yyyy-mm-dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js new file mode 100644 index 0000000000..2966f5414e --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-GB"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js new file mode 100644 index 0000000000..dc8f71c026 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-IE"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js new file mode 100644 index 0000000000..c374a8d40c --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-NZ"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"d/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js new file mode 100644 index 0000000000..885a928cba --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["en-ZA"]={days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",monthsTitle:"Months",clear:"Clear",weekStart:1,format:"yyyy/mm/d"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js new file mode 100644 index 0000000000..736db021ae --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.eo={days:["dimanĉo","lundo","mardo","merkredo","ĵaŭdo","vendredo","sabato"],daysShort:["dim.","lun.","mar.","mer.","ĵaŭ.","ven.","sam."],daysMin:["d","l","ma","me","ĵ","v","s"],months:["januaro","februaro","marto","aprilo","majo","junio","julio","aŭgusto","septembro","oktobro","novembro","decembro"],monthsShort:["jan.","feb.","mar.","apr.","majo","jun.","jul.","aŭg.","sep.","okt.","nov.","dec."],today:"Hodiaŭ",clear:"Nuligi",weekStart:1,format:"yyyy-mm-dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js new file mode 100644 index 0000000000..f3cef5d2b9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.es={days:["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"],daysShort:["Dom","Lun","Mar","Mié","Jue","Vie","Sáb"],daysMin:["Do","Lu","Ma","Mi","Ju","Vi","Sa"],months:["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],monthsShort:["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"],today:"Hoy",monthsTitle:"Meses",clear:"Borrar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js new file mode 100644 index 0000000000..34cd9c60e9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.et={days:["Pühapäev","Esmaspäev","Teisipäev","Kolmapäev","Neljapäev","Reede","Laupäev"],daysShort:["Pühap","Esmasp","Teisip","Kolmap","Neljap","Reede","Laup"],daysMin:["P","E","T","K","N","R","L"],months:["Jaanuar","Veebruar","Märts","Aprill","Mai","Juuni","Juuli","August","September","Oktoober","November","Detsember"],monthsShort:["Jaan","Veebr","Märts","Apr","Mai","Juuni","Juuli","Aug","Sept","Okt","Nov","Dets"],today:"Täna",clear:"Tühjenda",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js new file mode 100644 index 0000000000..c5aa359e48 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.eu={days:["Igandea","Astelehena","Asteartea","Asteazkena","Osteguna","Ostirala","Larunbata"],daysShort:["Ig","Al","Ar","Az","Og","Ol","Lr"],daysMin:["Ig","Al","Ar","Az","Og","Ol","Lr"],months:["Urtarrila","Otsaila","Martxoa","Apirila","Maiatza","Ekaina","Uztaila","Abuztua","Iraila","Urria","Azaroa","Abendua"],monthsShort:["Urt","Ots","Mar","Api","Mai","Eka","Uzt","Abu","Ira","Urr","Aza","Abe"],today:"Gaur",monthsTitle:"Hilabeteak",clear:"Ezabatu",weekStart:1,format:"yyyy/mm/dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js new file mode 100644 index 0000000000..8575237a07 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.fa={days:["یک‌شنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنج‌شنبه","جمعه","شنبه","یک‌شنبه"],daysShort:["یک","دو","سه","چهار","پنج","جمعه","شنبه","یک"],daysMin:["ی","د","س","چ","پ","ج","ش","ی"],months:["ژانویه","فوریه","مارس","آوریل","مه","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],monthsShort:["ژان","فور","مار","آور","مه","ژون","ژوی","اوت","سپت","اکت","نوا","دسا"],today:"امروز",clear:"پاک کن",weekStart:1,format:"yyyy/mm/dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js new file mode 100644 index 0000000000..239dfb796c --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.fi={days:["sunnuntai","maanantai","tiistai","keskiviikko","torstai","perjantai","lauantai"],daysShort:["sun","maa","tii","kes","tor","per","lau"],daysMin:["su","ma","ti","ke","to","pe","la"],months:["tammikuu","helmikuu","maaliskuu","huhtikuu","toukokuu","kesäkuu","heinäkuu","elokuu","syyskuu","lokakuu","marraskuu","joulukuu"],monthsShort:["tam","hel","maa","huh","tou","kes","hei","elo","syy","lok","mar","jou"],today:"tänään",clear:"Tyhjennä",weekStart:1,format:"d.m.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js new file mode 100644 index 0000000000..fa24e3a127 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.fo={days:["Sunnudagur","Mánadagur","Týsdagur","Mikudagur","Hósdagur","Fríggjadagur","Leygardagur"],daysShort:["Sun","Mán","Týs","Mik","Hós","Frí","Ley"],daysMin:["Su","Má","Tý","Mi","Hó","Fr","Le"],months:["Januar","Februar","Marts","Apríl","Mei","Juni","Juli","August","Septembur","Oktobur","Novembur","Desembur"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Aug","Sep","Okt","Nov","Des"],today:"Í Dag",clear:"Reinsa"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js new file mode 100644 index 0000000000..1c6bcdcbfc --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.fr={days:["Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi"],daysShort:["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],daysMin:["D","L","Ma","Me","J","V","S"],months:["Janvier","Février","Mars","Avril","Mai","Juin","Juillet","Août","Septembre","Octobre","Novembre","Décembre"],monthsShort:["Jan","Fév","Mar","Avr","Mai","Jui","Jul","Aou","Sep","Oct","Nov","Déc"],today:"Aujourd'hui",monthsTitle:"Mois",clear:"Effacer",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js new file mode 100644 index 0000000000..244cfba800 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.fr={days:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],daysShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],daysMin:["d","l","ma","me","j","v","s"],months:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthsShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],today:"Aujourd'hui",monthsTitle:"Mois",clear:"Effacer",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js new file mode 100644 index 0000000000..3d92606b3b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.gl={days:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],daysShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],daysMin:["Do","Lu","Ma","Me","Xo","Ve","Sa"],months:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthsShort:["Xan","Feb","Mar","Abr","Mai","Xun","Xul","Ago","Sep","Out","Nov","Dec"],today:"Hoxe",clear:"Limpar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js new file mode 100644 index 0000000000..191cb453a0 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.he={days:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת","ראשון"],daysShort:["א","ב","ג","ד","ה","ו","ש","א"],daysMin:["א","ב","ג","ד","ה","ו","ש","א"],months:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],monthsShort:["ינו","פבר","מרץ","אפר","מאי","יונ","יול","אוג","ספט","אוק","נוב","דצמ"],today:"היום",rtl:!0}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js new file mode 100644 index 0000000000..635baffa8b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.hi={days:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],daysShort:["सूर्य","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],daysMin:["र","सो","मं","बु","गु","शु","श"],months:["जनवरी","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितम्बर","अक्टूबर","नवंबर","दिसम्बर"],monthsShort:["जन","फ़रवरी","मार्च","अप्रैल","मई","जून","जुलाई","अगस्त","सितं","अक्टूबर","नवं","दिसम्बर"],today:"आज",monthsTitle:"महीने",clear:"साफ",weekStart:1,format:"dd / mm / yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js new file mode 100644 index 0000000000..8b34bce0fd --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.hr={days:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],daysMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],months:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],monthsShort:["Sij","Velj","Ožu","Tra","Svi","Lip","Srp","Kol","Ruj","Lis","Stu","Pro"],today:"Danas"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js new file mode 100644 index 0000000000..f9decf9a2c --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.hu={days:["vasárnap","hétfő","kedd","szerda","csütörtök","péntek","szombat"],daysShort:["vas","hét","ked","sze","csü","pén","szo"],daysMin:["V","H","K","Sze","Cs","P","Szo"],months:["január","február","március","április","május","június","július","augusztus","szeptember","október","november","december"],monthsShort:["jan","feb","már","ápr","máj","jún","júl","aug","sze","okt","nov","dec"],today:"ma",weekStart:1,clear:"töröl",titleFormat:"yyyy. MM",format:"yyyy.mm.dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js new file mode 100644 index 0000000000..a1cf653d38 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.hy={days:["Կիրակի","Երկուշաբթի","Երեքշաբթի","Չորեքշաբթի","Հինգշաբթի","Ուրբաթ","Շաբաթ"],daysShort:["Կիր","Երկ","Երե","Չոր","Հին","Ուրբ","Շաբ"],daysMin:["Կի","Եկ","Եք","Չո","Հի","Ու","Շա"],months:["Հունվար","Փետրվար","Մարտ","Ապրիլ","Մայիս","Հունիս","Հուլիս","Օգոստոս","Սեպտեմբեր","Հոկտեմբեր","Նոյեմբեր","Դեկտեմբեր"],monthsShort:["Հնվ","Փետ","Մար","Ապր","Մայ","Հուն","Հուլ","Օգս","Սեպ","Հոկ","Նոյ","Դեկ"],today:"Այսօր",clear:"Ջնջել",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Ամիսնէր"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js new file mode 100644 index 0000000000..7c3220a643 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.id={days:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],daysShort:["Mgu","Sen","Sel","Rab","Kam","Jum","Sab"],daysMin:["Mg","Sn","Sl","Ra","Ka","Ju","Sa"],months:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","November","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ags","Sep","Okt","Nov","Des"],today:"Hari Ini",clear:"Kosongkan"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.is.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.is.min.js new file mode 100644 index 0000000000..f49bd18cc2 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.is.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.is={days:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],daysShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],daysMin:["Su","Má","Þr","Mi","Fi","Fö","La"],months:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthsShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],today:"Í Dag"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it-CH.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it-CH.min.js new file mode 100644 index 0000000000..7e1adbb95d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it-CH.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.it={days:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],daysShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],daysMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],months:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthsShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],today:"Oggi",clear:"Cancella",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it.min.js new file mode 100644 index 0000000000..cc30766ffa --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.it={days:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],daysShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],daysMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],months:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthsShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],today:"Oggi",monthsTitle:"Mesi",clear:"Cancella",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js new file mode 100644 index 0000000000..e321f04ffe --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ja={days:["日曜","月曜","火曜","水曜","木曜","金曜","土曜"],daysShort:["日","月","火","水","木","金","土"],daysMin:["日","月","火","水","木","金","土"],months:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今日",format:"yyyy/mm/dd",titleFormat:"yyyy年mm月",clear:"クリア"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ka.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ka.min.js new file mode 100644 index 0000000000..84f14c0e90 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ka.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ka={days:["კვირა","ორშაბათი","სამშაბათი","ოთხშაბათი","ხუთშაბათი","პარასკევი","შაბათი"],daysShort:["კვი","ორშ","სამ","ოთხ","ხუთ","პარ","შაბ"],daysMin:["კვ","ორ","სა","ოთ","ხუ","პა","შა"],months:["იანვარი","თებერვალი","მარტი","აპრილი","მაისი","ივნისი","ივლისი","აგვისტო","სექტემბერი","ოქტომბერი","ნოემბერი","დეკემბერი"],monthsShort:["იან","თებ","მარ","აპრ","მაი","ივნ","ივლ","აგვ","სექ","ოქტ","ნოე","დეკ"],today:"დღეს",clear:"გასუფთავება",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kh.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kh.min.js new file mode 100644 index 0000000000..bf2abc5d82 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kh.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.kh={days:["អាទិត្យ","ចន្ទ","អង្គារ","ពុធ","ព្រហស្បតិ៍","សុក្រ","សៅរ៍"],daysShort:["អា.ទិ","ចន្ទ","អង្គារ","ពុធ","ព្រ.ហ","សុក្រ","សៅរ៍"],daysMin:["អា.ទិ","ចន្ទ","អង្គារ","ពុធ","ព្រ.ហ","សុក្រ","សៅរ៍"],months:["មករា","កុម្ភះ","មិនា","មេសា","ឧសភា","មិថុនា","កក្កដា","សីហា","កញ្ញា","តុលា","វិច្ឆិកា","ធ្នូ"],monthsShort:["មករា","កុម្ភះ","មិនា","មេសា","ឧសភា","មិថុនា","កក្កដា","សីហា","កញ្ញា","តុលា","វិច្ឆិកា","ធ្នូ"],today:"ថ្ងៃនេះ",clear:"សំអាត"},a.fn.datepicker.deprecated('The language code "kh" is deprecated and will be removed in 2.0. For Khmer support use "km" instead.')}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kk.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kk.min.js new file mode 100644 index 0000000000..f4e2f3f1a2 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kk.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.kk={days:["Жексенбі","Дүйсенбі","Сейсенбі","Сәрсенбі","Бейсенбі","Жұма","Сенбі"],daysShort:["Жек","Дүй","Сей","Сәр","Бей","Жұм","Сен"],daysMin:["Жк","Дс","Сс","Ср","Бс","Жм","Сн"],months:["Қаңтар","Ақпан","Наурыз","Сәуір","Мамыр","Маусым","Шілде","Тамыз","Қыркүйек","Қазан","Қараша","Желтоқсан"],monthsShort:["Қаң","Ақп","Нау","Сәу","Мам","Мау","Шіл","Там","Қыр","Қаз","Қар","Жел"],today:"Бүгін",weekStart:1}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.km.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.km.min.js new file mode 100644 index 0000000000..648d83f84f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.km.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.km={days:["អាទិត្យ","ចន្ទ","អង្គារ","ពុធ","ព្រហស្បតិ៍","សុក្រ","សៅរ៍"],daysShort:["អា.ទិ","ចន្ទ","អង្គារ","ពុធ","ព្រ.ហ","សុក្រ","សៅរ៍"],daysMin:["អា.ទិ","ចន្ទ","អង្គារ","ពុធ","ព្រ.ហ","សុក្រ","សៅរ៍"],months:["មករា","កុម្ភះ","មិនា","មេសា","ឧសភា","មិថុនា","កក្កដា","សីហា","កញ្ញា","តុលា","វិច្ឆិកា","ធ្នូ"],monthsShort:["មករា","កុម្ភះ","មិនា","មេសា","ឧសភា","មិថុនា","កក្កដា","សីហា","កញ្ញា","តុលា","វិច្ឆិកា","ធ្នូ"],today:"ថ្ងៃនេះ",clear:"សំអាត"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ko.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ko.min.js new file mode 100644 index 0000000000..9751ee5c22 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ko.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ko={days:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],daysShort:["일","월","화","수","목","금","토"],daysMin:["일","월","화","수","목","금","토"],months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthsShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],today:"오늘",clear:"삭제",format:"yyyy-mm-dd",titleFormat:"yyyy년mm월",weekStart:0}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kr.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kr.min.js new file mode 100644 index 0000000000..43393409e0 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kr.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.kr={days:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],daysShort:["일","월","화","수","목","금","토"],daysMin:["일","월","화","수","목","금","토"],months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthsShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"]},a.fn.datepicker.deprecated('The language code "kr" is deprecated and will be removed in 2.0. For korean support use "ko" instead.')}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lt.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lt.min.js new file mode 100644 index 0000000000..da78ea85f3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lt.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.lt={days:["Sekmadienis","Pirmadienis","Antradienis","Trečiadienis","Ketvirtadienis","Penktadienis","Šeštadienis"],daysShort:["S","Pr","A","T","K","Pn","Š"],daysMin:["Sk","Pr","An","Tr","Ke","Pn","Št"],months:["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],monthsShort:["Sau","Vas","Kov","Bal","Geg","Bir","Lie","Rugp","Rugs","Spa","Lap","Gru"],today:"Šiandien",monthsTitle:"Mėnesiai",clear:"Išvalyti",weekStart:1,format:"yyyy-mm-dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lv.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lv.min.js new file mode 100644 index 0000000000..89cea00f8b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lv.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.lv={days:["Svētdiena","Pirmdiena","Otrdiena","Trešdiena","Ceturtdiena","Piektdiena","Sestdiena"],daysShort:["Sv","P","O","T","C","Pk","S"],daysMin:["Sv","Pr","Ot","Tr","Ce","Pk","Se"],months:["Janvāris","Februāris","Marts","Aprīlis","Maijs","Jūnijs","Jūlijs","Augusts","Septembris","Oktobris","Novembris","Decembris"],monthsShort:["Jan","Feb","Mar","Apr","Mai","Jūn","Jūl","Aug","Sep","Okt","Nov","Dec"],monthsTitle:"Mēneši",today:"Šodien",clear:"Nodzēst",weekStart:1}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.me.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.me.min.js new file mode 100644 index 0000000000..c65a891646 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.me.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.me={days:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],daysMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],months:["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],today:"Danas",weekStart:1,clear:"Izbriši",format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mk.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mk.min.js new file mode 100644 index 0000000000..46423f7581 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mk.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.mk={days:["Недела","Понеделник","Вторник","Среда","Четврток","Петок","Сабота"],daysShort:["Нед","Пон","Вто","Сре","Чет","Пет","Саб"],daysMin:["Не","По","Вт","Ср","Че","Пе","Са"],months:["Јануари","Февруари","Март","Април","Мај","Јуни","Јули","Август","Септември","Октомври","Ноември","Декември"],monthsShort:["Јан","Фев","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Ное","Дек"],today:"Денес",format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mn.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mn.min.js new file mode 100644 index 0000000000..6ebaec9d86 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mn.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.mn={days:["Ням","Даваа","Мягмар","Лхагва","Пүрэв","Баасан","Бямба"],daysShort:["Ням","Дав","Мяг","Лха","Пүр","Баа","Бям"],daysMin:["Ня","Да","Мя","Лх","Пү","Ба","Бя"],months:["Хулгана","Үхэр","Бар","Туулай","Луу","Могой","Морь","Хонь","Бич","Тахиа","Нохой","Гахай"],monthsShort:["Хул","Үхэ","Бар","Туу","Луу","Мог","Мор","Хон","Бич","Тах","Нох","Гах"],today:"Өнөөдөр",clear:"Тодорхой",format:"yyyy.mm.dd",weekStart:1}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ms.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ms.min.js new file mode 100644 index 0000000000..47efafdc2f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ms.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ms={days:["Ahad","Isnin","Selasa","Rabu","Khamis","Jumaat","Sabtu"],daysShort:["Aha","Isn","Sel","Rab","Kha","Jum","Sab"],daysMin:["Ah","Is","Se","Ra","Kh","Ju","Sa"],months:["Januari","Februari","Mac","April","Mei","Jun","Julai","Ogos","September","Oktober","November","Disember"],monthsShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Ogo","Sep","Okt","Nov","Dis"],today:"Hari Ini",clear:"Bersihkan"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl-BE.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl-BE.min.js new file mode 100644 index 0000000000..85d3146df1 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl-BE.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["nl-BE"]={days:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],daysShort:["zo","ma","di","wo","do","vr","za"],daysMin:["zo","ma","di","wo","do","vr","za"],months:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthsShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],today:"Vandaag",monthsTitle:"Maanden",clear:"Leegmaken",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl.min.js new file mode 100644 index 0000000000..af977b71ea --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.nl={days:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],daysShort:["zo","ma","di","wo","do","vr","za"],daysMin:["zo","ma","di","wo","do","vr","za"],months:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthsShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],today:"Vandaag",monthsTitle:"Maanden",clear:"Wissen",weekStart:1,format:"dd-mm-yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js new file mode 100644 index 0000000000..0c5136e441 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.no={days:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],daysShort:["søn","man","tir","ons","tor","fre","lør"],daysMin:["sø","ma","ti","on","to","fr","lø"],months:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthsShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],today:"i dag",monthsTitle:"Måneder",clear:"Nullstill",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.oc.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.oc.min.js new file mode 100644 index 0000000000..630fa16b96 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.oc.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.oc={days:["Dimenge","Diluns","Dimars","Dimècres","Dijòus","Divendres","Dissabte"],daysShort:["Dim","Dil","Dmr","Dmc","Dij","Div","Dis"],daysMin:["dg","dl","dr","dc","dj","dv","ds"],months:["Genièr","Febrièr","Març","Abrial","Mai","Junh","Julhet","Agost","Setembre","Octobre","Novembre","Decembre"],monthsShort:["Gen","Feb","Mar","Abr","Mai","Jun","Jul","Ago","Set","Oct","Nov","Dec"],today:"Uèi",monthsTitle:"Meses",clear:"Escafar",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pl.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pl.min.js new file mode 100644 index 0000000000..ffb30ec8b1 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pl.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.pl={days:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],daysShort:["Niedz.","Pon.","Wt.","Śr.","Czw.","Piąt.","Sob."],daysMin:["Ndz.","Pn.","Wt.","Śr.","Czw.","Pt.","Sob."],months:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthsShort:["Sty.","Lut.","Mar.","Kwi.","Maj","Cze.","Lip.","Sie.","Wrz.","Paź.","Lis.","Gru."],today:"Dzisiaj",weekStart:1,clear:"Wyczyść",format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt-BR.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt-BR.min.js new file mode 100644 index 0000000000..2d3f8afdac --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt-BR.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["pt-BR"]={days:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"],daysShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],daysMin:["Do","Se","Te","Qu","Qu","Se","Sa"],months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthsShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],today:"Hoje",monthsTitle:"Meses",clear:"Limpar",format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js new file mode 100644 index 0000000000..e2b4e64d77 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.pt={days:["Domingo","Segunda","Terça","Quarta","Quinta","Sexta","Sábado"],daysShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],daysMin:["Do","Se","Te","Qu","Qu","Se","Sa"],months:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthsShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],today:"Hoje",monthsTitle:"Meses",clear:"Limpar",format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ro.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ro.min.js new file mode 100644 index 0000000000..5fff2986df --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ro.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ro={days:["Duminică","Luni","Marţi","Miercuri","Joi","Vineri","Sâmbătă"],daysShort:["Dum","Lun","Mar","Mie","Joi","Vin","Sâm"],daysMin:["Du","Lu","Ma","Mi","Jo","Vi","Sâ"],months:["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],monthsShort:["Ian","Feb","Mar","Apr","Mai","Iun","Iul","Aug","Sep","Oct","Nov","Dec"],today:"Astăzi",clear:"Șterge",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs-latin.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs-latin.min.js new file mode 100644 index 0000000000..e520c95733 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs-latin.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["rs-latin"]={days:["Nedelja","Ponedeljak","Utorak","Sreda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sre","Čet","Pet","Sub"],daysMin:["N","Po","U","Sr","Č","Pe","Su"],months:["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],today:"Danas",weekStart:1,format:"dd.mm.yyyy"},a.fn.datepicker.deprecated('This language code "rs-latin" is deprecated (invalid serbian language code) and will be removed in 2.0. For Serbian latin support use "sr-latin" instead.')}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs.min.js new file mode 100644 index 0000000000..ba95ae298f --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.rs={days:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],daysShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],daysMin:["Н","По","У","Ср","Ч","Пе","Су"],months:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthsShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],today:"Данас",weekStart:1,format:"dd.mm.yyyy"},a.fn.datepicker.deprecated('This language code "rs" is deprecated (invalid serbian language code) and will be removed in 2.0. For Serbian support use "sr" instead.')}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ru.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ru.min.js new file mode 100644 index 0000000000..52bc010b97 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ru.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ru={days:["Воскресенье","Понедельник","Вторник","Среда","Четверг","Пятница","Суббота"],daysShort:["Вск","Пнд","Втр","Срд","Чтв","Птн","Суб"],daysMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],months:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthsShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],today:"Сегодня",clear:"Очистить",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Месяцы"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.si.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.si.min.js new file mode 100644 index 0000000000..b9746b8fc1 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.si.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.si={days:["ඉරිදා","සඳුදා","අඟහරුවාදා","බදාදා","බ්‍රහස්පතින්දා","සිකුරාදා","සෙනසුරාදා"],daysShort:["ඉරි","සඳු","අඟ","බදා","බ්‍රහ","සිකු","සෙන"],daysMin:["ඉ","ස","අ","බ","බ්‍ර","සි","සෙ"],months:["ජනවාරි","පෙබරවාරි","මාර්තු","අප්‍රේල්","මැයි","ජුනි","ජූලි","අගෝස්තු","සැප්තැම්බර්","ඔක්තෝබර්","නොවැම්බර්","දෙසැම්බර්"],monthsShort:["ජන","පෙබ","මාර්","අප්‍රේ","මැයි","ජුනි","ජූලි","අගෝ","සැප්","ඔක්","නොවැ","දෙසැ"],today:"අද",monthsTitle:"මාස",clear:"මකන්න",weekStart:0,format:"yyyy-mm-dd"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js new file mode 100644 index 0000000000..79a9267fd5 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.sk={days:["Nedeľa","Pondelok","Utorok","Streda","Štvrtok","Piatok","Sobota"],daysShort:["Ned","Pon","Uto","Str","Štv","Pia","Sob"],daysMin:["Ne","Po","Ut","St","Št","Pia","So"],months:["Január","Február","Marec","Apríl","Máj","Jún","Júl","August","September","Október","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Máj","Jún","Júl","Aug","Sep","Okt","Nov","Dec"],today:"Dnes",clear:"Vymazať",weekStart:1,format:"d.m.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sl.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sl.min.js new file mode 100644 index 0000000000..831cf73903 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sl.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.sl={days:["Nedelja","Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota"],daysShort:["Ned","Pon","Tor","Sre","Čet","Pet","Sob"],daysMin:["Ne","Po","To","Sr","Če","Pe","So"],months:["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],today:"Danes",weekStart:1}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sq.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sq.min.js new file mode 100644 index 0000000000..8c586055a0 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sq.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.sq={days:["E Diel","E Hënë","E Martē","E Mërkurë","E Enjte","E Premte","E Shtunë"],daysShort:["Die","Hën","Mar","Mër","Enj","Pre","Shtu"],daysMin:["Di","Hë","Ma","Më","En","Pr","Sht"],months:["Janar","Shkurt","Mars","Prill","Maj","Qershor","Korrik","Gusht","Shtator","Tetor","Nëntor","Dhjetor"],monthsShort:["Jan","Shk","Mar","Pri","Maj","Qer","Korr","Gu","Sht","Tet","Nën","Dhjet"],monthsTitle:"Muaj",today:"Sot",weekStart:1,format:"dd/mm/yyyy",clear:"Pastro"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr-latin.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr-latin.min.js new file mode 100644 index 0000000000..c6b7001ace --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr-latin.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["sr-latin"]={days:["Nedelja","Ponedeljak","Utorak","Sreda","Četvrtak","Petak","Subota"],daysShort:["Ned","Pon","Uto","Sre","Čet","Pet","Sub"],daysMin:["N","Po","U","Sr","Č","Pe","Su"],months:["Januar","Februar","Mart","April","Maj","Jun","Jul","Avgust","Septembar","Oktobar","Novembar","Decembar"],monthsShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],today:"Danas",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr.min.js new file mode 100644 index 0000000000..4e46dbf64b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.sr={days:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],daysShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],daysMin:["Н","По","У","Ср","Ч","Пе","Су"],months:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthsShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],today:"Данас",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sv.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sv.min.js new file mode 100644 index 0000000000..7ab6becb92 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sv.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.sv={days:["söndag","måndag","tisdag","onsdag","torsdag","fredag","lördag"],daysShort:["sön","mån","tis","ons","tor","fre","lör"],daysMin:["sö","må","ti","on","to","fr","lö"],months:["januari","februari","mars","april","maj","juni","juli","augusti","september","oktober","november","december"],monthsShort:["jan","feb","mar","apr","maj","jun","jul","aug","sep","okt","nov","dec"],today:"Idag",format:"yyyy-mm-dd",weekStart:1,clear:"Rensa"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sw.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sw.min.js new file mode 100644 index 0000000000..454d3053cb --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sw.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.sw={days:["Jumapili","Jumatatu","Jumanne","Jumatano","Alhamisi","Ijumaa","Jumamosi"],daysShort:["J2","J3","J4","J5","Alh","Ij","J1"],daysMin:["2","3","4","5","A","I","1"],months:["Januari","Februari","Machi","Aprili","Mei","Juni","Julai","Agosti","Septemba","Oktoba","Novemba","Desemba"],monthsShort:["Jan","Feb","Mac","Apr","Mei","Jun","Jul","Ago","Sep","Okt","Nov","Des"],today:"Leo"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ta.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ta.min.js new file mode 100644 index 0000000000..e7909494aa --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ta.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.ta={days:["ஞாயிறு","திங்கள்","செவ்வாய்","புதன்","வியாழன்","வெள்ளி","சனி"],daysShort:["ஞாயி","திங்","செவ்","புத","வியா","வெள்","சனி"],daysMin:["ஞா","தி","செ","பு","வி","வெ","ச"],months:["ஜனவரி","பிப்ரவரி","மார்ச்","ஏப்ரல்","மே","ஜூன்","ஜூலை","ஆகஸ்டு","செப்டம்பர்","அக்டோபர்","நவம்பர்","டிசம்பர்"],monthsShort:["ஜன","பிப்","மார்","ஏப்","மே","ஜூன்","ஜூலை","ஆக","செப்","அக்","நவ","டிச"],today:"இன்று",monthsTitle:"மாதங்கள்",clear:"நீக்கு",weekStart:1,format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tg.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tg.min.js new file mode 100644 index 0000000000..104b6dd95b --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tg.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.tg={days:["Якшанбе","Душанбе","Сешанбе","Чоршанбе","Панҷшанбе","Ҷумъа","Шанбе"],daysShort:["Яшб","Дшб","Сшб","Чшб","Пшб","Ҷум","Шнб"],daysMin:["Яш","Дш","Сш","Чш","Пш","Ҷм","Шб"],months:["Январ","Феврал","Март","Апрел","Май","Июн","Июл","Август","Сентябр","Октябр","Ноябр","Декабр"],monthsShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],today:"Имрӯз",monthsTitle:"Моҳҳо",clear:"Тоза намудан",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.th.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.th.min.js new file mode 100644 index 0000000000..1e398ba8bc --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.th.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.th={days:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัส","ศุกร์","เสาร์","อาทิตย์"],daysShort:["อา","จ","อ","พ","พฤ","ศ","ส","อา"],daysMin:["อา","จ","อ","พ","พฤ","ศ","ส","อา"],months:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthsShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],today:"วันนี้"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tk.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tk.min.js new file mode 100644 index 0000000000..716edef2ee --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tk.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.tk={days:["Ýekşenbe","Duşenbe","Sişenbe","Çarşenbe","Penşenbe","Anna","Şenbe"],daysShort:["Ýek","Duş","Siş","Çar","Pen","Ann","Şen"],daysMin:["Ýe","Du","Si","Ça","Pe","An","Şe"],months:["Ýanwar","Fewral","Mart","Aprel","Maý","Iýun","Iýul","Awgust","Sentýabr","Oktýabr","Noýabr","Dekabr"],monthsShort:["Ýan","Few","Mar","Apr","Maý","Iýn","Iýl","Awg","Sen","Okt","Noý","Dek"],today:"Bu gün",monthsTitle:"Aýlar",clear:"Aýyr",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tr.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tr.min.js new file mode 100644 index 0000000000..7889b1135d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tr.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.tr={days:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],daysShort:["Pz","Pzt","Sal","Çrş","Prş","Cu","Cts"],daysMin:["Pz","Pzt","Sa","Çr","Pr","Cu","Ct"],months:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthsShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],today:"Bugün",clear:"Temizle",weekStart:1,format:"dd.mm.yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uk.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uk.min.js new file mode 100644 index 0000000000..41b02e6b28 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uk.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.uk={days:["Неділя","Понеділок","Вівторок","Середа","Четвер","П'ятниця","Субота"],daysShort:["Нед","Пнд","Втр","Срд","Чтв","Птн","Суб"],daysMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],months:["Cічень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthsShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],today:"Сьогодні",clear:"Очистити",format:"dd.mm.yyyy",weekStart:1}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-cyrl.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-cyrl.min.js new file mode 100644 index 0000000000..a0a8f213ce --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-cyrl.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["uz-cyrl"]={days:["Якшанба","Душанба","Сешанба","Чоршанба","Пайшанба","Жума","Шанба"],daysShort:["Якш","Ду","Се","Чор","Пай","Жу","Ша"],daysMin:["Як","Ду","Се","Чо","Па","Жу","Ша"],months:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthsShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],today:"Бугун",clear:"Ўчириш",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Ойлар"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-latn.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-latn.min.js new file mode 100644 index 0000000000..2f58e343ef --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-latn.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["uz-latn"]={days:["Yakshanba","Dushanba","Seshanba","Chorshanba","Payshanba","Juma","Shanba"],daysShort:["Yak","Du","Se","Chor","Pay","Ju","Sha"],daysMin:["Ya","Du","Se","Cho","Pa","Ju","Sha"],months:["Yanvar","Fevral","Mart","Aprel","May","Iyun","Iyul","Avgust","Sentabr","Oktabr","Noyabr","Dekabr"],monthsShort:["Yan","Fev","Mar","Apr","May","Iyn","Iyl","Avg","Sen","Okt","Noy","Dek"],today:"Bugun",clear:"O'chirish",format:"dd.mm.yyyy",weekStart:1,monthsTitle:"Oylar"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.vi.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.vi.min.js new file mode 100644 index 0000000000..3311d23f86 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.vi.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates.vi={days:["Chủ nhật","Thứ hai","Thứ ba","Thứ tư","Thứ năm","Thứ sáu","Thứ bảy"],daysShort:["CN","Thứ 2","Thứ 3","Thứ 4","Thứ 5","Thứ 6","Thứ 7"],daysMin:["CN","T2","T3","T4","T5","T6","T7"],months:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],monthsShort:["Th1","Th2","Th3","Th4","Th5","Th6","Th7","Th8","Th9","Th10","Th11","Th12"],today:"Hôm nay",clear:"Xóa",format:"dd/mm/yyyy"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js new file mode 100644 index 0000000000..8e6920b0c8 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["zh-CN"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["周日","周一","周二","周三","周四","周五","周六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",monthsTitle:"选择月份",clear:"清除",format:"yyyy-mm-dd",titleFormat:"yyyy年mm月",weekStart:1}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-TW.min.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-TW.min.js new file mode 100644 index 0000000000..e309c1d7d6 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-TW.min.js @@ -0,0 +1 @@ +!function(a){a.fn.datepicker.dates["zh-TW"]={days:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],daysShort:["週日","週一","週二","週三","週四","週五","週六"],daysMin:["日","一","二","三","四","五","六"],months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],today:"今天",format:"yyyy年mm月dd日",weekStart:1,clear:"清除"}}(jQuery); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/css/bootstrap.css b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/css/bootstrap.css new file mode 100644 index 0000000000..8eac957a51 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/css/bootstrap.css @@ -0,0 +1,10224 @@ +/*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors + * Copyright 2011-2019 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; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +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: 0.5rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title], +abbr[data-original-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; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + 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; +} + +pre, +code, +kbd, +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; +} + +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: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; +} + +th { + text-align: inherit; +} + +label { + display: inline-block; + margin-bottom: 0.5rem; +} + +button { + border-radius: 0; +} + +button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +select { + word-wrap: normal; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="radio"], +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"] { + -webkit-appearance: listbox; +} + +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: 0.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, 0.1); +} + +small, +.small { + font-size: 80%; + font-weight: 400; +} + +mark, +.mark { + padding: 0.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: 0.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: 0.25rem; + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.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: 0.2rem 0.4rem; + font-size: 87.5%; + color: #fff; + background-color: #212529; + border-radius: 0.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-sm, .container-md, .container-lg, .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-sm, .container-md { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container, .container-sm, .container-md, .container-lg { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container, .container-sm, .container-md, .container-lg, .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-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, +.col-auto, .col-sm-1, .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-10, .col-sm-11, .col-sm-12, .col-sm, +.col-sm-auto, .col-md-1, .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-10, .col-md-11, .col-md-12, .col-md, +.col-md-auto, .col-lg-1, .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-10, .col-lg-11, .col-lg-12, .col-lg, +.col-lg-auto, .col-xl-1, .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-10, .col-xl-11, .col-xl-12, .col-xl, +.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; + 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; + 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; + 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; + 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; + 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 th, +.table td { + padding: 0.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 th, +.table-sm td { + padding: 0.3rem; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +.table-bordered thead th, +.table-bordered thead td { + border-bottom-width: 2px; +} + +.table-borderless th, +.table-borderless td, +.table-borderless thead th, +.table-borderless tbody + tbody { + border: 0; +} + +.table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0, 0, 0, 0.05); +} + +.table-hover tbody tr:hover { + color: #212529; + background-color: rgba(0, 0, 0, 0.075); +} + +.table-primary, +.table-primary > th, +.table-primary > td { + background-color: #b8daff; +} + +.table-primary th, +.table-primary td, +.table-primary thead th, +.table-primary tbody + tbody { + 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 > th, +.table-secondary > td { + background-color: #d6d8db; +} + +.table-secondary th, +.table-secondary td, +.table-secondary thead th, +.table-secondary tbody + tbody { + 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 > th, +.table-success > td { + background-color: #c3e6cb; +} + +.table-success th, +.table-success td, +.table-success thead th, +.table-success tbody + tbody { + 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 > th, +.table-info > td { + background-color: #bee5eb; +} + +.table-info th, +.table-info td, +.table-info thead th, +.table-info tbody + tbody { + 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 > th, +.table-warning > td { + background-color: #ffeeba; +} + +.table-warning th, +.table-warning td, +.table-warning thead th, +.table-warning tbody + tbody { + 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 > th, +.table-danger > td { + background-color: #f5c6cb; +} + +.table-danger th, +.table-danger td, +.table-danger thead th, +.table-danger tbody + tbody { + 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 > th, +.table-light > td { + background-color: #fdfdfe; +} + +.table-light th, +.table-light td, +.table-light thead th, +.table-light tbody + tbody { + 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 > th, +.table-dark > td { + background-color: #c6c8ca; +} + +.table-dark th, +.table-dark td, +.table-dark thead th, +.table-dark tbody + tbody { + 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 > th, +.table-active > td { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover { + background-color: rgba(0, 0, 0, 0.075); +} + +.table-hover .table-active:hover > td, +.table-hover .table-active:hover > th { + background-color: rgba(0, 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 th, +.table-dark td, +.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, 0.05); +} + +.table-dark.table-hover tbody tr:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.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 + 0.75rem + 2px); + padding: 0.375rem 0.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: 0.25rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.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 0.2rem rgba(0, 123, 255, 0.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; +} + +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(0.375rem + 1px); + padding-bottom: calc(0.375rem + 1px); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + 1px); + padding-bottom: calc(0.5rem + 1px); + font-size: 1.25rem; + line-height: 1.5; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + 1px); + padding-bottom: calc(0.25rem + 1px); + font-size: 0.875rem; + line-height: 1.5; +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.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-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + height: calc(1.5em + 0.5rem + 2px); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.form-control-lg { + height: calc(1.5em + 1rem + 2px); + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +select.form-control[size], select.form-control[multiple] { + height: auto; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-text { + display: block; + margin-top: 0.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: 0.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: 0.75rem; +} + +.form-check-inline .form-check-input { + position: static; + margin-top: 0; + margin-right: 0.3125rem; + margin-left: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #28a745; +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(40, 167, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: #28a745; + padding-right: calc(1.5em + 0.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(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:valid, .custom-select.is-valid { + border-color: #28a745; + padding-right: calc(0.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 0.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(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: #28a745; +} + +.was-validated .form-check-input:valid ~ .valid-feedback, +.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, +.form-check-input.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { + color: #28a745; +} + +.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { + border-color: #34ce57; + background-color: #34ce57; +} + +.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { + border-color: #28a745; +} + +.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { + border-color: #28a745; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 80%; + color: #dc3545; +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: .1rem; + font-size: 0.875rem; + line-height: 1.5; + color: #fff; + background-color: rgba(220, 53, 69, 0.9); + border-radius: 0.25rem; +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: #dc3545; + padding-right: calc(1.5em + 0.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(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .custom-select:invalid, .custom-select.is-invalid { + border-color: #dc3545; + padding-right: calc(0.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 0.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(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: #dc3545; +} + +.was-validated .form-check-input:invalid ~ .invalid-feedback, +.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, +.form-check-input.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { + color: #dc3545; +} + +.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { + border-color: #e4606d; + background-color: #e4606d; +} + +.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); +} + +.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before, .custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { + border-color: #dc3545; +} + +.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.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 .input-group, + .form-inline .custom-select { + 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: 0.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; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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 0.2rem rgba(0, 123, 255, 0.25); +} + +.btn.disabled, .btn:disabled { + opacity: 0.65; +} + +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 0.2rem rgba(38, 143, 255, 0.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 0.2rem rgba(38, 143, 255, 0.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 0.2rem rgba(130, 138, 145, 0.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 0.2rem rgba(130, 138, 145, 0.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 0.2rem rgba(72, 180, 97, 0.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 0.2rem rgba(72, 180, 97, 0.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 0.2rem rgba(58, 176, 195, 0.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 0.2rem rgba(58, 176, 195, 0.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 0.2rem rgba(222, 170, 12, 0.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 0.2rem rgba(222, 170, 12, 0.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 0.2rem rgba(225, 83, 97, 0.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 0.2rem rgba(225, 83, 97, 0.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 0.2rem rgba(216, 217, 219, 0.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 0.2rem rgba(216, 217, 219, 0.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 0.2rem rgba(82, 88, 93, 0.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 0.2rem rgba(82, 88, 93, 0.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 0.2rem rgba(0, 123, 255, 0.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 0.2rem rgba(0, 123, 255, 0.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 0.2rem rgba(108, 117, 125, 0.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 0.2rem rgba(108, 117, 125, 0.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 0.2rem rgba(40, 167, 69, 0.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 0.2rem rgba(40, 167, 69, 0.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 0.2rem rgba(23, 162, 184, 0.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 0.2rem rgba(23, 162, 184, 0.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 0.2rem rgba(255, 193, 7, 0.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 0.2rem rgba(255, 193, 7, 0.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 0.2rem rgba(220, 53, 69, 0.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 0.2rem rgba(220, 53, 69, 0.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 0.2rem rgba(248, 249, 250, 0.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 0.2rem rgba(248, 249, 250, 0.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 0.2rem rgba(52, 58, 64, 0.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 0.2rem rgba(52, 58, 64, 0.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; + box-shadow: none; +} + +.btn-link:disabled, .btn-link.disabled { + color: #6c757d; + pointer-events: none; +} + +.btn-lg, .btn-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.btn-sm, .btn-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 0.5rem; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + transition: opacity 0.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 0.35s ease; +} + +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} + +.dropup, +.dropright, +.dropdown, +.dropleft { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} + +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.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: 0.5rem 0; + margin: 0.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, 0.15); + border-radius: 0.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: 0.125rem; +} + +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.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: 0.125rem; +} + +.dropright .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.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: 0.125rem; +} + +.dropleft .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} + +.dropleft .dropdown-toggle::after { + display: none; +} + +.dropleft .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} + +.dropleft .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { + right: auto; + bottom: auto; +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + overflow: hidden; + border-top: 1px solid #e9ecef; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + white-space: nowrap; + background-color: transparent; + border: 0; +} + +.dropdown-item:hover, .dropdown-item:focus { + 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: 0.5rem 1.5rem; + margin-bottom: 0; + font-size: 0.875rem; + color: #6c757d; + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: 0.25rem 1.5rem; + color: #212529; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover { + z-index: 1; +} + +.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + 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:not(:first-child), +.btn-group > .btn-group:not(:first-child) { + margin-left: -1px; +} + +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:not(:first-child), +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} + +.dropdown-toggle-split::after, +.dropup .dropdown-toggle-split::after, +.dropright .dropdown-toggle-split::after { + margin-left: 0; +} + +.dropleft .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.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:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: -1px; +} + +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + 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="radio"], +.btn-group-toggle > .btn input[type="checkbox"], +.btn-group-toggle > .btn-group > .btn input[type="radio"], +.btn-group-toggle > .btn-group > .btn input[type="checkbox"] { + 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 > .form-control, +.input-group > .form-control-plaintext, +.input-group > .custom-select, +.input-group > .custom-file { + position: relative; + -ms-flex: 1 1 0%; + flex: 1 1 0%; + min-width: 0; + margin-bottom: 0; +} + +.input-group > .form-control + .form-control, +.input-group > .form-control + .custom-select, +.input-group > .form-control + .custom-file, +.input-group > .form-control-plaintext + .form-control, +.input-group > .form-control-plaintext + .custom-select, +.input-group > .form-control-plaintext + .custom-file, +.input-group > .custom-select + .form-control, +.input-group > .custom-select + .custom-select, +.input-group > .custom-select + .custom-file, +.input-group > .custom-file + .form-control, +.input-group > .custom-file + .custom-select, +.input-group > .custom-file + .custom-file { + margin-left: -1px; +} + +.input-group > .form-control:focus, +.input-group > .custom-select:focus, +.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { + z-index: 3; +} + +.input-group > .custom-file .custom-file-input:focus { + z-index: 4; +} + +.input-group > .form-control:not(:last-child), +.input-group > .custom-select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > .form-control:not(:first-child), +.input-group > .custom-select: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-prepend, +.input-group-append { + display: -ms-flexbox; + display: flex; +} + +.input-group-prepend .btn, +.input-group-append .btn { + position: relative; + z-index: 2; +} + +.input-group-prepend .btn:focus, +.input-group-append .btn:focus { + z-index: 3; +} + +.input-group-prepend .btn + .btn, +.input-group-prepend .btn + .input-group-text, +.input-group-prepend .input-group-text + .input-group-text, +.input-group-prepend .input-group-text + .btn, +.input-group-append .btn + .btn, +.input-group-append .btn + .input-group-text, +.input-group-append .input-group-text + .input-group-text, +.input-group-append .input-group-text + .btn { + 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: 0.375rem 0.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: 0.25rem; +} + +.input-group-text input[type="radio"], +.input-group-text input[type="checkbox"] { + margin-top: 0; +} + +.input-group-lg > .form-control:not(textarea), +.input-group-lg > .custom-select { + height: calc(1.5em + 1rem + 2px); +} + +.input-group-lg > .form-control, +.input-group-lg > .custom-select, +.input-group-lg > .input-group-prepend > .input-group-text, +.input-group-lg > .input-group-append > .input-group-text, +.input-group-lg > .input-group-prepend > .btn, +.input-group-lg > .input-group-append > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + line-height: 1.5; + border-radius: 0.3rem; +} + +.input-group-sm > .form-control:not(textarea), +.input-group-sm > .custom-select { + height: calc(1.5em + 0.5rem + 2px); +} + +.input-group-sm > .form-control, +.input-group-sm > .custom-select, +.input-group-sm > .input-group-prepend > .input-group-text, +.input-group-sm > .input-group-append > .input-group-text, +.input-group-sm > .input-group-prepend > .btn, +.input-group-sm > .input-group-append > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; +} + +.input-group-lg > .custom-select, +.input-group-sm > .custom-select { + padding-right: 1.75rem; +} + +.input-group > .input-group-prepend > .btn, +.input-group > .input-group-prepend > .input-group-text, +.input-group > .input-group-append:not(:last-child) > .btn, +.input-group > .input-group-append:not(:last-child) > .input-group-text, +.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) { + 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:not(:first-child) > .btn, +.input-group > .input-group-prepend:not(:first-child) > .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) { + 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 0.2rem rgba(0, 123, 255, 0.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: 0.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: 0.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: 0.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, 0.5); +} + +.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.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, 0.5); +} + +.custom-switch { + padding-left: 2.25rem; +} + +.custom-switch .custom-control-label::before { + left: -2.25rem; + width: 1.75rem; + pointer-events: all; + border-radius: 0.5rem; +} + +.custom-switch .custom-control-label::after { + top: calc(0.25rem + 2px); + left: calc(-2.25rem + 2px); + width: calc(1rem - 4px); + height: calc(1rem - 4px); + background-color: #adb5bd; + border-radius: 0.5rem; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.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(0.75rem); + transform: translateX(0.75rem); +} + +.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before { + background-color: rgba(0, 123, 255, 0.5); +} + +.custom-select { + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 1.75rem 0.375rem 0.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 0.75rem center/8px 10px; + border: 1px solid #ced4da; + border-radius: 0.25rem; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.custom-select:focus { + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-select:focus::-ms-value { + color: #495057; + background-color: #fff; +} + +.custom-select[multiple], .custom-select[size]:not([size="1"]) { + height: auto; + padding-right: 0.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 + 0.5rem + 2px); + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; +} + +.custom-select-lg { + height: calc(1.5em + 1rem + 2px); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; +} + +.custom-file { + position: relative; + display: inline-block; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin-bottom: 0; +} + +.custom-file-input { + position: relative; + z-index: 2; + width: 100%; + height: calc(1.5em + 0.75rem + 2px); + margin: 0; + opacity: 0; +} + +.custom-file-input:focus ~ .custom-file-label { + border-color: #80bdff; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.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 + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.custom-file-label::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 3; + display: block; + height: calc(1.5em + 0.75rem); + padding: 0.375rem 0.75rem; + line-height: 1.5; + color: #495057; + content: "Browse"; + background-color: #e9ecef; + border-left: inherit; + border-radius: 0 0.25rem 0.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: none; +} + +.custom-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range:focus::-ms-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.custom-range::-moz-focus-outer { + border: 0; +} + +.custom-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.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 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.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: 0.2rem; + margin-left: 0.2rem; + background-color: #007bff; + border: 0; + border-radius: 1rem; + -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.5rem; + color: transparent; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0.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 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.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: 0.5rem 1rem; +} + +.nav-link:hover, .nav-link:focus { + 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: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + border-color: #e9ecef #e9ecef #dee2e6; +} + +.nav-tabs .nav-link.disabled { + color: #6c757d; + background-color: transparent; + border-color: transparent; +} + +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + 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: 0.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: 0.5rem 1rem; +} + +.navbar .container, +.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .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: 0.3125rem; + padding-bottom: 0.3125rem; + margin-right: 1rem; + font-size: 1.25rem; + line-height: inherit; + white-space: nowrap; +} + +.navbar-brand:hover, .navbar-brand:focus { + 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: 0.5rem; + padding-bottom: 0.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: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +.navbar-toggler:hover, .navbar-toggler:focus { + 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-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .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: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid, .navbar-expand-sm > .container-sm, .navbar-expand-sm > .container-md, .navbar-expand-sm > .container-lg, .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-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .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: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-md > .container, + .navbar-expand-md > .container-fluid, .navbar-expand-md > .container-sm, .navbar-expand-md > .container-md, .navbar-expand-md > .container-lg, .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-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .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: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-lg > .container, + .navbar-expand-lg > .container-fluid, .navbar-expand-lg > .container-sm, .navbar-expand-lg > .container-md, .navbar-expand-lg > .container-lg, .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-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .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: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-xl > .container, + .navbar-expand-xl > .container-fluid, .navbar-expand-xl > .container-sm, .navbar-expand-xl > .container-md, .navbar-expand-xl > .container-lg, .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-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .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: 0.5rem; + padding-left: 0.5rem; +} + +.navbar-expand > .container, +.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .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, 0.9); +} + +.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-nav .nav-link { + color: rgba(0, 0, 0, 0.5); +} + +.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { + color: rgba(0, 0, 0, 0.7); +} + +.navbar-light .navbar-nav .nav-link.disabled { + color: rgba(0, 0, 0, 0.3); +} + +.navbar-light .navbar-nav .show > .nav-link, +.navbar-light .navbar-nav .active > .nav-link, +.navbar-light .navbar-nav .nav-link.show, +.navbar-light .navbar-nav .nav-link.active { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-toggler { + color: rgba(0, 0, 0, 0.5); + border-color: rgba(0, 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(0, 0, 0, 0.5)' 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, 0.5); +} + +.navbar-light .navbar-text a { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { + color: rgba(0, 0, 0, 0.9); +} + +.navbar-dark .navbar-brand { + color: #fff; +} + +.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { + color: #fff; +} + +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, 0.5); +} + +.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { + color: rgba(255, 255, 255, 0.75); +} + +.navbar-dark .navbar-nav .nav-link.disabled { + color: rgba(255, 255, 255, 0.25); +} + +.navbar-dark .navbar-nav .show > .nav-link, +.navbar-dark .navbar-nav .active > .nav-link, +.navbar-dark .navbar-nav .nav-link.show, +.navbar-dark .navbar-nav .nav-link.active { + color: #fff; +} + +.navbar-dark .navbar-toggler { + color: rgba(255, 255, 255, 0.5); + border-color: rgba(255, 255, 255, 0.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(255, 255, 255, 0.5)' 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, 0.5); +} + +.navbar-dark .navbar-text a { + color: #fff; +} + +.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { + 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, 0.125); + border-radius: 0.25rem; +} + +.card > hr { + margin-right: 0; + margin-left: 0; +} + +.card > .list-group:first-child .list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.card > .list-group:last-child .list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.card-body { + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-height: 1px; + padding: 1.25rem; +} + +.card-title { + margin-bottom: 0.75rem; +} + +.card-subtitle { + margin-top: -0.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: 0.75rem 1.25rem; + margin-bottom: 0; + background-color: rgba(0, 0, 0, 0.03); + border-bottom: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-header:first-child { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; +} + +.card-header + .list-group .list-group-item:first-child { + border-top: 0; +} + +.card-footer { + padding: 0.75rem 1.25rem; + background-color: rgba(0, 0, 0, 0.03); + border-top: 1px solid rgba(0, 0, 0, 0.125); +} + +.card-footer:last-child { + border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); +} + +.card-header-tabs { + margin-right: -0.625rem; + margin-bottom: -0.75rem; + margin-left: -0.625rem; + border-bottom: 0; +} + +.card-header-pills { + margin-right: -0.625rem; + margin-left: -0.625rem; +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1.25rem; +} + +.card-img, +.card-img-top, +.card-img-bottom { + -ms-flex-negative: 0; + flex-shrink: 0; + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: calc(0.25rem - 1px); + border-top-right-radius: calc(0.25rem - 1px); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: calc(0.25rem - 1px); + border-bottom-left-radius: calc(0.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-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + 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-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.card-columns .card { + margin-bottom: 0.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: 0.75rem 1rem; + margin-bottom: 1rem; + list-style: none; + background-color: #e9ecef; + border-radius: 0.25rem; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} + +.breadcrumb-item + .breadcrumb-item::before { + display: inline-block; + padding-right: 0.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: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: 0.5rem 0.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 0.2rem rgba(0, 123, 255, 0.25); +} + +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.page-item:last-child .page-link { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.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: 0.75rem 1.5rem; + font-size: 1.25rem; + line-height: 1.5; +} + +.pagination-lg .page-item:first-child .page-link { + border-top-left-radius: 0.3rem; + border-bottom-left-radius: 0.3rem; +} + +.pagination-lg .page-item:last-child .page-link { + border-top-right-radius: 0.3rem; + border-bottom-right-radius: 0.3rem; +} + +.pagination-sm .page-link { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.pagination-sm .page-item:first-child .page-link { + border-top-left-radius: 0.2rem; + border-bottom-left-radius: 0.2rem; +} + +.pagination-sm .page-item:last-child .page-link { + border-top-right-radius: 0.2rem; + border-bottom-right-radius: 0.2rem; +} + +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .badge { + transition: none; + } +} + +a.badge:hover, a.badge:focus { + text-decoration: none; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} + +.badge-primary { + color: #fff; + background-color: #007bff; +} + +a.badge-primary:hover, a.badge-primary:focus { + color: #fff; + background-color: #0062cc; +} + +a.badge-primary:focus, a.badge-primary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); +} + +.badge-secondary { + color: #fff; + background-color: #6c757d; +} + +a.badge-secondary:hover, a.badge-secondary:focus { + color: #fff; + background-color: #545b62; +} + +a.badge-secondary:focus, a.badge-secondary.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); +} + +.badge-success { + color: #fff; + background-color: #28a745; +} + +a.badge-success:hover, a.badge-success:focus { + color: #fff; + background-color: #1e7e34; +} + +a.badge-success:focus, a.badge-success.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); +} + +.badge-info { + color: #fff; + background-color: #17a2b8; +} + +a.badge-info:hover, a.badge-info:focus { + color: #fff; + background-color: #117a8b; +} + +a.badge-info:focus, a.badge-info.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); +} + +.badge-warning { + color: #212529; + background-color: #ffc107; +} + +a.badge-warning:hover, a.badge-warning:focus { + color: #212529; + background-color: #d39e00; +} + +a.badge-warning:focus, a.badge-warning.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); +} + +.badge-danger { + color: #fff; + background-color: #dc3545; +} + +a.badge-danger:hover, a.badge-danger:focus { + color: #fff; + background-color: #bd2130; +} + +a.badge-danger:focus, a.badge-danger.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); +} + +.badge-light { + color: #212529; + background-color: #f8f9fa; +} + +a.badge-light:hover, a.badge-light:focus { + color: #212529; + background-color: #dae0e5; +} + +a.badge-light:focus, a.badge-light.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); +} + +.badge-dark { + color: #fff; + background-color: #343a40; +} + +a.badge-dark:hover, a.badge-dark:focus { + color: #fff; + background-color: #1d2124; +} + +a.badge-dark:focus, a.badge-dark.focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); +} + +.jumbotron { + padding: 2rem 1rem; + margin-bottom: 2rem; + background-color: #e9ecef; + border-radius: 0.3rem; +} + +@media (min-width: 576px) { + .jumbotron { + padding: 4rem 2rem; + } +} + +.jumbotron-fluid { + padding-right: 0; + padding-left: 0; + border-radius: 0; +} + +.alert { + position: relative; + padding: 0.75rem 1.25rem; + margin-bottom: 1rem; + border: 1px solid transparent; + border-radius: 0.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: 0.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; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.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 0.6s ease; +} + +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.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; +} + +.list-group-item-action { + width: 100%; + color: #495057; + text-align: inherit; +} + +.list-group-item-action:hover, .list-group-item-action:focus { + 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: 0.75rem 1.25rem; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.125); +} + +.list-group-item:first-child { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.list-group-item:last-child { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.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: 0.25rem; + border-top-right-radius: 0; +} + +.list-group-horizontal .list-group-item:last-child { + border-top-right-radius: 0.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: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-sm .list-group-item:last-child { + border-top-right-radius: 0.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: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-md .list-group-item:last-child { + border-top-right-radius: 0.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: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-lg .list-group-item:last-child { + border-top-right-radius: 0.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: 0.25rem; + border-top-right-radius: 0; + } + .list-group-horizontal-xl .list-group-item:last-child { + border-top-right-radius: 0.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 .list-group-item { + border-right-width: 0; + border-left-width: 0; + border-radius: 0; +} + +.list-group-flush .list-group-item:first-child { + border-top-width: 0; +} + +.list-group-flush:last-child .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:hover, .list-group-item-primary.list-group-item-action:focus { + 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:hover, .list-group-item-secondary.list-group-item-action:focus { + 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:hover, .list-group-item-success.list-group-item-action:focus { + 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:hover, .list-group-item-info.list-group-item-action:focus { + 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:hover, .list-group-item-warning.list-group-item-action:focus { + 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:hover, .list-group-item-danger.list-group-item-action:focus { + 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:hover, .list-group-item-light.list-group-item-action:focus { + 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:hover, .list-group-item-dark.list-group-item-action:focus { + 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):hover, .close:not(:disabled):not(.disabled):focus { + opacity: .75; +} + +button.close { + padding: 0; + background-color: transparent; + border: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +a.close.disabled { + pointer-events: none; +} + +.toast { + max-width: 350px; + overflow: hidden; + font-size: 0.875rem; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0; + border-radius: 0.25rem; +} + +.toast:not(:last-child) { + margin-bottom: 0.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: 0.25rem 0.75rem; + color: #6c757d; + background-color: rgba(255, 255, 255, 0.85); + background-clip: padding-box; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.toast-body { + padding: 0.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: 0.5rem; + pointer-events: none; +} + +.modal.fade .modal-dialog { + transition: -webkit-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + transition: transform 0.3s ease-out, -webkit-transform 0.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-header, +.modal-dialog-scrollable .modal-footer { + -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); + 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, 0.2); + border-radius: 0.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: 0.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(0.3rem - 1px); + border-top-right-radius: calc(0.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: 0.75rem; + border-top: 1px solid #dee2e6; + border-bottom-right-radius: calc(0.3rem - 1px); + border-bottom-left-radius: calc(0.3rem - 1px); +} + +.modal-footer > * { + margin: 0.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); + } + .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: 0.875rem; + word-wrap: break-word; + opacity: 0; +} + +.tooltip.show { + opacity: 0.9; +} + +.tooltip .arrow { + position: absolute; + display: block; + width: 0.8rem; + height: 0.4rem; +} + +.tooltip .arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { + padding: 0.4rem 0; +} + +.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { + bottom: 0; +} + +.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { + top: 0; + border-width: 0.4rem 0.4rem 0; + border-top-color: #000; +} + +.bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { + padding: 0 0.4rem; +} + +.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { + left: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { + right: 0; + border-width: 0.4rem 0.4rem 0.4rem 0; + border-right-color: #000; +} + +.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { + padding: 0.4rem 0; +} + +.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { + top: 0; +} + +.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { + bottom: 0; + border-width: 0 0.4rem 0.4rem; + border-bottom-color: #000; +} + +.bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { + padding: 0 0.4rem; +} + +.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { + right: 0; + width: 0.4rem; + height: 0.8rem; +} + +.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { + left: 0; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-left-color: #000; +} + +.tooltip-inner { + max-width: 200px; + padding: 0.25rem 0.5rem; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 0.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: 0.875rem; + word-wrap: break-word; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0.3rem; +} + +.popover .arrow { + position: absolute; + display: block; + width: 1rem; + height: 0.5rem; + margin: 0 0.3rem; +} + +.popover .arrow::before, .popover .arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-popover-top, .bs-popover-auto[x-placement^="top"] { + margin-bottom: 0.5rem; +} + +.bs-popover-top > .arrow, .bs-popover-auto[x-placement^="top"] > .arrow { + bottom: calc(-0.5rem - 1px); +} + +.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^="top"] > .arrow::before { + bottom: 0; + border-width: 0.5rem 0.5rem 0; + border-top-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^="top"] > .arrow::after { + bottom: 1px; + border-width: 0.5rem 0.5rem 0; + border-top-color: #fff; +} + +.bs-popover-right, .bs-popover-auto[x-placement^="right"] { + margin-left: 0.5rem; +} + +.bs-popover-right > .arrow, .bs-popover-auto[x-placement^="right"] > .arrow { + left: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^="right"] > .arrow::before { + left: 0; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^="right"] > .arrow::after { + left: 1px; + border-width: 0.5rem 0.5rem 0.5rem 0; + border-right-color: #fff; +} + +.bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { + margin-top: 0.5rem; +} + +.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^="bottom"] > .arrow { + top: calc(-0.5rem - 1px); +} + +.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^="bottom"] > .arrow::before { + top: 0; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^="bottom"] > .arrow::after { + top: 1px; + border-width: 0 0.5rem 0.5rem 0.5rem; + border-bottom-color: #fff; +} + +.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: 1rem; + margin-left: -0.5rem; + content: ""; + border-bottom: 1px solid #f7f7f7; +} + +.bs-popover-left, .bs-popover-auto[x-placement^="left"] { + margin-right: 0.5rem; +} + +.bs-popover-left > .arrow, .bs-popover-auto[x-placement^="left"] > .arrow { + right: calc(-0.5rem - 1px); + width: 0.5rem; + height: 1rem; + margin: 0.3rem 0; +} + +.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^="left"] > .arrow::before { + right: 0; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^="left"] > .arrow::after { + right: 1px; + border-width: 0.5rem 0 0.5rem 0.5rem; + border-left-color: #fff; +} + +.popover-header { + padding: 0.5rem 0.75rem; + margin-bottom: 0; + font-size: 1rem; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-top-left-radius: calc(0.3rem - 1px); + border-top-right-radius: calc(0.3rem - 1px); +} + +.popover-header:empty { + display: none; +} + +.popover-body { + padding: 0.5rem 0.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 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + -webkit-transform: translateX(100%); + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + -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.active, +.carousel-fade .carousel-item-next.carousel-item-left, +.carousel-fade .carousel-item-prev.carousel-item-right { + 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 0.6s; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-left, + .carousel-fade .active.carousel-item-right { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + 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: 0.5; + transition: opacity 0.15s ease; +} + +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} + +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-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 0.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: 0.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: 0.2em; +} + +@-webkit-keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + } +} + +@keyframes spinner-grow { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 50% { + opacity: 1; + } +} + +.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:hover, a.bg-primary:focus, +button.bg-primary:hover, +button.bg-primary:focus { + background-color: #0062cc !important; +} + +.bg-secondary { + background-color: #6c757d !important; +} + +a.bg-secondary:hover, a.bg-secondary:focus, +button.bg-secondary:hover, +button.bg-secondary:focus { + background-color: #545b62 !important; +} + +.bg-success { + background-color: #28a745 !important; +} + +a.bg-success:hover, a.bg-success:focus, +button.bg-success:hover, +button.bg-success:focus { + background-color: #1e7e34 !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} + +a.bg-info:hover, a.bg-info:focus, +button.bg-info:hover, +button.bg-info:focus { + background-color: #117a8b !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +a.bg-warning:hover, a.bg-warning:focus, +button.bg-warning:hover, +button.bg-warning:focus { + background-color: #d39e00 !important; +} + +.bg-danger { + background-color: #dc3545 !important; +} + +a.bg-danger:hover, a.bg-danger:focus, +button.bg-danger:hover, +button.bg-danger:focus { + background-color: #bd2130 !important; +} + +.bg-light { + background-color: #f8f9fa !important; +} + +a.bg-light:hover, a.bg-light:focus, +button.bg-light:hover, +button.bg-light:focus { + background-color: #dae0e5 !important; +} + +.bg-dark { + background-color: #343a40 !important; +} + +a.bg-dark:hover, a.bg-dark:focus, +button.bg-dark:hover, +button.bg-dark:focus { + 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: 0.2rem !important; +} + +.rounded { + border-radius: 0.25rem !important; +} + +.rounded-top { + border-top-left-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; +} + +.rounded-right { + border-top-right-radius: 0.25rem !important; + border-bottom-right-radius: 0.25rem !important; +} + +.rounded-bottom { + border-bottom-right-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-left { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.rounded-lg { + border-radius: 0.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 iframe, +.embed-responsive embed, +.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; + } +} + +.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 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 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; +} + +.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); +} + +.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: 0.25rem !important; +} + +.mt-1, +.my-1 { + margin-top: 0.25rem !important; +} + +.mr-1, +.mx-1 { + margin-right: 0.25rem !important; +} + +.mb-1, +.my-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1, +.mx-1 { + margin-left: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2, +.my-2 { + margin-top: 0.5rem !important; +} + +.mr-2, +.mx-2 { + margin-right: 0.5rem !important; +} + +.mb-2, +.my-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2, +.mx-2 { + margin-left: 0.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: 0.25rem !important; +} + +.pt-1, +.py-1 { + padding-top: 0.25rem !important; +} + +.pr-1, +.px-1 { + padding-right: 0.25rem !important; +} + +.pb-1, +.py-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1, +.px-1 { + padding-left: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2, +.py-2 { + padding-top: 0.5rem !important; +} + +.pr-2, +.px-2 { + padding-right: 0.5rem !important; +} + +.pb-2, +.py-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2, +.px-2 { + padding-left: 0.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: -0.25rem !important; +} + +.mt-n1, +.my-n1 { + margin-top: -0.25rem !important; +} + +.mr-n1, +.mx-n1 { + margin-right: -0.25rem !important; +} + +.mb-n1, +.my-n1 { + margin-bottom: -0.25rem !important; +} + +.ml-n1, +.mx-n1 { + margin-left: -0.25rem !important; +} + +.m-n2 { + margin: -0.5rem !important; +} + +.mt-n2, +.my-n2 { + margin-top: -0.5rem !important; +} + +.mr-n2, +.mx-n2 { + margin-right: -0.5rem !important; +} + +.mb-n2, +.my-n2 { + margin-bottom: -0.5rem !important; +} + +.ml-n2, +.mx-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-sm-1, + .my-sm-1 { + margin-top: 0.25rem !important; + } + .mr-sm-1, + .mx-sm-1 { + margin-right: 0.25rem !important; + } + .mb-sm-1, + .my-sm-1 { + margin-bottom: 0.25rem !important; + } + .ml-sm-1, + .mx-sm-1 { + margin-left: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .mt-sm-2, + .my-sm-2 { + margin-top: 0.5rem !important; + } + .mr-sm-2, + .mx-sm-2 { + margin-right: 0.5rem !important; + } + .mb-sm-2, + .my-sm-2 { + margin-bottom: 0.5rem !important; + } + .ml-sm-2, + .mx-sm-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-sm-1, + .py-sm-1 { + padding-top: 0.25rem !important; + } + .pr-sm-1, + .px-sm-1 { + padding-right: 0.25rem !important; + } + .pb-sm-1, + .py-sm-1 { + padding-bottom: 0.25rem !important; + } + .pl-sm-1, + .px-sm-1 { + padding-left: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .pt-sm-2, + .py-sm-2 { + padding-top: 0.5rem !important; + } + .pr-sm-2, + .px-sm-2 { + padding-right: 0.5rem !important; + } + .pb-sm-2, + .py-sm-2 { + padding-bottom: 0.5rem !important; + } + .pl-sm-2, + .px-sm-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-sm-n1, + .my-sm-n1 { + margin-top: -0.25rem !important; + } + .mr-sm-n1, + .mx-sm-n1 { + margin-right: -0.25rem !important; + } + .mb-sm-n1, + .my-sm-n1 { + margin-bottom: -0.25rem !important; + } + .ml-sm-n1, + .mx-sm-n1 { + margin-left: -0.25rem !important; + } + .m-sm-n2 { + margin: -0.5rem !important; + } + .mt-sm-n2, + .my-sm-n2 { + margin-top: -0.5rem !important; + } + .mr-sm-n2, + .mx-sm-n2 { + margin-right: -0.5rem !important; + } + .mb-sm-n2, + .my-sm-n2 { + margin-bottom: -0.5rem !important; + } + .ml-sm-n2, + .mx-sm-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-md-1, + .my-md-1 { + margin-top: 0.25rem !important; + } + .mr-md-1, + .mx-md-1 { + margin-right: 0.25rem !important; + } + .mb-md-1, + .my-md-1 { + margin-bottom: 0.25rem !important; + } + .ml-md-1, + .mx-md-1 { + margin-left: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .mt-md-2, + .my-md-2 { + margin-top: 0.5rem !important; + } + .mr-md-2, + .mx-md-2 { + margin-right: 0.5rem !important; + } + .mb-md-2, + .my-md-2 { + margin-bottom: 0.5rem !important; + } + .ml-md-2, + .mx-md-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-md-1, + .py-md-1 { + padding-top: 0.25rem !important; + } + .pr-md-1, + .px-md-1 { + padding-right: 0.25rem !important; + } + .pb-md-1, + .py-md-1 { + padding-bottom: 0.25rem !important; + } + .pl-md-1, + .px-md-1 { + padding-left: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .pt-md-2, + .py-md-2 { + padding-top: 0.5rem !important; + } + .pr-md-2, + .px-md-2 { + padding-right: 0.5rem !important; + } + .pb-md-2, + .py-md-2 { + padding-bottom: 0.5rem !important; + } + .pl-md-2, + .px-md-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-md-n1, + .my-md-n1 { + margin-top: -0.25rem !important; + } + .mr-md-n1, + .mx-md-n1 { + margin-right: -0.25rem !important; + } + .mb-md-n1, + .my-md-n1 { + margin-bottom: -0.25rem !important; + } + .ml-md-n1, + .mx-md-n1 { + margin-left: -0.25rem !important; + } + .m-md-n2 { + margin: -0.5rem !important; + } + .mt-md-n2, + .my-md-n2 { + margin-top: -0.5rem !important; + } + .mr-md-n2, + .mx-md-n2 { + margin-right: -0.5rem !important; + } + .mb-md-n2, + .my-md-n2 { + margin-bottom: -0.5rem !important; + } + .ml-md-n2, + .mx-md-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-lg-1, + .my-lg-1 { + margin-top: 0.25rem !important; + } + .mr-lg-1, + .mx-lg-1 { + margin-right: 0.25rem !important; + } + .mb-lg-1, + .my-lg-1 { + margin-bottom: 0.25rem !important; + } + .ml-lg-1, + .mx-lg-1 { + margin-left: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .mt-lg-2, + .my-lg-2 { + margin-top: 0.5rem !important; + } + .mr-lg-2, + .mx-lg-2 { + margin-right: 0.5rem !important; + } + .mb-lg-2, + .my-lg-2 { + margin-bottom: 0.5rem !important; + } + .ml-lg-2, + .mx-lg-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-lg-1, + .py-lg-1 { + padding-top: 0.25rem !important; + } + .pr-lg-1, + .px-lg-1 { + padding-right: 0.25rem !important; + } + .pb-lg-1, + .py-lg-1 { + padding-bottom: 0.25rem !important; + } + .pl-lg-1, + .px-lg-1 { + padding-left: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .pt-lg-2, + .py-lg-2 { + padding-top: 0.5rem !important; + } + .pr-lg-2, + .px-lg-2 { + padding-right: 0.5rem !important; + } + .pb-lg-2, + .py-lg-2 { + padding-bottom: 0.5rem !important; + } + .pl-lg-2, + .px-lg-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-lg-n1, + .my-lg-n1 { + margin-top: -0.25rem !important; + } + .mr-lg-n1, + .mx-lg-n1 { + margin-right: -0.25rem !important; + } + .mb-lg-n1, + .my-lg-n1 { + margin-bottom: -0.25rem !important; + } + .ml-lg-n1, + .mx-lg-n1 { + margin-left: -0.25rem !important; + } + .m-lg-n2 { + margin: -0.5rem !important; + } + .mt-lg-n2, + .my-lg-n2 { + margin-top: -0.5rem !important; + } + .mr-lg-n2, + .mx-lg-n2 { + margin-right: -0.5rem !important; + } + .mb-lg-n2, + .my-lg-n2 { + margin-bottom: -0.5rem !important; + } + .ml-lg-n2, + .mx-lg-n2 { + margin-left: -0.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: 0.25rem !important; + } + .mt-xl-1, + .my-xl-1 { + margin-top: 0.25rem !important; + } + .mr-xl-1, + .mx-xl-1 { + margin-right: 0.25rem !important; + } + .mb-xl-1, + .my-xl-1 { + margin-bottom: 0.25rem !important; + } + .ml-xl-1, + .mx-xl-1 { + margin-left: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .mt-xl-2, + .my-xl-2 { + margin-top: 0.5rem !important; + } + .mr-xl-2, + .mx-xl-2 { + margin-right: 0.5rem !important; + } + .mb-xl-2, + .my-xl-2 { + margin-bottom: 0.5rem !important; + } + .ml-xl-2, + .mx-xl-2 { + margin-left: 0.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: 0.25rem !important; + } + .pt-xl-1, + .py-xl-1 { + padding-top: 0.25rem !important; + } + .pr-xl-1, + .px-xl-1 { + padding-right: 0.25rem !important; + } + .pb-xl-1, + .py-xl-1 { + padding-bottom: 0.25rem !important; + } + .pl-xl-1, + .px-xl-1 { + padding-left: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .pt-xl-2, + .py-xl-2 { + padding-top: 0.5rem !important; + } + .pr-xl-2, + .px-xl-2 { + padding-right: 0.5rem !important; + } + .pb-xl-2, + .py-xl-2 { + padding-bottom: 0.5rem !important; + } + .pl-xl-2, + .px-xl-2 { + padding-left: 0.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: -0.25rem !important; + } + .mt-xl-n1, + .my-xl-n1 { + margin-top: -0.25rem !important; + } + .mr-xl-n1, + .mx-xl-n1 { + margin-right: -0.25rem !important; + } + .mb-xl-n1, + .my-xl-n1 { + margin-bottom: -0.25rem !important; + } + .ml-xl-n1, + .mx-xl-n1 { + margin-left: -0.25rem !important; + } + .m-xl-n2 { + margin: -0.5rem !important; + } + .mt-xl-n2, + .my-xl-n2 { + margin-top: -0.5rem !important; + } + .mr-xl-n2, + .mx-xl-n2 { + margin-right: -0.5rem !important; + } + .mb-xl-n2, + .my-xl-n2 { + margin-bottom: -0.5rem !important; + } + .ml-xl-n2, + .mx-xl-n2 { + margin-left: -0.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; + } +} + +.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:hover, a.text-primary:focus { + color: #0056b3 !important; +} + +.text-secondary { + color: #6c757d !important; +} + +a.text-secondary:hover, a.text-secondary:focus { + color: #494f54 !important; +} + +.text-success { + color: #28a745 !important; +} + +a.text-success:hover, a.text-success:focus { + color: #19692c !important; +} + +.text-info { + color: #17a2b8 !important; +} + +a.text-info:hover, a.text-info:focus { + color: #0f6674 !important; +} + +.text-warning { + color: #ffc107 !important; +} + +a.text-warning:hover, a.text-warning:focus { + color: #ba8b00 !important; +} + +.text-danger { + color: #dc3545 !important; +} + +a.text-danger:hover, a.text-danger:focus { + color: #a71d2a !important; +} + +.text-light { + color: #f8f9fa !important; +} + +a.text-light:hover, a.text-light:focus { + color: #cbd3da !important; +} + +.text-dark { + color: #343a40 !important; +} + +a.text-dark:hover, a.text-dark:focus { + color: #121416 !important; +} + +.text-body { + color: #212529 !important; +} + +.text-muted { + color: #6c757d !important; +} + +.text-black-50 { + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + color: rgba(255, 255, 255, 0.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-break: break-word !important; + overflow-wrap: break-word !important; +} + +.text-reset { + color: inherit !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +@media print { + *, + *::before, + *::after { + 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; + } + pre, + blockquote { + border: 1px solid #adb5bd; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + p, + h2, + h3 { + 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 th, + .table-bordered td { + border: 1px solid #dee2e6 !important; + } + .table-dark { + color: inherit; + } + .table-dark th, + .table-dark td, + .table-dark thead th, + .table-dark tbody + tbody { + border-color: #dee2e6; + } + .table .thead-dark th { + color: inherit; + border-color: #dee2e6; + } +} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/js/bootstrap.bundle.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/js/bootstrap.bundle.js new file mode 100644 index 0000000000..534452210e --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/js/bootstrap.bundle.js @@ -0,0 +1,7134 @@ +/*! + * Bootstrap v4.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery')) : + typeof define === 'function' && define.amd ? define(['exports', 'jquery'], factory) : + (global = global || self, factory(global.bootstrap = {}, global.jQuery)); +}(this, (function (exports, $) { 'use strict'; + + $ = $ && $.hasOwnProperty('default') ? $['default'] : $; + + 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); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + keys.push.apply(keys, symbols); + } + + return keys; + } + + function _objectSpread2(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + + if (i % 2) { + ownKeys(Object(source), true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(Object(source)).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + + return target; + } + + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; + } + + /** + * -------------------------------------------------------------------------- + * Bootstrap (v4.4.1): util.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + /** + * ------------------------------------------------------------------------ + * Private TransitionEnd Helpers + * ------------------------------------------------------------------------ + */ + + var TRANSITION_END = 'transitionend'; + var MAX_UID = 1000000; + var MILLISECONDS_MULTIPLIER = 1000; // Shoutout AngusCroll (https://goo.gl/pxwQGp) + + function toType(obj) { + return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase(); + } + + function getSpecialTransitionEndEvent() { + return { + bindType: TRANSITION_END, + delegateType: TRANSITION_END, + handle: function handle(event) { + if ($(event.target).is(this)) { + return event.handleObj.handler.apply(this, arguments); // eslint-disable-line prefer-rest-params + } + + return undefined; // eslint-disable-line no-undefined + } + }; + } + + function transitionEndEmulator(duration) { + var _this = this; + + var called = false; + $(this).one(Util.TRANSITION_END, function () { + called = true; + }); + setTimeout(function () { + if (!called) { + Util.triggerTransitionEnd(_this); + } + }, duration); + return this; + } + + function setTransitionEndSupport() { + $.fn.emulateTransitionEnd = transitionEndEmulator; + $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent(); + } + /** + * -------------------------------------------------------------------------- + * Public Util Api + * -------------------------------------------------------------------------- + */ + + + var Util = { + TRANSITION_END: 'bsTransitionEnd', + getUID: function getUID(prefix) { + do { + // eslint-disable-next-line no-bitwise + prefix += ~~(Math.random() * MAX_UID); // "~~" acts like a faster Math.floor() here + } while (document.getElementById(prefix)); + + return prefix; + }, + getSelectorFromElement: function getSelectorFromElement(element) { + var selector = element.getAttribute('data-target'); + + if (!selector || selector === '#') { + var hrefAttr = element.getAttribute('href'); + selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''; + } + + try { + return document.querySelector(selector) ? selector : null; + } catch (err) { + return null; + } + }, + getTransitionDurationFromElement: function getTransitionDurationFromElement(element) { + if (!element) { + return 0; + } // Get transition-duration of the element + + + var transitionDuration = $(element).css('transition-duration'); + var transitionDelay = $(element).css('transition-delay'); + var floatTransitionDuration = parseFloat(transitionDuration); + var floatTransitionDelay = parseFloat(transitionDelay); // Return 0 if element or transition duration is not found + + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } // If multiple durations are defined, take the first + + + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; + }, + reflow: function reflow(element) { + return element.offsetHeight; + }, + triggerTransitionEnd: function triggerTransitionEnd(element) { + $(element).trigger(TRANSITION_END); + }, + // TODO: Remove in v5 + supportsTransitionEnd: function supportsTransitionEnd() { + return Boolean(TRANSITION_END); + }, + isElement: function isElement(obj) { + return (obj[0] || obj).nodeType; + }, + typeCheckConfig: function typeCheckConfig(componentName, config, configTypes) { + for (var property in configTypes) { + if (Object.prototype.hasOwnProperty.call(configTypes, property)) { + var expectedTypes = configTypes[property]; + var value = config[property]; + var valueType = value && Util.isElement(value) ? 'element' : toType(value); + + if (!new RegExp(expectedTypes).test(valueType)) { + throw new Error(componentName.toUpperCase() + ": " + ("Option \"" + property + "\" provided type \"" + valueType + "\" ") + ("but expected type \"" + expectedTypes + "\".")); + } + } + } + }, + findShadowRoot: function findShadowRoot(element) { + if (!document.documentElement.attachShadow) { + return null; + } // Can find the shadow root otherwise it'll return the document + + + if (typeof element.getRootNode === 'function') { + var root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + + if (element instanceof ShadowRoot) { + return element; + } // when we don't find a shadow root + + + if (!element.parentNode) { + return null; + } + + return Util.findShadowRoot(element.parentNode); + }, + jQueryDetection: function jQueryDetection() { + if (typeof $ === 'undefined') { + throw new TypeError('Bootstrap\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\'s JavaScript.'); + } + + var version = $.fn.jquery.split(' ')[0].split('.'); + var minMajor = 1; + var ltMajor = 2; + var minMinor = 9; + var minPatch = 1; + var maxMajor = 4; + + if (version[0] < ltMajor && version[1] < minMinor || version[0] === minMajor && version[1] === minMinor && version[2] < minPatch || version[0] >= maxMajor) { + throw new Error('Bootstrap\'s JavaScript requires at least jQuery v1.9.1 but less than v4.0.0'); + } + } + }; + Util.jQueryDetection(); + setTransitionEndSupport(); + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME = 'alert'; + var VERSION = '4.4.1'; + var DATA_KEY = 'bs.alert'; + var EVENT_KEY = "." + DATA_KEY; + var DATA_API_KEY = '.data-api'; + var JQUERY_NO_CONFLICT = $.fn[NAME]; + var Selector = { + DISMISS: '[data-dismiss="alert"]' + }; + var Event = { + CLOSE: "close" + EVENT_KEY, + CLOSED: "closed" + EVENT_KEY, + CLICK_DATA_API: "click" + EVENT_KEY + DATA_API_KEY + }; + var ClassName = { + ALERT: 'alert', + FADE: 'fade', + SHOW: 'show' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Alert = + /*#__PURE__*/ + function () { + function Alert(element) { + this._element = element; + } // Getters + + + var _proto = Alert.prototype; + + // Public + _proto.close = function close(element) { + var rootElement = this._element; + + if (element) { + rootElement = this._getRootElement(element); + } + + var customEvent = this._triggerCloseEvent(rootElement); + + if (customEvent.isDefaultPrevented()) { + return; + } + + this._removeElement(rootElement); + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY); + this._element = null; + } // Private + ; + + _proto._getRootElement = function _getRootElement(element) { + var selector = Util.getSelectorFromElement(element); + var parent = false; + + if (selector) { + parent = document.querySelector(selector); + } + + if (!parent) { + parent = $(element).closest("." + ClassName.ALERT)[0]; + } + + return parent; + }; + + _proto._triggerCloseEvent = function _triggerCloseEvent(element) { + var closeEvent = $.Event(Event.CLOSE); + $(element).trigger(closeEvent); + return closeEvent; + }; + + _proto._removeElement = function _removeElement(element) { + var _this = this; + + $(element).removeClass(ClassName.SHOW); + + if (!$(element).hasClass(ClassName.FADE)) { + this._destroyElement(element); + + return; + } + + var transitionDuration = Util.getTransitionDurationFromElement(element); + $(element).one(Util.TRANSITION_END, function (event) { + return _this._destroyElement(element, event); + }).emulateTransitionEnd(transitionDuration); + }; + + _proto._destroyElement = function _destroyElement(element) { + $(element).detach().trigger(Event.CLOSED).remove(); + } // Static + ; + + Alert._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var $element = $(this); + var data = $element.data(DATA_KEY); + + if (!data) { + data = new Alert(this); + $element.data(DATA_KEY, data); + } + + if (config === 'close') { + data[config](this); + } + }); + }; + + Alert._handleDismiss = function _handleDismiss(alertInstance) { + return function (event) { + if (event) { + event.preventDefault(); + } + + alertInstance.close(this); + }; + }; + + _createClass(Alert, null, [{ + key: "VERSION", + get: function get() { + return VERSION; + } + }]); + + return Alert; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME] = Alert._jQueryInterface; + $.fn[NAME].Constructor = Alert; + + $.fn[NAME].noConflict = function () { + $.fn[NAME] = JQUERY_NO_CONFLICT; + return Alert._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$1 = 'button'; + var VERSION$1 = '4.4.1'; + var DATA_KEY$1 = 'bs.button'; + var EVENT_KEY$1 = "." + DATA_KEY$1; + var DATA_API_KEY$1 = '.data-api'; + var JQUERY_NO_CONFLICT$1 = $.fn[NAME$1]; + var ClassName$1 = { + ACTIVE: 'active', + BUTTON: 'btn', + FOCUS: 'focus' + }; + var Selector$1 = { + DATA_TOGGLE_CARROT: '[data-toggle^="button"]', + DATA_TOGGLES: '[data-toggle="buttons"]', + DATA_TOGGLE: '[data-toggle="button"]', + DATA_TOGGLES_BUTTONS: '[data-toggle="buttons"] .btn', + INPUT: 'input:not([type="hidden"])', + ACTIVE: '.active', + BUTTON: '.btn' + }; + var Event$1 = { + CLICK_DATA_API: "click" + EVENT_KEY$1 + DATA_API_KEY$1, + FOCUS_BLUR_DATA_API: "focus" + EVENT_KEY$1 + DATA_API_KEY$1 + " " + ("blur" + EVENT_KEY$1 + DATA_API_KEY$1), + LOAD_DATA_API: "load" + EVENT_KEY$1 + DATA_API_KEY$1 + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Button = + /*#__PURE__*/ + function () { + function Button(element) { + this._element = element; + } // Getters + + + var _proto = Button.prototype; + + // Public + _proto.toggle = function toggle() { + var triggerChangeEvent = true; + var addAriaPressed = true; + var rootElement = $(this._element).closest(Selector$1.DATA_TOGGLES)[0]; + + if (rootElement) { + var input = this._element.querySelector(Selector$1.INPUT); + + if (input) { + if (input.type === 'radio') { + if (input.checked && this._element.classList.contains(ClassName$1.ACTIVE)) { + triggerChangeEvent = false; + } else { + var activeElement = rootElement.querySelector(Selector$1.ACTIVE); + + if (activeElement) { + $(activeElement).removeClass(ClassName$1.ACTIVE); + } + } + } else if (input.type === 'checkbox') { + if (this._element.tagName === 'LABEL' && input.checked === this._element.classList.contains(ClassName$1.ACTIVE)) { + triggerChangeEvent = false; + } + } else { + // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input + triggerChangeEvent = false; + } + + if (triggerChangeEvent) { + input.checked = !this._element.classList.contains(ClassName$1.ACTIVE); + $(input).trigger('change'); + } + + input.focus(); + addAriaPressed = false; + } + } + + if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) { + if (addAriaPressed) { + this._element.setAttribute('aria-pressed', !this._element.classList.contains(ClassName$1.ACTIVE)); + } + + if (triggerChangeEvent) { + $(this._element).toggleClass(ClassName$1.ACTIVE); + } + } + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$1); + this._element = null; + } // Static + ; + + Button._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$1); + + if (!data) { + data = new Button(this); + $(this).data(DATA_KEY$1, data); + } + + if (config === 'toggle') { + data[config](); + } + }); + }; + + _createClass(Button, null, [{ + key: "VERSION", + get: function get() { + return VERSION$1; + } + }]); + + return Button; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$1.CLICK_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) { + var button = event.target; + + if (!$(button).hasClass(ClassName$1.BUTTON)) { + button = $(button).closest(Selector$1.BUTTON)[0]; + } + + if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) { + event.preventDefault(); // work around Firefox bug #1540995 + } else { + var inputBtn = button.querySelector(Selector$1.INPUT); + + if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) { + event.preventDefault(); // work around Firefox bug #1540995 + + return; + } + + Button._jQueryInterface.call($(button), 'toggle'); + } + }).on(Event$1.FOCUS_BLUR_DATA_API, Selector$1.DATA_TOGGLE_CARROT, function (event) { + var button = $(event.target).closest(Selector$1.BUTTON)[0]; + $(button).toggleClass(ClassName$1.FOCUS, /^focus(in)?$/.test(event.type)); + }); + $(window).on(Event$1.LOAD_DATA_API, function () { + // ensure correct active class is set to match the controls' actual values/states + // find all checkboxes/readio buttons inside data-toggle groups + var buttons = [].slice.call(document.querySelectorAll(Selector$1.DATA_TOGGLES_BUTTONS)); + + for (var i = 0, len = buttons.length; i < len; i++) { + var button = buttons[i]; + var input = button.querySelector(Selector$1.INPUT); + + if (input.checked || input.hasAttribute('checked')) { + button.classList.add(ClassName$1.ACTIVE); + } else { + button.classList.remove(ClassName$1.ACTIVE); + } + } // find all button toggles + + + buttons = [].slice.call(document.querySelectorAll(Selector$1.DATA_TOGGLE)); + + for (var _i = 0, _len = buttons.length; _i < _len; _i++) { + var _button = buttons[_i]; + + if (_button.getAttribute('aria-pressed') === 'true') { + _button.classList.add(ClassName$1.ACTIVE); + } else { + _button.classList.remove(ClassName$1.ACTIVE); + } + } + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$1] = Button._jQueryInterface; + $.fn[NAME$1].Constructor = Button; + + $.fn[NAME$1].noConflict = function () { + $.fn[NAME$1] = JQUERY_NO_CONFLICT$1; + return Button._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$2 = 'carousel'; + var VERSION$2 = '4.4.1'; + var DATA_KEY$2 = 'bs.carousel'; + var EVENT_KEY$2 = "." + DATA_KEY$2; + var DATA_API_KEY$2 = '.data-api'; + var JQUERY_NO_CONFLICT$2 = $.fn[NAME$2]; + var ARROW_LEFT_KEYCODE = 37; // KeyboardEvent.which value for left arrow key + + var ARROW_RIGHT_KEYCODE = 39; // KeyboardEvent.which value for right arrow key + + var TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch + + var SWIPE_THRESHOLD = 40; + var Default = { + interval: 5000, + keyboard: true, + slide: false, + pause: 'hover', + wrap: true, + touch: true + }; + var DefaultType = { + interval: '(number|boolean)', + keyboard: 'boolean', + slide: '(boolean|string)', + pause: '(string|boolean)', + wrap: 'boolean', + touch: 'boolean' + }; + var Direction = { + NEXT: 'next', + PREV: 'prev', + LEFT: 'left', + RIGHT: 'right' + }; + var Event$2 = { + SLIDE: "slide" + EVENT_KEY$2, + SLID: "slid" + EVENT_KEY$2, + KEYDOWN: "keydown" + EVENT_KEY$2, + MOUSEENTER: "mouseenter" + EVENT_KEY$2, + MOUSELEAVE: "mouseleave" + EVENT_KEY$2, + TOUCHSTART: "touchstart" + EVENT_KEY$2, + TOUCHMOVE: "touchmove" + EVENT_KEY$2, + TOUCHEND: "touchend" + EVENT_KEY$2, + POINTERDOWN: "pointerdown" + EVENT_KEY$2, + POINTERUP: "pointerup" + EVENT_KEY$2, + DRAG_START: "dragstart" + EVENT_KEY$2, + LOAD_DATA_API: "load" + EVENT_KEY$2 + DATA_API_KEY$2, + CLICK_DATA_API: "click" + EVENT_KEY$2 + DATA_API_KEY$2 + }; + var ClassName$2 = { + CAROUSEL: 'carousel', + ACTIVE: 'active', + SLIDE: 'slide', + RIGHT: 'carousel-item-right', + LEFT: 'carousel-item-left', + NEXT: 'carousel-item-next', + PREV: 'carousel-item-prev', + ITEM: 'carousel-item', + POINTER_EVENT: 'pointer-event' + }; + var Selector$2 = { + ACTIVE: '.active', + ACTIVE_ITEM: '.active.carousel-item', + ITEM: '.carousel-item', + ITEM_IMG: '.carousel-item img', + NEXT_PREV: '.carousel-item-next, .carousel-item-prev', + INDICATORS: '.carousel-indicators', + DATA_SLIDE: '[data-slide], [data-slide-to]', + DATA_RIDE: '[data-ride="carousel"]' + }; + var PointerType = { + TOUCH: 'touch', + PEN: 'pen' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Carousel = + /*#__PURE__*/ + function () { + function Carousel(element, config) { + this._items = null; + this._interval = null; + this._activeElement = null; + this._isPaused = false; + this._isSliding = false; + this.touchTimeout = null; + this.touchStartX = 0; + this.touchDeltaX = 0; + this._config = this._getConfig(config); + this._element = element; + this._indicatorsElement = this._element.querySelector(Selector$2.INDICATORS); + this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent); + + this._addEventListeners(); + } // Getters + + + var _proto = Carousel.prototype; + + // Public + _proto.next = function next() { + if (!this._isSliding) { + this._slide(Direction.NEXT); + } + }; + + _proto.nextWhenVisible = function nextWhenVisible() { + // Don't call next when the page isn't visible + // or the carousel or its parent isn't visible + if (!document.hidden && $(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden') { + this.next(); + } + }; + + _proto.prev = function prev() { + if (!this._isSliding) { + this._slide(Direction.PREV); + } + }; + + _proto.pause = function pause(event) { + if (!event) { + this._isPaused = true; + } + + if (this._element.querySelector(Selector$2.NEXT_PREV)) { + Util.triggerTransitionEnd(this._element); + this.cycle(true); + } + + clearInterval(this._interval); + this._interval = null; + }; + + _proto.cycle = function cycle(event) { + if (!event) { + this._isPaused = false; + } + + if (this._interval) { + clearInterval(this._interval); + this._interval = null; + } + + if (this._config.interval && !this._isPaused) { + this._interval = setInterval((document.visibilityState ? this.nextWhenVisible : this.next).bind(this), this._config.interval); + } + }; + + _proto.to = function to(index) { + var _this = this; + + this._activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM); + + var activeIndex = this._getItemIndex(this._activeElement); + + if (index > this._items.length - 1 || index < 0) { + return; + } + + if (this._isSliding) { + $(this._element).one(Event$2.SLID, function () { + return _this.to(index); + }); + return; + } + + if (activeIndex === index) { + this.pause(); + this.cycle(); + return; + } + + var direction = index > activeIndex ? Direction.NEXT : Direction.PREV; + + this._slide(direction, this._items[index]); + }; + + _proto.dispose = function dispose() { + $(this._element).off(EVENT_KEY$2); + $.removeData(this._element, DATA_KEY$2); + this._items = null; + this._config = null; + this._element = null; + this._interval = null; + this._isPaused = null; + this._isSliding = null; + this._activeElement = null; + this._indicatorsElement = null; + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default, {}, config); + Util.typeCheckConfig(NAME$2, config, DefaultType); + return config; + }; + + _proto._handleSwipe = function _handleSwipe() { + var absDeltax = Math.abs(this.touchDeltaX); + + if (absDeltax <= SWIPE_THRESHOLD) { + return; + } + + var direction = absDeltax / this.touchDeltaX; + this.touchDeltaX = 0; // swipe left + + if (direction > 0) { + this.prev(); + } // swipe right + + + if (direction < 0) { + this.next(); + } + }; + + _proto._addEventListeners = function _addEventListeners() { + var _this2 = this; + + if (this._config.keyboard) { + $(this._element).on(Event$2.KEYDOWN, function (event) { + return _this2._keydown(event); + }); + } + + if (this._config.pause === 'hover') { + $(this._element).on(Event$2.MOUSEENTER, function (event) { + return _this2.pause(event); + }).on(Event$2.MOUSELEAVE, function (event) { + return _this2.cycle(event); + }); + } + + if (this._config.touch) { + this._addTouchEventListeners(); + } + }; + + _proto._addTouchEventListeners = function _addTouchEventListeners() { + var _this3 = this; + + if (!this._touchSupported) { + return; + } + + var start = function start(event) { + if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) { + _this3.touchStartX = event.originalEvent.clientX; + } else if (!_this3._pointerEvent) { + _this3.touchStartX = event.originalEvent.touches[0].clientX; + } + }; + + var move = function move(event) { + // ensure swiping with one touch and not pinching + if (event.originalEvent.touches && event.originalEvent.touches.length > 1) { + _this3.touchDeltaX = 0; + } else { + _this3.touchDeltaX = event.originalEvent.touches[0].clientX - _this3.touchStartX; + } + }; + + var end = function end(event) { + if (_this3._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) { + _this3.touchDeltaX = event.originalEvent.clientX - _this3.touchStartX; + } + + _this3._handleSwipe(); + + if (_this3._config.pause === 'hover') { + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + _this3.pause(); + + if (_this3.touchTimeout) { + clearTimeout(_this3.touchTimeout); + } + + _this3.touchTimeout = setTimeout(function (event) { + return _this3.cycle(event); + }, TOUCHEVENT_COMPAT_WAIT + _this3._config.interval); + } + }; + + $(this._element.querySelectorAll(Selector$2.ITEM_IMG)).on(Event$2.DRAG_START, function (e) { + return e.preventDefault(); + }); + + if (this._pointerEvent) { + $(this._element).on(Event$2.POINTERDOWN, function (event) { + return start(event); + }); + $(this._element).on(Event$2.POINTERUP, function (event) { + return end(event); + }); + + this._element.classList.add(ClassName$2.POINTER_EVENT); + } else { + $(this._element).on(Event$2.TOUCHSTART, function (event) { + return start(event); + }); + $(this._element).on(Event$2.TOUCHMOVE, function (event) { + return move(event); + }); + $(this._element).on(Event$2.TOUCHEND, function (event) { + return end(event); + }); + } + }; + + _proto._keydown = function _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + + switch (event.which) { + case ARROW_LEFT_KEYCODE: + event.preventDefault(); + this.prev(); + break; + + case ARROW_RIGHT_KEYCODE: + event.preventDefault(); + this.next(); + break; + } + }; + + _proto._getItemIndex = function _getItemIndex(element) { + this._items = element && element.parentNode ? [].slice.call(element.parentNode.querySelectorAll(Selector$2.ITEM)) : []; + return this._items.indexOf(element); + }; + + _proto._getItemByDirection = function _getItemByDirection(direction, activeElement) { + var isNextDirection = direction === Direction.NEXT; + var isPrevDirection = direction === Direction.PREV; + + var activeIndex = this._getItemIndex(activeElement); + + var lastItemIndex = this._items.length - 1; + var isGoingToWrap = isPrevDirection && activeIndex === 0 || isNextDirection && activeIndex === lastItemIndex; + + if (isGoingToWrap && !this._config.wrap) { + return activeElement; + } + + var delta = direction === Direction.PREV ? -1 : 1; + var itemIndex = (activeIndex + delta) % this._items.length; + return itemIndex === -1 ? this._items[this._items.length - 1] : this._items[itemIndex]; + }; + + _proto._triggerSlideEvent = function _triggerSlideEvent(relatedTarget, eventDirectionName) { + var targetIndex = this._getItemIndex(relatedTarget); + + var fromIndex = this._getItemIndex(this._element.querySelector(Selector$2.ACTIVE_ITEM)); + + var slideEvent = $.Event(Event$2.SLIDE, { + relatedTarget: relatedTarget, + direction: eventDirectionName, + from: fromIndex, + to: targetIndex + }); + $(this._element).trigger(slideEvent); + return slideEvent; + }; + + _proto._setActiveIndicatorElement = function _setActiveIndicatorElement(element) { + if (this._indicatorsElement) { + var indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector$2.ACTIVE)); + $(indicators).removeClass(ClassName$2.ACTIVE); + + var nextIndicator = this._indicatorsElement.children[this._getItemIndex(element)]; + + if (nextIndicator) { + $(nextIndicator).addClass(ClassName$2.ACTIVE); + } + } + }; + + _proto._slide = function _slide(direction, element) { + var _this4 = this; + + var activeElement = this._element.querySelector(Selector$2.ACTIVE_ITEM); + + var activeElementIndex = this._getItemIndex(activeElement); + + var nextElement = element || activeElement && this._getItemByDirection(direction, activeElement); + + var nextElementIndex = this._getItemIndex(nextElement); + + var isCycling = Boolean(this._interval); + var directionalClassName; + var orderClassName; + var eventDirectionName; + + if (direction === Direction.NEXT) { + directionalClassName = ClassName$2.LEFT; + orderClassName = ClassName$2.NEXT; + eventDirectionName = Direction.LEFT; + } else { + directionalClassName = ClassName$2.RIGHT; + orderClassName = ClassName$2.PREV; + eventDirectionName = Direction.RIGHT; + } + + if (nextElement && $(nextElement).hasClass(ClassName$2.ACTIVE)) { + this._isSliding = false; + return; + } + + var slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName); + + if (slideEvent.isDefaultPrevented()) { + return; + } + + if (!activeElement || !nextElement) { + // Some weirdness is happening, so we bail + return; + } + + this._isSliding = true; + + if (isCycling) { + this.pause(); + } + + this._setActiveIndicatorElement(nextElement); + + var slidEvent = $.Event(Event$2.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }); + + if ($(this._element).hasClass(ClassName$2.SLIDE)) { + $(nextElement).addClass(orderClassName); + Util.reflow(nextElement); + $(activeElement).addClass(directionalClassName); + $(nextElement).addClass(directionalClassName); + var nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10); + + if (nextElementInterval) { + this._config.defaultInterval = this._config.defaultInterval || this._config.interval; + this._config.interval = nextElementInterval; + } else { + this._config.interval = this._config.defaultInterval || this._config.interval; + } + + var transitionDuration = Util.getTransitionDurationFromElement(activeElement); + $(activeElement).one(Util.TRANSITION_END, function () { + $(nextElement).removeClass(directionalClassName + " " + orderClassName).addClass(ClassName$2.ACTIVE); + $(activeElement).removeClass(ClassName$2.ACTIVE + " " + orderClassName + " " + directionalClassName); + _this4._isSliding = false; + setTimeout(function () { + return $(_this4._element).trigger(slidEvent); + }, 0); + }).emulateTransitionEnd(transitionDuration); + } else { + $(activeElement).removeClass(ClassName$2.ACTIVE); + $(nextElement).addClass(ClassName$2.ACTIVE); + this._isSliding = false; + $(this._element).trigger(slidEvent); + } + + if (isCycling) { + this.cycle(); + } + } // Static + ; + + Carousel._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$2); + + var _config = _objectSpread2({}, Default, {}, $(this).data()); + + if (typeof config === 'object') { + _config = _objectSpread2({}, _config, {}, config); + } + + var action = typeof config === 'string' ? config : _config.slide; + + if (!data) { + data = new Carousel(this, _config); + $(this).data(DATA_KEY$2, data); + } + + if (typeof config === 'number') { + data.to(config); + } else if (typeof action === 'string') { + if (typeof data[action] === 'undefined') { + throw new TypeError("No method named \"" + action + "\""); + } + + data[action](); + } else if (_config.interval && _config.ride) { + data.pause(); + data.cycle(); + } + }); + }; + + Carousel._dataApiClickHandler = function _dataApiClickHandler(event) { + var selector = Util.getSelectorFromElement(this); + + if (!selector) { + return; + } + + var target = $(selector)[0]; + + if (!target || !$(target).hasClass(ClassName$2.CAROUSEL)) { + return; + } + + var config = _objectSpread2({}, $(target).data(), {}, $(this).data()); + + var slideIndex = this.getAttribute('data-slide-to'); + + if (slideIndex) { + config.interval = false; + } + + Carousel._jQueryInterface.call($(target), config); + + if (slideIndex) { + $(target).data(DATA_KEY$2).to(slideIndex); + } + + event.preventDefault(); + }; + + _createClass(Carousel, null, [{ + key: "VERSION", + get: function get() { + return VERSION$2; + } + }, { + key: "Default", + get: function get() { + return Default; + } + }]); + + return Carousel; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$2.CLICK_DATA_API, Selector$2.DATA_SLIDE, Carousel._dataApiClickHandler); + $(window).on(Event$2.LOAD_DATA_API, function () { + var carousels = [].slice.call(document.querySelectorAll(Selector$2.DATA_RIDE)); + + for (var i = 0, len = carousels.length; i < len; i++) { + var $carousel = $(carousels[i]); + + Carousel._jQueryInterface.call($carousel, $carousel.data()); + } + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$2] = Carousel._jQueryInterface; + $.fn[NAME$2].Constructor = Carousel; + + $.fn[NAME$2].noConflict = function () { + $.fn[NAME$2] = JQUERY_NO_CONFLICT$2; + return Carousel._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$3 = 'collapse'; + var VERSION$3 = '4.4.1'; + var DATA_KEY$3 = 'bs.collapse'; + var EVENT_KEY$3 = "." + DATA_KEY$3; + var DATA_API_KEY$3 = '.data-api'; + var JQUERY_NO_CONFLICT$3 = $.fn[NAME$3]; + var Default$1 = { + toggle: true, + parent: '' + }; + var DefaultType$1 = { + toggle: 'boolean', + parent: '(string|element)' + }; + var Event$3 = { + SHOW: "show" + EVENT_KEY$3, + SHOWN: "shown" + EVENT_KEY$3, + HIDE: "hide" + EVENT_KEY$3, + HIDDEN: "hidden" + EVENT_KEY$3, + CLICK_DATA_API: "click" + EVENT_KEY$3 + DATA_API_KEY$3 + }; + var ClassName$3 = { + SHOW: 'show', + COLLAPSE: 'collapse', + COLLAPSING: 'collapsing', + COLLAPSED: 'collapsed' + }; + var Dimension = { + WIDTH: 'width', + HEIGHT: 'height' + }; + var Selector$3 = { + ACTIVES: '.show, .collapsing', + DATA_TOGGLE: '[data-toggle="collapse"]' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Collapse = + /*#__PURE__*/ + function () { + function Collapse(element, config) { + this._isTransitioning = false; + this._element = element; + this._config = this._getConfig(config); + this._triggerArray = [].slice.call(document.querySelectorAll("[data-toggle=\"collapse\"][href=\"#" + element.id + "\"]," + ("[data-toggle=\"collapse\"][data-target=\"#" + element.id + "\"]"))); + var toggleList = [].slice.call(document.querySelectorAll(Selector$3.DATA_TOGGLE)); + + for (var i = 0, len = toggleList.length; i < len; i++) { + var elem = toggleList[i]; + var selector = Util.getSelectorFromElement(elem); + var filterElement = [].slice.call(document.querySelectorAll(selector)).filter(function (foundElem) { + return foundElem === element; + }); + + if (selector !== null && filterElement.length > 0) { + this._selector = selector; + + this._triggerArray.push(elem); + } + } + + this._parent = this._config.parent ? this._getParent() : null; + + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._element, this._triggerArray); + } + + if (this._config.toggle) { + this.toggle(); + } + } // Getters + + + var _proto = Collapse.prototype; + + // Public + _proto.toggle = function toggle() { + if ($(this._element).hasClass(ClassName$3.SHOW)) { + this.hide(); + } else { + this.show(); + } + }; + + _proto.show = function show() { + var _this = this; + + if (this._isTransitioning || $(this._element).hasClass(ClassName$3.SHOW)) { + return; + } + + var actives; + var activesData; + + if (this._parent) { + actives = [].slice.call(this._parent.querySelectorAll(Selector$3.ACTIVES)).filter(function (elem) { + if (typeof _this._config.parent === 'string') { + return elem.getAttribute('data-parent') === _this._config.parent; + } + + return elem.classList.contains(ClassName$3.COLLAPSE); + }); + + if (actives.length === 0) { + actives = null; + } + } + + if (actives) { + activesData = $(actives).not(this._selector).data(DATA_KEY$3); + + if (activesData && activesData._isTransitioning) { + return; + } + } + + var startEvent = $.Event(Event$3.SHOW); + $(this._element).trigger(startEvent); + + if (startEvent.isDefaultPrevented()) { + return; + } + + if (actives) { + Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide'); + + if (!activesData) { + $(actives).data(DATA_KEY$3, null); + } + } + + var dimension = this._getDimension(); + + $(this._element).removeClass(ClassName$3.COLLAPSE).addClass(ClassName$3.COLLAPSING); + this._element.style[dimension] = 0; + + if (this._triggerArray.length) { + $(this._triggerArray).removeClass(ClassName$3.COLLAPSED).attr('aria-expanded', true); + } + + this.setTransitioning(true); + + var complete = function complete() { + $(_this._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).addClass(ClassName$3.SHOW); + _this._element.style[dimension] = ''; + + _this.setTransitioning(false); + + $(_this._element).trigger(Event$3.SHOWN); + }; + + var capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + var scrollSize = "scroll" + capitalizedDimension; + var transitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + this._element.style[dimension] = this._element[scrollSize] + "px"; + }; + + _proto.hide = function hide() { + var _this2 = this; + + if (this._isTransitioning || !$(this._element).hasClass(ClassName$3.SHOW)) { + return; + } + + var startEvent = $.Event(Event$3.HIDE); + $(this._element).trigger(startEvent); + + if (startEvent.isDefaultPrevented()) { + return; + } + + var dimension = this._getDimension(); + + this._element.style[dimension] = this._element.getBoundingClientRect()[dimension] + "px"; + Util.reflow(this._element); + $(this._element).addClass(ClassName$3.COLLAPSING).removeClass(ClassName$3.COLLAPSE).removeClass(ClassName$3.SHOW); + var triggerArrayLength = this._triggerArray.length; + + if (triggerArrayLength > 0) { + for (var i = 0; i < triggerArrayLength; i++) { + var trigger = this._triggerArray[i]; + var selector = Util.getSelectorFromElement(trigger); + + if (selector !== null) { + var $elem = $([].slice.call(document.querySelectorAll(selector))); + + if (!$elem.hasClass(ClassName$3.SHOW)) { + $(trigger).addClass(ClassName$3.COLLAPSED).attr('aria-expanded', false); + } + } + } + } + + this.setTransitioning(true); + + var complete = function complete() { + _this2.setTransitioning(false); + + $(_this2._element).removeClass(ClassName$3.COLLAPSING).addClass(ClassName$3.COLLAPSE).trigger(Event$3.HIDDEN); + }; + + this._element.style[dimension] = ''; + var transitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + }; + + _proto.setTransitioning = function setTransitioning(isTransitioning) { + this._isTransitioning = isTransitioning; + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$3); + this._config = null; + this._parent = null; + this._element = null; + this._triggerArray = null; + this._isTransitioning = null; + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default$1, {}, config); + config.toggle = Boolean(config.toggle); // Coerce string values + + Util.typeCheckConfig(NAME$3, config, DefaultType$1); + return config; + }; + + _proto._getDimension = function _getDimension() { + var hasWidth = $(this._element).hasClass(Dimension.WIDTH); + return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT; + }; + + _proto._getParent = function _getParent() { + var _this3 = this; + + var parent; + + if (Util.isElement(this._config.parent)) { + parent = this._config.parent; // It's a jQuery object + + if (typeof this._config.parent.jquery !== 'undefined') { + parent = this._config.parent[0]; + } + } else { + parent = document.querySelector(this._config.parent); + } + + var selector = "[data-toggle=\"collapse\"][data-parent=\"" + this._config.parent + "\"]"; + var children = [].slice.call(parent.querySelectorAll(selector)); + $(children).each(function (i, element) { + _this3._addAriaAndCollapsedClass(Collapse._getTargetFromElement(element), [element]); + }); + return parent; + }; + + _proto._addAriaAndCollapsedClass = function _addAriaAndCollapsedClass(element, triggerArray) { + var isOpen = $(element).hasClass(ClassName$3.SHOW); + + if (triggerArray.length) { + $(triggerArray).toggleClass(ClassName$3.COLLAPSED, !isOpen).attr('aria-expanded', isOpen); + } + } // Static + ; + + Collapse._getTargetFromElement = function _getTargetFromElement(element) { + var selector = Util.getSelectorFromElement(element); + return selector ? document.querySelector(selector) : null; + }; + + Collapse._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var $this = $(this); + var data = $this.data(DATA_KEY$3); + + var _config = _objectSpread2({}, Default$1, {}, $this.data(), {}, typeof config === 'object' && config ? config : {}); + + if (!data && _config.toggle && /show|hide/.test(config)) { + _config.toggle = false; + } + + if (!data) { + data = new Collapse(this, _config); + $this.data(DATA_KEY$3, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Collapse, null, [{ + key: "VERSION", + get: function get() { + return VERSION$3; + } + }, { + key: "Default", + get: function get() { + return Default$1; + } + }]); + + return Collapse; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$3.CLICK_DATA_API, Selector$3.DATA_TOGGLE, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.currentTarget.tagName === 'A') { + event.preventDefault(); + } + + var $trigger = $(this); + var selector = Util.getSelectorFromElement(this); + var selectors = [].slice.call(document.querySelectorAll(selector)); + $(selectors).each(function () { + var $target = $(this); + var data = $target.data(DATA_KEY$3); + var config = data ? 'toggle' : $trigger.data(); + + Collapse._jQueryInterface.call($target, config); + }); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$3] = Collapse._jQueryInterface; + $.fn[NAME$3].Constructor = Collapse; + + $.fn[NAME$3].noConflict = function () { + $.fn[NAME$3] = JQUERY_NO_CONFLICT$3; + return Collapse._jQueryInterface; + }; + + /**! + * @fileOverview Kickass library to create and place poppers near their reference elements. + * @version 1.16.0 + * @license + * Copyright (c) 2016 Federico Zivolo and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined'; + + var timeoutDuration = function () { + var longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox']; + for (var i = 0; i < longerTimeoutBrowsers.length; i += 1) { + if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) { + return 1; + } + } + return 0; + }(); + + function microtaskDebounce(fn) { + var called = false; + return function () { + if (called) { + return; + } + called = true; + window.Promise.resolve().then(function () { + called = false; + fn(); + }); + }; + } + + function taskDebounce(fn) { + var scheduled = false; + return function () { + if (!scheduled) { + scheduled = true; + setTimeout(function () { + scheduled = false; + fn(); + }, timeoutDuration); + } + }; + } + + var supportsMicroTasks = isBrowser && window.Promise; + + /** + * Create a debounced version of a method, that's asynchronously deferred + * but called in the minimum time possible. + * + * @method + * @memberof Popper.Utils + * @argument {Function} fn + * @returns {Function} + */ + var debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce; + + /** + * Check if the given variable is a function + * @method + * @memberof Popper.Utils + * @argument {Any} functionToCheck - variable to check + * @returns {Boolean} answer to: is a function? + */ + function isFunction(functionToCheck) { + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; + } + + /** + * Get CSS computed property of the given element + * @method + * @memberof Popper.Utils + * @argument {Eement} element + * @argument {String} property + */ + function getStyleComputedProperty(element, property) { + if (element.nodeType !== 1) { + return []; + } + // NOTE: 1 DOM access here + var window = element.ownerDocument.defaultView; + var css = window.getComputedStyle(element, null); + return property ? css[property] : css; + } + + /** + * Returns the parentNode or the host of the element + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} parent + */ + function getParentNode(element) { + if (element.nodeName === 'HTML') { + return element; + } + return element.parentNode || element.host; + } + + /** + * Returns the scrolling parent of the given element + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} scroll parent + */ + function getScrollParent(element) { + // Return body, `getScroll` will take care to get the correct `scrollTop` from it + if (!element) { + return document.body; + } + + switch (element.nodeName) { + case 'HTML': + case 'BODY': + return element.ownerDocument.body; + case '#document': + return element.body; + } + + // Firefox want us to check `-x` and `-y` variations as well + + var _getStyleComputedProp = getStyleComputedProperty(element), + overflow = _getStyleComputedProp.overflow, + overflowX = _getStyleComputedProp.overflowX, + overflowY = _getStyleComputedProp.overflowY; + + if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) { + return element; + } + + return getScrollParent(getParentNode(element)); + } + + /** + * Returns the reference node of the reference object, or the reference object itself. + * @method + * @memberof Popper.Utils + * @param {Element|Object} reference - the reference element (the popper will be relative to this) + * @returns {Element} parent + */ + function getReferenceNode(reference) { + return reference && reference.referenceNode ? reference.referenceNode : reference; + } + + var isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode); + var isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent); + + /** + * Determines if the browser is Internet Explorer + * @method + * @memberof Popper.Utils + * @param {Number} version to check + * @returns {Boolean} isIE + */ + function isIE(version) { + if (version === 11) { + return isIE11; + } + if (version === 10) { + return isIE10; + } + return isIE11 || isIE10; + } + + /** + * Returns the offset parent of the given element + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} offset parent + */ + function getOffsetParent(element) { + if (!element) { + return document.documentElement; + } + + var noOffsetParent = isIE(10) ? document.body : null; + + // NOTE: 1 DOM access here + var offsetParent = element.offsetParent || null; + // Skip hidden elements which don't have an offsetParent + while (offsetParent === noOffsetParent && element.nextElementSibling) { + offsetParent = (element = element.nextElementSibling).offsetParent; + } + + var nodeName = offsetParent && offsetParent.nodeName; + + if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') { + return element ? element.ownerDocument.documentElement : document.documentElement; + } + + // .offsetParent will return the closest TH, TD or TABLE in case + // no offsetParent is present, I hate this job... + if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') { + return getOffsetParent(offsetParent); + } + + return offsetParent; + } + + function isOffsetContainer(element) { + var nodeName = element.nodeName; + + if (nodeName === 'BODY') { + return false; + } + return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element; + } + + /** + * Finds the root node (document, shadowDOM root) of the given element + * @method + * @memberof Popper.Utils + * @argument {Element} node + * @returns {Element} root node + */ + function getRoot(node) { + if (node.parentNode !== null) { + return getRoot(node.parentNode); + } + + return node; + } + + /** + * Finds the offset parent common to the two provided nodes + * @method + * @memberof Popper.Utils + * @argument {Element} element1 + * @argument {Element} element2 + * @returns {Element} common offset parent + */ + function findCommonOffsetParent(element1, element2) { + // This check is needed to avoid errors in case one of the elements isn't defined for any reason + if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) { + return document.documentElement; + } + + // Here we make sure to give as "start" the element that comes first in the DOM + var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING; + var start = order ? element1 : element2; + var end = order ? element2 : element1; + + // Get common ancestor container + var range = document.createRange(); + range.setStart(start, 0); + range.setEnd(end, 0); + var commonAncestorContainer = range.commonAncestorContainer; + + // Both nodes are inside #document + + if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) { + if (isOffsetContainer(commonAncestorContainer)) { + return commonAncestorContainer; + } + + return getOffsetParent(commonAncestorContainer); + } + + // one of the nodes is inside shadowDOM, find which one + var element1root = getRoot(element1); + if (element1root.host) { + return findCommonOffsetParent(element1root.host, element2); + } else { + return findCommonOffsetParent(element1, getRoot(element2).host); + } + } + + /** + * Gets the scroll value of the given element in the given side (top and left) + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @argument {String} side `top` or `left` + * @returns {number} amount of scrolled pixels + */ + function getScroll(element) { + var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top'; + + var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft'; + var nodeName = element.nodeName; + + if (nodeName === 'BODY' || nodeName === 'HTML') { + var html = element.ownerDocument.documentElement; + var scrollingElement = element.ownerDocument.scrollingElement || html; + return scrollingElement[upperSide]; + } + + return element[upperSide]; + } + + /* + * Sum or subtract the element scroll values (left and top) from a given rect object + * @method + * @memberof Popper.Utils + * @param {Object} rect - Rect object you want to change + * @param {HTMLElement} element - The element from the function reads the scroll values + * @param {Boolean} subtract - set to true if you want to subtract the scroll values + * @return {Object} rect - The modifier rect object + */ + function includeScroll(rect, element) { + var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + var scrollTop = getScroll(element, 'top'); + var scrollLeft = getScroll(element, 'left'); + var modifier = subtract ? -1 : 1; + rect.top += scrollTop * modifier; + rect.bottom += scrollTop * modifier; + rect.left += scrollLeft * modifier; + rect.right += scrollLeft * modifier; + return rect; + } + + /* + * Helper to detect borders of a given element + * @method + * @memberof Popper.Utils + * @param {CSSStyleDeclaration} styles + * Result of `getStyleComputedProperty` on the given element + * @param {String} axis - `x` or `y` + * @return {number} borders - The borders size of the given axis + */ + + function getBordersSize(styles, axis) { + var sideA = axis === 'x' ? 'Left' : 'Top'; + var sideB = sideA === 'Left' ? 'Right' : 'Bottom'; + + return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10); + } + + function getSize(axis, body, html, computedStyle) { + return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0); + } + + function getWindowSizes(document) { + var body = document.body; + var html = document.documentElement; + var computedStyle = isIE(10) && getComputedStyle(html); + + return { + height: getSize('Height', body, html, computedStyle), + width: getSize('Width', body, html, computedStyle) + }; + } + + var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + + 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; + }; + }(); + + + + + + var defineProperty = function (obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + }; + + var _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + /** + * Given element offsets, generate an output similar to getBoundingClientRect + * @method + * @memberof Popper.Utils + * @argument {Object} offsets + * @returns {Object} ClientRect like output + */ + function getClientRect(offsets) { + return _extends({}, offsets, { + right: offsets.left + offsets.width, + bottom: offsets.top + offsets.height + }); + } + + /** + * Get bounding client rect of given element + * @method + * @memberof Popper.Utils + * @param {HTMLElement} element + * @return {Object} client rect + */ + function getBoundingClientRect(element) { + var rect = {}; + + // IE10 10 FIX: Please, don't ask, the element isn't + // considered in DOM in some circumstances... + // This isn't reproducible in IE10 compatibility mode of IE11 + try { + if (isIE(10)) { + rect = element.getBoundingClientRect(); + var scrollTop = getScroll(element, 'top'); + var scrollLeft = getScroll(element, 'left'); + rect.top += scrollTop; + rect.left += scrollLeft; + rect.bottom += scrollTop; + rect.right += scrollLeft; + } else { + rect = element.getBoundingClientRect(); + } + } catch (e) {} + + var result = { + left: rect.left, + top: rect.top, + width: rect.right - rect.left, + height: rect.bottom - rect.top + }; + + // subtract scrollbar size from sizes + var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {}; + var width = sizes.width || element.clientWidth || result.width; + var height = sizes.height || element.clientHeight || result.height; + + var horizScrollbar = element.offsetWidth - width; + var vertScrollbar = element.offsetHeight - height; + + // if an hypothetical scrollbar is detected, we must be sure it's not a `border` + // we make this check conditional for performance reasons + if (horizScrollbar || vertScrollbar) { + var styles = getStyleComputedProperty(element); + horizScrollbar -= getBordersSize(styles, 'x'); + vertScrollbar -= getBordersSize(styles, 'y'); + + result.width -= horizScrollbar; + result.height -= vertScrollbar; + } + + return getClientRect(result); + } + + function getOffsetRectRelativeToArbitraryNode(children, parent) { + var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + var isIE10 = isIE(10); + var isHTML = parent.nodeName === 'HTML'; + var childrenRect = getBoundingClientRect(children); + var parentRect = getBoundingClientRect(parent); + var scrollParent = getScrollParent(children); + + var styles = getStyleComputedProperty(parent); + var borderTopWidth = parseFloat(styles.borderTopWidth, 10); + var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10); + + // In cases where the parent is fixed, we must ignore negative scroll in offset calc + if (fixedPosition && isHTML) { + parentRect.top = Math.max(parentRect.top, 0); + parentRect.left = Math.max(parentRect.left, 0); + } + var offsets = getClientRect({ + top: childrenRect.top - parentRect.top - borderTopWidth, + left: childrenRect.left - parentRect.left - borderLeftWidth, + width: childrenRect.width, + height: childrenRect.height + }); + offsets.marginTop = 0; + offsets.marginLeft = 0; + + // Subtract margins of documentElement in case it's being used as parent + // we do this only on HTML because it's the only element that behaves + // differently when margins are applied to it. The margins are included in + // the box of the documentElement, in the other cases not. + if (!isIE10 && isHTML) { + var marginTop = parseFloat(styles.marginTop, 10); + var marginLeft = parseFloat(styles.marginLeft, 10); + + offsets.top -= borderTopWidth - marginTop; + offsets.bottom -= borderTopWidth - marginTop; + offsets.left -= borderLeftWidth - marginLeft; + offsets.right -= borderLeftWidth - marginLeft; + + // Attach marginTop and marginLeft because in some circumstances we may need them + offsets.marginTop = marginTop; + offsets.marginLeft = marginLeft; + } + + if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') { + offsets = includeScroll(offsets, parent); + } + + return offsets; + } + + function getViewportOffsetRectRelativeToArtbitraryNode(element) { + var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var html = element.ownerDocument.documentElement; + var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html); + var width = Math.max(html.clientWidth, window.innerWidth || 0); + var height = Math.max(html.clientHeight, window.innerHeight || 0); + + var scrollTop = !excludeScroll ? getScroll(html) : 0; + var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0; + + var offset = { + top: scrollTop - relativeOffset.top + relativeOffset.marginTop, + left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft, + width: width, + height: height + }; + + return getClientRect(offset); + } + + /** + * Check if the given element is fixed or is inside a fixed parent + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @argument {Element} customContainer + * @returns {Boolean} answer to "isFixed?" + */ + function isFixed(element) { + var nodeName = element.nodeName; + if (nodeName === 'BODY' || nodeName === 'HTML') { + return false; + } + if (getStyleComputedProperty(element, 'position') === 'fixed') { + return true; + } + var parentNode = getParentNode(element); + if (!parentNode) { + return false; + } + return isFixed(parentNode); + } + + /** + * Finds the first parent of an element that has a transformed property defined + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Element} first transformed parent or documentElement + */ + + function getFixedPositionOffsetParent(element) { + // This check is needed to avoid errors in case one of the elements isn't defined for any reason + if (!element || !element.parentElement || isIE()) { + return document.documentElement; + } + var el = element.parentElement; + while (el && getStyleComputedProperty(el, 'transform') === 'none') { + el = el.parentElement; + } + return el || document.documentElement; + } + + /** + * Computed the boundaries limits and return them + * @method + * @memberof Popper.Utils + * @param {HTMLElement} popper + * @param {HTMLElement} reference + * @param {number} padding + * @param {HTMLElement} boundariesElement - Element used to define the boundaries + * @param {Boolean} fixedPosition - Is in fixed position mode + * @returns {Object} Coordinates of the boundaries + */ + function getBoundaries(popper, reference, padding, boundariesElement) { + var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + + // NOTE: 1 DOM access here + + var boundaries = { top: 0, left: 0 }; + var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, getReferenceNode(reference)); + + // Handle viewport case + if (boundariesElement === 'viewport') { + boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition); + } else { + // Handle other cases based on DOM element used as boundaries + var boundariesNode = void 0; + if (boundariesElement === 'scrollParent') { + boundariesNode = getScrollParent(getParentNode(reference)); + if (boundariesNode.nodeName === 'BODY') { + boundariesNode = popper.ownerDocument.documentElement; + } + } else if (boundariesElement === 'window') { + boundariesNode = popper.ownerDocument.documentElement; + } else { + boundariesNode = boundariesElement; + } + + var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition); + + // In case of HTML, we need a different computation + if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) { + var _getWindowSizes = getWindowSizes(popper.ownerDocument), + height = _getWindowSizes.height, + width = _getWindowSizes.width; + + boundaries.top += offsets.top - offsets.marginTop; + boundaries.bottom = height + offsets.top; + boundaries.left += offsets.left - offsets.marginLeft; + boundaries.right = width + offsets.left; + } else { + // for all the other DOM elements, this one is good + boundaries = offsets; + } + } + + // Add paddings + padding = padding || 0; + var isPaddingNumber = typeof padding === 'number'; + boundaries.left += isPaddingNumber ? padding : padding.left || 0; + boundaries.top += isPaddingNumber ? padding : padding.top || 0; + boundaries.right -= isPaddingNumber ? padding : padding.right || 0; + boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0; + + return boundaries; + } + + function getArea(_ref) { + var width = _ref.width, + height = _ref.height; + + return width * height; + } + + /** + * Utility used to transform the `auto` placement to the placement with more + * available space. + * @method + * @memberof Popper.Utils + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) { + var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0; + + if (placement.indexOf('auto') === -1) { + return placement; + } + + var boundaries = getBoundaries(popper, reference, padding, boundariesElement); + + var rects = { + top: { + width: boundaries.width, + height: refRect.top - boundaries.top + }, + right: { + width: boundaries.right - refRect.right, + height: boundaries.height + }, + bottom: { + width: boundaries.width, + height: boundaries.bottom - refRect.bottom + }, + left: { + width: refRect.left - boundaries.left, + height: boundaries.height + } + }; + + var sortedAreas = Object.keys(rects).map(function (key) { + return _extends({ + key: key + }, rects[key], { + area: getArea(rects[key]) + }); + }).sort(function (a, b) { + return b.area - a.area; + }); + + var filteredAreas = sortedAreas.filter(function (_ref2) { + var width = _ref2.width, + height = _ref2.height; + return width >= popper.clientWidth && height >= popper.clientHeight; + }); + + var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key; + + var variation = placement.split('-')[1]; + + return computedPlacement + (variation ? '-' + variation : ''); + } + + /** + * Get offsets to the reference element + * @method + * @memberof Popper.Utils + * @param {Object} state + * @param {Element} popper - the popper element + * @param {Element} reference - the reference element (the popper will be relative to this) + * @param {Element} fixedPosition - is in fixed position mode + * @returns {Object} An object containing the offsets which will be applied to the popper + */ + function getReferenceOffsets(state, popper, reference) { + var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + + var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, getReferenceNode(reference)); + return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition); + } + + /** + * Get the outer sizes of the given element (offset size + margins) + * @method + * @memberof Popper.Utils + * @argument {Element} element + * @returns {Object} object containing width and height properties + */ + function getOuterSizes(element) { + var window = element.ownerDocument.defaultView; + var styles = window.getComputedStyle(element); + var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0); + var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0); + var result = { + width: element.offsetWidth + y, + height: element.offsetHeight + x + }; + return result; + } + + /** + * Get the opposite placement of the given one + * @method + * @memberof Popper.Utils + * @argument {String} placement + * @returns {String} flipped placement + */ + function getOppositePlacement(placement) { + var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' }; + return placement.replace(/left|right|bottom|top/g, function (matched) { + return hash[matched]; + }); + } + + /** + * Get offsets to the popper + * @method + * @memberof Popper.Utils + * @param {Object} position - CSS position the Popper will get applied + * @param {HTMLElement} popper - the popper element + * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this) + * @param {String} placement - one of the valid placement options + * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper + */ + function getPopperOffsets(popper, referenceOffsets, placement) { + placement = placement.split('-')[0]; + + // Get popper node sizes + var popperRect = getOuterSizes(popper); + + // Add position, width and height to our offsets object + var popperOffsets = { + width: popperRect.width, + height: popperRect.height + }; + + // depending by the popper placement we have to compute its offsets slightly differently + var isHoriz = ['right', 'left'].indexOf(placement) !== -1; + var mainSide = isHoriz ? 'top' : 'left'; + var secondarySide = isHoriz ? 'left' : 'top'; + var measurement = isHoriz ? 'height' : 'width'; + var secondaryMeasurement = !isHoriz ? 'height' : 'width'; + + popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2; + if (placement === secondarySide) { + popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement]; + } else { + popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)]; + } + + return popperOffsets; + } + + /** + * Mimics the `find` method of Array + * @method + * @memberof Popper.Utils + * @argument {Array} arr + * @argument prop + * @argument value + * @returns index or -1 + */ + function find(arr, check) { + // use native find if supported + if (Array.prototype.find) { + return arr.find(check); + } + + // use `filter` to obtain the same behavior of `find` + return arr.filter(check)[0]; + } + + /** + * Return the index of the matching object + * @method + * @memberof Popper.Utils + * @argument {Array} arr + * @argument prop + * @argument value + * @returns index or -1 + */ + function findIndex(arr, prop, value) { + // use native findIndex if supported + if (Array.prototype.findIndex) { + return arr.findIndex(function (cur) { + return cur[prop] === value; + }); + } + + // use `find` + `indexOf` if `findIndex` isn't supported + var match = find(arr, function (obj) { + return obj[prop] === value; + }); + return arr.indexOf(match); + } + + /** + * Loop trough the list of modifiers and run them in order, + * each of them will then edit the data object. + * @method + * @memberof Popper.Utils + * @param {dataObject} data + * @param {Array} modifiers + * @param {String} ends - Optional modifier name used as stopper + * @returns {dataObject} + */ + function runModifiers(modifiers, data, ends) { + var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends)); + + modifiersToRun.forEach(function (modifier) { + if (modifier['function']) { + // eslint-disable-line dot-notation + console.warn('`modifier.function` is deprecated, use `modifier.fn`!'); + } + var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation + if (modifier.enabled && isFunction(fn)) { + // Add properties to offsets to make them a complete clientRect object + // we do this before each modifier to make sure the previous one doesn't + // mess with these values + data.offsets.popper = getClientRect(data.offsets.popper); + data.offsets.reference = getClientRect(data.offsets.reference); + + data = fn(data, modifier); + } + }); + + return data; + } + + /** + * Updates the position of the popper, computing the new offsets and applying + * the new style.
+ * Prefer `scheduleUpdate` over `update` because of performance reasons. + * @method + * @memberof Popper + */ + function update() { + // if popper is destroyed, don't perform any further update + if (this.state.isDestroyed) { + return; + } + + var data = { + instance: this, + styles: {}, + arrowStyles: {}, + attributes: {}, + flipped: false, + offsets: {} + }; + + // compute reference element offsets + data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed); + + // compute auto placement, store placement inside the data object, + // modifiers will be able to edit `placement` if needed + // and refer to originalPlacement to know the original value + data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding); + + // store the computed placement inside `originalPlacement` + data.originalPlacement = data.placement; + + data.positionFixed = this.options.positionFixed; + + // compute the popper offsets + data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement); + + data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute'; + + // run the modifiers + data = runModifiers(this.modifiers, data); + + // the first `update` will call `onCreate` callback + // the other ones will call `onUpdate` callback + if (!this.state.isCreated) { + this.state.isCreated = true; + this.options.onCreate(data); + } else { + this.options.onUpdate(data); + } + } + + /** + * Helper used to know if the given modifier is enabled. + * @method + * @memberof Popper.Utils + * @returns {Boolean} + */ + function isModifierEnabled(modifiers, modifierName) { + return modifiers.some(function (_ref) { + var name = _ref.name, + enabled = _ref.enabled; + return enabled && name === modifierName; + }); + } + + /** + * Get the prefixed supported property name + * @method + * @memberof Popper.Utils + * @argument {String} property (camelCase) + * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix) + */ + function getSupportedPropertyName(property) { + var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O']; + var upperProp = property.charAt(0).toUpperCase() + property.slice(1); + + for (var i = 0; i < prefixes.length; i++) { + var prefix = prefixes[i]; + var toCheck = prefix ? '' + prefix + upperProp : property; + if (typeof document.body.style[toCheck] !== 'undefined') { + return toCheck; + } + } + return null; + } + + /** + * Destroys the popper. + * @method + * @memberof Popper + */ + function destroy() { + this.state.isDestroyed = true; + + // touch DOM only if `applyStyle` modifier is enabled + if (isModifierEnabled(this.modifiers, 'applyStyle')) { + this.popper.removeAttribute('x-placement'); + this.popper.style.position = ''; + this.popper.style.top = ''; + this.popper.style.left = ''; + this.popper.style.right = ''; + this.popper.style.bottom = ''; + this.popper.style.willChange = ''; + this.popper.style[getSupportedPropertyName('transform')] = ''; + } + + this.disableEventListeners(); + + // remove the popper if user explicitly asked for the deletion on destroy + // do not use `remove` because IE11 doesn't support it + if (this.options.removeOnDestroy) { + this.popper.parentNode.removeChild(this.popper); + } + return this; + } + + /** + * Get the window associated with the element + * @argument {Element} element + * @returns {Window} + */ + function getWindow(element) { + var ownerDocument = element.ownerDocument; + return ownerDocument ? ownerDocument.defaultView : window; + } + + function attachToScrollParents(scrollParent, event, callback, scrollParents) { + var isBody = scrollParent.nodeName === 'BODY'; + var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent; + target.addEventListener(event, callback, { passive: true }); + + if (!isBody) { + attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents); + } + scrollParents.push(target); + } + + /** + * Setup needed event listeners used to update the popper position + * @method + * @memberof Popper.Utils + * @private + */ + function setupEventListeners(reference, options, state, updateBound) { + // Resize event listener on window + state.updateBound = updateBound; + getWindow(reference).addEventListener('resize', state.updateBound, { passive: true }); + + // Scroll event listener on scroll parents + var scrollElement = getScrollParent(reference); + attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents); + state.scrollElement = scrollElement; + state.eventsEnabled = true; + + return state; + } + + /** + * It will add resize/scroll events and start recalculating + * position of the popper element when they are triggered. + * @method + * @memberof Popper + */ + function enableEventListeners() { + if (!this.state.eventsEnabled) { + this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate); + } + } + + /** + * Remove event listeners used to update the popper position + * @method + * @memberof Popper.Utils + * @private + */ + function removeEventListeners(reference, state) { + // Remove resize event listener on window + getWindow(reference).removeEventListener('resize', state.updateBound); + + // Remove scroll event listener on scroll parents + state.scrollParents.forEach(function (target) { + target.removeEventListener('scroll', state.updateBound); + }); + + // Reset state + state.updateBound = null; + state.scrollParents = []; + state.scrollElement = null; + state.eventsEnabled = false; + return state; + } + + /** + * It will remove resize/scroll events and won't recalculate popper position + * when they are triggered. It also won't trigger `onUpdate` callback anymore, + * unless you call `update` method manually. + * @method + * @memberof Popper + */ + function disableEventListeners() { + if (this.state.eventsEnabled) { + cancelAnimationFrame(this.scheduleUpdate); + this.state = removeEventListeners(this.reference, this.state); + } + } + + /** + * Tells if a given input is a number + * @method + * @memberof Popper.Utils + * @param {*} input to check + * @return {Boolean} + */ + function isNumeric(n) { + return n !== '' && !isNaN(parseFloat(n)) && isFinite(n); + } + + /** + * Set the style to the given popper + * @method + * @memberof Popper.Utils + * @argument {Element} element - Element to apply the style to + * @argument {Object} styles + * Object with a list of properties and values which will be applied to the element + */ + function setStyles(element, styles) { + Object.keys(styles).forEach(function (prop) { + var unit = ''; + // add unit if the value is numeric and is one of the following + if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) { + unit = 'px'; + } + element.style[prop] = styles[prop] + unit; + }); + } + + /** + * Set the attributes to the given popper + * @method + * @memberof Popper.Utils + * @argument {Element} element - Element to apply the attributes to + * @argument {Object} styles + * Object with a list of properties and values which will be applied to the element + */ + function setAttributes(element, attributes) { + Object.keys(attributes).forEach(function (prop) { + var value = attributes[prop]; + if (value !== false) { + element.setAttribute(prop, attributes[prop]); + } else { + element.removeAttribute(prop); + } + }); + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} data.styles - List of style properties - values to apply to popper element + * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The same data object + */ + function applyStyle(data) { + // any property present in `data.styles` will be applied to the popper, + // in this way we can make the 3rd party modifiers add custom styles to it + // Be aware, modifiers could override the properties defined in the previous + // lines of this modifier! + setStyles(data.instance.popper, data.styles); + + // any property present in `data.attributes` will be applied to the popper, + // they will be set as HTML attributes of the element + setAttributes(data.instance.popper, data.attributes); + + // if arrowElement is defined and arrowStyles has some properties + if (data.arrowElement && Object.keys(data.arrowStyles).length) { + setStyles(data.arrowElement, data.arrowStyles); + } + + return data; + } + + /** + * Set the x-placement attribute before everything else because it could be used + * to add margins to the popper margins needs to be calculated to get the + * correct popper offsets. + * @method + * @memberof Popper.modifiers + * @param {HTMLElement} reference - The reference element used to position the popper + * @param {HTMLElement} popper - The HTML element used as popper + * @param {Object} options - Popper.js options + */ + function applyStyleOnLoad(reference, popper, options, modifierOptions, state) { + // compute reference element offsets + var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed); + + // compute auto placement, store placement inside the data object, + // modifiers will be able to edit `placement` if needed + // and refer to originalPlacement to know the original value + var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding); + + popper.setAttribute('x-placement', placement); + + // Apply `position` to popper before anything else because + // without the position applied we can't guarantee correct computations + setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' }); + + return options; + } + + /** + * @function + * @memberof Popper.Utils + * @argument {Object} data - The data object generated by `update` method + * @argument {Boolean} shouldRound - If the offsets should be rounded at all + * @returns {Object} The popper's position offsets rounded + * + * The tale of pixel-perfect positioning. It's still not 100% perfect, but as + * good as it can be within reason. + * Discussion here: https://github.com/FezVrasta/popper.js/pull/715 + * + * Low DPI screens cause a popper to be blurry if not using full pixels (Safari + * as well on High DPI screens). + * + * Firefox prefers no rounding for positioning and does not have blurriness on + * high DPI screens. + * + * Only horizontal placement and left/right values need to be considered. + */ + function getRoundedOffsets(data, shouldRound) { + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + var round = Math.round, + floor = Math.floor; + + var noRound = function noRound(v) { + return v; + }; + + var referenceWidth = round(reference.width); + var popperWidth = round(popper.width); + + var isVertical = ['left', 'right'].indexOf(data.placement) !== -1; + var isVariation = data.placement.indexOf('-') !== -1; + var sameWidthParity = referenceWidth % 2 === popperWidth % 2; + var bothOddWidth = referenceWidth % 2 === 1 && popperWidth % 2 === 1; + + var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthParity ? round : floor; + var verticalToInteger = !shouldRound ? noRound : round; + + return { + left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left), + top: verticalToInteger(popper.top), + bottom: verticalToInteger(popper.bottom), + right: horizontalToInteger(popper.right) + }; + } + + var isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent); + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function computeStyle(data, options) { + var x = options.x, + y = options.y; + var popper = data.offsets.popper; + + // Remove this legacy support in Popper.js v2 + + var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) { + return modifier.name === 'applyStyle'; + }).gpuAcceleration; + if (legacyGpuAccelerationOption !== undefined) { + console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!'); + } + var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration; + + var offsetParent = getOffsetParent(data.instance.popper); + var offsetParentRect = getBoundingClientRect(offsetParent); + + // Styles + var styles = { + position: popper.position + }; + + var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox); + + var sideA = x === 'bottom' ? 'top' : 'bottom'; + var sideB = y === 'right' ? 'left' : 'right'; + + // if gpuAcceleration is set to `true` and transform is supported, + // we use `translate3d` to apply the position to the popper we + // automatically use the supported prefixed version if needed + var prefixedProperty = getSupportedPropertyName('transform'); + + // now, let's make a step back and look at this code closely (wtf?) + // If the content of the popper grows once it's been positioned, it + // may happen that the popper gets misplaced because of the new content + // overflowing its reference element + // To avoid this problem, we provide two options (x and y), which allow + // the consumer to define the offset origin. + // If we position a popper on top of a reference element, we can set + // `x` to `top` to make the popper grow towards its top instead of + // its bottom. + var left = void 0, + top = void 0; + if (sideA === 'bottom') { + // when offsetParent is the positioning is relative to the bottom of the screen (excluding the scrollbar) + // and not the bottom of the html element + if (offsetParent.nodeName === 'HTML') { + top = -offsetParent.clientHeight + offsets.bottom; + } else { + top = -offsetParentRect.height + offsets.bottom; + } + } else { + top = offsets.top; + } + if (sideB === 'right') { + if (offsetParent.nodeName === 'HTML') { + left = -offsetParent.clientWidth + offsets.right; + } else { + left = -offsetParentRect.width + offsets.right; + } + } else { + left = offsets.left; + } + if (gpuAcceleration && prefixedProperty) { + styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)'; + styles[sideA] = 0; + styles[sideB] = 0; + styles.willChange = 'transform'; + } else { + // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties + var invertTop = sideA === 'bottom' ? -1 : 1; + var invertLeft = sideB === 'right' ? -1 : 1; + styles[sideA] = top * invertTop; + styles[sideB] = left * invertLeft; + styles.willChange = sideA + ', ' + sideB; + } + + // Attributes + var attributes = { + 'x-placement': data.placement + }; + + // Update `data` attributes, styles and arrowStyles + data.attributes = _extends({}, attributes, data.attributes); + data.styles = _extends({}, styles, data.styles); + data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles); + + return data; + } + + /** + * Helper used to know if the given modifier depends from another one.
+ * It checks if the needed modifier is listed and enabled. + * @method + * @memberof Popper.Utils + * @param {Array} modifiers - list of modifiers + * @param {String} requestingName - name of requesting modifier + * @param {String} requestedName - name of requested modifier + * @returns {Boolean} + */ + function isModifierRequired(modifiers, requestingName, requestedName) { + var requesting = find(modifiers, function (_ref) { + var name = _ref.name; + return name === requestingName; + }); + + var isRequired = !!requesting && modifiers.some(function (modifier) { + return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order; + }); + + if (!isRequired) { + var _requesting = '`' + requestingName + '`'; + var requested = '`' + requestedName + '`'; + console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!'); + } + return isRequired; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function arrow(data, options) { + var _data$offsets$arrow; + + // arrow depends on keepTogether in order to work + if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) { + return data; + } + + var arrowElement = options.element; + + // if arrowElement is a string, suppose it's a CSS selector + if (typeof arrowElement === 'string') { + arrowElement = data.instance.popper.querySelector(arrowElement); + + // if arrowElement is not found, don't run the modifier + if (!arrowElement) { + return data; + } + } else { + // if the arrowElement isn't a query selector we must check that the + // provided DOM node is child of its popper node + if (!data.instance.popper.contains(arrowElement)) { + console.warn('WARNING: `arrow.element` must be child of its popper element!'); + return data; + } + } + + var placement = data.placement.split('-')[0]; + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var isVertical = ['left', 'right'].indexOf(placement) !== -1; + + var len = isVertical ? 'height' : 'width'; + var sideCapitalized = isVertical ? 'Top' : 'Left'; + var side = sideCapitalized.toLowerCase(); + var altSide = isVertical ? 'left' : 'top'; + var opSide = isVertical ? 'bottom' : 'right'; + var arrowElementSize = getOuterSizes(arrowElement)[len]; + + // + // extends keepTogether behavior making sure the popper and its + // reference have enough pixels in conjunction + // + + // top/left side + if (reference[opSide] - arrowElementSize < popper[side]) { + data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize); + } + // bottom/right side + if (reference[side] + arrowElementSize > popper[opSide]) { + data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide]; + } + data.offsets.popper = getClientRect(data.offsets.popper); + + // compute center of the popper + var center = reference[side] + reference[len] / 2 - arrowElementSize / 2; + + // Compute the sideValue using the updated popper offsets + // take popper margin in account because we don't have this info available + var css = getStyleComputedProperty(data.instance.popper); + var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10); + var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10); + var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide; + + // prevent arrowElement from being placed not contiguously to its popper + sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0); + + data.arrowElement = arrowElement; + data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow); + + return data; + } + + /** + * Get the opposite placement variation of the given one + * @method + * @memberof Popper.Utils + * @argument {String} placement variation + * @returns {String} flipped placement variation + */ + function getOppositeVariation(variation) { + if (variation === 'end') { + return 'start'; + } else if (variation === 'start') { + return 'end'; + } + return variation; + } + + /** + * List of accepted placements to use as values of the `placement` option.
+ * Valid placements are: + * - `auto` + * - `top` + * - `right` + * - `bottom` + * - `left` + * + * Each placement can have a variation from this list: + * - `-start` + * - `-end` + * + * Variations are interpreted easily if you think of them as the left to right + * written languages. Horizontally (`top` and `bottom`), `start` is left and `end` + * is right.
+ * Vertically (`left` and `right`), `start` is top and `end` is bottom. + * + * Some valid examples are: + * - `top-end` (on top of reference, right aligned) + * - `right-start` (on right of reference, top aligned) + * - `bottom` (on bottom, centered) + * - `auto-end` (on the side with more space available, alignment depends by placement) + * + * @static + * @type {Array} + * @enum {String} + * @readonly + * @method placements + * @memberof Popper + */ + var placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start']; + + // Get rid of `auto` `auto-start` and `auto-end` + var validPlacements = placements.slice(3); + + /** + * Given an initial placement, returns all the subsequent placements + * clockwise (or counter-clockwise). + * + * @method + * @memberof Popper.Utils + * @argument {String} placement - A valid placement (it accepts variations) + * @argument {Boolean} counter - Set to true to walk the placements counterclockwise + * @returns {Array} placements including their variations + */ + function clockwise(placement) { + var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + + var index = validPlacements.indexOf(placement); + var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index)); + return counter ? arr.reverse() : arr; + } + + var BEHAVIORS = { + FLIP: 'flip', + CLOCKWISE: 'clockwise', + COUNTERCLOCKWISE: 'counterclockwise' + }; + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function flip(data, options) { + // if `inner` modifier is enabled, we can't use the `flip` modifier + if (isModifierEnabled(data.instance.modifiers, 'inner')) { + return data; + } + + if (data.flipped && data.placement === data.originalPlacement) { + // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides + return data; + } + + var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed); + + var placement = data.placement.split('-')[0]; + var placementOpposite = getOppositePlacement(placement); + var variation = data.placement.split('-')[1] || ''; + + var flipOrder = []; + + switch (options.behavior) { + case BEHAVIORS.FLIP: + flipOrder = [placement, placementOpposite]; + break; + case BEHAVIORS.CLOCKWISE: + flipOrder = clockwise(placement); + break; + case BEHAVIORS.COUNTERCLOCKWISE: + flipOrder = clockwise(placement, true); + break; + default: + flipOrder = options.behavior; + } + + flipOrder.forEach(function (step, index) { + if (placement !== step || flipOrder.length === index + 1) { + return data; + } + + placement = data.placement.split('-')[0]; + placementOpposite = getOppositePlacement(placement); + + var popperOffsets = data.offsets.popper; + var refOffsets = data.offsets.reference; + + // using floor because the reference offsets may contain decimals we are not going to consider here + var floor = Math.floor; + var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom); + + var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left); + var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right); + var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top); + var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom); + + var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom; + + // flip the variation if required + var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; + + // flips variation if reference element overflows boundaries + var flippedVariationByRef = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom); + + // flips variation if popper content overflows boundaries + var flippedVariationByContent = !!options.flipVariationsByContent && (isVertical && variation === 'start' && overflowsRight || isVertical && variation === 'end' && overflowsLeft || !isVertical && variation === 'start' && overflowsBottom || !isVertical && variation === 'end' && overflowsTop); + + var flippedVariation = flippedVariationByRef || flippedVariationByContent; + + if (overlapsRef || overflowsBoundaries || flippedVariation) { + // this boolean to detect any flip loop + data.flipped = true; + + if (overlapsRef || overflowsBoundaries) { + placement = flipOrder[index + 1]; + } + + if (flippedVariation) { + variation = getOppositeVariation(variation); + } + + data.placement = placement + (variation ? '-' + variation : ''); + + // this object contains `position`, we want to preserve it along with + // any additional property we may add in the future + data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement)); + + data = runModifiers(data.instance.modifiers, data, 'flip'); + } + }); + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function keepTogether(data) { + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var placement = data.placement.split('-')[0]; + var floor = Math.floor; + var isVertical = ['top', 'bottom'].indexOf(placement) !== -1; + var side = isVertical ? 'right' : 'bottom'; + var opSide = isVertical ? 'left' : 'top'; + var measurement = isVertical ? 'width' : 'height'; + + if (popper[side] < floor(reference[opSide])) { + data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement]; + } + if (popper[opSide] > floor(reference[side])) { + data.offsets.popper[opSide] = floor(reference[side]); + } + + return data; + } + + /** + * Converts a string containing value + unit into a px value number + * @function + * @memberof {modifiers~offset} + * @private + * @argument {String} str - Value + unit string + * @argument {String} measurement - `height` or `width` + * @argument {Object} popperOffsets + * @argument {Object} referenceOffsets + * @returns {Number|String} + * Value in pixels, or original string if no values were extracted + */ + function toValue(str, measurement, popperOffsets, referenceOffsets) { + // separate value from unit + var split = str.match(/((?:\-|\+)?\d*\.?\d*)(.*)/); + var value = +split[1]; + var unit = split[2]; + + // If it's not a number it's an operator, I guess + if (!value) { + return str; + } + + if (unit.indexOf('%') === 0) { + var element = void 0; + switch (unit) { + case '%p': + element = popperOffsets; + break; + case '%': + case '%r': + default: + element = referenceOffsets; + } + + var rect = getClientRect(element); + return rect[measurement] / 100 * value; + } else if (unit === 'vh' || unit === 'vw') { + // if is a vh or vw, we calculate the size based on the viewport + var size = void 0; + if (unit === 'vh') { + size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + } else { + size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + } + return size / 100 * value; + } else { + // if is an explicit pixel unit, we get rid of the unit and keep the value + // if is an implicit unit, it's px, and we return just the value + return value; + } + } + + /** + * Parse an `offset` string to extrapolate `x` and `y` numeric offsets. + * @function + * @memberof {modifiers~offset} + * @private + * @argument {String} offset + * @argument {Object} popperOffsets + * @argument {Object} referenceOffsets + * @argument {String} basePlacement + * @returns {Array} a two cells array with x and y offsets in numbers + */ + function parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) { + var offsets = [0, 0]; + + // Use height if placement is left or right and index is 0 otherwise use width + // in this way the first offset will use an axis and the second one + // will use the other one + var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1; + + // Split the offset string to obtain a list of values and operands + // The regex addresses values with the plus or minus sign in front (+10, -20, etc) + var fragments = offset.split(/(\+|\-)/).map(function (frag) { + return frag.trim(); + }); + + // Detect if the offset string contains a pair of values or a single one + // they could be separated by comma or space + var divider = fragments.indexOf(find(fragments, function (frag) { + return frag.search(/,|\s/) !== -1; + })); + + if (fragments[divider] && fragments[divider].indexOf(',') === -1) { + console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.'); + } + + // If divider is found, we divide the list of values and operands to divide + // them by ofset X and Y. + var splitRegex = /\s*,\s*|\s+/; + var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments]; + + // Convert the values with units to absolute pixels to allow our computations + ops = ops.map(function (op, index) { + // Most of the units rely on the orientation of the popper + var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width'; + var mergeWithPrevious = false; + return op + // This aggregates any `+` or `-` sign that aren't considered operators + // e.g.: 10 + +5 => [10, +, +5] + .reduce(function (a, b) { + if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) { + a[a.length - 1] = b; + mergeWithPrevious = true; + return a; + } else if (mergeWithPrevious) { + a[a.length - 1] += b; + mergeWithPrevious = false; + return a; + } else { + return a.concat(b); + } + }, []) + // Here we convert the string values into number values (in px) + .map(function (str) { + return toValue(str, measurement, popperOffsets, referenceOffsets); + }); + }); + + // Loop trough the offsets arrays and execute the operations + ops.forEach(function (op, index) { + op.forEach(function (frag, index2) { + if (isNumeric(frag)) { + offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1); + } + }); + }); + return offsets; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @argument {Number|String} options.offset=0 + * The offset value as described in the modifier description + * @returns {Object} The data object, properly modified + */ + function offset(data, _ref) { + var offset = _ref.offset; + var placement = data.placement, + _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var basePlacement = placement.split('-')[0]; + + var offsets = void 0; + if (isNumeric(+offset)) { + offsets = [+offset, 0]; + } else { + offsets = parseOffset(offset, popper, reference, basePlacement); + } + + if (basePlacement === 'left') { + popper.top += offsets[0]; + popper.left -= offsets[1]; + } else if (basePlacement === 'right') { + popper.top += offsets[0]; + popper.left += offsets[1]; + } else if (basePlacement === 'top') { + popper.left += offsets[0]; + popper.top -= offsets[1]; + } else if (basePlacement === 'bottom') { + popper.left += offsets[0]; + popper.top += offsets[1]; + } + + data.popper = popper; + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function preventOverflow(data, options) { + var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper); + + // If offsetParent is the reference element, we really want to + // go one step up and use the next offsetParent as reference to + // avoid to make this modifier completely useless and look like broken + if (data.instance.reference === boundariesElement) { + boundariesElement = getOffsetParent(boundariesElement); + } + + // NOTE: DOM access here + // resets the popper's position so that the document size can be calculated excluding + // the size of the popper element itself + var transformProp = getSupportedPropertyName('transform'); + var popperStyles = data.instance.popper.style; // assignment to help minification + var top = popperStyles.top, + left = popperStyles.left, + transform = popperStyles[transformProp]; + + popperStyles.top = ''; + popperStyles.left = ''; + popperStyles[transformProp] = ''; + + var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed); + + // NOTE: DOM access here + // restores the original style properties after the offsets have been computed + popperStyles.top = top; + popperStyles.left = left; + popperStyles[transformProp] = transform; + + options.boundaries = boundaries; + + var order = options.priority; + var popper = data.offsets.popper; + + var check = { + primary: function primary(placement) { + var value = popper[placement]; + if (popper[placement] < boundaries[placement] && !options.escapeWithReference) { + value = Math.max(popper[placement], boundaries[placement]); + } + return defineProperty({}, placement, value); + }, + secondary: function secondary(placement) { + var mainSide = placement === 'right' ? 'left' : 'top'; + var value = popper[mainSide]; + if (popper[placement] > boundaries[placement] && !options.escapeWithReference) { + value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height)); + } + return defineProperty({}, mainSide, value); + } + }; + + order.forEach(function (placement) { + var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary'; + popper = _extends({}, popper, check[side](placement)); + }); + + data.offsets.popper = popper; + + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function shift(data) { + var placement = data.placement; + var basePlacement = placement.split('-')[0]; + var shiftvariation = placement.split('-')[1]; + + // if shift shiftvariation is specified, run the modifier + if (shiftvariation) { + var _data$offsets = data.offsets, + reference = _data$offsets.reference, + popper = _data$offsets.popper; + + var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1; + var side = isVertical ? 'left' : 'top'; + var measurement = isVertical ? 'width' : 'height'; + + var shiftOffsets = { + start: defineProperty({}, side, reference[side]), + end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement]) + }; + + data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]); + } + + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by update method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function hide(data) { + if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) { + return data; + } + + var refRect = data.offsets.reference; + var bound = find(data.instance.modifiers, function (modifier) { + return modifier.name === 'preventOverflow'; + }).boundaries; + + if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) { + // Avoid unnecessary DOM access if visibility hasn't changed + if (data.hide === true) { + return data; + } + + data.hide = true; + data.attributes['x-out-of-boundaries'] = ''; + } else { + // Avoid unnecessary DOM access if visibility hasn't changed + if (data.hide === false) { + return data; + } + + data.hide = false; + data.attributes['x-out-of-boundaries'] = false; + } + + return data; + } + + /** + * @function + * @memberof Modifiers + * @argument {Object} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {Object} The data object, properly modified + */ + function inner(data) { + var placement = data.placement; + var basePlacement = placement.split('-')[0]; + var _data$offsets = data.offsets, + popper = _data$offsets.popper, + reference = _data$offsets.reference; + + var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1; + + var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1; + + popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0); + + data.placement = getOppositePlacement(placement); + data.offsets.popper = getClientRect(popper); + + return data; + } + + /** + * Modifier function, each modifier can have a function of this type assigned + * to its `fn` property.
+ * These functions will be called on each update, this means that you must + * make sure they are performant enough to avoid performance bottlenecks. + * + * @function ModifierFn + * @argument {dataObject} data - The data object generated by `update` method + * @argument {Object} options - Modifiers configuration and options + * @returns {dataObject} The data object, properly modified + */ + + /** + * Modifiers are plugins used to alter the behavior of your poppers.
+ * Popper.js uses a set of 9 modifiers to provide all the basic functionalities + * needed by the library. + * + * Usually you don't want to override the `order`, `fn` and `onLoad` props. + * All the other properties are configurations that could be tweaked. + * @namespace modifiers + */ + var modifiers = { + /** + * Modifier used to shift the popper on the start or end of its reference + * element.
+ * It will read the variation of the `placement` property.
+ * It can be one either `-end` or `-start`. + * @memberof modifiers + * @inner + */ + shift: { + /** @prop {number} order=100 - Index used to define the order of execution */ + order: 100, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: shift + }, + + /** + * The `offset` modifier can shift your popper on both its axis. + * + * It accepts the following units: + * - `px` or unit-less, interpreted as pixels + * - `%` or `%r`, percentage relative to the length of the reference element + * - `%p`, percentage relative to the length of the popper element + * - `vw`, CSS viewport width unit + * - `vh`, CSS viewport height unit + * + * For length is intended the main axis relative to the placement of the popper.
+ * This means that if the placement is `top` or `bottom`, the length will be the + * `width`. In case of `left` or `right`, it will be the `height`. + * + * You can provide a single value (as `Number` or `String`), or a pair of values + * as `String` divided by a comma or one (or more) white spaces.
+ * The latter is a deprecated method because it leads to confusion and will be + * removed in v2.
+ * Additionally, it accepts additions and subtractions between different units. + * Note that multiplications and divisions aren't supported. + * + * Valid examples are: + * ``` + * 10 + * '10%' + * '10, 10' + * '10%, 10' + * '10 + 10%' + * '10 - 5vh + 3%' + * '-10px + 5vh, 5px - 6%' + * ``` + * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap + * > with their reference element, unfortunately, you will have to disable the `flip` modifier. + * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373). + * + * @memberof modifiers + * @inner + */ + offset: { + /** @prop {number} order=200 - Index used to define the order of execution */ + order: 200, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: offset, + /** @prop {Number|String} offset=0 + * The offset value as described in the modifier description + */ + offset: 0 + }, + + /** + * Modifier used to prevent the popper from being positioned outside the boundary. + * + * A scenario exists where the reference itself is not within the boundaries.
+ * We can say it has "escaped the boundaries" — or just "escaped".
+ * In this case we need to decide whether the popper should either: + * + * - detach from the reference and remain "trapped" in the boundaries, or + * - if it should ignore the boundary and "escape with its reference" + * + * When `escapeWithReference` is set to`true` and reference is completely + * outside its boundaries, the popper will overflow (or completely leave) + * the boundaries in order to remain attached to the edge of the reference. + * + * @memberof modifiers + * @inner + */ + preventOverflow: { + /** @prop {number} order=300 - Index used to define the order of execution */ + order: 300, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: preventOverflow, + /** + * @prop {Array} [priority=['left','right','top','bottom']] + * Popper will try to prevent overflow following these priorities by default, + * then, it could overflow on the left and on top of the `boundariesElement` + */ + priority: ['left', 'right', 'top', 'bottom'], + /** + * @prop {number} padding=5 + * Amount of pixel used to define a minimum distance between the boundaries + * and the popper. This makes sure the popper always has a little padding + * between the edges of its container + */ + padding: 5, + /** + * @prop {String|HTMLElement} boundariesElement='scrollParent' + * Boundaries used by the modifier. Can be `scrollParent`, `window`, + * `viewport` or any DOM element. + */ + boundariesElement: 'scrollParent' + }, + + /** + * Modifier used to make sure the reference and its popper stay near each other + * without leaving any gap between the two. Especially useful when the arrow is + * enabled and you want to ensure that it points to its reference element. + * It cares only about the first axis. You can still have poppers with margin + * between the popper and its reference element. + * @memberof modifiers + * @inner + */ + keepTogether: { + /** @prop {number} order=400 - Index used to define the order of execution */ + order: 400, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: keepTogether + }, + + /** + * This modifier is used to move the `arrowElement` of the popper to make + * sure it is positioned between the reference element and its popper element. + * It will read the outer size of the `arrowElement` node to detect how many + * pixels of conjunction are needed. + * + * It has no effect if no `arrowElement` is provided. + * @memberof modifiers + * @inner + */ + arrow: { + /** @prop {number} order=500 - Index used to define the order of execution */ + order: 500, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: arrow, + /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */ + element: '[x-arrow]' + }, + + /** + * Modifier used to flip the popper's placement when it starts to overlap its + * reference element. + * + * Requires the `preventOverflow` modifier before it in order to work. + * + * **NOTE:** this modifier will interrupt the current update cycle and will + * restart it if it detects the need to flip the placement. + * @memberof modifiers + * @inner + */ + flip: { + /** @prop {number} order=600 - Index used to define the order of execution */ + order: 600, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: flip, + /** + * @prop {String|Array} behavior='flip' + * The behavior used to change the popper's placement. It can be one of + * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid + * placements (with optional variations) + */ + behavior: 'flip', + /** + * @prop {number} padding=5 + * The popper will flip if it hits the edges of the `boundariesElement` + */ + padding: 5, + /** + * @prop {String|HTMLElement} boundariesElement='viewport' + * The element which will define the boundaries of the popper position. + * The popper will never be placed outside of the defined boundaries + * (except if `keepTogether` is enabled) + */ + boundariesElement: 'viewport', + /** + * @prop {Boolean} flipVariations=false + * The popper will switch placement variation between `-start` and `-end` when + * the reference element overlaps its boundaries. + * + * The original placement should have a set variation. + */ + flipVariations: false, + /** + * @prop {Boolean} flipVariationsByContent=false + * The popper will switch placement variation between `-start` and `-end` when + * the popper element overlaps its reference boundaries. + * + * The original placement should have a set variation. + */ + flipVariationsByContent: false + }, + + /** + * Modifier used to make the popper flow toward the inner of the reference element. + * By default, when this modifier is disabled, the popper will be placed outside + * the reference element. + * @memberof modifiers + * @inner + */ + inner: { + /** @prop {number} order=700 - Index used to define the order of execution */ + order: 700, + /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */ + enabled: false, + /** @prop {ModifierFn} */ + fn: inner + }, + + /** + * Modifier used to hide the popper when its reference element is outside of the + * popper boundaries. It will set a `x-out-of-boundaries` attribute which can + * be used to hide with a CSS selector the popper when its reference is + * out of boundaries. + * + * Requires the `preventOverflow` modifier before it in order to work. + * @memberof modifiers + * @inner + */ + hide: { + /** @prop {number} order=800 - Index used to define the order of execution */ + order: 800, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: hide + }, + + /** + * Computes the style that will be applied to the popper element to gets + * properly positioned. + * + * Note that this modifier will not touch the DOM, it just prepares the styles + * so that `applyStyle` modifier can apply it. This separation is useful + * in case you need to replace `applyStyle` with a custom implementation. + * + * This modifier has `850` as `order` value to maintain backward compatibility + * with previous versions of Popper.js. Expect the modifiers ordering method + * to change in future major versions of the library. + * + * @memberof modifiers + * @inner + */ + computeStyle: { + /** @prop {number} order=850 - Index used to define the order of execution */ + order: 850, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: computeStyle, + /** + * @prop {Boolean} gpuAcceleration=true + * If true, it uses the CSS 3D transformation to position the popper. + * Otherwise, it will use the `top` and `left` properties + */ + gpuAcceleration: true, + /** + * @prop {string} [x='bottom'] + * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin. + * Change this if your popper should grow in a direction different from `bottom` + */ + x: 'bottom', + /** + * @prop {string} [x='left'] + * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin. + * Change this if your popper should grow in a direction different from `right` + */ + y: 'right' + }, + + /** + * Applies the computed styles to the popper element. + * + * All the DOM manipulations are limited to this modifier. This is useful in case + * you want to integrate Popper.js inside a framework or view library and you + * want to delegate all the DOM manipulations to it. + * + * Note that if you disable this modifier, you must make sure the popper element + * has its position set to `absolute` before Popper.js can do its work! + * + * Just disable this modifier and define your own to achieve the desired effect. + * + * @memberof modifiers + * @inner + */ + applyStyle: { + /** @prop {number} order=900 - Index used to define the order of execution */ + order: 900, + /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */ + enabled: true, + /** @prop {ModifierFn} */ + fn: applyStyle, + /** @prop {Function} */ + onLoad: applyStyleOnLoad, + /** + * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier + * @prop {Boolean} gpuAcceleration=true + * If true, it uses the CSS 3D transformation to position the popper. + * Otherwise, it will use the `top` and `left` properties + */ + gpuAcceleration: undefined + } + }; + + /** + * The `dataObject` is an object containing all the information used by Popper.js. + * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks. + * @name dataObject + * @property {Object} data.instance The Popper.js instance + * @property {String} data.placement Placement applied to popper + * @property {String} data.originalPlacement Placement originally defined on init + * @property {Boolean} data.flipped True if popper has been flipped by flip modifier + * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper + * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier + * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`) + * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`) + * @property {Object} data.boundaries Offsets of the popper boundaries + * @property {Object} data.offsets The measurements of popper, reference and arrow elements + * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values + * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values + * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0 + */ + + /** + * Default options provided to Popper.js constructor.
+ * These can be overridden using the `options` argument of Popper.js.
+ * To override an option, simply pass an object with the same + * structure of the `options` object, as the 3rd argument. For example: + * ``` + * new Popper(ref, pop, { + * modifiers: { + * preventOverflow: { enabled: false } + * } + * }) + * ``` + * @type {Object} + * @static + * @memberof Popper + */ + var Defaults = { + /** + * Popper's placement. + * @prop {Popper.placements} placement='bottom' + */ + placement: 'bottom', + + /** + * Set this to true if you want popper to position it self in 'fixed' mode + * @prop {Boolean} positionFixed=false + */ + positionFixed: false, + + /** + * Whether events (resize, scroll) are initially enabled. + * @prop {Boolean} eventsEnabled=true + */ + eventsEnabled: true, + + /** + * Set to true if you want to automatically remove the popper when + * you call the `destroy` method. + * @prop {Boolean} removeOnDestroy=false + */ + removeOnDestroy: false, + + /** + * Callback called when the popper is created.
+ * By default, it is set to no-op.
+ * Access Popper.js instance with `data.instance`. + * @prop {onCreate} + */ + onCreate: function onCreate() {}, + + /** + * Callback called when the popper is updated. This callback is not called + * on the initialization/creation of the popper, but only on subsequent + * updates.
+ * By default, it is set to no-op.
+ * Access Popper.js instance with `data.instance`. + * @prop {onUpdate} + */ + onUpdate: function onUpdate() {}, + + /** + * List of modifiers used to modify the offsets before they are applied to the popper. + * They provide most of the functionalities of Popper.js. + * @prop {modifiers} + */ + modifiers: modifiers + }; + + /** + * @callback onCreate + * @param {dataObject} data + */ + + /** + * @callback onUpdate + * @param {dataObject} data + */ + + // Utils + // Methods + var Popper = function () { + /** + * Creates a new Popper.js instance. + * @class Popper + * @param {Element|referenceObject} reference - The reference element used to position the popper + * @param {Element} popper - The HTML / XML element used as the popper + * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults) + * @return {Object} instance - The generated Popper.js instance + */ + function Popper(reference, popper) { + var _this = this; + + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + classCallCheck(this, Popper); + + this.scheduleUpdate = function () { + return requestAnimationFrame(_this.update); + }; + + // make update() debounced, so that it only runs at most once-per-tick + this.update = debounce(this.update.bind(this)); + + // with {} we create a new object with the options inside it + this.options = _extends({}, Popper.Defaults, options); + + // init state + this.state = { + isDestroyed: false, + isCreated: false, + scrollParents: [] + }; + + // get reference and popper elements (allow jQuery wrappers) + this.reference = reference && reference.jquery ? reference[0] : reference; + this.popper = popper && popper.jquery ? popper[0] : popper; + + // Deep merge modifiers options + this.options.modifiers = {}; + Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) { + _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {}); + }); + + // Refactoring modifiers' list (Object => Array) + this.modifiers = Object.keys(this.options.modifiers).map(function (name) { + return _extends({ + name: name + }, _this.options.modifiers[name]); + }) + // sort the modifiers by order + .sort(function (a, b) { + return a.order - b.order; + }); + + // modifiers have the ability to execute arbitrary code when Popper.js get inited + // such code is executed in the same order of its modifier + // they could add new properties to their options configuration + // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`! + this.modifiers.forEach(function (modifierOptions) { + if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) { + modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state); + } + }); + + // fire the first update to position the popper in the right place + this.update(); + + var eventsEnabled = this.options.eventsEnabled; + if (eventsEnabled) { + // setup event listeners, they will take care of update the position in specific situations + this.enableEventListeners(); + } + + this.state.eventsEnabled = eventsEnabled; + } + + // We can't use class properties because they don't get listed in the + // class prototype and break stuff like Sinon stubs + + + createClass(Popper, [{ + key: 'update', + value: function update$$1() { + return update.call(this); + } + }, { + key: 'destroy', + value: function destroy$$1() { + return destroy.call(this); + } + }, { + key: 'enableEventListeners', + value: function enableEventListeners$$1() { + return enableEventListeners.call(this); + } + }, { + key: 'disableEventListeners', + value: function disableEventListeners$$1() { + return disableEventListeners.call(this); + } + + /** + * Schedules an update. It will run on the next UI update available. + * @method scheduleUpdate + * @memberof Popper + */ + + + /** + * Collection of utilities useful when writing custom modifiers. + * Starting from version 1.7, this method is available only if you + * include `popper-utils.js` before `popper.js`. + * + * **DEPRECATION**: This way to access PopperUtils is deprecated + * and will be removed in v2! Use the PopperUtils module directly instead. + * Due to the high instability of the methods contained in Utils, we can't + * guarantee them to follow semver. Use them at your own risk! + * @static + * @private + * @type {Object} + * @deprecated since version 1.8 + * @member Utils + * @memberof Popper + */ + + }]); + return Popper; + }(); + + /** + * The `referenceObject` is an object that provides an interface compatible with Popper.js + * and lets you use it as replacement of a real DOM node.
+ * You can use this method to position a popper relatively to a set of coordinates + * in case you don't have a DOM node to use as reference. + * + * ``` + * new Popper(referenceObject, popperNode); + * ``` + * + * NB: This feature isn't supported in Internet Explorer 10. + * @name referenceObject + * @property {Function} data.getBoundingClientRect + * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method. + * @property {number} data.clientWidth + * An ES6 getter that will return the width of the virtual reference element. + * @property {number} data.clientHeight + * An ES6 getter that will return the height of the virtual reference element. + */ + + + Popper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils; + Popper.placements = placements; + Popper.Defaults = Defaults; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$4 = 'dropdown'; + var VERSION$4 = '4.4.1'; + var DATA_KEY$4 = 'bs.dropdown'; + var EVENT_KEY$4 = "." + DATA_KEY$4; + var DATA_API_KEY$4 = '.data-api'; + var JQUERY_NO_CONFLICT$4 = $.fn[NAME$4]; + var ESCAPE_KEYCODE = 27; // KeyboardEvent.which value for Escape (Esc) key + + var SPACE_KEYCODE = 32; // KeyboardEvent.which value for space key + + var TAB_KEYCODE = 9; // KeyboardEvent.which value for tab key + + var ARROW_UP_KEYCODE = 38; // KeyboardEvent.which value for up arrow key + + var ARROW_DOWN_KEYCODE = 40; // KeyboardEvent.which value for down arrow key + + var RIGHT_MOUSE_BUTTON_WHICH = 3; // MouseEvent.which value for the right button (assuming a right-handed mouse) + + var REGEXP_KEYDOWN = new RegExp(ARROW_UP_KEYCODE + "|" + ARROW_DOWN_KEYCODE + "|" + ESCAPE_KEYCODE); + var Event$4 = { + HIDE: "hide" + EVENT_KEY$4, + HIDDEN: "hidden" + EVENT_KEY$4, + SHOW: "show" + EVENT_KEY$4, + SHOWN: "shown" + EVENT_KEY$4, + CLICK: "click" + EVENT_KEY$4, + CLICK_DATA_API: "click" + EVENT_KEY$4 + DATA_API_KEY$4, + KEYDOWN_DATA_API: "keydown" + EVENT_KEY$4 + DATA_API_KEY$4, + KEYUP_DATA_API: "keyup" + EVENT_KEY$4 + DATA_API_KEY$4 + }; + var ClassName$4 = { + DISABLED: 'disabled', + SHOW: 'show', + DROPUP: 'dropup', + DROPRIGHT: 'dropright', + DROPLEFT: 'dropleft', + MENURIGHT: 'dropdown-menu-right', + MENULEFT: 'dropdown-menu-left', + POSITION_STATIC: 'position-static' + }; + var Selector$4 = { + DATA_TOGGLE: '[data-toggle="dropdown"]', + FORM_CHILD: '.dropdown form', + MENU: '.dropdown-menu', + NAVBAR_NAV: '.navbar-nav', + VISIBLE_ITEMS: '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)' + }; + var AttachmentMap = { + TOP: 'top-start', + TOPEND: 'top-end', + BOTTOM: 'bottom-start', + BOTTOMEND: 'bottom-end', + RIGHT: 'right-start', + RIGHTEND: 'right-end', + LEFT: 'left-start', + LEFTEND: 'left-end' + }; + var Default$2 = { + offset: 0, + flip: true, + boundary: 'scrollParent', + reference: 'toggle', + display: 'dynamic', + popperConfig: null + }; + var DefaultType$2 = { + offset: '(number|string|function)', + flip: 'boolean', + boundary: '(string|element)', + reference: '(string|element)', + display: 'string', + popperConfig: '(null|object)' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Dropdown = + /*#__PURE__*/ + function () { + function Dropdown(element, config) { + this._element = element; + this._popper = null; + this._config = this._getConfig(config); + this._menu = this._getMenuElement(); + this._inNavbar = this._detectNavbar(); + + this._addEventListeners(); + } // Getters + + + var _proto = Dropdown.prototype; + + // Public + _proto.toggle = function toggle() { + if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED)) { + return; + } + + var isActive = $(this._menu).hasClass(ClassName$4.SHOW); + + Dropdown._clearMenus(); + + if (isActive) { + return; + } + + this.show(true); + }; + + _proto.show = function show(usePopper) { + if (usePopper === void 0) { + usePopper = false; + } + + if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || $(this._menu).hasClass(ClassName$4.SHOW)) { + return; + } + + var relatedTarget = { + relatedTarget: this._element + }; + var showEvent = $.Event(Event$4.SHOW, relatedTarget); + + var parent = Dropdown._getParentFromElement(this._element); + + $(parent).trigger(showEvent); + + if (showEvent.isDefaultPrevented()) { + return; + } // Disable totally Popper.js for Dropdown in Navbar + + + if (!this._inNavbar && usePopper) { + /** + * Check for Popper dependency + * Popper - https://popper.js.org + */ + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)'); + } + + var referenceElement = this._element; + + if (this._config.reference === 'parent') { + referenceElement = parent; + } else if (Util.isElement(this._config.reference)) { + referenceElement = this._config.reference; // Check if it's jQuery element + + if (typeof this._config.reference.jquery !== 'undefined') { + referenceElement = this._config.reference[0]; + } + } // If boundary is not `scrollParent`, then set position to `static` + // to allow the menu to "escape" the scroll parent's boundaries + // https://github.com/twbs/bootstrap/issues/24251 + + + if (this._config.boundary !== 'scrollParent') { + $(parent).addClass(ClassName$4.POSITION_STATIC); + } + + this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig()); + } // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + + + if ('ontouchstart' in document.documentElement && $(parent).closest(Selector$4.NAVBAR_NAV).length === 0) { + $(document.body).children().on('mouseover', null, $.noop); + } + + this._element.focus(); + + this._element.setAttribute('aria-expanded', true); + + $(this._menu).toggleClass(ClassName$4.SHOW); + $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.SHOWN, relatedTarget)); + }; + + _proto.hide = function hide() { + if (this._element.disabled || $(this._element).hasClass(ClassName$4.DISABLED) || !$(this._menu).hasClass(ClassName$4.SHOW)) { + return; + } + + var relatedTarget = { + relatedTarget: this._element + }; + var hideEvent = $.Event(Event$4.HIDE, relatedTarget); + + var parent = Dropdown._getParentFromElement(this._element); + + $(parent).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + return; + } + + if (this._popper) { + this._popper.destroy(); + } + + $(this._menu).toggleClass(ClassName$4.SHOW); + $(parent).toggleClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget)); + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$4); + $(this._element).off(EVENT_KEY$4); + this._element = null; + this._menu = null; + + if (this._popper !== null) { + this._popper.destroy(); + + this._popper = null; + } + }; + + _proto.update = function update() { + this._inNavbar = this._detectNavbar(); + + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + } // Private + ; + + _proto._addEventListeners = function _addEventListeners() { + var _this = this; + + $(this._element).on(Event$4.CLICK, function (event) { + event.preventDefault(); + event.stopPropagation(); + + _this.toggle(); + }); + }; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, this.constructor.Default, {}, $(this._element).data(), {}, config); + Util.typeCheckConfig(NAME$4, config, this.constructor.DefaultType); + return config; + }; + + _proto._getMenuElement = function _getMenuElement() { + if (!this._menu) { + var parent = Dropdown._getParentFromElement(this._element); + + if (parent) { + this._menu = parent.querySelector(Selector$4.MENU); + } + } + + return this._menu; + }; + + _proto._getPlacement = function _getPlacement() { + var $parentDropdown = $(this._element.parentNode); + var placement = AttachmentMap.BOTTOM; // Handle dropup + + if ($parentDropdown.hasClass(ClassName$4.DROPUP)) { + placement = AttachmentMap.TOP; + + if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) { + placement = AttachmentMap.TOPEND; + } + } else if ($parentDropdown.hasClass(ClassName$4.DROPRIGHT)) { + placement = AttachmentMap.RIGHT; + } else if ($parentDropdown.hasClass(ClassName$4.DROPLEFT)) { + placement = AttachmentMap.LEFT; + } else if ($(this._menu).hasClass(ClassName$4.MENURIGHT)) { + placement = AttachmentMap.BOTTOMEND; + } + + return placement; + }; + + _proto._detectNavbar = function _detectNavbar() { + return $(this._element).closest('.navbar').length > 0; + }; + + _proto._getOffset = function _getOffset() { + var _this2 = this; + + var offset = {}; + + if (typeof this._config.offset === 'function') { + offset.fn = function (data) { + data.offsets = _objectSpread2({}, data.offsets, {}, _this2._config.offset(data.offsets, _this2._element) || {}); + return data; + }; + } else { + offset.offset = this._config.offset; + } + + return offset; + }; + + _proto._getPopperConfig = function _getPopperConfig() { + var popperConfig = { + placement: this._getPlacement(), + modifiers: { + offset: this._getOffset(), + flip: { + enabled: this._config.flip + }, + preventOverflow: { + boundariesElement: this._config.boundary + } + } + }; // Disable Popper.js if we have a static display + + if (this._config.display === 'static') { + popperConfig.modifiers.applyStyle = { + enabled: false + }; + } + + return _objectSpread2({}, popperConfig, {}, this._config.popperConfig); + } // Static + ; + + Dropdown._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$4); + + var _config = typeof config === 'object' ? config : null; + + if (!data) { + data = new Dropdown(this, _config); + $(this).data(DATA_KEY$4, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + Dropdown._clearMenus = function _clearMenus(event) { + if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) { + return; + } + + var toggles = [].slice.call(document.querySelectorAll(Selector$4.DATA_TOGGLE)); + + for (var i = 0, len = toggles.length; i < len; i++) { + var parent = Dropdown._getParentFromElement(toggles[i]); + + var context = $(toggles[i]).data(DATA_KEY$4); + var relatedTarget = { + relatedTarget: toggles[i] + }; + + if (event && event.type === 'click') { + relatedTarget.clickEvent = event; + } + + if (!context) { + continue; + } + + var dropdownMenu = context._menu; + + if (!$(parent).hasClass(ClassName$4.SHOW)) { + continue; + } + + if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) { + continue; + } + + var hideEvent = $.Event(Event$4.HIDE, relatedTarget); + $(parent).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + continue; + } // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + + + if ('ontouchstart' in document.documentElement) { + $(document.body).children().off('mouseover', null, $.noop); + } + + toggles[i].setAttribute('aria-expanded', 'false'); + + if (context._popper) { + context._popper.destroy(); + } + + $(dropdownMenu).removeClass(ClassName$4.SHOW); + $(parent).removeClass(ClassName$4.SHOW).trigger($.Event(Event$4.HIDDEN, relatedTarget)); + } + }; + + Dropdown._getParentFromElement = function _getParentFromElement(element) { + var parent; + var selector = Util.getSelectorFromElement(element); + + if (selector) { + parent = document.querySelector(selector); + } + + return parent || element.parentNode; + } // eslint-disable-next-line complexity + ; + + Dropdown._dataApiKeydownHandler = function _dataApiKeydownHandler(event) { + // If not input/textarea: + // - And not a key in REGEXP_KEYDOWN => not a dropdown command + // If input/textarea: + // - If space key => not a dropdown command + // - If key is other than escape + // - If key is not up or down => not a dropdown command + // - If trigger inside the menu => not a dropdown command + if (/input|textarea/i.test(event.target.tagName) ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE && (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE || $(event.target).closest(Selector$4.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (this.disabled || $(this).hasClass(ClassName$4.DISABLED)) { + return; + } + + var parent = Dropdown._getParentFromElement(this); + + var isActive = $(parent).hasClass(ClassName$4.SHOW); + + if (!isActive && event.which === ESCAPE_KEYCODE) { + return; + } + + if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) { + if (event.which === ESCAPE_KEYCODE) { + var toggle = parent.querySelector(Selector$4.DATA_TOGGLE); + $(toggle).trigger('focus'); + } + + $(this).trigger('click'); + return; + } + + var items = [].slice.call(parent.querySelectorAll(Selector$4.VISIBLE_ITEMS)).filter(function (item) { + return $(item).is(':visible'); + }); + + if (items.length === 0) { + return; + } + + var index = items.indexOf(event.target); + + if (event.which === ARROW_UP_KEYCODE && index > 0) { + // Up + index--; + } + + if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { + // Down + index++; + } + + if (index < 0) { + index = 0; + } + + items[index].focus(); + }; + + _createClass(Dropdown, null, [{ + key: "VERSION", + get: function get() { + return VERSION$4; + } + }, { + key: "Default", + get: function get() { + return Default$2; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType$2; + } + }]); + + return Dropdown; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$4.KEYDOWN_DATA_API, Selector$4.DATA_TOGGLE, Dropdown._dataApiKeydownHandler).on(Event$4.KEYDOWN_DATA_API, Selector$4.MENU, Dropdown._dataApiKeydownHandler).on(Event$4.CLICK_DATA_API + " " + Event$4.KEYUP_DATA_API, Dropdown._clearMenus).on(Event$4.CLICK_DATA_API, Selector$4.DATA_TOGGLE, function (event) { + event.preventDefault(); + event.stopPropagation(); + + Dropdown._jQueryInterface.call($(this), 'toggle'); + }).on(Event$4.CLICK_DATA_API, Selector$4.FORM_CHILD, function (e) { + e.stopPropagation(); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$4] = Dropdown._jQueryInterface; + $.fn[NAME$4].Constructor = Dropdown; + + $.fn[NAME$4].noConflict = function () { + $.fn[NAME$4] = JQUERY_NO_CONFLICT$4; + return Dropdown._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$5 = 'modal'; + var VERSION$5 = '4.4.1'; + var DATA_KEY$5 = 'bs.modal'; + var EVENT_KEY$5 = "." + DATA_KEY$5; + var DATA_API_KEY$5 = '.data-api'; + var JQUERY_NO_CONFLICT$5 = $.fn[NAME$5]; + var ESCAPE_KEYCODE$1 = 27; // KeyboardEvent.which value for Escape (Esc) key + + var Default$3 = { + backdrop: true, + keyboard: true, + focus: true, + show: true + }; + var DefaultType$3 = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + focus: 'boolean', + show: 'boolean' + }; + var Event$5 = { + HIDE: "hide" + EVENT_KEY$5, + HIDE_PREVENTED: "hidePrevented" + EVENT_KEY$5, + HIDDEN: "hidden" + EVENT_KEY$5, + SHOW: "show" + EVENT_KEY$5, + SHOWN: "shown" + EVENT_KEY$5, + FOCUSIN: "focusin" + EVENT_KEY$5, + RESIZE: "resize" + EVENT_KEY$5, + CLICK_DISMISS: "click.dismiss" + EVENT_KEY$5, + KEYDOWN_DISMISS: "keydown.dismiss" + EVENT_KEY$5, + MOUSEUP_DISMISS: "mouseup.dismiss" + EVENT_KEY$5, + MOUSEDOWN_DISMISS: "mousedown.dismiss" + EVENT_KEY$5, + CLICK_DATA_API: "click" + EVENT_KEY$5 + DATA_API_KEY$5 + }; + var ClassName$5 = { + SCROLLABLE: 'modal-dialog-scrollable', + SCROLLBAR_MEASURER: 'modal-scrollbar-measure', + BACKDROP: 'modal-backdrop', + OPEN: 'modal-open', + FADE: 'fade', + SHOW: 'show', + STATIC: 'modal-static' + }; + var Selector$5 = { + DIALOG: '.modal-dialog', + MODAL_BODY: '.modal-body', + DATA_TOGGLE: '[data-toggle="modal"]', + DATA_DISMISS: '[data-dismiss="modal"]', + FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top', + STICKY_CONTENT: '.sticky-top' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Modal = + /*#__PURE__*/ + function () { + function Modal(element, config) { + this._config = this._getConfig(config); + this._element = element; + this._dialog = element.querySelector(Selector$5.DIALOG); + this._backdrop = null; + this._isShown = false; + this._isBodyOverflowing = false; + this._ignoreBackdropClick = false; + this._isTransitioning = false; + this._scrollbarWidth = 0; + } // Getters + + + var _proto = Modal.prototype; + + // Public + _proto.toggle = function toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget); + }; + + _proto.show = function show(relatedTarget) { + var _this = this; + + if (this._isShown || this._isTransitioning) { + return; + } + + if ($(this._element).hasClass(ClassName$5.FADE)) { + this._isTransitioning = true; + } + + var showEvent = $.Event(Event$5.SHOW, { + relatedTarget: relatedTarget + }); + $(this._element).trigger(showEvent); + + if (this._isShown || showEvent.isDefaultPrevented()) { + return; + } + + this._isShown = true; + + this._checkScrollbar(); + + this._setScrollbar(); + + this._adjustDialog(); + + this._setEscapeEvent(); + + this._setResizeEvent(); + + $(this._element).on(Event$5.CLICK_DISMISS, Selector$5.DATA_DISMISS, function (event) { + return _this.hide(event); + }); + $(this._dialog).on(Event$5.MOUSEDOWN_DISMISS, function () { + $(_this._element).one(Event$5.MOUSEUP_DISMISS, function (event) { + if ($(event.target).is(_this._element)) { + _this._ignoreBackdropClick = true; + } + }); + }); + + this._showBackdrop(function () { + return _this._showElement(relatedTarget); + }); + }; + + _proto.hide = function hide(event) { + var _this2 = this; + + if (event) { + event.preventDefault(); + } + + if (!this._isShown || this._isTransitioning) { + return; + } + + var hideEvent = $.Event(Event$5.HIDE); + $(this._element).trigger(hideEvent); + + if (!this._isShown || hideEvent.isDefaultPrevented()) { + return; + } + + this._isShown = false; + var transition = $(this._element).hasClass(ClassName$5.FADE); + + if (transition) { + this._isTransitioning = true; + } + + this._setEscapeEvent(); + + this._setResizeEvent(); + + $(document).off(Event$5.FOCUSIN); + $(this._element).removeClass(ClassName$5.SHOW); + $(this._element).off(Event$5.CLICK_DISMISS); + $(this._dialog).off(Event$5.MOUSEDOWN_DISMISS); + + if (transition) { + var transitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, function (event) { + return _this2._hideModal(event); + }).emulateTransitionEnd(transitionDuration); + } else { + this._hideModal(); + } + }; + + _proto.dispose = function dispose() { + [window, this._element, this._dialog].forEach(function (htmlElement) { + return $(htmlElement).off(EVENT_KEY$5); + }); + /** + * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API` + * Do not move `document` in `htmlElements` array + * It will remove `Event.CLICK_DATA_API` event that should remain + */ + + $(document).off(Event$5.FOCUSIN); + $.removeData(this._element, DATA_KEY$5); + this._config = null; + this._element = null; + this._dialog = null; + this._backdrop = null; + this._isShown = null; + this._isBodyOverflowing = null; + this._ignoreBackdropClick = null; + this._isTransitioning = null; + this._scrollbarWidth = null; + }; + + _proto.handleUpdate = function handleUpdate() { + this._adjustDialog(); + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default$3, {}, config); + Util.typeCheckConfig(NAME$5, config, DefaultType$3); + return config; + }; + + _proto._triggerBackdropTransition = function _triggerBackdropTransition() { + var _this3 = this; + + if (this._config.backdrop === 'static') { + var hideEventPrevented = $.Event(Event$5.HIDE_PREVENTED); + $(this._element).trigger(hideEventPrevented); + + if (hideEventPrevented.defaultPrevented) { + return; + } + + this._element.classList.add(ClassName$5.STATIC); + + var modalTransitionDuration = Util.getTransitionDurationFromElement(this._element); + $(this._element).one(Util.TRANSITION_END, function () { + _this3._element.classList.remove(ClassName$5.STATIC); + }).emulateTransitionEnd(modalTransitionDuration); + + this._element.focus(); + } else { + this.hide(); + } + }; + + _proto._showElement = function _showElement(relatedTarget) { + var _this4 = this; + + var transition = $(this._element).hasClass(ClassName$5.FADE); + var modalBody = this._dialog ? this._dialog.querySelector(Selector$5.MODAL_BODY) : null; + + if (!this._element.parentNode || this._element.parentNode.nodeType !== Node.ELEMENT_NODE) { + // Don't move modal's DOM position + document.body.appendChild(this._element); + } + + this._element.style.display = 'block'; + + this._element.removeAttribute('aria-hidden'); + + this._element.setAttribute('aria-modal', true); + + if ($(this._dialog).hasClass(ClassName$5.SCROLLABLE) && modalBody) { + modalBody.scrollTop = 0; + } else { + this._element.scrollTop = 0; + } + + if (transition) { + Util.reflow(this._element); + } + + $(this._element).addClass(ClassName$5.SHOW); + + if (this._config.focus) { + this._enforceFocus(); + } + + var shownEvent = $.Event(Event$5.SHOWN, { + relatedTarget: relatedTarget + }); + + var transitionComplete = function transitionComplete() { + if (_this4._config.focus) { + _this4._element.focus(); + } + + _this4._isTransitioning = false; + $(_this4._element).trigger(shownEvent); + }; + + if (transition) { + var transitionDuration = Util.getTransitionDurationFromElement(this._dialog); + $(this._dialog).one(Util.TRANSITION_END, transitionComplete).emulateTransitionEnd(transitionDuration); + } else { + transitionComplete(); + } + }; + + _proto._enforceFocus = function _enforceFocus() { + var _this5 = this; + + $(document).off(Event$5.FOCUSIN) // Guard against infinite focus loop + .on(Event$5.FOCUSIN, function (event) { + if (document !== event.target && _this5._element !== event.target && $(_this5._element).has(event.target).length === 0) { + _this5._element.focus(); + } + }); + }; + + _proto._setEscapeEvent = function _setEscapeEvent() { + var _this6 = this; + + if (this._isShown && this._config.keyboard) { + $(this._element).on(Event$5.KEYDOWN_DISMISS, function (event) { + if (event.which === ESCAPE_KEYCODE$1) { + _this6._triggerBackdropTransition(); + } + }); + } else if (!this._isShown) { + $(this._element).off(Event$5.KEYDOWN_DISMISS); + } + }; + + _proto._setResizeEvent = function _setResizeEvent() { + var _this7 = this; + + if (this._isShown) { + $(window).on(Event$5.RESIZE, function (event) { + return _this7.handleUpdate(event); + }); + } else { + $(window).off(Event$5.RESIZE); + } + }; + + _proto._hideModal = function _hideModal() { + var _this8 = this; + + this._element.style.display = 'none'; + + this._element.setAttribute('aria-hidden', true); + + this._element.removeAttribute('aria-modal'); + + this._isTransitioning = false; + + this._showBackdrop(function () { + $(document.body).removeClass(ClassName$5.OPEN); + + _this8._resetAdjustments(); + + _this8._resetScrollbar(); + + $(_this8._element).trigger(Event$5.HIDDEN); + }); + }; + + _proto._removeBackdrop = function _removeBackdrop() { + if (this._backdrop) { + $(this._backdrop).remove(); + this._backdrop = null; + } + }; + + _proto._showBackdrop = function _showBackdrop(callback) { + var _this9 = this; + + var animate = $(this._element).hasClass(ClassName$5.FADE) ? ClassName$5.FADE : ''; + + if (this._isShown && this._config.backdrop) { + this._backdrop = document.createElement('div'); + this._backdrop.className = ClassName$5.BACKDROP; + + if (animate) { + this._backdrop.classList.add(animate); + } + + $(this._backdrop).appendTo(document.body); + $(this._element).on(Event$5.CLICK_DISMISS, function (event) { + if (_this9._ignoreBackdropClick) { + _this9._ignoreBackdropClick = false; + return; + } + + if (event.target !== event.currentTarget) { + return; + } + + _this9._triggerBackdropTransition(); + }); + + if (animate) { + Util.reflow(this._backdrop); + } + + $(this._backdrop).addClass(ClassName$5.SHOW); + + if (!callback) { + return; + } + + if (!animate) { + callback(); + return; + } + + var backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop); + $(this._backdrop).one(Util.TRANSITION_END, callback).emulateTransitionEnd(backdropTransitionDuration); + } else if (!this._isShown && this._backdrop) { + $(this._backdrop).removeClass(ClassName$5.SHOW); + + var callbackRemove = function callbackRemove() { + _this9._removeBackdrop(); + + if (callback) { + callback(); + } + }; + + if ($(this._element).hasClass(ClassName$5.FADE)) { + var _backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop); + + $(this._backdrop).one(Util.TRANSITION_END, callbackRemove).emulateTransitionEnd(_backdropTransitionDuration); + } else { + callbackRemove(); + } + } else if (callback) { + callback(); + } + } // ---------------------------------------------------------------------- + // the following methods are used to handle overflowing modals + // todo (fat): these should probably be refactored out of modal.js + // ---------------------------------------------------------------------- + ; + + _proto._adjustDialog = function _adjustDialog() { + var isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; + + if (!this._isBodyOverflowing && isModalOverflowing) { + this._element.style.paddingLeft = this._scrollbarWidth + "px"; + } + + if (this._isBodyOverflowing && !isModalOverflowing) { + this._element.style.paddingRight = this._scrollbarWidth + "px"; + } + }; + + _proto._resetAdjustments = function _resetAdjustments() { + this._element.style.paddingLeft = ''; + this._element.style.paddingRight = ''; + }; + + _proto._checkScrollbar = function _checkScrollbar() { + var rect = document.body.getBoundingClientRect(); + this._isBodyOverflowing = rect.left + rect.right < window.innerWidth; + this._scrollbarWidth = this._getScrollbarWidth(); + }; + + _proto._setScrollbar = function _setScrollbar() { + var _this10 = this; + + if (this._isBodyOverflowing) { + // Note: DOMNode.style.paddingRight returns the actual value or '' if not set + // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set + var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT)); + var stickyContent = [].slice.call(document.querySelectorAll(Selector$5.STICKY_CONTENT)); // Adjust fixed content padding + + $(fixedContent).each(function (index, element) { + var actualPadding = element.style.paddingRight; + var calculatedPadding = $(element).css('padding-right'); + $(element).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + _this10._scrollbarWidth + "px"); + }); // Adjust sticky content margin + + $(stickyContent).each(function (index, element) { + var actualMargin = element.style.marginRight; + var calculatedMargin = $(element).css('margin-right'); + $(element).data('margin-right', actualMargin).css('margin-right', parseFloat(calculatedMargin) - _this10._scrollbarWidth + "px"); + }); // Adjust body padding + + var actualPadding = document.body.style.paddingRight; + var calculatedPadding = $(document.body).css('padding-right'); + $(document.body).data('padding-right', actualPadding).css('padding-right', parseFloat(calculatedPadding) + this._scrollbarWidth + "px"); + } + + $(document.body).addClass(ClassName$5.OPEN); + }; + + _proto._resetScrollbar = function _resetScrollbar() { + // Restore fixed content padding + var fixedContent = [].slice.call(document.querySelectorAll(Selector$5.FIXED_CONTENT)); + $(fixedContent).each(function (index, element) { + var padding = $(element).data('padding-right'); + $(element).removeData('padding-right'); + element.style.paddingRight = padding ? padding : ''; + }); // Restore sticky content + + var elements = [].slice.call(document.querySelectorAll("" + Selector$5.STICKY_CONTENT)); + $(elements).each(function (index, element) { + var margin = $(element).data('margin-right'); + + if (typeof margin !== 'undefined') { + $(element).css('margin-right', margin).removeData('margin-right'); + } + }); // Restore body padding + + var padding = $(document.body).data('padding-right'); + $(document.body).removeData('padding-right'); + document.body.style.paddingRight = padding ? padding : ''; + }; + + _proto._getScrollbarWidth = function _getScrollbarWidth() { + // thx d.walsh + var scrollDiv = document.createElement('div'); + scrollDiv.className = ClassName$5.SCROLLBAR_MEASURER; + document.body.appendChild(scrollDiv); + var scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; + } // Static + ; + + Modal._jQueryInterface = function _jQueryInterface(config, relatedTarget) { + return this.each(function () { + var data = $(this).data(DATA_KEY$5); + + var _config = _objectSpread2({}, Default$3, {}, $(this).data(), {}, typeof config === 'object' && config ? config : {}); + + if (!data) { + data = new Modal(this, _config); + $(this).data(DATA_KEY$5, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](relatedTarget); + } else if (_config.show) { + data.show(relatedTarget); + } + }); + }; + + _createClass(Modal, null, [{ + key: "VERSION", + get: function get() { + return VERSION$5; + } + }, { + key: "Default", + get: function get() { + return Default$3; + } + }]); + + return Modal; + }(); + /** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + + + $(document).on(Event$5.CLICK_DATA_API, Selector$5.DATA_TOGGLE, function (event) { + var _this11 = this; + + var target; + var selector = Util.getSelectorFromElement(this); + + if (selector) { + target = document.querySelector(selector); + } + + var config = $(target).data(DATA_KEY$5) ? 'toggle' : _objectSpread2({}, $(target).data(), {}, $(this).data()); + + if (this.tagName === 'A' || this.tagName === 'AREA') { + event.preventDefault(); + } + + var $target = $(target).one(Event$5.SHOW, function (showEvent) { + if (showEvent.isDefaultPrevented()) { + // Only register focus restorer if modal will actually get shown + return; + } + + $target.one(Event$5.HIDDEN, function () { + if ($(_this11).is(':visible')) { + _this11.focus(); + } + }); + }); + + Modal._jQueryInterface.call($(target), config, this); + }); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + $.fn[NAME$5] = Modal._jQueryInterface; + $.fn[NAME$5].Constructor = Modal; + + $.fn[NAME$5].noConflict = function () { + $.fn[NAME$5] = JQUERY_NO_CONFLICT$5; + return Modal._jQueryInterface; + }; + + /** + * -------------------------------------------------------------------------- + * Bootstrap (v4.4.1): tools/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + var uriAttrs = ['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']; + var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; + var DefaultWhitelist = { + // Global attributes allowed on any supplied element below. + '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], + a: ['target', 'href', 'title', 'rel'], + area: [], + b: [], + br: [], + col: [], + code: [], + div: [], + em: [], + hr: [], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + i: [], + img: ['src', 'alt', 'title', 'width', 'height'], + li: [], + ol: [], + p: [], + pre: [], + s: [], + small: [], + span: [], + sub: [], + sup: [], + strong: [], + u: [], + ul: [] + }; + /** + * A pattern that recognizes a commonly useful subset of URLs that are safe. + * + * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts + */ + + var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi; + /** + * A pattern that matches safe data URLs. Only matches image, video and audio types. + * + * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts + */ + + var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; + + function allowedAttribute(attr, allowedAttributeList) { + var attrName = attr.nodeName.toLowerCase(); + + if (allowedAttributeList.indexOf(attrName) !== -1) { + if (uriAttrs.indexOf(attrName) !== -1) { + return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN)); + } + + return true; + } + + var regExp = allowedAttributeList.filter(function (attrRegex) { + return attrRegex instanceof RegExp; + }); // Check if a regular expression validates the attribute. + + for (var i = 0, l = regExp.length; i < l; i++) { + if (attrName.match(regExp[i])) { + return true; + } + } + + return false; + } + + function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { + if (unsafeHtml.length === 0) { + return unsafeHtml; + } + + if (sanitizeFn && typeof sanitizeFn === 'function') { + return sanitizeFn(unsafeHtml); + } + + var domParser = new window.DOMParser(); + var createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + var whitelistKeys = Object.keys(whiteList); + var elements = [].slice.call(createdDocument.body.querySelectorAll('*')); + + var _loop = function _loop(i, len) { + var el = elements[i]; + var elName = el.nodeName.toLowerCase(); + + if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) { + el.parentNode.removeChild(el); + return "continue"; + } + + var attributeList = [].slice.call(el.attributes); + var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []); + attributeList.forEach(function (attr) { + if (!allowedAttribute(attr, whitelistedAttributes)) { + el.removeAttribute(attr.nodeName); + } + }); + }; + + for (var i = 0, len = elements.length; i < len; i++) { + var _ret = _loop(i); + + if (_ret === "continue") continue; + } + + return createdDocument.body.innerHTML; + } + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$6 = 'tooltip'; + var VERSION$6 = '4.4.1'; + var DATA_KEY$6 = 'bs.tooltip'; + var EVENT_KEY$6 = "." + DATA_KEY$6; + var JQUERY_NO_CONFLICT$6 = $.fn[NAME$6]; + var CLASS_PREFIX = 'bs-tooltip'; + var BSCLS_PREFIX_REGEX = new RegExp("(^|\\s)" + CLASS_PREFIX + "\\S+", 'g'); + var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']; + var DefaultType$4 = { + animation: 'boolean', + template: 'string', + title: '(string|element|function)', + trigger: 'string', + delay: '(number|object)', + html: 'boolean', + selector: '(string|boolean)', + placement: '(string|function)', + offset: '(number|string|function)', + container: '(string|element|boolean)', + fallbackPlacement: '(string|array)', + boundary: '(string|element)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + whiteList: 'object', + popperConfig: '(null|object)' + }; + var AttachmentMap$1 = { + AUTO: 'auto', + TOP: 'top', + RIGHT: 'right', + BOTTOM: 'bottom', + LEFT: 'left' + }; + var Default$4 = { + animation: true, + template: '', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + selector: false, + placement: 'top', + offset: 0, + container: false, + fallbackPlacement: 'flip', + boundary: 'scrollParent', + sanitize: true, + sanitizeFn: null, + whiteList: DefaultWhitelist, + popperConfig: null + }; + var HoverState = { + SHOW: 'show', + OUT: 'out' + }; + var Event$6 = { + HIDE: "hide" + EVENT_KEY$6, + HIDDEN: "hidden" + EVENT_KEY$6, + SHOW: "show" + EVENT_KEY$6, + SHOWN: "shown" + EVENT_KEY$6, + INSERTED: "inserted" + EVENT_KEY$6, + CLICK: "click" + EVENT_KEY$6, + FOCUSIN: "focusin" + EVENT_KEY$6, + FOCUSOUT: "focusout" + EVENT_KEY$6, + MOUSEENTER: "mouseenter" + EVENT_KEY$6, + MOUSELEAVE: "mouseleave" + EVENT_KEY$6 + }; + var ClassName$6 = { + FADE: 'fade', + SHOW: 'show' + }; + var Selector$6 = { + TOOLTIP: '.tooltip', + TOOLTIP_INNER: '.tooltip-inner', + ARROW: '.arrow' + }; + var Trigger = { + HOVER: 'hover', + FOCUS: 'focus', + CLICK: 'click', + MANUAL: 'manual' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Tooltip = + /*#__PURE__*/ + function () { + function Tooltip(element, config) { + if (typeof Popper === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Popper.js (https://popper.js.org/)'); + } // private + + + this._isEnabled = true; + this._timeout = 0; + this._hoverState = ''; + this._activeTrigger = {}; + this._popper = null; // Protected + + this.element = element; + this.config = this._getConfig(config); + this.tip = null; + + this._setListeners(); + } // Getters + + + var _proto = Tooltip.prototype; + + // Public + _proto.enable = function enable() { + this._isEnabled = true; + }; + + _proto.disable = function disable() { + this._isEnabled = false; + }; + + _proto.toggleEnabled = function toggleEnabled() { + this._isEnabled = !this._isEnabled; + }; + + _proto.toggle = function toggle(event) { + if (!this._isEnabled) { + return; + } + + if (event) { + var dataKey = this.constructor.DATA_KEY; + var context = $(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $(event.currentTarget).data(dataKey, context); + } + + context._activeTrigger.click = !context._activeTrigger.click; + + if (context._isWithActiveTrigger()) { + context._enter(null, context); + } else { + context._leave(null, context); + } + } else { + if ($(this.getTipElement()).hasClass(ClassName$6.SHOW)) { + this._leave(null, this); + + return; + } + + this._enter(null, this); + } + }; + + _proto.dispose = function dispose() { + clearTimeout(this._timeout); + $.removeData(this.element, this.constructor.DATA_KEY); + $(this.element).off(this.constructor.EVENT_KEY); + $(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler); + + if (this.tip) { + $(this.tip).remove(); + } + + this._isEnabled = null; + this._timeout = null; + this._hoverState = null; + this._activeTrigger = null; + + if (this._popper) { + this._popper.destroy(); + } + + this._popper = null; + this.element = null; + this.config = null; + this.tip = null; + }; + + _proto.show = function show() { + var _this = this; + + if ($(this.element).css('display') === 'none') { + throw new Error('Please use show on visible elements'); + } + + var showEvent = $.Event(this.constructor.Event.SHOW); + + if (this.isWithContent() && this._isEnabled) { + $(this.element).trigger(showEvent); + var shadowRoot = Util.findShadowRoot(this.element); + var isInTheDom = $.contains(shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement, this.element); + + if (showEvent.isDefaultPrevented() || !isInTheDom) { + return; + } + + var tip = this.getTipElement(); + var tipId = Util.getUID(this.constructor.NAME); + tip.setAttribute('id', tipId); + this.element.setAttribute('aria-describedby', tipId); + this.setContent(); + + if (this.config.animation) { + $(tip).addClass(ClassName$6.FADE); + } + + var placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.element) : this.config.placement; + + var attachment = this._getAttachment(placement); + + this.addAttachmentClass(attachment); + + var container = this._getContainer(); + + $(tip).data(this.constructor.DATA_KEY, this); + + if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) { + $(tip).appendTo(container); + } + + $(this.element).trigger(this.constructor.Event.INSERTED); + this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment)); + $(tip).addClass(ClassName$6.SHOW); // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + + if ('ontouchstart' in document.documentElement) { + $(document.body).children().on('mouseover', null, $.noop); + } + + var complete = function complete() { + if (_this.config.animation) { + _this._fixTransition(); + } + + var prevHoverState = _this._hoverState; + _this._hoverState = null; + $(_this.element).trigger(_this.constructor.Event.SHOWN); + + if (prevHoverState === HoverState.OUT) { + _this._leave(null, _this); + } + }; + + if ($(this.tip).hasClass(ClassName$6.FADE)) { + var transitionDuration = Util.getTransitionDurationFromElement(this.tip); + $(this.tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + } else { + complete(); + } + } + }; + + _proto.hide = function hide(callback) { + var _this2 = this; + + var tip = this.getTipElement(); + var hideEvent = $.Event(this.constructor.Event.HIDE); + + var complete = function complete() { + if (_this2._hoverState !== HoverState.SHOW && tip.parentNode) { + tip.parentNode.removeChild(tip); + } + + _this2._cleanTipClass(); + + _this2.element.removeAttribute('aria-describedby'); + + $(_this2.element).trigger(_this2.constructor.Event.HIDDEN); + + if (_this2._popper !== null) { + _this2._popper.destroy(); + } + + if (callback) { + callback(); + } + }; + + $(this.element).trigger(hideEvent); + + if (hideEvent.isDefaultPrevented()) { + return; + } + + $(tip).removeClass(ClassName$6.SHOW); // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + + if ('ontouchstart' in document.documentElement) { + $(document.body).children().off('mouseover', null, $.noop); + } + + this._activeTrigger[Trigger.CLICK] = false; + this._activeTrigger[Trigger.FOCUS] = false; + this._activeTrigger[Trigger.HOVER] = false; + + if ($(this.tip).hasClass(ClassName$6.FADE)) { + var transitionDuration = Util.getTransitionDurationFromElement(tip); + $(tip).one(Util.TRANSITION_END, complete).emulateTransitionEnd(transitionDuration); + } else { + complete(); + } + + this._hoverState = ''; + }; + + _proto.update = function update() { + if (this._popper !== null) { + this._popper.scheduleUpdate(); + } + } // Protected + ; + + _proto.isWithContent = function isWithContent() { + return Boolean(this.getTitle()); + }; + + _proto.addAttachmentClass = function addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(CLASS_PREFIX + "-" + attachment); + }; + + _proto.getTipElement = function getTipElement() { + this.tip = this.tip || $(this.config.template)[0]; + return this.tip; + }; + + _proto.setContent = function setContent() { + var tip = this.getTipElement(); + this.setElementContent($(tip.querySelectorAll(Selector$6.TOOLTIP_INNER)), this.getTitle()); + $(tip).removeClass(ClassName$6.FADE + " " + ClassName$6.SHOW); + }; + + _proto.setElementContent = function setElementContent($element, content) { + if (typeof content === 'object' && (content.nodeType || content.jquery)) { + // Content is a DOM node or a jQuery + if (this.config.html) { + if (!$(content).parent().is($element)) { + $element.empty().append(content); + } + } else { + $element.text($(content).text()); + } + + return; + } + + if (this.config.html) { + if (this.config.sanitize) { + content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn); + } + + $element.html(content); + } else { + $element.text(content); + } + }; + + _proto.getTitle = function getTitle() { + var title = this.element.getAttribute('data-original-title'); + + if (!title) { + title = typeof this.config.title === 'function' ? this.config.title.call(this.element) : this.config.title; + } + + return title; + } // Private + ; + + _proto._getPopperConfig = function _getPopperConfig(attachment) { + var _this3 = this; + + var defaultBsConfig = { + placement: attachment, + modifiers: { + offset: this._getOffset(), + flip: { + behavior: this.config.fallbackPlacement + }, + arrow: { + element: Selector$6.ARROW + }, + preventOverflow: { + boundariesElement: this.config.boundary + } + }, + onCreate: function onCreate(data) { + if (data.originalPlacement !== data.placement) { + _this3._handlePopperPlacementChange(data); + } + }, + onUpdate: function onUpdate(data) { + return _this3._handlePopperPlacementChange(data); + } + }; + return _objectSpread2({}, defaultBsConfig, {}, this.config.popperConfig); + }; + + _proto._getOffset = function _getOffset() { + var _this4 = this; + + var offset = {}; + + if (typeof this.config.offset === 'function') { + offset.fn = function (data) { + data.offsets = _objectSpread2({}, data.offsets, {}, _this4.config.offset(data.offsets, _this4.element) || {}); + return data; + }; + } else { + offset.offset = this.config.offset; + } + + return offset; + }; + + _proto._getContainer = function _getContainer() { + if (this.config.container === false) { + return document.body; + } + + if (Util.isElement(this.config.container)) { + return $(this.config.container); + } + + return $(document).find(this.config.container); + }; + + _proto._getAttachment = function _getAttachment(placement) { + return AttachmentMap$1[placement.toUpperCase()]; + }; + + _proto._setListeners = function _setListeners() { + var _this5 = this; + + var triggers = this.config.trigger.split(' '); + triggers.forEach(function (trigger) { + if (trigger === 'click') { + $(_this5.element).on(_this5.constructor.Event.CLICK, _this5.config.selector, function (event) { + return _this5.toggle(event); + }); + } else if (trigger !== Trigger.MANUAL) { + var eventIn = trigger === Trigger.HOVER ? _this5.constructor.Event.MOUSEENTER : _this5.constructor.Event.FOCUSIN; + var eventOut = trigger === Trigger.HOVER ? _this5.constructor.Event.MOUSELEAVE : _this5.constructor.Event.FOCUSOUT; + $(_this5.element).on(eventIn, _this5.config.selector, function (event) { + return _this5._enter(event); + }).on(eventOut, _this5.config.selector, function (event) { + return _this5._leave(event); + }); + } + }); + + this._hideModalHandler = function () { + if (_this5.element) { + _this5.hide(); + } + }; + + $(this.element).closest('.modal').on('hide.bs.modal', this._hideModalHandler); + + if (this.config.selector) { + this.config = _objectSpread2({}, this.config, { + trigger: 'manual', + selector: '' + }); + } else { + this._fixTitle(); + } + }; + + _proto._fixTitle = function _fixTitle() { + var titleType = typeof this.element.getAttribute('data-original-title'); + + if (this.element.getAttribute('title') || titleType !== 'string') { + this.element.setAttribute('data-original-title', this.element.getAttribute('title') || ''); + this.element.setAttribute('title', ''); + } + }; + + _proto._enter = function _enter(event, context) { + var dataKey = this.constructor.DATA_KEY; + context = context || $(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $(event.currentTarget).data(dataKey, context); + } + + if (event) { + context._activeTrigger[event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER] = true; + } + + if ($(context.getTipElement()).hasClass(ClassName$6.SHOW) || context._hoverState === HoverState.SHOW) { + context._hoverState = HoverState.SHOW; + return; + } + + clearTimeout(context._timeout); + context._hoverState = HoverState.SHOW; + + if (!context.config.delay || !context.config.delay.show) { + context.show(); + return; + } + + context._timeout = setTimeout(function () { + if (context._hoverState === HoverState.SHOW) { + context.show(); + } + }, context.config.delay.show); + }; + + _proto._leave = function _leave(event, context) { + var dataKey = this.constructor.DATA_KEY; + context = context || $(event.currentTarget).data(dataKey); + + if (!context) { + context = new this.constructor(event.currentTarget, this._getDelegateConfig()); + $(event.currentTarget).data(dataKey, context); + } + + if (event) { + context._activeTrigger[event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER] = false; + } + + if (context._isWithActiveTrigger()) { + return; + } + + clearTimeout(context._timeout); + context._hoverState = HoverState.OUT; + + if (!context.config.delay || !context.config.delay.hide) { + context.hide(); + return; + } + + context._timeout = setTimeout(function () { + if (context._hoverState === HoverState.OUT) { + context.hide(); + } + }, context.config.delay.hide); + }; + + _proto._isWithActiveTrigger = function _isWithActiveTrigger() { + for (var trigger in this._activeTrigger) { + if (this._activeTrigger[trigger]) { + return true; + } + } + + return false; + }; + + _proto._getConfig = function _getConfig(config) { + var dataAttributes = $(this.element).data(); + Object.keys(dataAttributes).forEach(function (dataAttr) { + if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) { + delete dataAttributes[dataAttr]; + } + }); + config = _objectSpread2({}, this.constructor.Default, {}, dataAttributes, {}, typeof config === 'object' && config ? config : {}); + + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay + }; + } + + if (typeof config.title === 'number') { + config.title = config.title.toString(); + } + + if (typeof config.content === 'number') { + config.content = config.content.toString(); + } + + Util.typeCheckConfig(NAME$6, config, this.constructor.DefaultType); + + if (config.sanitize) { + config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn); + } + + return config; + }; + + _proto._getDelegateConfig = function _getDelegateConfig() { + var config = {}; + + if (this.config) { + for (var key in this.config) { + if (this.constructor.Default[key] !== this.config[key]) { + config[key] = this.config[key]; + } + } + } + + return config; + }; + + _proto._cleanTipClass = function _cleanTipClass() { + var $tip = $(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX); + + if (tabClass !== null && tabClass.length) { + $tip.removeClass(tabClass.join('')); + } + }; + + _proto._handlePopperPlacementChange = function _handlePopperPlacementChange(popperData) { + var popperInstance = popperData.instance; + this.tip = popperInstance.popper; + + this._cleanTipClass(); + + this.addAttachmentClass(this._getAttachment(popperData.placement)); + }; + + _proto._fixTransition = function _fixTransition() { + var tip = this.getTipElement(); + var initConfigAnimation = this.config.animation; + + if (tip.getAttribute('x-placement') !== null) { + return; + } + + $(tip).removeClass(ClassName$6.FADE); + this.config.animation = false; + this.hide(); + this.show(); + this.config.animation = initConfigAnimation; + } // Static + ; + + Tooltip._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$6); + + var _config = typeof config === 'object' && config; + + if (!data && /dispose|hide/.test(config)) { + return; + } + + if (!data) { + data = new Tooltip(this, _config); + $(this).data(DATA_KEY$6, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Tooltip, null, [{ + key: "VERSION", + get: function get() { + return VERSION$6; + } + }, { + key: "Default", + get: function get() { + return Default$4; + } + }, { + key: "NAME", + get: function get() { + return NAME$6; + } + }, { + key: "DATA_KEY", + get: function get() { + return DATA_KEY$6; + } + }, { + key: "Event", + get: function get() { + return Event$6; + } + }, { + key: "EVENT_KEY", + get: function get() { + return EVENT_KEY$6; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType$4; + } + }]); + + return Tooltip; + }(); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + + $.fn[NAME$6] = Tooltip._jQueryInterface; + $.fn[NAME$6].Constructor = Tooltip; + + $.fn[NAME$6].noConflict = function () { + $.fn[NAME$6] = JQUERY_NO_CONFLICT$6; + return Tooltip._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$7 = 'popover'; + var VERSION$7 = '4.4.1'; + var DATA_KEY$7 = 'bs.popover'; + var EVENT_KEY$7 = "." + DATA_KEY$7; + var JQUERY_NO_CONFLICT$7 = $.fn[NAME$7]; + var CLASS_PREFIX$1 = 'bs-popover'; + var BSCLS_PREFIX_REGEX$1 = new RegExp("(^|\\s)" + CLASS_PREFIX$1 + "\\S+", 'g'); + + var Default$5 = _objectSpread2({}, Tooltip.Default, { + placement: 'right', + trigger: 'click', + content: '', + template: '' + }); + + var DefaultType$5 = _objectSpread2({}, Tooltip.DefaultType, { + content: '(string|element|function)' + }); + + var ClassName$7 = { + FADE: 'fade', + SHOW: 'show' + }; + var Selector$7 = { + TITLE: '.popover-header', + CONTENT: '.popover-body' + }; + var Event$7 = { + HIDE: "hide" + EVENT_KEY$7, + HIDDEN: "hidden" + EVENT_KEY$7, + SHOW: "show" + EVENT_KEY$7, + SHOWN: "shown" + EVENT_KEY$7, + INSERTED: "inserted" + EVENT_KEY$7, + CLICK: "click" + EVENT_KEY$7, + FOCUSIN: "focusin" + EVENT_KEY$7, + FOCUSOUT: "focusout" + EVENT_KEY$7, + MOUSEENTER: "mouseenter" + EVENT_KEY$7, + MOUSELEAVE: "mouseleave" + EVENT_KEY$7 + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var Popover = + /*#__PURE__*/ + function (_Tooltip) { + _inheritsLoose(Popover, _Tooltip); + + function Popover() { + return _Tooltip.apply(this, arguments) || this; + } + + var _proto = Popover.prototype; + + // Overrides + _proto.isWithContent = function isWithContent() { + return this.getTitle() || this._getContent(); + }; + + _proto.addAttachmentClass = function addAttachmentClass(attachment) { + $(this.getTipElement()).addClass(CLASS_PREFIX$1 + "-" + attachment); + }; + + _proto.getTipElement = function getTipElement() { + this.tip = this.tip || $(this.config.template)[0]; + return this.tip; + }; + + _proto.setContent = function setContent() { + var $tip = $(this.getTipElement()); // We use append for html objects to maintain js events + + this.setElementContent($tip.find(Selector$7.TITLE), this.getTitle()); + + var content = this._getContent(); + + if (typeof content === 'function') { + content = content.call(this.element); + } + + this.setElementContent($tip.find(Selector$7.CONTENT), content); + $tip.removeClass(ClassName$7.FADE + " " + ClassName$7.SHOW); + } // Private + ; + + _proto._getContent = function _getContent() { + return this.element.getAttribute('data-content') || this.config.content; + }; + + _proto._cleanTipClass = function _cleanTipClass() { + var $tip = $(this.getTipElement()); + var tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX$1); + + if (tabClass !== null && tabClass.length > 0) { + $tip.removeClass(tabClass.join('')); + } + } // Static + ; + + Popover._jQueryInterface = function _jQueryInterface(config) { + return this.each(function () { + var data = $(this).data(DATA_KEY$7); + + var _config = typeof config === 'object' ? config : null; + + if (!data && /dispose|hide/.test(config)) { + return; + } + + if (!data) { + data = new Popover(this, _config); + $(this).data(DATA_KEY$7, data); + } + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError("No method named \"" + config + "\""); + } + + data[config](); + } + }); + }; + + _createClass(Popover, null, [{ + key: "VERSION", + // Getters + get: function get() { + return VERSION$7; + } + }, { + key: "Default", + get: function get() { + return Default$5; + } + }, { + key: "NAME", + get: function get() { + return NAME$7; + } + }, { + key: "DATA_KEY", + get: function get() { + return DATA_KEY$7; + } + }, { + key: "Event", + get: function get() { + return Event$7; + } + }, { + key: "EVENT_KEY", + get: function get() { + return EVENT_KEY$7; + } + }, { + key: "DefaultType", + get: function get() { + return DefaultType$5; + } + }]); + + return Popover; + }(Tooltip); + /** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + + + $.fn[NAME$7] = Popover._jQueryInterface; + $.fn[NAME$7].Constructor = Popover; + + $.fn[NAME$7].noConflict = function () { + $.fn[NAME$7] = JQUERY_NO_CONFLICT$7; + return Popover._jQueryInterface; + }; + + /** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + + var NAME$8 = 'scrollspy'; + var VERSION$8 = '4.4.1'; + var DATA_KEY$8 = 'bs.scrollspy'; + var EVENT_KEY$8 = "." + DATA_KEY$8; + var DATA_API_KEY$6 = '.data-api'; + var JQUERY_NO_CONFLICT$8 = $.fn[NAME$8]; + var Default$6 = { + offset: 10, + method: 'auto', + target: '' + }; + var DefaultType$6 = { + offset: 'number', + method: 'string', + target: '(string|element)' + }; + var Event$8 = { + ACTIVATE: "activate" + EVENT_KEY$8, + SCROLL: "scroll" + EVENT_KEY$8, + LOAD_DATA_API: "load" + EVENT_KEY$8 + DATA_API_KEY$6 + }; + var ClassName$8 = { + DROPDOWN_ITEM: 'dropdown-item', + DROPDOWN_MENU: 'dropdown-menu', + ACTIVE: 'active' + }; + var Selector$8 = { + DATA_SPY: '[data-spy="scroll"]', + ACTIVE: '.active', + NAV_LIST_GROUP: '.nav, .list-group', + NAV_LINKS: '.nav-link', + NAV_ITEMS: '.nav-item', + LIST_ITEMS: '.list-group-item', + DROPDOWN: '.dropdown', + DROPDOWN_ITEMS: '.dropdown-item', + DROPDOWN_TOGGLE: '.dropdown-toggle' + }; + var OffsetMethod = { + OFFSET: 'offset', + POSITION: 'position' + }; + /** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + + var ScrollSpy = + /*#__PURE__*/ + function () { + function ScrollSpy(element, config) { + var _this = this; + + this._element = element; + this._scrollElement = element.tagName === 'BODY' ? window : element; + this._config = this._getConfig(config); + this._selector = this._config.target + " " + Selector$8.NAV_LINKS + "," + (this._config.target + " " + Selector$8.LIST_ITEMS + ",") + (this._config.target + " " + Selector$8.DROPDOWN_ITEMS); + this._offsets = []; + this._targets = []; + this._activeTarget = null; + this._scrollHeight = 0; + $(this._scrollElement).on(Event$8.SCROLL, function (event) { + return _this._process(event); + }); + this.refresh(); + + this._process(); + } // Getters + + + var _proto = ScrollSpy.prototype; + + // Public + _proto.refresh = function refresh() { + var _this2 = this; + + var autoMethod = this._scrollElement === this._scrollElement.window ? OffsetMethod.OFFSET : OffsetMethod.POSITION; + var offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method; + var offsetBase = offsetMethod === OffsetMethod.POSITION ? this._getScrollTop() : 0; + this._offsets = []; + this._targets = []; + this._scrollHeight = this._getScrollHeight(); + var targets = [].slice.call(document.querySelectorAll(this._selector)); + targets.map(function (element) { + var target; + var targetSelector = Util.getSelectorFromElement(element); + + if (targetSelector) { + target = document.querySelector(targetSelector); + } + + if (target) { + var targetBCR = target.getBoundingClientRect(); + + if (targetBCR.width || targetBCR.height) { + // TODO (fat): remove sketch reliance on jQuery position/offset + return [$(target)[offsetMethod]().top + offsetBase, targetSelector]; + } + } + + return null; + }).filter(function (item) { + return item; + }).sort(function (a, b) { + return a[0] - b[0]; + }).forEach(function (item) { + _this2._offsets.push(item[0]); + + _this2._targets.push(item[1]); + }); + }; + + _proto.dispose = function dispose() { + $.removeData(this._element, DATA_KEY$8); + $(this._scrollElement).off(EVENT_KEY$8); + this._element = null; + this._scrollElement = null; + this._config = null; + this._selector = null; + this._offsets = null; + this._targets = null; + this._activeTarget = null; + this._scrollHeight = null; + } // Private + ; + + _proto._getConfig = function _getConfig(config) { + config = _objectSpread2({}, Default$6, {}, typeof config === 'object' && config ? config : {}); + + if (typeof config.target !== 'string') { + var id = $(config.target).attr('id'); + + if (!id) { + id = Util.getUID(NAME$8); + $(config.target).attr('id', id); + } + + config.target = "#" + id; + } + + Util.typeCheckConfig(NAME$8, config, DefaultType$6); + return config; + }; + + _proto._getScrollTop = function _getScrollTop() { + return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop; + }; + + _proto._getScrollHeight = function _getScrollHeight() { + return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); + }; + + _proto._getOffsetHeight = function _getOffsetHeight() { + return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height; + }; + + _proto._process = function _process() { + var scrollTop = this._getScrollTop() + this._config.offset; + + var scrollHeight = this._getScrollHeight(); + + var maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight(); + + if (this._scrollHeight !== scrollHeight) { + this.refresh(); + } + + if (scrollTop >= maxScroll) { + var target = this._targets[this._targets.length - 1]; + + if (this._activeTarget !== target) { + this._activate(target); + } + + return; + } + + if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { + this._activeTarget = null; + + this._clear(); + + return; + } + + var offsetLength = this._offsets.length; + + for (var i = offsetLength; i--;) { + var isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]); + + if (isActiveTarget) { + this._activate(this._targets[i]); + } + } + }; + + _proto._activate = function _activate(target) { + this._activeTarget = target; + + this._clear(); + + var queries = this._selector.split(',').map(function (selector) { + return selector + "[data-target=\"" + target + "\"]," + selector + "[href=\"" + target + "\"]"; + }); + + var $link = $([].slice.call(document.querySelectorAll(queries.join(',')))); + + if ($link.hasClass(ClassName$8.DROPDOWN_ITEM)) { + $link.closest(Selector$8.DROPDOWN).find(Selector$8.DROPDOWN_TOGGLE).addClass(ClassName$8.ACTIVE); + $link.addClass(ClassName$8.ACTIVE); + } else { + // Set triggered link as active + $link.addClass(ClassName$8.ACTIVE); // Set triggered links parents as active + // With both