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..5ebdf0b2eb 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", @@ -83,6 +84,7 @@ "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" } } \ 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 1b4c9a69f8..3df70437b4 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -5,7 +5,9 @@ "OrganizationList": "Organization list", "Volo.AbpIo.Commercial:010003": "You are not owner of this organization!", "OrganizationNotFoundMessage": "No organization found!", - "DeveloperCount": "Developer count", + "DeveloperCount": "Allocated / total developers", + "QuestionCount": "Remaining / total questions", + "Unlimited": "Unlimited", "Owners": "Owners", "AddMember": "Add member", "AddOwner": "Add owner", @@ -29,5 +31,5 @@ "SuccessfullyAddedToNewsletter": "Thanks you for subscribing to our newsletter!", "ManageProfile": "Manage your 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/common.props b/common.props index 1cccebd015..92d87d3d76 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 2.1.0 + 2.3.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..eb15f45389 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) 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/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..3c3c2ec26c --- /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 "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 + +`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..3fb52377b3 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -201,7 +201,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 +219,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: @@ -292,6 +294,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/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md index b7453b9aff..d47b266839 100644 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ b/docs/en/AspNet-Boilerplate-Migration-Guide.md @@ -38,6 +38,10 @@ We also suggest you to compare the features of two products based on your needs. If you have an ASP.NET Zero based solution and want to migrate to the ABP Commercial, this guide will also help you. +### ASP.NET MVC 5.x Projects + +The ABP Framework doesn't support ASP.NET MVC 5.x, it only works with ASP.NET Core. So, if you migrate your ASP.NET MVC 5.x based projects, you will also deal with the .NET Core migration. + ## The Migration Progress We've designed the ABP Framework by **getting the best parts** of the ASP.NET Boilerplate framework, so it will be familiar to you if you've developed ASP.NET Boilerplate based applications. @@ -277,6 +281,79 @@ ABP Framework doesn't include the ` ICustomValidate ` that does exists in the AS ## The Infrastructure Layer +### Namespaces + +ASP.NET Boilerplate uses the `Abp.*` namespaces while the ABP Framework uses the `Volo.Abp.*` namespaces for the framework and pre-built fundamental modules. + +In addition, there are also some pre-built application modules (like docs and blog modules) those are using the `Volo.*` namespaces (like `Volo.Blogging.*` and `Volo.Docs.*`). We consider these modules as standalone open source products developed by Volosoft rather than add-ons or generic modules completing the ABP Framework and used in the applications. We've developed them as a module to make them re-usable as a part of a bigger solution. + +### Module System + +Both of the ASP.NET Boilerplate and the ABP Framework have the `AbpModule` while they are a bit different. + +ASP.NET Boilerplate's `AbpModule` class has `PreInitialize`, `Initialize` and `PostInitialize` methods you can override and configure the framework and the depended modules. You can also register and resolve dependencies in these methods. + +ABP Framework's `AbpModule` class has the `ConfigureServices` and `OnApplicationInitialization` methods (and their Pre and Post versions). It is similar to ASP.NET Core's Startup class. You configure other services and register dependencies in the `ConfigureServices`. However, you can now resolve dependencies in that point. You can resolve dependencies and configure the ASP.NET Core pipeline in the `OnApplicationInitialization` method while you can not register dependencies here. So, the new module classes separate dependency registration phase from dependency resolution phase since it follows the ASP.NET Core's approach. + +### Dependency Injection + +#### The DI Framework + +ASP.NET Boilerplate is using the [Castle Windsor](http://www.castleproject.org/projects/windsor/) as the dependency injection framework. This is a fundamental dependency of the ASP.NET Boilerplate framework. We've got a lot of feedback to make the ASP.NET Boilerplate DI framework agnostic, but it was not so easy because of the design. + +ABP Framework is dependency injection framework independent since it uses Microsoft's [Dependency Injection Extensions](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) library as an abstraction. None of the ABP Framework or module packages depends on any specific library. + +However, ABP Framework doesn't use the Microsoft's base DI library because it has some missing features ABP Framework needs to: Property Injection and Interception. All the startup templates and the samples are using the [Autofac](https://autofac.org/) as the DI library and it is the only [officially integrated](Autofac-Integration.md) library to the ABP Framework. We suggest you to use the Autofac with the ABP Framework if you have not a good reason. If you have a good reason, please create an [issue](https://github.com/abpframework/abp/issues/new) on GitHub to request it or just implement it and send a pull request :) + +#### Registering the Dependencies + +Registering the dependencies are similar and mostly handled by the framework conventionally (like repositories, application services, controllers... etc). Implement the same `ITransientDependency`, `ISingletonDependency` and `IScopedDependency` interfaces for the services not registered by conventions. + +When you need to manually register dependencies, use the `context.Services` in the `ConfigureServices` method of your module. Example: + +````csharp +public class BlogModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //Register an instance as singleton + context.Services.AddSingleton(new TaxCalculator(taxRatio: 0.18)); + + //Register a factory method that resolves from IServiceProvider + context.Services.AddScoped( + sp => sp.GetRequiredService() + ); + } +} +```` + +See the ABP Framework [dependency injection document](https://docs.abp.io/en/abp/latest/Dependency-Injection) for details. + +### Configuration vs Options System + +ASP.NET Boilerplate has its own configuration system to configure the framework and the modules. For example, you could disable the audit logging in the `Initialize` method of your [module](https://aspnetboilerplate.com/Pages/Documents/Module-System): + +````csharp +public override void Initialize() +{ + Configuration.Auditing.IsEnabled = false; +} +```` + +ABP Framework uses [the options pattern](Options.md) to configure the framework and the modules. You typically configure the options in the `ConfigureServices` method of your [module](Module-Development-Basics.md): + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.IsEnabled = false; + }); +} +```` + +Instead of a central configuration object, there are separated option classes for every module and feature those are defined in the related documents. + ### IAbpSession vs ICurrentUser and ICurrentTenant ASP.NET Boilerplate's `IAbpSession` service is used to obtain the current user and tenant information, like ` UserId ` and `TenantId`. @@ -301,16 +378,357 @@ You inherit from the `AuthorizationProvider` in the ASP.NET Boilerplate to defin ### Unit of Work -TODO +Unit of work system has been designed to work seamlessly. For most of the cases, you don't need to change anything. + +`UnitOfWork` attribute of the ABP Framework doesn't have the `ScopeOption` (type of `TransactionScopeOption`) property. Instead, use `IUnitOfWorkManager.Begin()` method with `requiresNew = true` to create an independent inner transaction in a transaction scope. + +#### Data Filters + +ASP.NET Boilerplate implements the data filtering system as a part of the unit of work. ABP Framework has a separate `IDataFilter` service. + +See the [data filtering document](Data-Filtering.md) to learn how to enable/disable a filter. + +See [the UOW documentation](Unit-Of-Work.md) for more about the UOW system. ### Multi-Tenancy -TODO +#### IMustHaveTenant & IMayHaveTenant vs IMultiTenant + +ASP.NET Boilerplate defines `IMustHaveTenant` and `IMayHaveTenant` interfaces to implement them for your entities. In this way, your entities are automatically filtered according to the current tenant. Because of the design, there was a problem: You had to create a "Default" tenant in the database with "1" as the Id if you want to create a non multi-tenant application (this "Default" tenant was used as the single tenant). + +ABP Framework has a single interface for multi-tenant entities: `IMultiTenant` which defines a nullable `TenantId` property of type `Guid`. If your application is not multi-tenant, then your entities will have null TenantId (instead of a default one). + +On the migration, you need to change the TenantId field type and replace these interfaces with the `IMultiTenant` + +#### Switch Between Tenants + +In some cases you might need to switch to a tenant for a code scope and work with the tenant's data in this scope. + +In ASP.NET Boilerplate, it is done using the `IUnitOfWorkManager` service: + +````csharp +public async Task> GetProducts(int tenantId) +{ + using (_unitOfWorkManager.Current.SetTenantId(tenantId)) + { + return await _productRepository.GetAllListAsync(); + } +} +```` + +In the ABP Framework it is done with the `ICurrentTenant` service: + +````csharp +public async Task> GetProducts(Guid tenantId) +{ + using (_currentTenant.Change(tenantId)) + { + return await _productRepository.GetListAsync(); + } +} +```` + +Pass `null` to the `Change` method to switch to the host side. + +### Caching + +ASP.NET Boilerplate has its [own distributed caching abstraction](https://aspnetboilerplate.com/Pages/Documents/Caching) which has in-memory and Redis implementations. You typically inject the `ICacheManager` service and use its `GetCache(...)` method to obtain a cache, then get and set objects in the cache. + +ABP Framework uses and extends ASP.NET Core's [distributed caching abstraction](Caching.md). It defines the `IDistributedCache` services to inject a cache and get/set objects. + +### Logging + +ASP.NET Boilerplate uses Castle Windsor's [logging facility](http://docs.castleproject.org/Windsor.Logging-Facility.ashx) as an abstraction and supports multiple logging providers including Log4Net (the default one comes with the startup projects) and Serilog. You typically property-inject the logger: + +````csharp +using Castle.Core.Logging; //1: Import Logging namespace + +public class TaskAppService : ITaskAppService +{ + //2: Getting a logger using property injection + public ILogger Logger { get; set; } + + public TaskAppService() + { + //3: Do not write logs if no Logger supplied. + Logger = NullLogger.Instance; + } + + public void CreateTask(CreateTaskInput input) + { + //4: Write logs + Logger.Info("Creating a new task with description: " + input.Description); + //... + } +} +```` + +ABP Framework depends on Microsoft's [logging extensions](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging) library which is also an abstraction and there are many providers implement it. Startup templates are using the Serilog as the pre-configured logging libary while it is easy to change in your project. The usage pattern is similar: + +````csharp +//1: Import the Logging namespaces +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +public class TaskAppService : ITaskAppService +{ + //2: Getting a logger using property injection + public ILogger Logger { get; set; } + + public TaskAppService() + { + //3: Do not write logs if no Logger supplied. + Logger = NullLogger.Instance; + } + + public void CreateTask(CreateTaskInput input) + { + //4: Write logs + Logger.Info("Creating a new task with description: " + input.Description); + //... + } +} +```` + +You inject the `ILogger` instead of the `ILogger`. + +### Object to Object Mapping + +#### IObjectMapper Service + +ASP.NET Boilerplate defines an `IObjectMapper` service ([see](https://aspnetboilerplate.com/Pages/Documents/Object-To-Object-Mapping)) and has an integration to the [AutoMapper](https://automapper.org/) library. + +Example usage: Create a `User` object with the given `CreateUserInput` object: + +````csharp +public void CreateUser(CreateUserInput input) +{ + var user = ObjectMapper.Map(input); + ... +} +```` + +Example: Update an existing `User` properties with the given `UpdateUserInput` object: + +````csharp +public async Task UpdateUserAsync(Guid id, UpdateUserInput input) +{ + var user = await _userRepository.GetAsync(id); + ObjectMapper.Map(input, user); +} +```` + +ABP Framework has the same `IObjectMapper` service ([see](Object-To-Object-Mapping.md)) and the AutoMapper integration with a slightly different mapping methods. + +Example usage: Create a `User` object with the given `CreateUserInput` object: + +````csharp +public void CreateUser(CreateUserInput input) +{ + var user = ObjectMapper.Map(input); +} +```` + +This time you need to explicitly declare the source type and target type (while ASP.NET Boilerplate was requiring only the target type). + +Example: Update an existing `User` properties with the given `UpdateUserInput` object: + +````csharp +public async Task UpdateUserAsync(Guid id, UpdateUserInput input) +{ + var user = await _userRepository.GetAsync(id); + ObjectMapper.Map(input, user); +} +```` + +Again, ABP Framework expects to explicitly set the source and target types. + +#### AutoMapper Integration + +##### Auto Mapping Attributes + +ASP.NET Boilerplate has `AutoMapTo`, `AutoMapFrom` and `AutoMap` attributes to automatically create mappings for the declared types. Example: + +````csharp +[AutoMapTo(typeof(User))] +public class CreateUserInput +{ + public string Name { get; set; } + public string Surname { get; set; } + ... +} +```` + +ABP Framework has no such attributes, because AutoMapper as a [similar attribute](https://automapper.readthedocs.io/en/latest/Attribute-mapping.html) now. You need to switch to AutoMapper's attribute. + +##### Mapping Definitions + +ABP Framework follows AutoMapper principles closely. You can define classes derived from the `Profile` class to define your mappings. + +##### Configuration Validation + +Configuration validation is a best practice for the AutoMapper to maintain your mapping configuration in a safe way. + +See [the documentation](Object-To-Object-Mapping.md) for more information related to the object mapping. + +### Setting Management + +#### Defining the Settings + +In an ASP.NET Boilerplate based application, you create a class deriving from the `SettingProvider` class, implement the `GetSettingDefinitions` method and add your class to the `Configuration.Settings.Providers` list. + +In the ABP Framework, you need to derive your class from the `SettingDefinitionProvider` and implement the `Define` method. You don't need to register your class since the ABP Framework automatically discovers it. + +#### Getting the Setting Values + +ASP.NET Boilerplate provides the `ISettingManager` to read the setting values in the server side and `abp.setting.get(...)` method in the JavaScript side. + +ABP Framework has the `ISettingProvider` service to read the setting values in the server side and `abp.setting.get(...)` method in the JavaScript side. + +#### Setting the Setting Values + +For ASP.NET Boilerplate, you use the same `ISettingManager` service to change the setting values. + +ABP Framework separates it and provides the setting management module (pre-added to the startup projects) which has the ` ISettingManager ` to change the setting values. This separation was introduced to support tiered deployment scenarios (where `ISettingProvider` can also work in the client application while `ISettingManager ` can also work in the server (API) side). + +### Clock + +ASP.NET Boilerplate has a static `Clock` service ([see](https://aspnetboilerplate.com/Pages/Documents/Timing)) which is used to abstract the `DateTime` kind, so you can easily switch between Local and UTC times. You don't inject it, but just use the `Clock.Now` static method to obtain the current time. + +ABP Framework has the `IClock` service ([see](Clock.md)) which has a similar goal, but now you need to inject it whenever you need it. + +### Event Bus + +ASP.NET Boilerplate has an in-process event bus system. You typically inject the `IEventBus` (or use the static instance `EventBus.Default`) to trigger an event. It automatically triggers events for entity changes (like `EntityCreatingEventData` and `EntityUpdatedEventData`). You create a class by implementing the `IEventHandler` interface. + +ABP Framework separates the event bus into two services: `ILocalEventBus` and `IDistributedEventBus`. + +The local event bus is similar to the event bus of the ASP.NET Boilerplate while the distributed event bus is new feature introduced in the ABP Framework. + +So, to migrate your code; + +* Use the `ILocalEventBus` instead of the `IEventBus`. +* Implement the `ILocalEventHandler` instead of the `IEventHandler`. + +> Note that ABP Framework has also an `IEventBus` interface, but it does exists to be a common interface for the local and distributed event bus. It is not injected and directly used. + +### Feature Management + +Feature system is used in multi-tenant applications to define features of your application check if given feature is available for the current tenant. + +#### Defining Features + +In the ASP.NET Boilerplate ([see](https://aspnetboilerplate.com/Pages/Documents/Feature-Management)), you create a class inheriting from the `FeatureProvider`, override the `SetFeatures` method and add your class to the `Configuration.Features.Providers` list. + +In the ABP Framework ([see](Features.md)), you derive your class from the `FeatureDefinitionProvider` and override the `Define` method. No need to add your class to the configuration, it is automatically discovered by the framework. + +#### Checking Features + +You can continue to use the `RequiresFeature` attribute and `IFeatureChecker` service to check if a feature is enabled for the current tenant. + +#### Changing the Feature Values + +In the ABP Framework you use the `IFeatureManager` to change a feature value for a tenant. + +### Audit Logging + +The ASP.NET Boilerplate ([see](https://aspnetboilerplate.com/Pages/Documents/Audit-Logging)) and the ABP Framework ([see](Audit-Logging.md)) has similar audit logging systems. ABP Framework requires to add `UseAuditing()` middleware to the ASP.NET Core pipeline, which is already added in the startup templates. So, most of the times it will be work out of the box. + +### Localization + +ASP.NET Boilerplate supports XML and JSON files to define the localization key-values for the UI ([see](https://aspnetboilerplate.com/Pages/Documents/Localization)). ABP Framework only supports the JSON formatter localization files ([see](Localization.md)). So, you need to convert your XML file to JSON. + +The ASP.NET Boilerplate has its own the `ILocalizationManager` service to be injected and used for the localization in the server side. + +The ABP Framework uses [Microsoft localization extension](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) library, so it is completely integrated to ASP.NET Core. You use the `IStringLocalizer` service to get a localized text. Example: + +````csharp +public class MyService +{ + private readonly IStringLocalizer _localizer; + + public MyService(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public void Foo() + { + var str = _localizer["HelloWorld"]; //Get a localized text + } +} +```` + +So, you need to replace `ILocalizationManager` usage by the `IStringLocalizer`. + +It also provides API used in the client side: + +````js +var testResource = abp.localization.getResource('Test'); +var str = testResource('HelloWorld'); +```` + +It was like `abp.localization.localize(...)` in the ASP.NET Boilerplate. + +### Navigation vs Menu + +In ASP.NET you create a class deriving from the `NavigationProvider` to define your menu elements. Menu items has `requiredPermissionName` attributes to restrict access to a menu element. Menu items were static and your class is executed only one time. + +Int the ABP Framework you need to create a class implements the `IMenuContributor` interface. Your class is executed whenever the menu needs to be rendered. So, you can conditionally add menu items. + +As an example, this is the menu contributor of the tenant management module: + +````csharp +public class AbpTenantManagementWebMainMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + //Add items only to the main menu + if (context.Menu.Name != StandardMenus.Main) + { + return; + } + + //Get the standard administration menu item + var administrationMenu = context.Menu.GetAdministration(); + + //Resolve some needed services from the DI container + var authorizationService = context.ServiceProvider + .GetRequiredService(); + var l = context.ServiceProvider + .GetRequiredService>(); + + var tenantManagementMenuItem = new ApplicationMenuItem( + TenantManagementMenuNames.GroupName, + l["Menu:TenantManagement"], + icon: "fa fa-users"); + + administrationMenu.AddItem(tenantManagementMenuItem); + + //Conditionally add the "Tenants" menu item based on the permission + if (await authorizationService + .IsGrantedAsync(TenantManagementPermissions.Tenants.Default)) + { + tenantManagementMenuItem.AddItem( + new ApplicationMenuItem( + TenantManagementMenuNames.Tenants, + l["Tenants"], + url: "/TenantManagement/Tenants")); + } + } +} +```` + +So, you need to check permission using the `IAuthorizationService` if you want to show a menu item only when the user has the related permission. + +> Navigation/Menu system is only for ASP.NET Core MVC / Razor Pages applications. Angular applications has a different system implemented in the startup templates. ## Missing Features -The following features are not present for the ABP Framework. Here, a list of major missing features (and the related issue for that feature waiting on the ABP Framework GitHub repository): +The following features are not present for the ABP Framework. Here, a list of some major missing features (and the related issue for that feature waiting on the ABP Framework GitHub repository): * [Multi-Lingual Entities](https://aspnetboilerplate.com/Pages/Documents/Multi-Lingual-Entities) ([#1754](https://github.com/abpframework/abp/issues/1754)) +* [Real time notification system](https://aspnetboilerplate.com/Pages/Documents/Notification-System) ([#633](https://github.com/abpframework/abp/issues/633)) +* [NHibernate Integration](https://aspnetboilerplate.com/Pages/Documents/NHibernate-Integration) ([#339](https://github.com/abpframework/abp/issues/339)) - We don't intent to work on this, but any community contribution welcome. -Most of these features will eventually be implemented. However, you can implement them yourself if they are important for you. If you want, you can [contribute](Contribution/Index.md) to the framework by implementing these yourself. \ No newline at end of file +Some of these features will eventually be implemented. However, you can implement them yourself if they are important for you. If you want, you can [contribute](Contribution/Index.md) to the framework, it is appreciated. \ No newline at end of file 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/Audit-Logging.md b/docs/en/Audit-Logging.md index 610efc0451..799b3b76c3 100644 --- a/docs/en/Audit-Logging.md +++ b/docs/en/Audit-Logging.md @@ -39,6 +39,7 @@ Here, a list of the options you can configure: * `IsEnabled` (default: `true`): A root switch to enable or disable the auditing system. Other options is not used if this value is `false`. * `HideErrors` (default: `true`): Audit log system hides and write regular [logs](Logging.md) if any error occurs while saving the audit log objects. If saving the audit logs is critical for your system, set this to `false` to throw exception in case of hiding the errors. * `IsEnabledForAnonymousUsers` (default: `true`): If you want to write audit logs only for the authenticated users, set this to `false`. If you save audit logs for anonymous users, you will see `null` for `UserId` values for these users. +* `AlwaysLogOnException` (default: `true`): If you set to true, it always saves the audit log on an exception/error case without checking other options (except `IsEnabled`, which completely disables the audit logging). * `IsEnabledForGetRequests` (default: `false`): HTTP GET requests should not make any change in the database normally and audit log system doesn't save audit log objects for GET request. Set this to `true` to enable it also for the GET requests. * `ApplicationName`: If multiple applications saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. * `IgnoredTypes`: A list of `Type`s to be ignored for audit logging. If this is an entity type, changes for this type of entities will not be saved. This list is also used while serializing the action parameters. @@ -369,4 +370,4 @@ You can call other services, they may call others, they may change entities and The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. It supports multiple database providers. This module is added to the startup templates by default. -See [the Audit Logging Module document](Modules/Audit-Logging.md) for more about it. \ No newline at end of file +See [the Audit Logging Module document](Modules/Audit-Logging.md) for more about it. diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index 6cf6917ba6..1bceda951f 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,21 +126,21 @@ 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! ); -```` +``` #### Child Permissions @@ -148,12 +148,12 @@ A permission may have child permissions. It is especially useful when you want t 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 +161,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 +193,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 +204,17 @@ 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. + ## 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 +227,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 +235,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 +254,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 +268,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 +288,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 +300,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 +318,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 +329,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: -````csharp +```csharp Configure(options => { options.ValueProviders.Add(); }); -```` +``` ### Permission Store @@ -354,16 +358,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-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 a0ea708896..2bfdd87140 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. 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/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-02-22/Post.md b/docs/en/Blog-Posts/2019-02-22/Post.md index 35f8219014..b27a50ec05 100644 --- a/docs/en/Blog-Posts/2019-02-22/Post.md +++ b/docs/en/Blog-Posts/2019-02-22/Post.md @@ -1,6 +1,6 @@ # Microservice Demo, Projects Status and Road Map -After [the first announcement](https://abp.io/blog/abp/Abp-vNext-Announcement) on the ABP vNext, we have a lot of improvements on the codebase (1100+ commits on the [GitHub repository](https://github.com/abpframework/abp)). We've created features, samples, documentation and much more. In this post, I want to inform you about some news and the status of the project. +After [the first announcement](https://blog.abp.io/Abp-vNext-Announcement) on the ABP vNext, we have a lot of improvements on the codebase (1100+ commits on the [GitHub repository](https://github.com/abpframework/abp)). We've created features, samples, documentation and much more. In this post, I want to inform you about some news and the status of the project. ## Microservice Demo Solution @@ -38,7 +38,7 @@ First release may not include a SPA template. However, we want to prepare a simp ## Chinese Web Site -There is a big ABP community in China. They have created a Chinese version of the abp.io web site: https://cn.abp.io/ They are keeping it up to date. Thanks to the Chinese developers and especially to [Liming Ma](https://github.com/maliming). +There is a big ABP community in China. They have created a Chinese version of the abp.io web site: https://abp.io/ They are keeping it up to date. Thanks to the Chinese developers and especially to [Liming Ma](https://github.com/maliming). ## NDC {London} 2019 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/CLI.md b/docs/en/CLI.md index a1d3d51138..dcab1b672e 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -48,13 +48,17 @@ 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`). ### add-package @@ -108,6 +112,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 +133,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 @@ -161,5 +192,4 @@ Examples: ````bash abp help # Shows a general help. abp help new # Shows help about the "new" command. -```` - +```` \ No newline at end of file diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 8aa9f00657..bb11b2b45a 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -2,6 +2,32 @@ ABP framework extends ASP.NET Core's distributed caching system. +## Volo.Abp.Caching Package + +> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually. + +Volo.Abp.Caching is the core package of the caching system. Install it to your project using the package manager console (PMC): + +``` +Install-Package Volo.Abp.Caching +``` + +Then you can add **AbpCachingModule** dependency to your module: + +```c# +using Volo.Abp.Modularity; +using Volo.Abp.Caching; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpCachingModule))] + public class MyModule : AbpModule + { + //... + } +} +``` + ## `IDistributedCache` Interface ASP.NET Core defines the `IDistributedCache` interface to get/set cache values. But it has some difficulties: diff --git a/docs/en/Clock.md b/docs/en/Clock.md new file mode 100644 index 0000000000..46ef1235e0 --- /dev/null +++ b/docs/en/Clock.md @@ -0,0 +1,3 @@ +# Clock + +TODO \ No newline at end of file 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..def121910e --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -0,0 +1,149 @@ +# 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 entities. + +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 and get or set the value as shown above. Overriding services will be discussed below. + +## 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 + +* [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..7f913f366a --- /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 source code as it is in your own 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..16fbc9d2a4 --- /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 `IIdentityAppService`, 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..afb701eacd --- /dev/null +++ b/docs/en/Customizing-Application-Modules-Overriding-User-Interface.md @@ -0,0 +1,9 @@ +# 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) + + + diff --git a/docs/en/Entities.md b/docs/en/Entities.md index b5d196cccb..f0e6c7f301 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -295,6 +295,99 @@ 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), it is stored in a single `ExtraProperties` field as a `JSON` string. 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. +* 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**. +* It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects. +* It **doesn't create fields** in the database table for EF Core, so it will not be easy to create indexes or search/order by this field in the database side. + +### 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..23b17293f1 --- /dev/null +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -0,0 +1,912 @@ +# 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 custom [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 table "AbpUsers" with the IdentityUser + b.ToTable("AbpUsers"); + + //Configure base properties + b.ConfigureByConvention(); + b.ConfigureAbpUser(); + + //Moved customization of the "AbpUsers" table to an extension method + b.ConfigureCustomUserProperties(); + }); + + /* 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 customizations for entities from the modules included */ + builder.Entity(b => + { + b.ConfigureCustomUserProperties(); + }); + + /* 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. +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.ConfigureCustomRoleProperties(); + }); + + ... + + /* 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.ConfigureCustomRoleProperties(); +}); +```` + +* 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. + +`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project): + +````csharp +public static void ConfigureCustomRoleProperties(this EntityTypeBuilder b) + where TRole : class, IEntity +{ + b.Property(nameof(AppRole.Title)).HasMaxLength(128); +} +```` + +* This method only defines the **custom properties** of your entity. +* Unfortunately, we can not utilize the fully **type safety** here (by referencing the `AppRole` entity). The best we can do is to use the `Title` name as type safe. This is because of EF Core migration system can not map two unrelated entity classes to the same database table. + +You've configured the custom property for your `DbContext` used by your application on the runtime. We also need to configure the `MigrationsDbContext`. + +Open the `MigrationsDbContext` (`BookStoreMigrationsDbContext` for this example) and change as shown below: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + ... + + /* Configure customizations for entities from the modules included */ + + //CONFIGURE THE CUSTOM ROLE PROPERTIES + builder.Entity(b => + { + b.ConfigureCustomRoleProperties(); + }); + + ... + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + + builder.ConfigureBookStore(); +} +```` + +Only added the following lines: + +````csharp +builder.Entity(b => +{ + b.ConfigureCustomRoleProperties(); +}); +```` + +In this way, we re-used the extension method that is used to configure custom property mappings for the role. But, this time, did the same customization for the `IdentityRole` entity. + +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, 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. + +###### 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). + +#### 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..a08ac6d169 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 diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 586c8d4a01..788f3234ed 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 diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md index 93bad2b6ed..0e0a530416 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -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)); }); @@ -285,7 +285,7 @@ The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP 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); }); diff --git a/docs/en/Features.md b/docs/en/Features.md new file mode 100644 index 0000000000..4b31ca16e9 --- /dev/null +++ b/docs/en/Features.md @@ -0,0 +1,3 @@ +# Features + +TODO \ No newline at end of file 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..f3fafca7df 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) 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/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/Logging.md b/docs/en/Logging.md index 9b9c604bed..7fa45c03b9 100644 --- a/docs/en/Logging.md +++ b/docs/en/Logging.md @@ -2,4 +2,5 @@ ABP Framework doesn't implement any logging infrastructure. It uses the [ASP.NET Core's logging system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging). -> .NET Core's logging system is actually independent from the ASP.NET Core. It is usable in any type of application. \ No newline at end of file +> .NET Core's logging system is actually independent from the ASP.NET Core. It is usable in any type of application. + 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/Audit-Logging.md b/docs/en/Modules/Audit-Logging.md index 922f99284b..039c61d3a2 100644 --- a/docs/en/Modules/Audit-Logging.md +++ b/docs/en/Modules/Audit-Logging.md @@ -1,6 +1,6 @@ # Audit Logging Module -The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. +The Audit Logging Module basically implements the `IAuditingStore` to save the audit log objects to a database. > Audit Logging module is already installed and configured for [the startup templates](../Startup-Templates/Index.md). So, most of the times you don't need to manually add this module to your application. 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 c42a3c2bcc..db752ceb74 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 @@ -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]) 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') +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. @@ -366,7 +375,7 @@ You can use [ABP Framework](https://github.com/abpframework/abp/) GitHub documen 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]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL) +INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL, N'') ``` Add one of the sample projects above and run the application. In the menu you will see `Documents` link, click the menu link to open the documents page. @@ -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/Feature-Management.md b/docs/en/Modules/Feature-Management.md new file mode 100644 index 0000000000..5c41277dd8 --- /dev/null +++ b/docs/en/Modules/Feature-Management.md @@ -0,0 +1,3 @@ +# Feature Management Module + +TODO \ 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/Index.md b/docs/en/Modules/Index.md index 812f22e8dd..2ffb509667 100644 --- a/docs/en/Modules/Index.md +++ b/docs/en/Modules/Index.md @@ -16,6 +16,7 @@ There are some **free and open source** application modules developed and mainta * **Background Jobs**: Persist background jobs when using the default background job manager. * **Blogging**: Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module. * [**Docs**](Docs.md): Used to create technical documentation pages. ABP's [own documentation](https://docs.abp.io) already using this module. +* **Feature Management**: Used to persist and manage the [features](../Features.md). * **Identity**: Manages roles, users and their permissions, based on the Microsoft Identity library. * **IdentityServer**: Integrates to IdentityServer4. * **Permission Management**: Used to persist permissions. 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..1e1e4e9f81 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/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() @@ -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/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..3a5c6c3bb2 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -5,8 +5,8 @@ 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: * 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). +* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/Part-1.md?UI=MVC) 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/Part-1.md?UI=NG) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider). ## How to Start With? @@ -145,7 +145,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 +153,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. @@ -270,4 +270,4 @@ The files under the `angular/src/environments` folder has the essential configur ## 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. +- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md) to learn how to develop applications using this template. 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..b17c61ae73 --- /dev/null +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -0,0 +1,52 @@ +# 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) + +## 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..72c8e6c6fd --- /dev/null +++ b/docs/en/UI/Angular/Config-State.md @@ -0,0 +1,290 @@ +# 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). diff --git a/docs/en/UI/Angular/Custom-Setting-Page.md b/docs/en/UI/Angular/Custom-Setting-Page.md new file mode 100644 index 0000000000..7c51cf2cca --- /dev/null +++ b/docs/en/UI/Angular/Custom-Setting-Page.md @@ -0,0 +1,42 @@ +# Custom Setting Page + +There are several settings tabs from different modules. You can add custom settings page to your project in 3 steps. + +1. Create a Component + +```js +import { Select } from '@ngxs/store'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-your-custom-settings', + template: ` + custom-settings works! + `, +}) +export class YourCustomSettingsComponent { + // Your component logic +} +``` + +2. Add the `YourCustomSettingsComponent` to `declarations` and the `entryComponents` arrays in the `AppModule`. + +3. Open the `app.component.ts` and add the below content to the `ngOnInit` + +```js +import { addSettingTab } from '@abp/ng.theme.shared'; +// ... + +ngOnInit() { + addSettingTab({ + component: YourCustomSettingsComponent, + name: 'Type here the setting tab title (you can type a localization key, e.g: AbpAccount::Login', + order: 4, + requiredPolicy: 'type here a policy key' + }); +} +``` + +Navigate to `/setting-management` route to see the changes: + +![Custom Settings Tab](./images/custom-settings.png) diff --git a/docs/en/UI/Angular/Customization-User-Interface.md b/docs/en/UI/Angular/Customization-User-Interface.md new file mode 100644 index 0000000000..20809a1d87 --- /dev/null +++ b/docs/en/UI/Angular/Customization-User-Interface.md @@ -0,0 +1,3 @@ +# Angular User Interface Customization Guide + +* [Replacing a component](Component-Replacement.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md new file mode 100644 index 0000000000..01394a3fea --- /dev/null +++ b/docs/en/UI/Angular/Localization.md @@ -0,0 +1,136 @@ +# Localization + +Before you read about _the Localization Pipe_ and _the Localization Service_, you should know about localization keys. + +The Localization key format consists of 2 sections which are **Resource Name** and **Key**. +`ResourceName::Key` + +> If you do not specify the resource name, it will be `defaultResourceName` which is declared in `environment.ts` + +```js +const environment = { + //... + localization: { + defaultResourceName: 'MyProjectName', + }, +}; +``` + +So these two are the same: + +```html +

{%{{{ '::Key' | abpLocalization }}}%}

+ +

{%{{{ 'MyProjectName::Key' | abpLocalization }}}%}

+``` + +## Using the Localization Pipe + +You can use the `abpLocalization` pipe to get localized text as in this example: + +```html +

{%{{{ 'Resource::Key' | abpLocalization }}}%}

+``` + +The pipe will replace the key with the localized text. + +You can also specify a default value as shown below: + +```html +

{%{{{ { key: 'Resource::Key', defaultValue: 'Default Value' } | abpLocalization }}}%}

+``` + +To use interpolation, you must give the values for interpolation as pipe parameters, for example: + +Localization data is stored in key-value pairs: + +```js +{ + //... + AbpAccount: { // AbpAccount is the resource name + Key: "Value", + PagerInfo: "Showing {0} to {1} of {2} entries" + } +} +``` + +So we can use this key like this: + +```html +

{%{{{ 'AbpAccount::PagerInfo' | abpLocalization:'20':'30':'50' }}}%}

+ + +``` + +### Using the Localization Service + +First of all you should import the `LocalizationService` from **@abp/ng.core** + +```js +import { LocalizationService } from '@abp/ng.core'; + +class MyClass { + constructor(private localizationService: LocalizationService) {} +} +``` + +After that, you are able to use localization service. + +> You can add interpolation parameters as arguments to `instant()` and `get()` methods. + +```js +this.localizationService.instant('AbpIdentity::UserDeletionConfirmation', 'John'); + +// with fallback value +this.localizationService.instant( + { key: 'AbpIdentity::UserDeletionConfirmation', defaultValue: 'Default Value' }, + 'John', +); + +// Output +// User 'John' will be deleted. Do you confirm that? +``` + +To get a localized text as [_Observable_](https://rxjs.dev/guide/observable) use `get` method instead of `instant`: + +```js +this.localizationService.get('Resource::Key'); + +// with fallback value +this.localizationService.get({ key: 'Resource::Key', defaultValue: 'Default Value' }); +``` + +### Using the Config State + +In order to you `getLocalization` method you should import ConfigState. + +```js +import { ConfigState } from '@abp/ng.core'; +``` + +Then you can use it as followed: + +```js +this.store.selectSnapshot(ConfigState.getLocalization('ResourceName::Key')); +``` + +`getLocalization` method can be used with both `localization key` and [`LocalizationWithDefault`](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L34) interface. + +```js +this.store.selectSnapshot( + ConfigState.getLocalization( + { + key: 'AbpIdentity::UserDeletionConfirmation', + defaultValue: 'Default Value', + }, + 'John', + ), +); +``` + +Localization resources are stored in the `localization` property of `ConfigState`. + + +## See Also + +* [Localization in ASP.NET Core](../../Localization.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md new file mode 100644 index 0000000000..6e5c5d7155 --- /dev/null +++ b/docs/en/UI/Angular/Permission-Management.md @@ -0,0 +1,79 @@ +# Permission Management + +A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../Authorization.md) document. + +You can get permission of authenticated user using `getGrantedPolicy` selector of `ConfigState`. + +You can get permission as boolean value from store: + +```js +import { Store } from '@ngxs/store'; +import { ConfigState } from '../states'; + +export class YourComponent { + constructor(private store: Store) {} + + ngOnInit(): void { + const canCreate = this.store.selectSnapshot(ConfigState.getGrantedPolicy('AbpIdentity.Roles.Create')); + } + + // ... +} +``` + +Or you can get it via `ConfigStateService`: + +```js +import { ConfigStateService } from '../services/config-state.service'; + +export class YourComponent { + constructor(private configStateService: ConfigStateService) {} + + ngOnInit(): void { + const canCreate = this.configStateService.getGrantedPolicy('AbpIdentity.Roles.Create'); + } + + // ... +} +``` + +## Permission Directive + +You can use the `PermissionDirective` to manage visibility of a DOM Element accordingly to user's permission. + +```html +
+ This content is only visible if the user has 'AbpIdentity.Roles' permission. +
+``` + +As shown above you can remove elements from DOM with `abpPermission` structural directive. + +The directive can also be used as an attribute directive but we recommend to you to use it as a structural directive. + +## Permission Guard + +You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation. + +Add `requiredPolicy` to the `routes` property in your routing module. + +```js +const routes: Routes = [ + { + path: 'path', + component: YourComponent, + canActivate: [PermissionGuard], + data: { + routes: { + requiredPolicy: 'AbpIdentity.Roles.Create', + }, + }, + }, +]; +``` + +Granted Policies are stored in the `auth` property of `ConfigState`. + +## What's Next? + +* [Component Replacement](./Component-Replacement.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/images/component-replacement.gif b/docs/en/UI/Angular/images/component-replacement.gif new file mode 100644 index 0000000000..3a88500e53 Binary files /dev/null and b/docs/en/UI/Angular/images/component-replacement.gif differ diff --git a/docs/en/UI/Angular/images/custom-settings.png b/docs/en/UI/Angular/images/custom-settings.png new file mode 100644 index 0000000000..05dd3f695a Binary files /dev/null and b/docs/en/UI/Angular/images/custom-settings.png differ diff --git a/docs/en/UI/AspNetCore/Bundling-Minification.md b/docs/en/UI/AspNetCore/Bundling-Minification.md new file mode 100644 index 0000000000..5af3f177d5 --- /dev/null +++ b/docs/en/UI/AspNetCore/Bundling-Minification.md @@ -0,0 +1,352 @@ + +# ASP.NET Core MVC Bundling & Minification + +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 depended 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) diff --git a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md new file mode 100644 index 0000000000..63c5cec225 --- /dev/null +++ b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md @@ -0,0 +1,116 @@ + +## ASP.NET Core MVC Client Side Package Management + +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://classic.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://classic.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) diff --git a/docs/en/UI/AspNetCore/Customization-User-Interface.md b/docs/en/UI/AspNetCore/Customization-User-Interface.md new file mode 100644 index 0000000000..c5c974e5eb --- /dev/null +++ b/docs/en/UI/AspNetCore/Customization-User-Interface.md @@ -0,0 +1,470 @@ +# ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide + +This document explains how to override the user interface of a depended [application module](../../Modules/Index.md) for ASP.NET Core MVC / Razor Page applications. + +## Overriding a Page + +This section covers the [Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/) development, which is the recommended approach to create server rendered user interface for ASP.NET Core. Pre-built modules typically uses the Razor Pages approach instead of the classic MVC pattern (next sections will cover the MVC pattern too). + +You typically have three kind of override requirement for a page: + +* Overriding **only the Page Model** (C#) side to perform additional logic without changing the page UI. +* Overriding **only the Razor Page** (.chtml file) to change the UI without changing the c# behind the page. +* **Completely overriding** the page. + +### Overriding a Page Model (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 + } + } +} +```` + +* This class inherits from and replaces the `EditModalModel` for the users and overrides the `OnPostAsync` method to perform additional logic before and after the underlying code. +* It uses `ExposeServices` and `Dependency` attributes to replace the class. + +### Overriding a Razor Page (.CSHTML) + +Overriding a `.cshtml` file (razor page, razor view, view component... etc.) is possible through the [Virtual File System](../../Virtual-File-System.md). + +Virtual File system allows us to **embed resources into assemblies**. In this way, pre-built modules define the razor pages inside their NuGet packages. When you depend a module, you can override any file added to the virtual file system by that module, including pages/views. + +#### Example + +This example overrides the **login page** UI defined by the [Account Module](../../Modules/Account.md). + +Physical files override the embedded files defined in the same location. The account module defines a `Login.cshtml` file under the `Pages/Account` folder. So, you can override it by creating a file in the same path: + +![overriding-login-cshtml](../../images/overriding-login-cshtml.png) + +You typically want to copy the original `.cshtml` file of the module, then make the necessary changes. You can find the original file [here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml). Do not copy the `Login.cshtml.cs` file which is the code behind file for the razor page and we don't want to override it yet (see the next section). + +That's all, you can change the file content however you like. + +### Completely Overriding a Razor Page + +You may want to completely override a page; the razor and the c# file related to the page. + +In such a case; + +1. Override the C# page model class just like described above, but don't replace the existing page model class. +2. Override the Razor Page just described above, but also change the @model directive to point your new page model. + +#### Example + +This example overrides the **login page** defined by the [Account Module](../../Modules/Account.md). + +Create a page model class deriving from the ` LoginModel ` (defined in the ` Volo.Abp.Account.Web.Pages.Account ` namespace): + +````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... +} +```` + +You can override any method or add new properties/methods if needed. + +> Notice that we didn't use `[Dependency(ReplaceServices = true)]` or `[ExposeServices(typeof(LoginModel))]` since we don't want to replace the existing class in the dependency injection, we define a new one. + +Copy `Login.cshtml` file into your solution as just described above. Change the **@model** directive to point to the `MyLoginModel`: + +````xml +@page +... +@model Acme.BookStore.Web.Pages.Account.MyLoginModel +... +```` + +That's all! Make any change in the view and run your application. + +#### Replacing Page Model Without Inheritance + +You don't have to inherit from the original page model class (like done in the previous example). Instead, you can completely **re-implement** the page yourself. In this case, just derive from `PageModel`, `AbpPageModel` or any suitable base class you need. + +## Overriding a View Component + +The ABP Framework, pre-built themes and modules define some **re-usable view components**. These view components can be replaced just like a page described above. + +### Example + +The screenshot below was taken from the **basic theme** comes with the application startup template. + +![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) + +[The basic theme](../../Themes/Basic.md) defines some view components for the layout. For example, the highlighted area with the red rectangle above is called **Brand component**. You probably want to customize this component by adding your **own application logo**. Let's see how to do it. + +First, create your logo and place under a folder in your web application. We used `wwwroot/logos/bookstore-logo.png` path. Then copy the Brand component's view ([from here](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml)) from the basic theme files under the `Themes/Basic/Components/Brand` folder. The result should be similar the picture below: + +![bookstore-added-brand-files](../../images/bookstore-added-brand-files.png) + +Then change the `Default.cshtml` as you like. Example content can be like that: + +````xml + + + +```` + +Now, you can run the application to see the result: + +![bookstore-added-logo](../../images/bookstore-added-logo.png) + +If you need, you can also replace [the code behind c# class](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/MainNavbarBrandViewComponent.cs) of the component just using the dependency injection system. + +### Overriding the Theme + +Just as explained above, you can replace any component, layout or c# class of the used theme. See the [theming document](Theming.md) for more information on the theming system. + +## Overriding Static Resources + +Overriding a static embedded resource (like JavaScript, Css or image files) of a module is pretty easy. Just place a file in the same path in your solution and let the Virtual File System to handle it. + +## Manipulating the Bundles + +The [Bundling & Minification](Bundling-Minification.md) system provides an **extensible and dynamic** system to create **script** and **style** bundles. It allows you to extend and manipulate the existing bundles. + +### Example: Add a Global CSS File + +For example, ABP Framework defines a **global style bundle** which is added to every page (actually, added to the layout by the themes). Let's add a **custom style file** to the end of the bundle files, so we can override any global style. + +First, create a CSS file and locate it in a folder inside the `wwwroot`: + +![bookstore-global-css-file](../../images/bookstore-global-css-file.png) + +Define some custom CSS rules inside the file. Example: + +````css +.card-title { + color: orange; + font-size: 2em; + text-decoration: underline; +} + +.btn-primary { + background-color: red; +} +```` + +Then add this file to the standard global style bundle in the `ConfigureServices` method of your [module](../../Module-Development-Basics.md): + +````csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, //The bundle name! + bundleConfiguration => + { + bundleConfiguration.AddFiles("/styles/my-global-styles.css"); + } + ); +}); +```` + +#### The Global Script Bundle + +Just like the `StandardBundles.Styles.Global`, there is a `StandardBundles.Scripts.Global` that you can add files or manipulate the existing ones. + +### Example: Manipulate the Bundle Files + +The example above adds a new file to the bundle. You can do more if you create a **bundle contributor** class. Example: + +````csharp +public class MyGlobalStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Clear(); + context.Files.Add("/styles/my-global-styles.css"); + } +} +```` + +Then you can add the contributor to an existing bundle: + +````csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundleConfiguration => + { + bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor)); + } + ); +}); +```` + +It is not a good idea to clear all CSS files. In a real world scenario, you can find and replace a specific file with your own file. + +### Example: Add a JavaScript File for a Specific Page + +The examples above works with the global bundle added to the layout. What if you want to add a CSS/JavaScript file (or replace a file) for a specific page defines inside a depended module? + +Assume that you want to run a **JavaScript code** once the user enters to the **Role Management** page of the Identity Module. + +First, create a standard JavaScript file under the `wwwroot`, `Pages` or `Views` folder (ABP support to add static resources inside these folders by default). We prefer the `Pages/Identity/Roles` folder to follow the conventions: + +![bookstore-added-role-js-file](../../images/bookstore-added-role-js-file.png) + +Content of the file is simple: + +````js +$(function() { + abp.log.info('My custom role script file has been loaded!'); +}); +```` + +Then add this file to the bundle of the role management page: + +````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` is the safe way to get the bundle name for the role management page. + +> Notice that not every page defines such page bundles. They define only if needed. + +In addition to adding new CSS/JavaScript file to a page, you also can replace the existing one (by defining a bundle contributor). + +## Layout Customization + +Layouts are defined by the theme ([see the theming](Theming.md)) by design. They are not included in a downloaded application solution. In this way you can easily **upgrade** the theme and get new features. You can not **directly change** the layout code in your application unless you replace it by your own layout (will be explained in the next sections). + +There are some common ways to **customize the layout** described in the next sections. + +### Menu Contributors + +There are two **standard menus** defined by the ABP Framework: + +![bookstore-menus-highlighted](../../images/bookstore-menus-highlighted.png) + +* `StandardMenus.Main`: The main menu of the application. +* `StandardMenus.User`: The user menu (generally at the top right of the screen). + +Rendering the menus is a responsibility of the theme, but **menu items** are determined by the modules and your application code. Just implement the `IMenuContributor` interface and **manipulate the menu items** in the `ConfigureMenuAsync` method. + +Menu contributors are executed whenever need to render the menu. There is already a menu contributor defined in the **application startup template**, so you can take it as an example and improve if necessary. See the [navigation menu](Navigation-Menu.md) document for more. + +### Toolbar Contributors + +[Toolbar system](Toolbars.md) is used to define **toolbars** on the user interface. Modules (or your application) can add **items** to a toolbar, then the theme renders the toolbar on the **layout**. + +There is only one **standard toolbar** (named "Main" - defined as a constant: `StandardToolbars.Main`). For the basic theme, it is rendered as shown below:![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) + +In the screenshot above, there are two items added to the main toolbar: Language switch component & user menu. You can add your own items here. + +#### Example: Add a Notification Icon + +In this example, we will add a **notification (bell) icon** to the left of the language switch item. A item in the toolbar should be a **view component**. So, first, create a new view component in your project: + +![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 +
+ +
+```` + +Now, we can create a class implementing the `IToolbarContributor` interface: + +````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; + } +} +```` + +This class adds the `NotificationViewComponent` as the first item in the `Main` toolbar. + +Finally, you need to add this contributor to the `AbpToolbarOptions`, in the `ConfigureServices` of your module: + +````csharp +Configure(options => +{ + options.Contributors.Add(new MyToolbarContributor()); +}); +```` + +That's all, you will see the notification icon on the toolbar when you run the application: + +![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) + +`NotificationViewComponent` in this sample simply returns a view without any data. In real life, you probably want to **query database** (or call an HTTP API) to get notifications and pass to the view. If you need, you can add a `JavaScript` or `CSS` file to the global bundle (as described before) for your toolbar item. + +See the [toolbars document](Toolbars.md) for more about the toolbar system. + +### Layout Hooks + +[Layout Hooks](Layout-Hooks.md) system allows you to **add code** at some specific parts of the layout. All layouts of all themes should implement these hooks. Then you can then add a **view component** into a hook point. + +#### Example: Add Google Analytics Script + +Assume that you need to add the Google Analytics script to the layout (that will be available for all the pages). First, **create a view component** in your project: + +![bookstore-google-analytics-view-component](../../images/bookstore-google-analytics-view-component.png) + +**NotificationViewComponent.cs** + +````csharp +public class GoogleAnalyticsViewComponent : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); + } +} +```` + +**Default.cshtml** + +````html + +```` + +Change `UA-xxxxxx-1` with your own code. + +You can then add this component to any of the hook points in the `ConfigureServices` of your module: + +````csharp +Configure(options => +{ + options.Add( + LayoutHooks.Head.Last, //The hook name + typeof(GoogleAnalyticsViewComponent) //The component to add + ); +}); +```` + +Now, the GA code will be inserted in the `head` of the page as the last item. 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/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/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/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/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md new file mode 100644 index 0000000000..d7e393933e --- /dev/null +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -0,0 +1,26 @@ +# 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) +* ... + +> 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/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/_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 6657ac629a..c273e59346 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -38,14 +38,45 @@ "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": "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": "Migrating from the ASP.NET Boilerplate", + "path": "AspNet-Boilerplate-Migration-Guide.md" } ] }, @@ -104,6 +135,10 @@ "text": "Caching", "path": "Caching.md" }, + { + "text": "Logging", + "path": "Logging.md" + }, { "text": "Audit Logging", "path": "Audit-Logging.md" @@ -111,6 +146,10 @@ { "text": "Settings", "path": "Settings.md" + }, + { + "text": "Data Filtering", + "path": "Data-Filtering.md" } ] }, @@ -203,7 +242,8 @@ "text": "Domain Services" }, { - "text": "Specifications" + "text": "Specifications", + "path": "Specifications.md" } ] }, @@ -225,43 +265,68 @@ ] }, { - "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": "Angular", + "items": [ + { + "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" } ] } @@ -283,20 +348,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": "Database Migrations", + "path": "Entity-Framework-Core-Migrations.md" }, { - "text": "Switch to SQLite", - "path": "Entity-Framework-Core-SQLite.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" + } + ] } ] }, @@ -326,6 +397,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" } ] } @@ -374,4 +459,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/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/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..331e66c18e --- /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/" + } + } +} +```` + +查看下面的"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 + +默认情况下`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/Ambient-Context-Pattern.md b/docs/zh-Hans/Ambient-Context-Pattern.md new file mode 100644 index 0000000000..d00a721ce2 --- /dev/null +++ b/docs/zh-Hans/Ambient-Context-Pattern.md @@ -0,0 +1,3 @@ +## Ambient Context Pattern + +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/Audit-Logging.md b/docs/zh-Hans/Audit-Logging.md new file mode 100644 index 0000000000..a028e640b2 --- /dev/null +++ b/docs/zh-Hans/Audit-Logging.md @@ -0,0 +1,375 @@ +# 审计日志 + +[维基百科](https://en.wikipedia.org/wiki/Audit_trail): "*审计跟踪(也称为**审计日志**)是一种安全相关的按时间顺序记录,记录集或记录目的和来源. 这种记录提供了在任何特定时间的操作,过程或事件产生影响活动顺序的文件证据* ". + +ABP框架提供一个可扩展的**审计日志系统**,自动化的根据**约定**记录审计日志,并提供**配置**控制审计日志的级别. + +一个**审计日志对象**(参见下面的审计日志对象部分)通常是针对每个web请求创建和保存的.包括; + +* **请求和响应的细节** (如URL,HTTP方法,浏览器信息,HTTP状态代码...等). +* **执行的动作** (控制器操作和应用服务方法调用及其参数). +* **实体的变化** (在Web请求中). +* **异常信息** (如果在执行请求发生操作). +* **请求时长** (测量应用程序的性能). + +> [启动模板](Startup-Templates/Index.md)已经将审计日志系统配置为适用于大多数应用程序. 本文档介绍了对审计日志系统更精细的控制. + +## 数据库提供程序支持 + +* [Entity Framework Core](Entity-Framework-Core.md)提供程序完全支持. +* [MongoDB](MongoDB.md)提供程序不支持实体更改审计记录. 其他功能按预期工作. + +## UseAuditing() + +`UseAuditing()` 中间件应该被添加到ASP.NET Core请求管道,用于创建和保存审计日志. 如果你使用[启动模板](Startup-Templates/Index.md)创建的应用程序,它已经默认添加. + +## AbpAuditingOptions + +`AbpAuditingOptions` 是配置审计日志系统的主要[options对象](Options.md). 你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中进行配置: + +````csharp +Configure(options => +{ + options.IsEnabled = false; //Disables the auditing system +}); +```` + +这里是你可以配置的选项列表: + +* `IsEnabled` (默认值: `true`): 启用或禁用审计系统的总开关. 如果值为 `false`,则不使用其他选项. +* `HideErrors` (默认值: `true`): 在保存审计日志对象时如果发生任何错误,审计日志系统会将错误隐藏并写入常规[日志](Logging.md). 如果保存审计日志对系统非常重要那么将其设置为 `false` 以便在隐藏错误时抛出异常. +* `IsEnabledForAnonymousUsers` (默认值: `true`): 如果只想为经过身份验证的用户记录审计日志,请设置为 `false`.如果为匿名用户保存审计日志,你将看到这些用户的 `UserId` 值为 `null`. +* `AlwaysLogOnException`(默认值: `true`): 如果设置为 `true`,将始终在异常/错误情况下保存审计日志,不检查其他选项(`IsEnabled` 除外,它完全禁用了审计日志). +* `IsEnabledForGetRequests` (默认值: `false`): HTTP GET请求通常不应该在数据库进行任何更改,审计日志系统不会为GET请求保存审计日志对象. 将此值设置为 `true` 可为GET请求启用审计日志系统. +* `ApplicationName`: 如果有多个应用程序保存审计日志到单一的数据库,使用此属性设置为你的应用程序名称区分不同的应用程序日志. +* `IgnoredTypes`: 审计日志系统忽略的 `Type` 列表. 如果它是实体类型,则不会保存此类型实体的更改. 在序列化操作参数时也使用此列表. +* `EntityHistorySelectors`:选择器列表,用于确定是否选择了用于保存实体更改的实体类型. 有关详细信息请参阅下面的部分. +* `Contributors`: `AuditLogContributor` 实现的列表. 贡献者是扩展审计日志系统的一种方式. 有关详细信息请参阅下面的"审计日志贡献者"部分. + +### 实体历史选择器 + +保存您的所有实体的所有变化将需要大量的数据库空间. 出于这个原因**审计日志系统不保存为实体的任何改变,除非你明确地对其进行配置**. + +要保存的所有实体的所有更改,只需使用 `AddAllEntities()` 扩展方法. + +````csharp +Configure(options => +{ + options.EntityHistorySelectors.AddAllEntities(); +}); +```` + +`options.EntityHistorySelectors` 实际上是一个类型谓词的列表,你可以写一个lambda表达式定义过滤器. + +下面的示例中与使用 `AddAllEntities()` 扩展方法效果相同: + +````csharp +Configure(options => +{ + options.EntityHistorySelectors.Add( + new NamedTypeSelector( + "MySelectorName", + type => + { + if (typeof(IEntity).IsAssignableFrom(type)) + { + return true; + } + else + { + return false; + } + } + ) + ); +}); +```` + +条件 `typeof(IEntity).IsAssignableFrom(type)` 对于任何实现 `IEntity` 接口的类(从技术上来这些都是你应用程序中的实体) 结果都为 `true` . 你可以根据自己的逻辑编写条件并返回 `true` 或 `false`. + +`options.EntityHistorySelectors` 是一种灵活动态的选择实体进行审计日志记录的方法. 另一种方法是为每个实体使用 `Audited` 和 `DisableAuditing` attribute. + +## 启用/禁用审计日志服务 + +### 启用/禁用 Controllers & Actions + +默认所有的控制器动作都会被记录下来(有关GET请求,请参阅上面的 `IsEnabledForGetRequests` ). + +你可以使用 `[DisableAuditing]` 来禁用特定的控制器: + +````csharp +[DisableAuditing] +public class HomeController : AbpController +{ + //... +} +```` + +使用 `[DisableAuditing]` 在action级别控制: + +````csharp +public class HomeController : AbpController +{ + [DisableAuditing] + public async Task Home() + { + //... + } + + public async Task OtherActionLogged() + { + //... + } +} +```` + +### 启用/禁用 应用服务&方法 + +[应用服务](Application-Services.md)也默认包含在审计日志中. 你可在服务或方法级别使用 `[DisableAuditing]`. + +#### 启用/禁用 其他服务 + +可以为任何类型的类(注册到[依赖注入](Dependency-Injection.md)并从依赖注入解析)启用审计日志,默认情况下仅对控制器和应用程序服务启用. + +对于任何需要被审计记录的类或方法都可以使用 `[Audited]` 和`IAuditingEnabled`.此外,您的类可以(直接或固有的)实现 `IAuditingEnabled` 接口以认启用该类的审计日志记录. + +### 启用/禁用 实体 & 属性 + +以下情况下实体在实体更改审计日志记录中忽略实体; + +* 如果将实体类型添加到 `AbpAuditingOptions.IgnoredTypes`(如前所述),它在审计日志系统中被完全忽略. +* 如果对象不是[实体](Entities.md)(没有直接或固有的实现 `IEntity` - 所有实体默认实现这个接口). +* 如果实体访问级别不是public的. + +你可以使用 `Audited` 来启用实体更改审计日志: + +````csharp +[Audited] +public class MyEntity : Entity +{ + //... +} +```` + +或者禁用实体: + +````csharp +[DisableAuditing] +public class MyEntity : Entity +{ + //... +} +```` + +只有前面提到的 `AbpAuditingOptions.EntityHistorySelector` 选择实体时才有必要禁用审计日志记录. + +你可以仅禁用实体的某些属性的审计,以审计日志记录进行精细控制: + +````csharp +[Audited] +public class MyUser : Entity +{ + public string Name { get; set; } + + public string Email { get; set; } + + [DisableAuditing] //Ignore the Passoword on audit logging + public string Password { get; set; } +} +```` + +审计日志系统保存 `MyUser` 实体的更改,出于安全的目的忽略 `Password` 属性. + +在某些情况下你可能要保存一些属性,但忽略所有其他属性. 为忽略的属性编写 `[DisableAuditing]` 将很乏味. 这种情况下将 `[Audited]` 用于所需的属性,使用 `[DisableAuditing]` 属性标记该实体: + +````csharp +[DisableAuditing] +public class MyUser : Entity +{ + [Audited] //Only log the Name change + public string Name { get; set; } + + public string Email { get; set; } + + public string Password { get; set; } +} +```` + +## IAuditingStore + +`IAuditingStore` 是一个接口,用于保存ABP框架的审计日志对象(下面说明). 如果需要将审计日志对象保存到自定义数据存储中,可以在自己的应用程序中实现 `IAuditingStore` 并在[依赖注入系统](Dependency-Injection.md)替换. + +如果没有注册审计存储,则使用 `SimpleLogAuditingStore`. 它只是将审计对象写入标准[日志系统](Logging.md). + +[审计日志模块](Modules/Audit-Logging.md)已在[启动模板](Startup-Templates/Index.md)中配置,它将审计日志对象保存到数据库中(支持多个数据库提供程序). 所以大多数时候你并不需要关心 `IAuditingStore` 是如何实现和使用的. + +## 审计日志对象 + +默认为每个**web请求**创建一个**审计日志对象**,审计日志对象可以由以下关系图表示: + +![**auditlog-object-diagram**](images/auditlog-object-diagram.png) + +* **AuditLogInfo**: 具有以下属性: + * `ApplicationName`: 当你保存不同的应用审计日志到同一个数据库,这个属性用来区分应用程序. + * `UserId`:当前用户的Id,用户未登录为 `null`. + * `UserName`:当前用户的用户名,如果用户已经登录(这里的值不依赖于标识模块/系统进行查找). + * `TenantId`: 当前租户的Id,对于多租户应用. + * `TenantName`: 当前租户的名称,对于多租户应用. + * `ExecutionTime`: 审计日志对象创建的时间. + * `ExecutionDuration`: 请求的总执行时间,以毫秒为单位. 可以用来观察应用程序的性能. + * `ClientId`: 当前客户端的Id,如果客户端已经通过认证.客户端通常是使用HTTP API的第三方应用程序. + * `ClientName`: 当前客户端的名称,如果有的话. + * `ClientIpAddress`: 客户端/用户设备的IP地址. + * `CorrelationId`: 当前[相关Id](CorrelationId.md). 相关Id用于在单个逻辑操作中关联由不同应用程序(或微服务)写入的审计日志. + * `BrowserInfo`: 当前用户的浏览器名称/版本信息,如果有的话. + * `HttpMethod`: 当前HTTP请求的方法(GET,POST,PUT,DELETE ...等). + * `HttpStatusCode`: HTTP响应状态码. + * `Url`: 请求的URL. +* **AuditLogActionInfo**: 一个 审计日志动作通常是web请求期间控制器动作或[应用服务](Application-Services.md)方法调用. 一个审计日志可以包含多个动作. 动作对象具有以下属性: + * `ServiceName`:执行的控制器/服务的名称. + * `MethodName`:控制器/服务执行的方法的名称. + * `Parameters`:传递给方法的参数的JSON格文本. + * `ExecutionTime`: 执行的时间. + * `ExecutionDuration`: 方法执行时长,以毫秒为单位. 可以用来观察方法的性能. +* **EntityChangeInfo**: 表示一个实体在Web请求中的变更. 审计日志可以包含0个或多个实体的变更. 实体变更具有以下属性: + * `ChangeTime`: 当实体被改变的时间. + * `ChangeType`:具有以下字段的枚举: `Created`(0), `Updated`(1)和 `Deleted`(2). + * `EntityId`: 更改实体的Id. + * `EntityTenantId`:实体所属的租户Id. + * `EntityTypeFullName`: 实体的类型(类)的完整命名空间名称(例如Book实体的*Acme.BookStore.Book*. +* **EntityPropertyChangeInfo**: 表示一个实体的属性的更改.一个实体的更改信息(上面已说明)可含有具有以下属性的一个或多个属性的更改: + * `NewValue`: 属性的新值. 如果实体已被删除为 `null`. + * `OriginalValue`:变更前旧/初始值. 如果实体是新创建为 `null`. + * `PropertyName`: 实体类的属性名称. + * `PropertyTypeFullName`:属性类型的完整命名空间名称. +* **Exception**: 审计日志对象可能包含零个或多个异常. 可以得到失败请求的异常信息. +* **Comment**:用于将自定义消息添加到审计日志条目的任意字符串值. 审计日志对象可能包含零个或多个注释. + +除了上面说明的标准属性之外,`AuditLogInfo`, `AuditLogActionInfo` 和 `EntityChangeInfo` 对象还实现了`IHasExtraProperties` 接口,你可以向这些对象添加自定义属性. + +## 审计日志贡献者 + +你可以创建类继承 `AuditLogContributor`类 来扩展审计系统,该类定义了 `PreContribute` 和 `PostContribute` 方法. + +唯一预构建的贡献者是 `AspNetCoreAuditLogContributor` 类,它设置HTTP请求的相关属性. + +贡献者可以设置 `AuditLogInfo` 类的属性和集合来添加更多信息. + +例: + +````csharp +public class MyAuditLogContributor : AuditLogContributor +{ + public override void PreContribute(AuditLogContributionContext context) + { + var currentUser = context.ServiceProvider.GetRequiredService(); + context.AuditInfo.SetProperty( + "MyCustomClaimValue", + currentUser.FindClaimValue("MyCustomClaim") + ); + } + + public override void PostContribute(AuditLogContributionContext context) + { + context.AuditInfo.Comments.Add("Some comment..."); + } +} +```` + +* `context.ServiceProvider` 可以从[依赖注入系统](Dependency-Injection.md)中解析服务. +* `context.AuditInfo` 可以用来访问当前审计日志的对象并进行操作. + +创建贡献者后,需要将其添加到 `AbpAuditingOptions.Contributors` 列表中: + +````csharp +Configure(options => +{ + options.Contributors.Add(new MyAuditLogContributor()); +}); +```` + +## IAuditLogScope & IAuditingManager + +本节介绍用于高级用例的 `IAuditLogScope` 和 `IAuditingManager` 服务. + +**审计日志范围**是**构建**和**保存**审计日志对象的[环境范围](Ambient-Context-Pattern.md)(前面解释过). 默认审计日志中间件会为Web请求创建审计日志范围(请参阅上面的 `UseAuditing()` 部分). + +### 获取当前审计日志范围 + +上面提到,审计日志贡献者是操作审计日志对象的全局方法. 你可从服务中获得值. + +如果需要在应用程序的任意位置上操作审计日志对象,可以访问当前审计日志范围并获取当前审计日志对象(与范围的管理方式无关). +例: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IAuditingManager _auditingManager; + + public MyService(IAuditingManager auditingManager) + { + _auditingManager = auditingManager; + } + + public async Task DoItAsync() + { + var currentAuditLogScope = _auditingManager.Current; + if (currentAuditLogScope != null) + { + currentAuditLogScope.Log.Comments.Add( + "Executed the MyService.DoItAsync method :)" + ); + + currentAuditLogScope.Log.SetProperty("MyCustomProperty", 42); + } + } +} +```` + +总是检查 `_auditingManager.Current` 是否为空,因为它是在外部范围中控制的,在调用方法之前你不知道是否创建了审计日志范围. + +### 手动创建审计日志范围 + +你很少需要手动创建审计日志的范围,但如果你需要,可以使用 `IAuditingManager` 创建审计日志的范围. +例: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IAuditingManager _auditingManager; + + public MyService(IAuditingManager auditingManager) + { + _auditingManager = auditingManager; + } + + public async Task DoItAsync() + { + using (var auditingScope = _auditingManager.BeginScope()) + { + try + { + //Call other services... + } + catch (Exception ex) + { + //Add exceptions + _auditingManager.Current.Log.Exceptions.Add(ex); + } + finally + { + //Always save the log + await auditingScope.SaveAsync(); + } + } + } +} +```` + +您可以调用其他服务,它们可能调用其他服务,它们可能更改实体,等等. 所有这些交互都保存为finally块中的一个审计日志对象. + +## 审计日志模块 + +审计日志模块基本上实现了 `IAuditingStore`, 将审计日志对象保存到数据库中并支持多个数据库提供程序. 默认此模块已添加到启动模板中. + +参见[审计日志模块文档](Modules/Audit-Logging.md)了解更多. \ No newline at end of file diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index 0a15f6e2ca..7ae525e1f4 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -367,4 +367,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-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 30b6f0f1ad..398a8efc8b 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 提供了创建后台作业和队列作业所需要的服务. 如果你的模块只依赖这个包,那么它可以独立于其实现/集成. @@ -110,7 +110,7 @@ Enqueue方法接收一些可选参数用于控制后台作业: 你可能希望在你的应用程序中禁用后台作业执行. 如果你希望在另一个进程中执行后台作业并在当前进程中禁用它,通常可以使用以下命令. -使用 `BackgroundJobOptions` 配置作业执行: +使用 `AbpBackgroundJobOptions` 配置作业执行: ````csharp [DependsOn(typeof(AbpBackgroundJobsModule))] @@ -118,7 +118,7 @@ public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { options.IsJobExecutionEnabled = false; //禁用作业执行 }); @@ -142,7 +142,7 @@ ABP framework 包含一个简单的 `IBackgroundJobManager` 实现; ### 配置 -在你的[模块类](Module-Development-Basics.md)中使用 `BackgroundJobWorkerOptions` 配置默认作业管理器. +在你的[模块类](Module-Development-Basics.md)中使用 `AbpBackgroundJobWorkerOptions` 配置默认作业管理器. 示例中更改后台作业的的超时时间: ````csharp @@ -151,7 +151,7 @@ public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => + Configure(options => { options.DefaultTimeout = 864000; //10 days (as seconds) }); @@ -174,4 +174,5 @@ 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) \ 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..675ca882ec --- /dev/null +++ b/docs/zh-Hans/Background-Workers.md @@ -0,0 +1,3 @@ +# 后台工作者 + +TODO \ No newline at end of file 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-02-22/Post.md b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md index 69a8603baf..30a5d691e9 100644 --- a/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md +++ b/docs/zh-Hans/Blog-Posts/2019-02-22/Post.md @@ -1,12 +1,12 @@ # 微服务演示,项目状态和路线图 -在ABP vNext上的[第一个公告](https://cn.abp.io/blog/abp/Abp-vNext-Announcement)之后,我们对代码库进行了很多改进([GitHub存储库](https://github.com/abpframework/abp)上的1100多次提交).我们已经创建了功能,示例,文档等等.在这篇文章中,我想告诉你一些新闻和项目的状态. +在ABP vNext上的[第一个公告](https://abp.io/blog/abp/Abp-vNext-Announcement)之后,我们对代码库进行了很多改进([GitHub存储库](https://github.com/abpframework/abp)上的1100多次提交).我们已经创建了功能,示例,文档等等.在这篇文章中,我想告诉你一些新闻和项目的状态. ## 微服务演示解决方案 -ABP框架的主要目标之一是提供[创建微服务解决方案的便利基础设施](https://cn.abp.io/documents/abp/latest/Microservice-Architecture). +ABP框架的主要目标之一是提供[创建微服务解决方案的便利基础设施](https://abp.io/documents/abp/latest/Microservice-Architecture). -我们一直在努力开发微服务解决方案演示.初始版本已完成并[文档化](https://cn.abp.io/documents/abp/latest/Samples/Microservice-Demo).该示例解决方案旨在演示一个简单而完整的微服务解决方案; +我们一直在努力开发微服务解决方案演示.初始版本已完成并[文档化](https://abp.io/documents/abp/latest/Samples/Microservice-Demo).该示例解决方案旨在演示一个简单而完整的微服务解决方案; - 具有多个独立的,可自我部署的**微服务**. - 多个**Web应用程序**,每个都使用不同的API网关. @@ -20,7 +20,7 @@ ABP框架的主要目标之一是提供[创建微服务解决方案的便利基 - 使用[Docker](https://www.docker.com/)和[Kubernates](https://kubernetes.io/)**部署**并运行所有服务和应用程序. - 使用[Elasticsearch](https://www.elastic.co/products/elasticsearch)和[Kibana](https://www.elastic.co/products/kibana)存储和可视化日志(使用[Serilog](https://serilog.net/)编写). -有关解决方案的详细说明,请参阅[其文档](https://cn.abp.io/documents/abp/latest/Samples/Microservice-Demo). +有关解决方案的详细说明,请参阅[其文档](https://abp.io/documents/abp/latest/Samples/Microservice-Demo). ## 改进/功能 @@ -32,13 +32,13 @@ ABP框架的主要目标之一是提供[创建微服务解决方案的便利基 根据我们的估计,我们计划在2019年第二季度(可能在五月或六月)发布v1.0.所以,不用等待太长时间了.我们也对第一个稳定版本感到非常兴奋. -我们还将完善[文档](https://cn.abp.io/documents/abp/latest),因为它现在还远未完成. +我们还将完善[文档](https://abp.io/documents/abp/latest),因为它现在还远未完成. 第一个版本可能不包含SPA模板.但是,如果可能的话,我们想要准备一个简单些的.SPA框架还没有确定下来.备选有:**Angular,React和Blazor**.请将您的想法写为对此帖的评论. ## 中文网 -中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://cn.abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). +中国有一个大型的ABP社区.他们创建了一个中文版的abp.io网站:https://abp.io/. 他们一直在保持更新.感谢中国的开发人员,特别是[Liming Ma](https://github.com/maliming). ## NDC {London} 2019 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/CLI.md b/docs/zh-Hans/CLI.md index f5f230d9f2..071ef0f47a 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -1,4 +1,4 @@ -# ABP CLI +# ABP CLI ABP CLI (命令行接口) 是一个命令行工具,用来执行基于ABP解决方案的一些常见操作. @@ -43,7 +43,7 @@ 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. 这个模板还有一些额外的选项: @@ -53,8 +53,9 @@ abp new Acme.BookStore * `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`). ### add-package @@ -108,6 +109,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 +127,46 @@ 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-path` 或 `-sp`: 指定解决方案(.sln)文件路径. 如果未指定,CLI试寻找当前目录中的.sln文件. + +### login + +CLI的一些功能需要登录到abp.io平台. 使用你的用户名登录 + +```bash +abp login +``` + +请注意,新的登录将终止先前的会话并创建一个新的会话. + +### logout + +通过从计算机中删除会话令牌来注销. + +``` +abp logout +``` ### help diff --git a/docs/zh-Hans/Caching.md b/docs/zh-Hans/Caching.md index 7379104f51..0557ddcb4d 100644 --- a/docs/zh-Hans/Caching.md +++ b/docs/zh-Hans/Caching.md @@ -2,6 +2,32 @@ ABP框架扩展了ASP.NET Core的分布式缓存系统. +## Volo.Abp.Caching Package + +> 默认情况下启动模板已经安装了这个包,所以大部分情况下你不需要手动安装. + +Volo.Abp.Caching是缓存系统的核心包.使用包管理控制台(PMC)安装到项目: + +``` +Install-Package Volo.Abp.Caching +``` + +然后将 **AbpCachingModule** 依赖添加到你的模块: + +```c# +using Volo.Abp.Modularity; +using Volo.Abp.Caching; + +namespace MyCompany.MyProject +{ + [DependsOn(typeof(AbpCachingModule))] + public class MyModule : AbpModule + { + //... + } +} +``` + ## `IDistributedCache` 接口 ASP.NET Core 定义了 `IDistributedCache` 接口用于 get/set 缓存值 . 但是会有以下问题: diff --git a/docs/zh-Hans/Connection-Strings.md b/docs/zh-Hans/Connection-Strings.md index 2df4137b2b..562ae700ce 100644 --- a/docs/zh-Hans/Connection-Strings.md +++ b/docs/zh-Hans/Connection-Strings.md @@ -35,6 +35,22 @@ ABP框架的设计是[模块化](Module-Development-Basics.md), [微服务兼容 [预构建的应用程序模块](Modules/Index.md) 为连接字符串名称定义常量. 例如IdentityServer模块在 `AbpIdentityServerDbProperties` 类(位于 `Volo.Abp.IdentityServer` 命名空间)定义了 `ConnectionStringName` 常量 . 其他的模块类似的定义常量,你可以查看连接字符串的名称. +### AbpDbConnectionOptions + +ABP实际上使用 `AbpDbConnectionOptions` 获取连接字符串. 如果如上所述设置了连接字符串, `AbpDbConnectionOptions` 会被自动填充. 但是你也可以使用[选项模式](Options.md)设置或覆盖连接字符串. 你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置`AbpDbConnectionOptions`). +如下所示: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.ConnectionStrings.Default = "..."; + options.ConnectionStrings["AbpPermissionManagement"] = "..."; + }); +} +```` + ## 设置连接字符串名称 模块通常使用 `ConnectionStringName` attribute 为 `DbContext` 类关联一个唯一的连接字符串名称. 示例: diff --git a/docs/zh-Hans/Contribution/Localization-Text-Files.md b/docs/zh-Hans/Contribution/Localization-Text-Files.md index 36bae2890a..8055d9cae2 100644 --- a/docs/zh-Hans/Contribution/Localization-Text-Files.md +++ b/docs/zh-Hans/Contribution/Localization-Text-Files.md @@ -3,7 +3,7 @@ 这是一个来自框架的本地化文本文件列表, 任何人都可以做出贡献. 我们会将此列表保持最新: * https://github.com/abpframework/abp/tree/master/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json -* https://github.com/abpframework/abp/tree/master/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpValidation/en.json +* https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/en.json * https://github.com/abpframework/abp/tree/master/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/en.json * https://github.com/abpframework/abp/tree/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json * https://github.com/abpframework/abp/tree/master/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/Resource/en.json @@ -12,29 +12,20 @@ * https://github.com/abpframework/abp/tree/master/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/en.json * https://github.com/abpframework/abp/tree/master/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/en.json * https://github.com/abpframework/abp/tree/master/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/SourceExt/en.json -* https://github.com/abpframework/abp/tree/master/modules/account/src/Volo.Abp.Account.Web/Localization/Resources/AbpAccount/Web/en.json +* https://github.com/abpframework/abp/blob/master/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json * https://github.com/abpframework/abp/tree/master/modules/blogging/src/Volo.Blogging.Application.Contracts/Volo/Blogging/Localization/Resources/Blogging/ApplicationContracts/en.json -* https://github.com/abpframework/abp/tree/master/modules/blogging/src/Volo.Blogging.Web/Localization/Resources/Blogging/Web/en.json +* https://github.com/abpframework/abp/tree/master/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/en.json * https://github.com/abpframework/abp/tree/master/modules/docs/app/VoloDocs.Web/Localization/Resources/VoloDocs/Web/en.json * https://github.com/abpframework/abp/tree/master/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/en.json -* https://github.com/abpframework/abp/tree/master/modules/docs/src/Volo.Docs.Admin.Web/Localization/Resources/Docs/Web/en.json * https://github.com/abpframework/abp/tree/master/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json -* https://github.com/abpframework/abp/tree/master/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/Localization/ApplicationContracts/en.json -* https://github.com/abpframework/abp/tree/master/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/Localization/Domain/en.json -* https://github.com/abpframework/abp/tree/master/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Localization/Resources/FeatureManagement/en.json -* https://github.com/abpframework/abp/tree/master/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/Localization/ApplicationContracts/en.json -* https://github.com/abpframework/abp/tree/master/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/Localization/Domain/en.json -* https://github.com/abpframework/abp/tree/master/modules/identity/src/Volo.Abp.Identity.Web/Localization/Resources/AbpIdentity/en.json -* https://github.com/abpframework/abp/tree/master/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Localization/Resources/AbpPermissionManagement/en.json -* https://github.com/abpframework/abp/tree/master/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Localization/Resources/AbpSettingManagement/en.json -* https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/Localization/ApplicationContracts/en.json -* https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Localization/Resources/AbpTenantManagement/Web/en.json +* https://github.com/abpframework/abp/tree/master/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json +* https://github.com/abpframework/abp/tree/master/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json +* https://github.com/abpframework/abp/tree/master/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json +* https://github.com/abpframework/abp/tree/master/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json +* https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json * https://github.com/abpframework/abp/tree/master/samples/BookStore/src/Acme.BookStore.Domain.Shared/Localization/BookStore/en.json -* https://github.com/abpframework/abp/tree/master/samples/DashboardDemo/src/DashboardDemo.Domain/Localization/DashboardDemo/en.json +* https://github.com/abpframework/abp/tree/master/samples/DashboardDemo/src/DashboardDemo.Domain.Shared/Localization/DashboardDemo/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application.Contracts/ProductManagement/Localization/ApplicationContracts/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Localization/Domain/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Web/Localization/Resources/ProductManagement/en.json -* https://github.com/abpframework/abp/tree/master/templates/mvc-module/src/MyCompanyName.MyProjectName.Application.Contracts/Localization/MyProjectName/ApplicationContracts/en.json -* https://github.com/abpframework/abp/tree/master/templates/mvc-module/src/MyCompanyName.MyProjectName.Domain.Shared/Localization/MyProjectName/DomainShared/en.json -* https://github.com/abpframework/abp/tree/master/templates/mvc-module/src/MyCompanyName.MyProjectName.Web/Localization/MyProjectName/Web/en.json -* https://github.com/abpframework/abp/tree/master/templates/mvc/src/MyCompanyName.MyProjectName.Domain.Shared/Localization/MyProjectName/en.json +* https://github.com/abpframework/abp/tree/master/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/Localization/MyProjectName/en.json \ No newline at end of file diff --git a/docs/zh-Hans/Dependency-Injection.md b/docs/zh-Hans/Dependency-Injection.md index 381b77ec35..2264a12930 100644 --- a/docs/zh-Hans/Dependency-Injection.md +++ b/docs/zh-Hans/Dependency-Injection.md @@ -319,4 +319,4 @@ public class AppModule : AbpModule ### 请参阅 -* [ASP.NET Core依赖注入最佳实践,提示和技巧](https://cn.abp.io/blog/Abp/asp-net-core-dependency-injection-best-practices-tips-tricks) +* [ASP.NET Core依赖注入最佳实践,提示和技巧](https://blog.abp.io/asp-net-core-dependency-injection-best-practices-tips-tricks) diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md index 9c3066fef4..e5cd3cf839 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,166 @@ 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)系统自动完成. +* 对于 [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..bcb0ac864e --- /dev/null +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -0,0 +1,911 @@ + +# 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 table "AbpUsers" with the IdentityUser + b.ToTable("AbpUsers"); + + //Configure base properties + b.ConfigureByConvention(); + b.ConfigureAbpUser(); + + //Moved customization of the "AbpUsers" table to an extension method + b.ConfigureCustomUserProperties(); + }); + + /* 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 customizations for entities from the modules included */ + builder.Entity(b => + { + b.ConfigureCustomUserProperties(); + }); + + /* 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. 你可以**直接使用模块定义的实体**. +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.ConfigureCustomRoleProperties(); + }); + + ... + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + + builder.ConfigureBookStore(); +} +```` + +我们添加了以下几行: + +````csharp +builder.Entity(b => +{ + b.ToTable("AbpRoles"); + + b.ConfigureByConvention(); + + b.ConfigureCustomRoleProperties(); +}); +```` + +* 它映射到 `AbpRoles` 表,与 `IdentityRole` 实体共享. +* `ConfigureByConvention()` 配置了标准/基本属性(像`TenantId`),建议总是调用它. + +`ConfigureCustomRoleProperties()` 还不存在. 在 `BookStoreDbContextModelCreatingExtensions` 类中定义它 (在 `.EntityFrameworkCore` 项目的 `DbContext` 附近): + +````csharp +public static void ConfigureCustomRoleProperties(this EntityTypeBuilder b) + where TRole : class, IEntity +{ + b.Property(nameof(AppRole.Title)).HasMaxLength(128); +} +```` + +* 这个方法只定义实体的**自定义属性**. +* 遗憾的是,我们不能在这里充分的利用**类型安全**(通过引用`AppRole`实体). 我们能做的最好就是使用 `Title` 名称做为类型安全。 + +你已经为运行应用程序使用的 `DbContext` 配置了自定义属性. 我们还需要配置 `MigrationsDbContext`. + +打开`MigrationsDbContext`(本例是 `BookStoreMigrationsDbContext`)进行以下更改: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + ... + + /* Configure customizations for entities from the modules included */ + + //CONFIGURE THE CUSTOM ROLE PROPERTIES + builder.Entity(b => + { + b.ConfigureCustomRoleProperties(); + }); + + ... + + /* Configure your own tables/entities inside the ConfigureBookStore method */ + + builder.ConfigureBookStore(); +} +```` + +只增加下面几行: + +````csharp +builder.Entity(b => +{ + b.ConfigureCustomRoleProperties(); +}); +```` + +通过这种方式,我们重用了用于为角色配置自定义属性映射的扩展方法. 但是对 `IdentityRole` 实体进行了相同的自定义. + +现在你可以在包管理控制台(记得选择 `.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` 属性中存储键值对, 它是 `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查询将更加困难. +* 属性名称是字符串,他们**不是类型安全的**. 建议这些类型的属性定义常量,以防止拼写错误. + +###### 创建新表 + +你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `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..a1af1c5849 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. diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md index 328a4b74cc..0bec4d1ab9 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 @@ -181,7 +176,7 @@ public interface IBookRepository : IRepository } ```` -你通常希望从IRepository派生以继承标准存储库方法. 然而,你没有必要这样做. 仓储接口在分层应用程序的领域层中定义,它在数据访问/基础设施层([启动模板](https://cn.abp.io/Templates)中的`EntityFrameworkCore`项目)中实现 +你通常希望从IRepository派生以继承标准存储库方法. 然而,你没有必要这样做. 仓储接口在分层应用程序的领域层中定义,它在数据访问/基础设施层([启动模板](https://abp.io/Templates)中的`EntityFrameworkCore`项目)中实现 IBookRepository接口的实现示例: 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-AspNetCore-MVC-Template.md b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md index 4d6695c79b..90291e968f 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-MVC-Template.md @@ -2,7 +2,7 @@ ### 创建新项目 -本教程使用 **ABP CLI** 创建一个新项目. 更多选项, 请参阅[入门](https://cn.abp.io/get-started)页面. +本教程使用 **ABP CLI** 创建一个新项目. 更多选项, 请参阅[入门](https://abp.io/get-started)页面. 如果你之前未安装,请使用命令行安装ABP CLI: @@ -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/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/Audit-Logging.md b/docs/zh-Hans/Modules/Audit-Logging.md new file mode 100644 index 0000000000..0748b27160 --- /dev/null +++ b/docs/zh-Hans/Modules/Audit-Logging.md @@ -0,0 +1,7 @@ +# 审计日志模块 + +审计日志模块实现了 `IAuditingStore` 将审计日志对象保存到数据库中. + +> [启动模板](../Startup-Templates/Index.md)已经安装并配置了审计日志模块,所以你不需要手动安装到你的应用程序. + +参阅[审计日志系统](../Audit-Logging.md)文档了解更多关于审计日志的内容. \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index cfe287082b..20382d24f2 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -14,29 +14,31 @@ ### 版本 -当你使用GitHub存储文档时,文档模块支持多版本. 如果你的文档具有多个版本, UI上有一个组合框,用于切换版本. 如果你选择使用文件系统存储文档, 那么它不支持多版本. +当你使用GitHub存储文档时,文档模块支持多版本. 如果你的文档具有多个版本, UI上有一个组合框,用于切换版本. 如果你选择使用文件系统存储文档, 那么它不支持多版本. -ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. +ABP框架的[文档](docs.abp.io)也是使用的此模块. > 文档模块遵循 [模块化架构最佳实践](../Best-Practices/Module-Architecture.md) 指南. - - ## 安装 ### 1- 下载 -如果你没有现有的ABP项目, 这个步骤向你展示如何在[abp.io](https://cn.abp.io)创建一个新项目并添加文档模块. 如果你本地已经有了一个ABP项目, 那么你可以跳过这一步. +如果你没有现有的ABP项目, 这个步骤向你展示如何在[abp.io](https://abp.io)创建一个新项目并添加文档模块. 如果你本地已经有了一个ABP项目, 那么你可以跳过这一步. + +推荐使用ABP CLI创建新项目,使用以下命令行: -打开 https://cn.abp.io/Templates. 输入项目名称为 `Acme.MyProject`, 选择 `ASP.NET Core Mvc Application` 和选择 `Entity Framework Core` 做为数据库提供者. +`abp new Acme.MyProject` -请注意,本文档包含了 `Entity Framework Core` 提供者 不过你也可以选择 `MongoDB` 做为数据库提供者. +你也可以在浏览器中导航到 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) @@ -45,12 +47,12 @@ ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. ```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项目已经创建完成! 现在你可以运行项目并且查看网站. @@ -58,40 +60,30 @@ ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. ### 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 - - ``` -* [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) 需要安装到 `Acme.MyProject.EntityFrameworkCore` 项目. +`abp add-module Volo.Docs` - - 修改 `Acme.MyProject.EntityFrameworkCore.csproj`文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. +或者你也可以手动安装nuget包到每个项目: - ```csharp - - ``` -* [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) 需要安装到 `Acme.MyProject.Application` 项目. +* 安装[Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Domain` 项目. - * 修改 `Acme.MyProject.Application.csproj`文件并且添加以下行. 需要注意它要设置(v0.9.0)为Latest版本. + `Install-Package Volo.Docs.Domain` - ```csharp - - ``` -* [Volo.Docs.Web ](https://www.nuget.org/packages/Volo.Docs.Web/) 需要安装到 `Acme.MyProject.Web` 项目. +* 安装[Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget包到 `Acme.MyProject.EntityFrameworkCore` 项目. - - 修改 `Acme.MyProject.Web.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` 项目. + `Install-Package Volo.Docs.Application` +* 安装[Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget包到 `Acme.MyProject.Web` 项目. + `Install-Package Volo.Docs.Web` -### 3- 添加模块添加 +### 3- 添加模块依赖 一个ABP模块必须声明 `[DependsOn]` attribute 如果它依赖于另一个模块. 每个模块都必须在相关的项目的`[DependsOn]`Attribute 中添加. @@ -155,7 +147,6 @@ ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. } ``` - * 打开 `MyProjectWebModule.cs`并且添加 `typeof(DocsWebModule)` 如下所示; ```csharp @@ -174,43 +165,62 @@ ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. } ``` - - ### 4- 数据库集成 #### 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 @@ -218,7 +228,6 @@ ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. 最后你可以查看数据库中创建的新表,例如你可以看到 `DocsProjects` 表已经添加到数据库中. - ### 5- 链接文档模块 文档模块的默认路由是; @@ -262,7 +271,7 @@ ABP框架的[文档](https://abp.io/documents/)也是使用的此模块. "texts": { "Menu:Home": "首页", "Welcome": "欢迎", - "LongWelcomeMessage": "欢迎来到该应用程序. 这是一个基于ABP框架的启动项目. 有关更多信息, 请访问 cn.abp.io.", + "LongWelcomeMessage": "欢迎来到该应用程序. 这是一个基于ABP框架的启动项目. 有关更多信息, 请访问 abp.io.", "Menu:Docs": "文档" } } @@ -314,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/ @@ -326,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]) 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/zh-Hans/","GitHubAccessToken":"***"}', N'/', N'master') +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文档来配置你的文件系统存储. @@ -352,10 +363,10 @@ INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocume - ExtraProperties: ```json - {"Path":"C:\\Github\\abp\\docs\\zh-Hans"} + {"Path":"C:\\Github\\abp\\docs"} ``` - 请注意 `Path` 必须使用本地docs目录替换. 你可以从https://github.com/abpframework/abp/tree/master/docs/zh-hans获取ABP Framework的文档并且复制到该目录 `C:\\Github\\abp\\docs\\zh-Hans` 使其正常工作. + 请注意 `Path` 必须使用本地docs目录替换. 你可以从https://github.com/abpframework/abp/tree/master/docs获取ABP Framework的文档并且复制到该目录 `C:\\Github\\abp\\docs` 使其正常工作. - MainWebsiteUrl: `/` @@ -364,7 +375,7 @@ INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocume 对于 `SQL` 数据库,你可以使用下面的 `T-SQL` 命令将指定的示例插入到 `DocsProjects` 表中: ```mssql -INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs\\zh-Hans"}', N'/', NULL) +INSERT [dbo].[DocsProjects] ([Id], [Name], [ShortName], [Format], [DefaultDocumentName], [NavigationDocumentName], [MinimumVersion], [DocumentStoreType], [ExtraProperties], [MainWebsiteUrl], [LatestVersionBranchName], [ParametersDocumentName]) VALUES (N'12f21123-e08e-4f15-bedb-ae0b2d939659', N'ABP framework (FileSystem)', N'abp', N'md', N'Index', N'docs-nav.json', NULL, N'FileSystem', N'{"Path":"C:\\Github\\abp\\docs"}', N'/', NULL, N'') ``` 添加上面的一个示例项目后运行该应用程序. 在菜单中你会看到`文档` 链接,点击菜单链接打开文档页面. @@ -408,6 +419,101 @@ public class Person [https://github.com/abpframework/abp/blob/master/docs/zh-Hans/](https://github.com/abpframework/abp/blob/master/docs/zh-Hans/) +#### 有条件的部分功能(使用Scriban) + +文档模块使用[Scriban]()有条件的显示或隐藏文档的某些部分. 使用该功能你需要为每一种语言创建一个JSON文件做为**参数文档**. 它包含所有键值以及它们的显示名称. + +例如 [en/docs-params.json](https://github.com/abpio/abp-commercial-docs/blob/master/en/docs-params.json): + +```json +{ + "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" + } + }] +} +``` + +因为并不是项目中的每个文档都有章节或者不需要所有的参数,你必须声明哪些参数将用于对文档进行分段,在文档的任何地方都可以使用JSON块. + +例如 [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/blob/master/en/getting-started.md): + +``` +..... + +​````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF", "Mongo"], + "Tiered": ["Yes", "No"] +} +​```` + +........ +``` + +这个部分会在渲染时自动删除.前提是这些键值必须与**参数文档**中的键值匹配. + +![Interface](../images/docs-section-ui.png) + +现在你可以使用 **Scriban** 语法在文档中创建章节. + +示例 : + +```` +{{ if UI == "NG" }} + +* `-u` argument specifies the UI framework, `angular` in this case. + +{{ end }} + +{{ if DB == "Mongo" }} + +* `-d` argument specifies the database provider, `mongodb` in this case. + +{{ end }} + +{{ if Tiered == "Yes" }} + +* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. + +{{ end }} + +```` + +还可以在文本中使用变量,在其键中添加 **_Value** 后缀: + +```` +This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. +```` + +如果你想要得到的当前文档的语言或版本,可以使用预定义的 **Document_Language_Code** 和 **DOCUMENT_VERSION** 键(这对于创建重定向到另一个地区中另一个文档系统的链接很有用). + +------ + +**重要提示**: Scriban 的语法是 "{{" and "}}". 如果要在文档(如Angular文档)中使用转义,则必须使用转义块. 参阅 [Scriban文档]( ) 了解更多信息. + ### 8- 创建文档导航 导航文档是文档页面的主菜单. 它位于页面的左侧,是一个`JSON` 文件. 请查看以下示例导航文档以了解结构. @@ -464,4 +570,32 @@ public class Person ![Navigation menu](../images/docs-module_download-sample-navigation-menu.png) -最后,为您的项目添加了一个新的Docs模块, 该模块由GitHub提供. \ No newline at end of file +最后,为您的项目添加了一个新的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/Index.md b/docs/zh-Hans/Modules/Index.md index 8e5edf35b6..9b44967c53 100644 --- a/docs/zh-Hans/Modules/Index.md +++ b/docs/zh-Hans/Modules/Index.md @@ -11,16 +11,20 @@ ABP是一个 **模块化的应用程序框架** 由十多个 **nuget packages** 有一些由ABP社区开发和维护的 **开源免费** 的应用程序模块: -* **Account**: 用于用户登录/注册应用程序. -* **Audit Logging**: 用于将审计日志持久化到数据库. +* **Account**: 提供账户管理UI,并允许用户登录/注册应用程序. +* [**Audit Logging**](Audit-Logging.md): 用于将审计日志持久化到数据库. * **Background Jobs**: 用于在使用默认后台作业管理器时保存后台作业. -* **Blogging**: 用于创建精美的博客. ABP的[博客](https://abp.io/blog/abp/) 就使用了此模块. +* **Blogging**: 用于创建精美的博客. ABP的[博客](https://blog.abp.io/) 就使用了此模块. * [**Docs**](Docs.md): 用于创建技术文档页面. ABP的[文档](https://abp.io/documents/) 就使用了此模块. -* **Identity**: 用于管理角色,用户和他们的权限. +* **Identity**: 基于Microsoft Identity管理角色,用户和他们的权限. * **Identity Server**: 集成了IdentityServer4. * **Permission Management**: 用于保存权限. * **Setting Management**: 用于保存设置. -* **Tenant Management**: 用于管理[多租户](../Multi-Tenancy.md)应用程序的租户. -* **Users**: 用于抽象用户, 因此其他模块可以依赖此模块而不是Identity模块. +* **Tenant Management**: 管理[多租户](../Multi-Tenancy.md)应用程序的租户. +* **Users**: 抽象用户, 因此其他模块可以依赖此模块而不是Identity模块. -模块化文档正在编写中. 请参阅[这个仓库](https://github.com/abpframework/abp/tree/master/modules)获取所有模块的源代码. \ No newline at end of file +模块化文档正在编写中. 请参阅[这个仓库](https://github.com/abpframework/abp/tree/master/modules)获取所有模块的源代码. + +## 商业应用模块 + +[ABP商业](https://commercial.abp.io/)许可证在ABP框架上提供了额外的预构建应用程序模块. 参见ABP商业版提供的[模块列表](https://commercial.abp.io/module). \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Setting-Management.md b/docs/zh-Hans/Modules/Setting-Management.md index 31ea2505ea..fd87dd734c 100644 --- a/docs/zh-Hans/Modules/Setting-Management.md +++ b/docs/zh-Hans/Modules/Setting-Management.md @@ -67,6 +67,8 @@ namespace Demo 你可以从不同的设置值提供程序中(默认,全局,用户,租户...等)中获取或设定设置值. +> 如果只需要读取设置值,建议使用 `ISettingProvider` 而不是`ISettingManager`,因为它实现了缓存并支持所有部署场景. 如果要创建设置管理UI,可以使用ISettingManager. + ### Setting Cache 设置值缓存在 [分布式缓存](../Caching.md) 系统中. 建议始终使用 `ISettingManager` 更改设置值. 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..53e826ab7c 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -142,7 +142,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 +150,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 f9314e5030..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; @@ -50,11 +50,23 @@ namespace Acme.BookStore public DateTime PublishDate { get; set; } public float Price { get; set; } + + protected Book() + { + } + public Book(Guid id, string name, BookType type, DateTime publishDate, float price) + :base(id) + { + Name = name; + Type = type; + PublishDate = publishDate; + Price = price; + } } } ```` -* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](../../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). @@ -126,7 +138,7 @@ PM> Update-Database ### 创建应用服务 -下一步是创建[应用服务](../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: +下一步是创建[应用服务](../../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: * `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口. * `Acme.BookStore.Application`包含应用程序服务的实现. @@ -154,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`类一样具有一些审计属性. @@ -205,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`实体的映射: @@ -269,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 @@ -349,7 +361,7 @@ successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 ```` * 此代码更改了Razor View Page Model的默认继承,因此它从`BookStorePage`类(而不是`PageModel`)继承.启动模板附带的`BookStorePage`类,提供所有页面使用的一些共享属性/方法. -* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Pages.Books`命名空间,或者在`Index.cshtml`中更新它. +* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Books`命名空间,或者在`Index.cshtml`中更新它. #### 将Books页面添加到主菜单 @@ -380,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的标准行为). 运行该应用程序,看到新菜单项已添加到顶部栏: @@ -425,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.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 5bbe0e3424..a5e51134fa 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md @@ -56,33 +56,19 @@ namespace Acme.BookStore 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 - } - ); + new Book(_guidGenerator.Create(), "Test book 1", + BookType.Fantastic, new DateTime(2015, 05, 24), 21)); await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 2", - Type = BookType.Science, - PublishDate = new DateTime(2014, 02, 11), - Price = 15 - } - ); + new Book(_guidGenerator.Create(), "Test book 2", + BookType.Science, new DateTime(2014, 02, 11), 15)); } } } ```` * 注入`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..c0f999f996 --- /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"); + } +} +```` + +然后你可以配置 `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) 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/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/Buttons.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Buttons.md new file mode 100644 index 0000000000..76307ad820 --- /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` (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` + +指定按钮的大小. 应为以下值之一: + +* `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/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/Index.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md new file mode 100644 index 0000000000..acbb9e872b --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Index.md @@ -0,0 +1,26 @@ +# 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) +* ... + +> 在为所有的标签助手完成文档之前,你可以访问 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/fa-address-card.png b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/fa-address-card.png new file mode 100644 index 0000000000..a3b2c815b8 Binary files /dev/null and b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/fa-address-card.png differ 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 44856b34ed..6835f417cf 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -29,6 +29,7 @@ }, { "text": "教程", + "path": "Tutorials/Index.md", "items": [ { "text": "应用开发", @@ -97,8 +98,13 @@ "path": "Caching.md" }, { - "text": "审计" - }, + "text": "日志", + "path": "Logging.md" + }, + { + "text": "审计日志", + "path": "Audit-Logging.md" + }, { "text": "设置管理", "path": "Settings.md" @@ -192,7 +198,8 @@ "text": "领域服务" }, { - "text": "规约" + "text": "规约", + "path": "Specifications.md" } ] }, @@ -214,46 +221,72 @@ ] }, { - "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" + "items":[ + { + "text": "在线演示", + "path": "UI/AspNetCore/Tag-Helpers/Index.md" + }, + { + "text": "按钮", + "path": "UI/AspNetCore/Tag-Helpers/Buttons.md" + } + ] }, { "text": "仪表板和小部件(Widget)系统", - "path": "AspNetCore/Widgets.md" + "path": "UI/AspNetCore/Widgets.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" + } + ] + } ] }, { @@ -272,20 +305,27 @@ "path": "Entity-Framework-Core.md", "items": [ { - "text": "切换到MySql", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "切换到PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" + "text": "数据库迁移", + "path": "Entity-Framework-Core-Migrations.md" }, { - "text": "切换到SQLite", - "path": "Entity-Framework-Core-SQLite.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" + } + ] } ] }, @@ -315,8 +355,36 @@ { "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" } ] }, diff --git a/docs/zh-Hans/images/auditlog-object-diagram.png b/docs/zh-Hans/images/auditlog-object-diagram.png new file mode 100644 index 0000000000..a7e86a2bb2 Binary files /dev/null and b/docs/zh-Hans/images/auditlog-object-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/docs-section-ui.png b/docs/zh-Hans/images/docs-section-ui.png new file mode 100644 index 0000000000..7856454e5d Binary files /dev/null and b/docs/zh-Hans/images/docs-section-ui.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/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..88c8385b63 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,27 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -774,6 +789,34 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -906,6 +949,13 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} 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/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.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.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/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.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/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file 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/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/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/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/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/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/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs index be84495336..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,5 @@ -using System; +using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -34,6 +35,10 @@ namespace Volo.Abp.AspNetCore.Auditing try { await next(context); + if (_auditingManager.Current.Log.Exceptions.Any()) + { + hasError = true; + } } catch (Exception) { diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs index db13bdaf74..58a0713014 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs @@ -30,10 +30,10 @@ namespace Volo.Abp.Auditing /// Default: true. /// public bool IsEnabledForAnonymousUsers { get; set; } - - /// - /// Audit log on exceptions. - /// Default: true. + + /// + /// Audit log on exceptions. + /// Default: true. /// public bool AlwaysLogOnException { get; set; } 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.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/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/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xml b/framework/src/Volo.Abp.BackgroundJobs.Quartz/FodyWeavers.xml similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/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/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.RabbitMq/FodyWeavers.xml b/framework/src/Volo.Abp.BackgroundWorkers.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.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/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.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs index 34bf95fdf1..63fb4f8eee 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 @@ -30,6 +30,8 @@ namespace Volo.Abp.Cli options.Commands["login"] = typeof(LoginCommand); options.Commands["logout"] = typeof(LogoutCommand); 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/GetSourceCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs index e1536c2973..c57833c1f7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs @@ -7,6 +7,7 @@ using ICSharpCode.SharpZipLib.Zip; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.Commands.Services; using Volo.Abp.Cli.ProjectBuilding; using Volo.Abp.Cli.ProjectBuilding.Building; using Volo.Abp.DependencyInjection; @@ -15,12 +16,14 @@ namespace Volo.Abp.Cli.Commands { public class GetSourceCommand : IConsoleCommand, ITransientDependency { + private readonly SourceCodeDownloadService _sourceCodeDownloadService; public ModuleProjectBuilder ModuleProjectBuilder { get; } public ILogger 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/LoginCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs index fe9ed2d98f..32bc6671bf 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs @@ -33,15 +33,19 @@ namespace Volo.Abp.Cli.Commands ); } - Console.Write("Password: "); - var password = ConsoleHelper.ReadSecret(); - if (password.IsNullOrWhiteSpace()) + var password = commandLineArgs.Options.GetOrNull(Options.Password.Short, Options.Password.Long); + if (password == null) { - throw new CliUsageException( - "Password is missing!" + - Environment.NewLine + Environment.NewLine + - GetUsageInfo() - ); + Console.Write("Password: "); + password = ConsoleHelper.ReadSecret(); + if (password.IsNullOrWhiteSpace()) + { + throw new CliUsageException( + "Password is missing!" + + Environment.NewLine + Environment.NewLine + + GetUsageInfo() + ); + } } await AuthService.LoginAsync( @@ -60,10 +64,12 @@ namespace Volo.Abp.Cli.Commands sb.AppendLine(""); sb.AppendLine("Usage:"); sb.AppendLine(" abp login "); + sb.AppendLine(" abp login -p "); sb.AppendLine(""); sb.AppendLine("Example:"); sb.AppendLine(""); sb.AppendLine(" abp login john"); + sb.AppendLine(" abp login john -p 1234"); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); @@ -82,6 +88,12 @@ namespace Volo.Abp.Cli.Commands public const string Short = "o"; public const string Long = "organization"; } + + public static class Password + { + public const string Short = "p"; + public const string Long = "password"; + } } } } \ No newline at end of file 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..04b9168a44 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); @@ -67,12 +67,24 @@ namespace Volo.Abp.Cli.Commands Logger.LogInformation("UI Framework: " + uiFramework); } + 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 templateSource = commandLineArgs.Options.GetOrNull(Options.TemplateSource.Short, Options.TemplateSource.Long); + if (templateSource != null) + { + Logger.LogInformation("Template Source: " + templateSource); + } + var outputFolder = commandLineArgs.Options.GetOrNull(Options.OutputFolder.Short, Options.OutputFolder.Long); outputFolder = Path.Combine(outputFolder != null ? Path.GetFullPath(outputFolder) : Directory.GetCurrentDirectory(), @@ -94,7 +106,9 @@ namespace Volo.Abp.Cli.Commands version, databaseProvider, uiFramework, + mobileApp, gitHubLocalRepositoryPath, + templateSource, commandLineArgs.Options ) ); @@ -148,9 +162,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,10 +178,13 @@ 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(""); sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI"); @@ -208,6 +227,20 @@ namespace Volo.Abp.Cli.Commands } } + private 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 +277,18 @@ 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"; + } } } } 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..0b70ae6cca 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 @@ -95,21 +95,19 @@ namespace Volo.Abp.Cli.NuGet .OrResult(msg => !msg.IsSuccessStatusCode) .WaitAndRetryAsync(new[] { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(3), - TimeSpan.FromSeconds(7) + TimeSpan.FromSeconds(1) }, (responseMessage, timeSpan, retryCount, context) => { if (responseMessage.Exception != null) { - Logger.LogWarning( + Logger.LogDebug( $"{retryCount}. HTTP request attempt failed to {url} with an error: HTTP {(int)responseMessage.Result.StatusCode}-{responseMessage.Exception.Message}. " + $"Waiting {timeSpan.TotalSeconds} secs for the next try..."); } else if (responseMessage.Result != null) { - Logger.LogWarning( + Logger.LogDebug( $"{retryCount}. HTTP request attempt failed to {url} with an error: {(int)responseMessage.Result.StatusCode}-{responseMessage.Result.ReasonPhrase}. " + $"Waiting {timeSpan.TotalSeconds} secs for the next try..."); } 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..263b675a2a 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 @@ -2,9 +2,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -46,7 +44,8 @@ namespace Volo.Abp.Cli.ProjectBuilding public async Task GetAsync( string name, string type, - string version = null) + string version = null, + string templateSource = null) { var latestVersion = await GetLatestSourceCodeVersionAsync(name, type); @@ -55,13 +54,21 @@ namespace Volo.Abp.Cli.ProjectBuilding version = latestVersion; } + var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version; + DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache); + 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,16 +78,17 @@ 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); } @@ -104,7 +112,38 @@ namespace Volo.Abp.Cli.ProjectBuilding var result = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(result).Version; + return JsonSerializer.Deserialize(result).Version; + } + } + + private async Task GetTemplateNugetVersionAsync(string name, string type, string version) + { + try + { + using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) + { + var response = await client.PostAsync( + $"{CliUrls.WwwAbpIo}api/download/{type}/get-nuget-version/", + 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) + { + return null; } } @@ -114,11 +153,20 @@ namespace Volo.Abp.Cli.ProjectBuilding using (var client = new CliHttpClient(TimeSpan.FromMinutes(10))) { - var responseMessage = await client.PostAsync( - $"{CliUrls.WwwAbpIo}api/download/{input.Type}/", - new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), - CancellationTokenProvider.Token - ); + HttpResponseMessage responseMessage; + + if (input.TemplateSource.IsNullOrWhiteSpace()) + { + responseMessage = await client.PostAsync( + $"{CliUrls.WwwAbpIo}api/download/{input.Type}/", + new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), + CancellationTokenProvider.Token + ); + } + else + { + responseMessage = await client.GetAsync(input.TemplateSource, CancellationTokenProvider.Token); + } await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); @@ -126,6 +174,11 @@ namespace Volo.Abp.Cli.ProjectBuilding } } + private bool IsNetworkSource(string source) + { + return source.ToLower().StartsWith("http"); + } + public class SourceCodeDownloadInputDto { public string Name { get; set; } @@ -133,6 +186,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 +195,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/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/ProjectReferenceReplaceStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs index 29f5afb19c..5aaf4b1488 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 @@ -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 { @@ -28,7 +29,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps } else { - var nugetPackageVersion = context.TemplateFile.Version; + var nugetPackageVersion = context.TemplateFile.RepositoryNugetVersion; if (IsBranchName(nugetPackageVersion)) { @@ -92,7 +93,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); } @@ -125,16 +126,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/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/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..ea207bc426 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,19 +19,26 @@ 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; } + [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, + [CanBeNull] string templateSource = null, Dictionary extraProperties = null) { SolutionName = Check.NotNull(solutionName, nameof(solutionName)); @@ -39,7 +46,9 @@ namespace Volo.Abp.Cli.ProjectBuilding Version = version; DatabaseProvider = databaseProvider; UiFramework = uiFramework; + MobileApp = mobileApp; AbpGitHubLocalRepositoryPath = abpGitHubLocalRepositoryPath; + TemplateSource = templateSource; ExtraProperties = extraProperties ?? new Dictionary(); } } 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..f7d9eaf3be 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 @@ -24,11 +24,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 +40,7 @@ namespace Volo.Abp.Cli.ProjectBuilding Logger = NullLogger.Instance; } - + public async Task BuildAsync(ProjectBuildArgs args) { var templateInfo = GetTemplateInfo(args); @@ -50,9 +50,10 @@ 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) { @@ -82,14 +83,18 @@ namespace Volo.Abp.Cli.ProjectBuilding 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.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 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..2d1e237933 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularModuleSourceCodeAdder.cs @@ -0,0 +1,198 @@ +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"); + + File.Delete(jestConfigPath); + + var testPath = Path.Combine(angularProjectsPath, project, "src", "test.ts"); + + 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/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index 41fd33b4fb..91eaec48e7 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 @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; @@ -18,18 +19,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,7 +45,7 @@ namespace Volo.Abp.Cli.ProjectModification foreach (var file in fileList) { - UpdatePackagesInFile(file, out var packagesUpdated); + var packagesUpdated = await UpdatePackagesInFile(file, includePreviews, switchToStable); if (packagesUpdated) { @@ -64,21 +67,21 @@ namespace Volo.Abp.Cli.ProjectModification 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,18 +92,23 @@ 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 updated = false; + var currentVersion = (string)package.Value; + + var version = await GetLatestVersion(package, currentVersion, includePreviews, switchToStable); var versionWithPrefix = $"^{version}"; - if (versionWithPrefix == (string)package.Value) + if (versionWithPrefix == currentVersion) { - updated = false; - return; + return false; } else { @@ -110,25 +118,41 @@ namespace Volo.Abp.Cli.ProjectModification package.Value.Replace(versionWithPrefix); Logger.LogInformation($"Updated {package.Name} to {version} in {file.Replace(Directory.GetCurrentDirectory(), "")}."); + return updated; } - 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/NugetPackageToLocalReferenceConverter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs new file mode 100644 index 0000000000..31dd41cbf1 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NugetPackageToLocalReferenceConverter.cs @@ -0,0 +1,141 @@ +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.Abp')]"); + + 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)) + { + 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/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..e369690785 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,6 +78,69 @@ 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(module, solutionFile); + } + + ModifyDbContext(projectFiles, module, startupProject, skipDbMigrations); + } + + private async Task HandleAngularProject(ModuleWithMastersInfo module, string solutionFilePath) + { + var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular"); + + if (!Directory.Exists(angularPath)) + { + return; + } + + await AngularModuleSourceCodeAdder.AddAsync(solutionFilePath, angularPath); + } + + 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 DeleteAppFolderAsync(targetModuleFolder); + + if (module.MasterModuleInfos == null) + { + return; + } + + foreach (var masterModule in module.MasterModuleInfos) + { + await DownloadSourceCodesToSolutionFolder(masterModule, modulesFolderInSolution, version); + } + } + + private async Task DeleteAppFolderAsync(string targetModuleFolder) + { + var appFolder = Path.Combine(targetModuleFolder, "app"); + if (Directory.Exists(appFolder)) + { + Directory.Delete(appFolder, true); + } + } + + private async Task AddNugetAndNpmReferences(ModuleWithMastersInfo module, string[] projectFiles) + { foreach (var nugetPackage in module.NugetPackages) { var targetProjectFile = ProjectFinder.FindNuGetTargetProjectFile(projectFiles, nugetPackage.Target); @@ -84,7 +162,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 +174,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 +204,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,7 +231,7 @@ namespace Volo.Abp.Cli.ProjectModification } var responseContent = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(responseContent); + return JsonSerializer.Deserialize(responseContent); } } } 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..eaaa0d442b 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,19 @@ 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(); } } + + 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/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/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj index 5ffc15d19f..0ccc8990d4 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 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.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 92ebdd4568..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 @@ -16,6 +16,19 @@ namespace Volo.Abp.Domain.Entities 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)) @@ -82,10 +95,10 @@ namespace Volo.Abp.Domain.Entities var lambdaBody = Expression.Equal(leftExpression, rightExpression); return Expression.Lambda>(lambdaBody, lambdaParam); } - + public static void TrySetId( - IEntity entity, - Func idFactory, + IEntity entity, + Func idFactory, bool checkForDisableGuidGenerationAttribute = false) { //TODO: Can be optimized (by caching per entity type)? 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 29814f4de6..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 @@ -8,16 +8,19 @@ using System.Threading.Tasks; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; namespace Volo.Abp.Domain.Repositories { - public abstract class RepositoryBase : BasicRepositoryBase, IRepository + public abstract class RepositoryBase : BasicRepositoryBase, IRepository, IUnitOfWorkManagerAccessor where TEntity : class, IEntity { public IDataFilter DataFilter { get; set; } public ICurrentTenant CurrentTenant { get; set; } + public IUnitOfWorkManager UnitOfWorkManager { get; set; } + public virtual Type ElementType => GetQueryable().ElementType; public virtual Expression Expression => GetQueryable().Expression; @@ -46,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.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs index 40ebca015e..ef44d6830d 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.Domain.Entities; using Volo.Abp.DynamicProxy; +using Volo.Abp.Uow; namespace Volo.Abp.Domain.Repositories { @@ -41,5 +42,56 @@ namespace Volo.Abp.Domain.Repositories await repo.EnsurePropertyLoadedAsync(entity, propertyExpression, cancellationToken); } } + + public static async Task HardDeleteAsync( + this IBasicRepository repository, + TEntity entity, + bool autoSave = false, + CancellationToken cancellationToken = default + ) + where TEntity : class, IEntity, ISoftDelete + { + if (!(ProxyHelper.UnProxy(repository) is IUnitOfWorkManagerAccessor unitOfWorkManagerAccessor)) + { + throw new AbpException($"The given repository (of type {repository.GetType().AssemblyQualifiedName}) should implement the {typeof(IUnitOfWorkManagerAccessor).AssemblyQualifiedName} interface in order to invoke the {nameof(HardDeleteAsync)} method!"); + } + + var uowManager = unitOfWorkManagerAccessor.UnitOfWorkManager; + if (uowManager == null) + { + throw new AbpException($"{nameof(unitOfWorkManagerAccessor.UnitOfWorkManager)} property of the given {nameof(repository)} object is null!"); + } + + if (uowManager.Current == null) + { + using (var uow = uowManager.Begin()) + { + await HardDeleteWithUnitOfWorkAsync(repository, entity, autoSave, cancellationToken, uowManager.Current); + await uow.CompleteAsync(cancellationToken); + } + } + else + { + await HardDeleteWithUnitOfWorkAsync(repository, entity, autoSave, cancellationToken, uowManager.Current); + } + } + + private static async Task HardDeleteWithUnitOfWorkAsync( + IBasicRepository repository, + TEntity entity, + bool autoSave, + CancellationToken cancellationToken, IUnitOfWork currentUow + ) + where TEntity : class, IEntity, ISoftDelete + { + var hardDeleteEntities = (HashSet) currentUow.Items.GetOrAdd( + UnitOfWorkItemNames.HardDeletedEntities, + () => new HashSet() + ); + + hardDeleteEntities.Add(entity); + + await repository.DeleteAsync(entity, autoSave, cancellationToken); + } } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkItemNames.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkItemNames.cs new file mode 100644 index 0000000000..4ec067ceec --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/UnitOfWorkItemNames.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Domain.Repositories +{ + public static class UnitOfWorkItemNames + { + public const string HardDeletedEntities = "AbpHardDeletedEntities"; + } +} 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/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 6c3eb689d8..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 @@ -80,7 +80,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore { return includeDetails ? await WithDetails().ToListAsync(GetCancellationToken(cancellationToken)) - : await DbSet.ToListAsync(GetCancellationToken(cancellationToken)); + : await DbSet.ToListAsync(GetCancellationToken(cancellationToken)); } public override async Task GetCountAsync(CancellationToken cancellationToken = default) @@ -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,18 +199,11 @@ 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 ? await WithDetails().FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken)) - : await DbSet.FindAsync(new object[] { id }, GetCancellationToken(cancellationToken)); + : await DbSet.FindAsync(new object[] {id}, GetCancellationToken(cancellationToken)); } public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) 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 16c5c38389..ba9cee70ee 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -16,6 +16,7 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.EntityHistory; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; @@ -23,6 +24,7 @@ using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Timing; +using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore { @@ -49,6 +51,8 @@ namespace Volo.Abp.EntityFrameworkCore public IAuditingManager AuditingManager { get; set; } + public IUnitOfWorkManager UnitOfWorkManager { get; set; } + public IClock Clock { get; set; } public ILogger> Logger { get; set; } @@ -102,7 +106,7 @@ namespace Volo.Abp.EntityFrameworkCore .Invoke(this, new object[] { modelBuilder, entityType }); } } - + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { try @@ -196,12 +200,26 @@ namespace Volo.Abp.EntityFrameworkCore protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, EntityChangeReport changeReport) { - CancelDeletionForSoftDelete(entry); - UpdateConcurrencyStamp(entry); - SetDeletionAuditProperties(entry); + if (TryCancelDeletionForSoftDelete(entry)) + { + UpdateConcurrencyStamp(entry); + SetDeletionAuditProperties(entry); + } + changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted)); } + protected virtual bool IsHardDeleted(EntityEntry entry) + { + var hardDeletedEntities = UnitOfWorkManager?.Current?.Items.GetOrDefault(UnitOfWorkItemNames.HardDeletedEntities) as HashSet; + if (hardDeletedEntities == null) + { + return false; + } + + return hardDeletedEntities.Contains(entry.Entity); + } + protected virtual void AddDomainEvents(EntityChangeReport changeReport, object entityAsObj) { var generatesDomainEventsEntity = entityAsObj as IGeneratesDomainEvents; @@ -253,16 +271,22 @@ namespace Volo.Abp.EntityFrameworkCore entity.ConcurrencyStamp = Guid.NewGuid().ToString("N"); } - protected virtual void CancelDeletionForSoftDelete(EntityEntry entry) + protected virtual bool TryCancelDeletionForSoftDelete(EntityEntry entry) { if (!(entry.Entity is ISoftDelete)) { - return; + return false; + } + + if (IsHardDeleted(entry)) + { + return false; } entry.Reload(); entry.State = EntityState.Modified; entry.Entity.As().IsDeleted = true; + return true; } protected virtual void CheckAndSetId(EntityEntry entry) @@ -382,7 +406,7 @@ namespace Volo.Abp.EntityFrameworkCore return; } - var idPropertyBuilder = modelBuilder.Entity().Property(x => ((IEntity) x).Id); + var idPropertyBuilder = modelBuilder.Entity().Property(x => ((IEntity)x).Id); if (idPropertyBuilder.Metadata.PropertyInfo.IsDefined(typeof(DatabaseGeneratedAttribute), true)) { return; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs index e3501294be..006097825c 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; namespace Volo.Abp.EventBus { + //TODO: Move to the right namespace in v3.0 public interface ILocalEventHandler : IEventHandler { /// diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/FodyWeavers.xml b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xml similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/FodyWeavers.xml rename to framework/src/Volo.Abp.Http.Client.IdentityModel.Web/FodyWeavers.xml 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..99f480ee63 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,18 +1,16 @@ - + - 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..38e6878bed 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; @@ -57,7 +57,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying 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.IdentityModel/Volo.Abp.IdentityModel.csproj b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj index 3e4ae78b91..afda5e0002 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..e69d3f67c7 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; } @@ -95,7 +98,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 +112,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 +137,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 +152,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.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 4e8ccaa82e..5533cb9f74 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 @@ -111,7 +111,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB await ApplyAbpConceptsForDeletedEntityAsync(entity); var oldConcurrencyStamp = SetNewConcurrencyStamp(entity); - if (entity is ISoftDelete softDeleteEntity) + if (entity is ISoftDelete softDeleteEntity && !IsHardDeleted(entity)) { softDeleteEntity.IsDeleted = true; var result = await Collection.ReplaceOneAsync( @@ -169,17 +169,37 @@ 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( Collection.AsQueryable() ); } + protected virtual bool IsHardDeleted(TEntity entity) + { + var hardDeletedEntities = UnitOfWorkManager?.Current?.Items.GetOrDefault(UnitOfWorkItemNames.HardDeletedEntities) as HashSet; + if (hardDeletedEntities == null) + { + return false; + } + + return hardDeletedEntities.Contains(entity); + } protected virtual FilterDefinition CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null) { throw new NotImplementedException( - $"{nameof(CreateEntityFilter)} is not implemented for MongoDB by default. It should be overrided and implemented by the deriving class!" + $"{nameof(CreateEntityFilter)} is not implemented for MongoDB by default. It should be overriden and implemented by the deriving class!" ); } 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..c16ab14f4d --- /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(); + _scheduler.Start(); + } + + public override void OnApplicationShutdown(ApplicationShutdownContext context) + { + //TODO: ABP may provide two methods for application shutdown: OnPreApplicationShutdown & OnApplicationShutdown + _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.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.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.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs index d1d90b6f6f..882c8d6a70 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -26,6 +27,8 @@ namespace Volo.Abp.Uow public IServiceProvider ServiceProvider => _parent.ServiceProvider; + public Dictionary Items => _parent.Items; + private readonly IUnitOfWork _parent; public ChildUnitOfWork([NotNull] IUnitOfWork parent) diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs index 32ef781133..ee5baa58bc 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -9,6 +10,8 @@ namespace Volo.Abp.Uow { Guid Id { get; } + Dictionary Items { get; } + //TODO: Switch to OnFailed (sync) and OnDisposed (sync) methods to be compatible with OnCompleted event EventHandler Failed; diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs new file mode 100644 index 0000000000..a12b77855e --- /dev/null +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkManagerAccessor.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Uow +{ + public interface IUnitOfWorkManagerAccessor + { + IUnitOfWorkManager UnitOfWorkManager { get; } + } +} diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs index e7e86774b5..05f49df180 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; @@ -31,6 +32,9 @@ namespace Volo.Abp.Uow public IServiceProvider ServiceProvider { get; } + [NotNull] + public Dictionary Items { get; } + private readonly Dictionary _databaseApis; private readonly Dictionary _transactionApis; private readonly AbpUnitOfWorkDefaultOptions _defaultOptions; @@ -46,6 +50,8 @@ namespace Volo.Abp.Uow _databaseApis = new Dictionary(); _transactionApis = new Dictionary(); + + Items = new Dictionary(); } public virtual void Initialize(AbpUnitOfWorkOptions options) 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/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 9b4892cea4..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 @@ -1,30 +1,34 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Volo.Abp.Auditing; - -namespace Volo.Abp.AspNetCore.Mvc.Auditing -{ - [Route("api/audit-test")] - [Audited] - public class AuditTestController : AbpController - { - private readonly AbpAuditingOptions _options; - - public AuditTestController(IOptions options) - { - _options = options.Value; - } - - [Route("audit-success")] - public IActionResult AuditSuccessForGetRequests() - { - return Ok(); - } - - [Route("audit-fail")] - public IActionResult AuditFailForGetRequests() - { - throw new UserFriendlyException("Exception occurred!"); - } - } -} +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Volo.Abp.Auditing; + +namespace Volo.Abp.AspNetCore.Mvc.Auditing +{ + [Route("api/audit-test")] + public class AuditTestController : AbpController + { + private readonly AbpAuditingOptions _options; + + public AuditTestController(IOptions options) + { + _options = options.Value; + } + + [Route("audit-success")] + public IActionResult AuditSuccessForGetRequests() + { + return Ok(); + } + + [Route("audit-fail")] + public IActionResult AuditFailForGetRequests() + { + throw new UserFriendlyException("Exception occurred!"); + } + [Route("audit-fail-object")] + public object AuditFailForGetRequestsReturningObject() + { + throw new UserFriendlyException("Exception occurred!"); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs index 733fab3f49..49bc689e14 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs @@ -1,55 +1,65 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using NSubstitute; -using System.Threading.Tasks; -using Volo.Abp.Auditing; -using Xunit; - -namespace Volo.Abp.AspNetCore.Mvc.Auditing -{ - public class AuditTestController_Tests : AspNetCoreMvcTestBase - { - private readonly AbpAuditingOptions _options; - private IAuditingStore _auditingStore; - - public AuditTestController_Tests() - { - _options = ServiceProvider.GetRequiredService>().Value; - _auditingStore = ServiceProvider.GetRequiredService(); - } - - protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) - { - _auditingStore = Substitute.For(); - services.Replace(ServiceDescriptor.Singleton(_auditingStore)); - base.ConfigureServices(context, services); - } - - [Fact] - public async Task Should_Trigger_Middleware_And_AuditLog_Success_For_GetRequests() - { - _options.IsEnabledForGetRequests = true; - _options.AlwaysLogOnException = false; - await GetResponseAsync("api/audit-test/audit-success"); - await _auditingStore.Received().SaveAsync(Arg.Any()); //Won't work, save happens out of scope - } - - [Fact] - public async Task Should_Trigger_Middleware_And_AuditLog_Exception_Always() - { - _options.IsEnabled = true; - _options.AlwaysLogOnException = true; - - try - { - await GetResponseAsync("api/audit-test/audit-fail", System.Net.HttpStatusCode.Forbidden); - } - catch { } - - await _auditingStore.Received().SaveAsync(Arg.Any()); //Won't work, save happens out of scope - } - } -} +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using NSubstitute; +using System.Threading.Tasks; +using Volo.Abp.Auditing; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Auditing +{ + public class AuditTestController_Tests : AspNetCoreMvcTestBase + { + private readonly AbpAuditingOptions _options; + private IAuditingStore _auditingStore; + + public AuditTestController_Tests() + { + _options = ServiceProvider.GetRequiredService>().Value; + _auditingStore = ServiceProvider.GetRequiredService(); + } + + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + _auditingStore = Substitute.For(); + services.Replace(ServiceDescriptor.Singleton(_auditingStore)); + base.ConfigureServices(context, services); + } + + [Fact] + public async Task Should_Trigger_Middleware_And_AuditLog_Success_For_GetRequests() + { + _options.IsEnabledForGetRequests = true; + _options.AlwaysLogOnException = false; + await GetResponseAsync("api/audit-test/audit-success"); + await _auditingStore.Received().SaveAsync(Arg.Any()); + } + + [Fact] + public async Task Should_Trigger_Middleware_And_AuditLog_Exception_Always() + { + _options.IsEnabled = true; + _options.AlwaysLogOnException = true; + + try + { + await GetResponseAsync("api/audit-test/audit-fail", System.Net.HttpStatusCode.Forbidden); + } + catch { } + + await _auditingStore.Received().SaveAsync(Arg.Any()); + } + [Fact] + public async Task Should_Trigger_Middleware_And_AuditLog_Exception_When_Returns_Object() + { + _options.IsEnabled = true; + _options.AlwaysLogOnException = true; + + await GetResponseAsync("api/audit-test/audit-fail-object", System.Net.HttpStatusCode.Forbidden); + + await _auditingStore.Received().SaveAsync(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/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/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} 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