diff --git a/.agents/skills/abp-angular/SKILL.md b/.agents/skills/abp-angular/SKILL.md new file mode 100644 index 0000000000..3723cc3266 --- /dev/null +++ b/.agents/skills/abp-angular/SKILL.md @@ -0,0 +1,220 @@ +--- +name: abp-angular +description: ABP Angular UI patterns - generate-proxy, ListService, PermissionGuard, abpLocalization pipe, ConfirmationService, ToasterService, ConfigStateService. Use when building or reviewing Angular UI components, routing, or service integration in ABP Angular projects. +--- + +# ABP Angular UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/angular/overview + +## Project Structure +``` +src/app/ +├── proxy/ # Auto-generated service proxies +├── shared/ # Shared components, pipes, directives +├── book/ # Feature module +│ ├── book.module.ts +│ ├── book-routing.module.ts +│ ├── book-list/ +│ │ ├── book-list.component.ts +│ │ ├── book-list.component.html +│ │ └── book-list.component.scss +│ └── book-detail/ +``` + +## Generate Service Proxies +```bash +abp generate-proxy -t ng +``` + +This generates typed service classes in `src/app/proxy/`. + +## List Component Pattern +```typescript +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html' +}) +export class BookListComponent implements OnInit { + books = { items: [], totalCount: 0 } as PagedResultDto; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private confirmation: ConfirmationService + ) {} + + ngOnInit(): void { + this.hookToQuery(); + } + + private hookToQuery(): void { + this.list.hookToQuery(query => + this.bookService.getList(query) + ).subscribe(response => { + this.books = response; + }); + } + + create(): void { + // Open create modal + } + + delete(book: BookDto): void { + this.confirmation + .warn('::AreYouSureToDelete', '::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.bookService.delete(book.id).subscribe(() => this.list.get()); + } + }); + } +} +``` + +## Localization +```typescript +// In component +constructor(private localizationService: LocalizationService) {} + +getText(): string { + return this.localizationService.instant('::Books'); +} +``` + +```html + +

{{ '::Books' | abpLocalization }}

+ + +

{{ '::WelcomeMessage' | abpLocalization: userName }}

+``` + +## Authorization + +### Permission Directive +```html + +``` + +### Permission Guard +```typescript +const routes: Routes = [ + { + path: '', + component: BookListComponent, + canActivate: [PermissionGuard], + data: { + requiredPolicy: 'BookStore.Books' + } + } +]; +``` + +### Programmatic Check +```typescript +constructor(private permissionService: PermissionService) {} + +canCreate(): boolean { + return this.permissionService.getGrantedPolicy('BookStore.Books.Create'); +} +``` + +## Forms with Validation +```typescript +@Component({...}) +export class BookFormComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.buildForm(); + } + + buildForm(): void { + this.form = this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(128)]], + price: [0, [Validators.required, Validators.min(0)]] + }); + } + + save(): void { + if (this.form.invalid) return; + + this.bookService.create(this.form.value).subscribe(() => { + // Handle success + }); + } +} +``` + +```html +
+
+ + +
+ + +
+``` + +## Configuration API +```typescript +constructor(private configService: ConfigStateService) {} + +getCurrentUser(): CurrentUserDto { + return this.configService.getOne('currentUser'); +} + +getSettings(): void { + const setting = this.configService.getSetting('MyApp.MaxItemCount'); +} +``` + +## Modal Service +```typescript +constructor(private modalService: ModalService) {} + +openCreateModal(): void { + const modalRef = this.modalService.open(BookFormComponent, { + size: 'lg' + }); + + modalRef.result.then(result => { + if (result) { + this.list.get(); + } + }); +} +``` + +## Toast Notifications +```typescript +constructor(private toaster: ToasterService) {} + +showSuccess(): void { + this.toaster.success('::BookCreatedSuccessfully', '::Success'); +} + +showError(error: string): void { + this.toaster.error(error, '::Error'); +} +``` + +## Lazy Loading Modules +```typescript +// app-routing.module.ts +const routes: Routes = [ + { + path: 'books', + loadChildren: () => import('./book/book.module').then(m => m.BookModule) + } +]; +``` + +## Theme & Styling +- Use Bootstrap classes +- ABP provides theme variables via CSS custom properties +- Component-specific styles in `.component.scss` diff --git a/.agents/skills/abp-app-nolayers/SKILL.md b/.agents/skills/abp-app-nolayers/SKILL.md new file mode 100644 index 0000000000..74603e288e --- /dev/null +++ b/.agents/skills/abp-app-nolayers/SKILL.md @@ -0,0 +1,78 @@ +--- +name: abp-app-nolayers +description: ABP Single-Layer (No-Layers / nolayers) application template - single project structure, feature-based file organization, no separate Domain/Application.Contracts projects. Use when working with the single-layer web application template or when the project has no layered separation. +--- + +# ABP Single-Layer Application Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/single-layer-web-application + +## Solution Structure + +Single project containing everything: + +``` +MyProject/ +├── src/ +│ └── MyProject/ +│ ├── Data/ # DbContext, migrations +│ ├── Entities/ # Domain entities +│ ├── Services/ # Application services + DTOs +│ ├── Pages/ # Razor pages / Blazor components +│ └── MyProjectModule.cs +└── test/ + └── MyProject.Tests/ +``` + +## Key Differences from Layered + +| Layered Template | Single-Layer Template | +|------------------|----------------------| +| DTOs in Application.Contracts | DTOs in Services folder (same project) | +| Repository interfaces in Domain | Use generic `IRepository` directly | +| Separate Domain.Shared for constants | Constants in same project | +| Multiple module classes | Single module class | + +## File Organization + +Group related files by feature: + +``` +Services/ +├── Books/ +│ ├── BookAppService.cs +│ ├── BookDto.cs +│ ├── CreateBookDto.cs +│ └── IBookAppService.cs +└── Authors/ + ├── AuthorAppService.cs + └── ... +``` + +## Simplified Entity (Still keep invariants) + +Single-layer templates are structurally simpler, but you may still have real business invariants. + +- For **trivial CRUD** entities, public setters can be acceptable. +- For **non-trivial business rules**, still prefer encapsulation (private setters + methods) to prevent invalid states. + +```csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } // OK for trivial CRUD only + public decimal Price { get; set; } +} +``` + +## No Custom Repository Needed + +Use generic repository directly - no need to define custom interfaces: + +```csharp +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; + + // Generic repository is sufficient for single-layer apps +} +``` diff --git a/.agents/skills/abp-application-layer/SKILL.md b/.agents/skills/abp-application-layer/SKILL.md new file mode 100644 index 0000000000..d5507c2a7e --- /dev/null +++ b/.agents/skills/abp-application-layer/SKILL.md @@ -0,0 +1,239 @@ +--- +name: abp-application-layer +description: ABP Application Services, DTOs, CRUD service, object mapping (Mapperly/AutoMapper), validation, error handling. Use when creating or reviewing application services, DTOs, or working in the Application or Application.Contracts projects. +--- + +# ABP Application Layer Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services + +## Anti-Patterns to Avoid + +- **Entity name in method**: use `GetAsync` not `GetBookAsync` +- **ID inside UpdateDto**: pass `id` as a separate parameter, not inside the DTO +- **Calling other app services in the same module**: use domain services or repositories directly +- **Using `IFormFile`/`Stream` in app service**: accept `byte[]` from controllers instead +- **Business logic in app service**: put it in domain entities or domain services + +## Application Service Structure + +### Interface (Application.Contracts) +```csharp +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(GetBookListInput input); + Task CreateAsync(CreateBookDto input); + Task UpdateAsync(Guid id, UpdateBookDto input); + Task DeleteAsync(Guid id); +} +``` + +### Implementation (Application) +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IBookRepository _bookRepository; + private readonly BookManager _bookManager; + private readonly BookMapper _bookMapper; + + public BookAppService( + IBookRepository bookRepository, + BookManager bookManager, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookManager = bookManager; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = await _bookManager.CreateAsync(input.Name, input.Price); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Edit)] + public async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await _bookRepository.GetAsync(id); + await _bookManager.ChangeNameAsync(book, input.Name); + book.SetPrice(input.Price); + await _bookRepository.UpdateAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +## Application Service Best Practices +- Don't repeat entity name in method names (`GetAsync` not `GetBookAsync`) +- Accept/return DTOs only, never entities +- ID not inside UpdateDto - pass separately +- Use custom repositories when you need custom queries, generic repository is fine for simple CRUD +- Call `UpdateAsync` explicitly (don't assume change tracking) +- Don't call other app services in same module +- Don't use `IFormFile`/`Stream` - pass `byte[]` from controllers +- Use base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) instead of injecting these services + +## DTO Naming Conventions + +| Purpose | Convention | Example | +|---------|------------|---------| +| Query input | `Get{Entity}Input` | `GetBookInput` | +| List query input | `Get{Entity}ListInput` | `GetBookListInput` | +| Create input | `Create{Entity}Dto` | `CreateBookDto` | +| Update input | `Update{Entity}Dto` | `UpdateBookDto` | +| Single entity output | `{Entity}Dto` | `BookDto` | +| List item output | `{Entity}ListItemDto` | `BookListItemDto` | + +## DTO Location +- Define DTOs in `*.Application.Contracts` project +- This allows sharing with clients (Blazor, HttpApi.Client) + +## Validation + +### Data Annotations +```csharp +public class CreateBookDto +{ + [Required] + [StringLength(100, MinimumLength = 3)] + public string Name { get; set; } + + [Range(0, 999.99)] + public decimal Price { get; set; } +} +``` + +### Custom Validation with IValidatableObject +Before adding custom validation, decide if it's a **domain rule** or **application rule**: +- **Domain rule**: Put validation in entity constructor or domain service (enforces business invariants) +- **Application rule**: Use DTO validation (input format, required fields) + +Only use `IValidatableObject` for application-level validation that can't be expressed with data annotations: + +```csharp +public class CreateBookDto : IValidatableObject +{ + public string Name { get; set; } + public string Description { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Name == Description) + { + yield return new ValidationResult( + "Name and Description cannot be the same!", + new[] { nameof(Name), nameof(Description) } + ); + } + } +} +``` + +### FluentValidation +```csharp +public class CreateBookDtoValidator : AbstractValidator +{ + public CreateBookDtoValidator() + { + RuleFor(x => x.Name).NotEmpty().Length(3, 100); + RuleFor(x => x.Price).GreaterThan(0); + } +} +``` + +## Error Handling + +### Business Exceptions +```csharp +throw new BusinessException("BookStore:010001") + .WithData("BookName", name); +``` + +### Entity Not Found +```csharp +var book = await _bookRepository.FindAsync(id); +if (book == null) +{ + throw new EntityNotFoundException(typeof(Book), id); +} +``` + +### User-Friendly Exceptions +```csharp +throw new UserFriendlyException(L["BookNotAvailable"]); +``` + +### HTTP Status Code Mapping +Status code mapping is **configurable** in ABP (do not rely on a fixed mapping in business logic). + +| Exception | Typical HTTP Status | +|-----------|-------------| +| `AbpValidationException` | 400 | +| `AbpAuthorizationException` | 401/403 | +| `EntityNotFoundException` | 404 | +| `BusinessException` | 403 (but configurable) | +| Other exceptions | 500 | + +## Auto API Controllers +ABP automatically generates API controllers for application services: +- Interface must inherit `IApplicationService` (which already has `[RemoteService]` attribute) +- HTTP methods determined by method name prefix (Get, Create, Update, Delete) +- Use `[RemoteService(false)]` to disable auto API generation for specific methods + +## Object Mapping (Mapperly / AutoMapper) +ABP supports **both Mapperly and AutoMapper** integrations. But the default mapping library is Mapperly. You need to first check the project's active mapping library. +- Prefer the mapping provider already used in the solution (check existing mapping files / loaded modules). +- In mixed solutions, explicitly setting the default provider may be required (see `docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md`). + +### Mapperly (compile-time) +Define mappers as partial classes: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddSingleton(); +} +``` + +Usage in application service: +```csharp +public class BookAppService : ApplicationService +{ + private readonly BookMapper _bookMapper; + + public BookAppService(BookMapper bookMapper) + { + _bookMapper = bookMapper; + } + + public BookDto GetBook(Book book) + { + return _bookMapper.MapToDto(book); + } +} +``` + +> **Note**: Mapperly generates mapping code at compile-time, providing better performance than runtime mappers. + +### AutoMapper (runtime) +If the solution uses AutoMapper, mappings are typically defined in `Profile` classes and registered via ABP's AutoMapper integration. diff --git a/.agents/skills/abp-authorization/SKILL.md b/.agents/skills/abp-authorization/SKILL.md new file mode 100644 index 0000000000..a805f5f067 --- /dev/null +++ b/.agents/skills/abp-authorization/SKILL.md @@ -0,0 +1,182 @@ +--- +name: abp-authorization +description: ABP permission system - PermissionDefinitionProvider, [Authorize] attribute, CheckPolicyAsync, IsGrantedAsync, ICurrentUser, IPermissionManager, multi-tenancy side. Use when working with permissions, authorization, role-based access, or security in ABP projects. +--- + +# ABP Authorization + +> **Docs**: https://abp.io/docs/latest/framework/fundamentals/authorization + +## Permission Definition +Define permissions in `*.Application.Contracts` project: + +```csharp +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } +} +``` + +Register in provider: +```csharp +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); + + var booksPermission = bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books")); + + booksPermission.AddChild( + BookStorePermissions.Books.Create, + L("Permission:Books.Create")); + + booksPermission.AddChild( + BookStorePermissions.Books.Edit, + L("Permission:Books.Edit")); + + booksPermission.AddChild( + BookStorePermissions.Books.Delete, + L("Permission:Books.Delete")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} +``` + +## Using Permissions + +### Declarative (Attribute) +```csharp +[Authorize(BookStorePermissions.Books.Create)] +public virtual async Task CreateAsync(CreateBookDto input) +{ + // Only users with Books.Create permission can execute +} +``` + +### Programmatic Check +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Check and throw if not granted + await CheckPolicyAsync(BookStorePermissions.Books.Edit); + + // Or check without throwing + if (await IsGrantedAsync(BookStorePermissions.Books.Delete)) + { + // Has permission + } + } +} +``` + +### Allow Anonymous Access +```csharp +[AllowAnonymous] +public virtual async Task GetPublicBookAsync(Guid id) +{ + // No authentication required +} +``` + +## Current User +Access authenticated user info via `CurrentUser` property (available in base classes like `ApplicationService`, `DomainService`, `AbpController`): + +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // CurrentUser is available from base class - no injection needed + var userId = CurrentUser.Id; + var userName = CurrentUser.UserName; + var email = CurrentUser.Email; + var isAuthenticated = CurrentUser.IsAuthenticated; + var roles = CurrentUser.Roles; + var tenantId = CurrentUser.TenantId; + } +} + +// In other services, inject ICurrentUser +public class MyService : ITransientDependency +{ + private readonly ICurrentUser _currentUser; + public MyService(ICurrentUser currentUser) => _currentUser = currentUser; +} +``` + +### Ownership Validation +```csharp +public async Task UpdateMyBookAsync(Guid bookId, UpdateBookDto input) +{ + var book = await _bookRepository.GetAsync(bookId); + + if (book.CreatorId != CurrentUser.Id) + { + throw new AbpAuthorizationException(); + } + + // Update book... +} +``` + +## Multi-Tenancy Permissions +Control permission availability per tenant side: + +```csharp +bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books"), + multiTenancySide: MultiTenancySides.Tenant // Only for tenants +); +``` + +Options: `MultiTenancySides.Host`, `Tenant`, or `Both` + +## Feature-Dependent Permissions +```csharp +booksPermission.RequireFeatures("BookStore.PremiumFeature"); +``` + +## Permission Management +Grant/revoke permissions programmatically: + +```csharp +public class MyService : ITransientDependency +{ + private readonly IPermissionManager _permissionManager; + + public async Task GrantPermissionToUserAsync(Guid userId, string permissionName) + { + await _permissionManager.SetForUserAsync(userId, permissionName, true); + } + + public async Task GrantPermissionToRoleAsync(string roleName, string permissionName) + { + await _permissionManager.SetForRoleAsync(roleName, permissionName, true); + } +} +``` + +## Security Best Practices +- Never trust client input for user identity +- Use `CurrentUser` property (from base class) or inject `ICurrentUser` +- Validate ownership in application service methods +- Filter queries by current user when appropriate +- Don't expose sensitive fields in DTOs diff --git a/.agents/skills/abp-blazor/SKILL.md b/.agents/skills/abp-blazor/SKILL.md new file mode 100644 index 0000000000..ff0ef325dc --- /dev/null +++ b/.agents/skills/abp-blazor/SKILL.md @@ -0,0 +1,206 @@ +--- +name: abp-blazor +description: ABP Blazor UI patterns - AbpComponentBase, AbpCrudPageBase, DataGrid, IMenuContributor, Message/Notify, Validations, JavaScript interop. Use when building or reviewing Blazor Server or WebAssembly UI components in ABP projects. +--- + +# ABP Blazor UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/blazor/overall + +## Component Base Classes + +### Basic Component +```razor +@inherits AbpComponentBase + +

@L["Books"]

+``` + +### CRUD Page +```razor +@page "/books" +@inherits AbpCrudPageBase + + + + + +

@L["Books"]

+
+ + @if (HasCreatePermission) + { + + } + +
+
+ + + + + + + + + + + + + + + + +
+``` + +## Localization +```razor +@* Using L property from base class *@ +

@L["PageTitle"]

+ +@* With parameters *@ +

@L["WelcomeMessage", CurrentUser.UserName]

+``` + +## Authorization +```razor +@* Check permission before rendering *@ +@if (await AuthorizationService.IsGrantedAsync("MyPermission")) +{ + +} + +@* Using policy-based authorization *@ + + +

You have access!

+
+
+``` + +## Navigation & Menu +Configure in `*MenuContributor.cs`: + +```csharp +public class MyMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var bookMenu = new ApplicationMenuItem( + "Books", + l["Menu:Books"], + "/books", + icon: "fa fa-book" + ); + + if (await context.IsGrantedAsync(MyPermissions.Books.Default)) + { + context.Menu.AddItem(bookMenu); + } + } + } +} +``` + +## Notifications & Messages +```csharp +// Success message +await Message.Success(L["BookCreatedSuccessfully"]); + +// Confirmation dialog +if (await Message.Confirm(L["AreYouSure"])) +{ + // User confirmed +} + +// Toast notification +await Notify.Success(L["OperationCompleted"]); +``` + +## Forms & Validation +```razor +
+ + + + @L["Name"] + + + + + + + + +
+``` + +## JavaScript Interop +```csharp +@inject IJSRuntime JsRuntime + +@code { + private async Task CallJavaScript() + { + await JsRuntime.InvokeVoidAsync("myFunction", arg1, arg2); + var result = await JsRuntime.InvokeAsync("myFunctionWithReturn"); + } +} +``` + +## State Management +```csharp +// Inject service proxy from HttpApi.Client +@inject IBookAppService BookAppService + +@code { + private List Books { get; set; } + + protected override async Task OnInitializedAsync() + { + var result = await BookAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + Books = result.Items.ToList(); + } +} +``` + +## Code-Behind Pattern +**Books.razor:** +```razor +@page "/books" +@inherits BooksBase +``` + +**Books.razor.cs:** +```csharp +public partial class Books : BooksBase +{ + // Component logic here +} +``` + +**BooksBase.cs:** +```csharp +public abstract class BooksBase : AbpComponentBase +{ + [Inject] + protected IBookAppService BookAppService { get; set; } +} +``` diff --git a/.agents/skills/abp-cli/SKILL.md b/.agents/skills/abp-cli/SKILL.md new file mode 100644 index 0000000000..da08280b39 --- /dev/null +++ b/.agents/skills/abp-cli/SKILL.md @@ -0,0 +1,89 @@ +--- +name: abp-cli +description: ABP CLI commands - generate-proxy, install-libs, add-package-ref, new-module, install-module, abp update, abp clean, abp suite generate. Use when the user asks how to run ABP CLI commands, generate proxies, install libraries, or use ABP Suite. +--- + +# ABP CLI Commands + +> **Full documentation**: https://abp.io/docs/latest/cli +> Use `abp help [command]` for detailed options. + +## Generate Client Proxies + +```bash +# URL flag: `-u` (short) or `--url` (long). Use whichever your team prefers, but keep it consistent. +# +# Angular (host must be running) +abp generate-proxy -t ng + +# C# client proxies +abp generate-proxy -t csharp -u https://localhost:44300 + +# Integration services only (microservices) +abp generate-proxy -t csharp -u https://localhost:44300 -st integration + +# JavaScript +abp generate-proxy -t js -u https://localhost:44300 +``` + +## Install Client-Side Libraries + +```bash +# Install NPM packages for MVC/Blazor Server +abp install-libs +``` + +## Add Package Reference + +```bash +# Add project reference with module dependency +abp add-package-ref Acme.BookStore.Domain +abp add-package-ref Acme.BookStore.Domain -t Acme.BookStore.Application +``` + +## Module Operations + +```bash +# Create new module in solution +abp new-module Acme.OrderManagement -t module:ddd + +# Install published module +abp install-module Volo.Blogging + +# Add ABP NuGet package +abp add-package Volo.Abp.Caching.StackExchangeRedis +``` + +## Update & Clean + +```bash +abp update # Update all ABP packages +abp update --version 8.0.0 # Specific version +abp clean # Delete bin/obj folders +``` + +## ABP Suite (CRUD Generation) + +Generate CRUD pages from entity JSON (created via Suite UI): + +```bash +abp suite generate --entity .suite/entities/Book.json --solution ./Acme.BookStore.sln +``` + +> **Note**: Entity JSON files are created when you generate an entity via ABP Suite UI. They are stored in `.suite/entities/` folder. +> **Suite docs**: https://abp.io/docs/latest/suite + +## Quick Reference + +| Task | Command | +|------|---------| +| Angular proxies | `abp generate-proxy -t ng` | +| C# proxies | `abp generate-proxy -t csharp -u URL` | +| Install JS libs | `abp install-libs` | +| Add reference | `abp add-package-ref PackageName` | +| Create module | `abp new-module ModuleName` | +| Install module | `abp install-module ModuleName` | +| Update packages | `abp update` | +| Clean solution | `abp clean` | +| Suite CRUD | `abp suite generate -e entity.json -s solution.sln` | +| Get help | `abp help [command]` | diff --git a/.agents/skills/abp-core/SKILL.md b/.agents/skills/abp-core/SKILL.md new file mode 100644 index 0000000000..b1f7bca91b --- /dev/null +++ b/.agents/skills/abp-core/SKILL.md @@ -0,0 +1,190 @@ +--- +name: abp-core +description: Core ABP Framework conventions - module system, DI registration, base classes (ApplicationService, DomainService), IClock, BusinessException, localization, async patterns. Use when working on any ABP project, asking about ABP fundamentals, or unsure which skill applies. +--- + +# ABP Core Conventions + +> **Documentation**: https://abp.io/docs/latest +> **API Reference**: https://abp.io/docs/api/ + +## Key Rules + +- Use `IClock` / `Clock.Now` instead of `DateTime.Now` / `DateTime.UtcNow` +- Use `ITransientDependency` / `ISingletonDependency` instead of `AddScoped/AddTransient/AddSingleton` +- Use `IRepository` instead of injecting `DbContext` directly +- Check base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) before injecting services +- Use `BusinessException` with namespaced error codes for domain rule violations + +## Module System +Every ABP application/module has a module class that configures services: + +```csharp +[DependsOn( + typeof(AbpDddDomainModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class MyAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Service registration and configuration + } +} +``` + +> **Note**: Middleware configuration (`OnApplicationInitialization`) should only be done in the final host application, not in reusable modules. + +## Dependency Injection Conventions + +### Automatic Registration +ABP automatically registers services implementing marker interfaces: +- `ITransientDependency` → Transient lifetime +- `ISingletonDependency` → Singleton lifetime +- `IScopedDependency` → Scoped lifetime + +Classes inheriting from `ApplicationService`, `DomainService`, `AbpController` are also auto-registered. + +### Repository Usage +You can use the generic `IRepository` for simple CRUD operations. Define custom repository interfaces only when you need custom query methods: + +```csharp +// Simple CRUD - Generic repository is fine +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; // ✅ OK for simple operations +} + +// Custom queries needed - Define custom interface +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); // Custom query +} + +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ Use custom when needed +} +``` + +### Exposing Services +```csharp +[ExposeServices(typeof(IMyService))] +public class MyService : IMyService, ITransientDependency { } +``` + +## Important Base Classes + +| Base Class | Purpose | +|------------|---------| +| `Entity` | Basic entity with ID | +| `AggregateRoot` | DDD aggregate root | +| `DomainService` | Domain business logic | +| `ApplicationService` | Use case orchestration | +| `AbpController` | REST API controller | + +ABP base classes already inject commonly used services as properties. Before injecting a service, check if it's already available: + +| Property | Available In | Description | +|----------|--------------|-------------| +| `GuidGenerator` | All base classes | Generate GUIDs | +| `Clock` | All base classes | Current time (use instead of `DateTime`) | +| `CurrentUser` | All base classes | Authenticated user info | +| `CurrentTenant` | All base classes | Multi-tenancy context | +| `L` (StringLocalizer) | `ApplicationService`, `AbpController` | Localization | +| `AuthorizationService` | `ApplicationService`, `AbpController` | Permission checks | +| `FeatureChecker` | `ApplicationService`, `AbpController` | Feature availability | +| `DataFilter` | All base classes | Data filtering (soft-delete, tenant) | +| `UnitOfWorkManager` | `ApplicationService`, `DomainService` | Unit of work management | +| `LoggerFactory` | All base classes | Create loggers | +| `Logger` | All base classes | Logging (auto-created) | +| `LazyServiceProvider` | All base classes | Lazy service resolution | + +**Useful methods from base classes:** +- `CheckPolicyAsync()` - Check permission and throw if not granted +- `IsGrantedAsync()` - Check permission without throwing + +## Async Best Practices +- Use async all the way - never use `.Result` or `.Wait()` +- All async methods should end with `Async` suffix +- ABP automatically handles `CancellationToken` in most cases (e.g., from `HttpContext.RequestAborted`) +- Only pass `CancellationToken` explicitly when implementing custom cancellation logic + +## Time Handling +Never use `DateTime.Now` or `DateTime.UtcNow` directly. Use ABP's `IClock` service: + +```csharp +// In classes inheriting from base classes (ApplicationService, DomainService, etc.) +public class BookAppService : ApplicationService +{ + public void DoSomething() + { + var now = Clock.Now; // ✅ Already available as property + } +} + +// In other services - inject IClock +public class MyService : ITransientDependency +{ + private readonly IClock _clock; + + public MyService(IClock clock) => _clock = clock; + + public void DoSomething() + { + var now = _clock.Now; // ✅ Correct + // var now = DateTime.Now; // ❌ Wrong - not testable, ignores timezone settings + } +} +``` + +> **Tip**: Before injecting a service, check if it's already available as a property in your base classes. + +## Business Exceptions +Use `BusinessException` for domain rule violations with namespaced error codes: + +```csharp +throw new BusinessException("MyModule:BookNameAlreadyExists") + .WithData("Name", bookName); +``` + +Configure localization mapping: +```csharp +Configure(options => +{ + options.MapCodeNamespace("MyModule", typeof(MyModuleResource)); +}); +``` + +## Localization +- In base classes (`ApplicationService`, `AbpController`, etc.): Use `L["Key"]` - this is the `IStringLocalizer` property +- In other services: Inject `IStringLocalizer` +- Always localize user-facing messages and exceptions + +**Localization file location**: `*.Domain.Shared/Localization/{ResourceName}/{lang}.json` + +```json +// Example: MyProject.Domain.Shared/Localization/MyProject/en.json +{ + "culture": "en", + "texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "BookName": "Book Name" + } +} +``` + +## ❌ Never Use (ABP Anti-Patterns) + +| Don't Use | Use Instead | +|-----------|-------------| +| Minimal APIs | ABP Controllers or Auto API Controllers | +| MediatR | Application Services | +| `DbContext` directly in App Services | `IRepository` | +| `AddScoped/AddTransient/AddSingleton` | `ITransientDependency`, `ISingletonDependency` | +| `DateTime.Now` | `IClock` / `Clock.Now` | +| Custom UnitOfWork | ABP's `IUnitOfWorkManager` | +| Manual HTTP calls from UI | ABP client proxies (`generate-proxy`) | +| Hardcoded role checks | Permission-based authorization | +| Business logic in Controllers | Application Services | diff --git a/.agents/skills/abp-ddd/SKILL.md b/.agents/skills/abp-ddd/SKILL.md new file mode 100644 index 0000000000..885324130d --- /dev/null +++ b/.agents/skills/abp-ddd/SKILL.md @@ -0,0 +1,248 @@ +--- +name: abp-ddd +description: ABP DDD patterns - Entities, Aggregate Roots, value objects, Repositories, Domain Services, Domain Events, Specifications. Use when designing domain layer, creating entities, repositories, or domain services in ABP projects. +--- + +# ABP DDD Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design + +## Anti-Patterns to Avoid + +- **Anemic entities**: public setters with no behavior — use private setters + methods that enforce invariants +- **Repository for child entities**: only aggregate roots get repositories — access child entities through their root +- **Generating GUID in entity constructor**: use `IGuidGenerator` from outside and pass `id` parameter +- **Navigation properties to other aggregates**: reference by `Id` only, never add full navigation properties across aggregates +- **Domain service depending on current user**: accept values from the application layer instead + +## Rich Domain Model vs Anemic Domain Model + +ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior: + +| Anemic (Anti-pattern) | Rich (Recommended) | +|----------------------|-------------------| +| Entity = data only | Entity = data + behavior | +| Logic in services | Logic in entity methods | +| Public setters | Private setters with methods | +| No validation in entity | Entity enforces invariants | + +**Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods. + +## Entities + +### Entity Example (Rich Model) +```csharp +public class OrderLine : Entity +{ + public Guid ProductId { get; private set; } + public int Count { get; private set; } + public decimal Price { get; private set; } + + protected OrderLine() { } // For ORM + + internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id) + { + ProductId = productId; + SetCount(count); // Validates through method + Price = price; + } + + public void SetCount(int count) + { + if (count <= 0) + throw new BusinessException("Orders:InvalidCount"); + Count = count; + } +} +``` + +## Aggregate Roots + +Aggregate roots are consistency boundaries that: +- Own their child entities +- Enforce business rules +- Publish domain events + +```csharp +public class Order : AggregateRoot +{ + public string OrderNumber { get; private set; } + public Guid CustomerId { get; private set; } + public OrderStatus Status { get; private set; } + public ICollection Lines { get; private set; } + + protected Order() { } // For ORM + + public Order(Guid id, string orderNumber, Guid customerId) : base(id) + { + OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber)); + CustomerId = customerId; + Status = OrderStatus.Created; + Lines = new List(); + } + + public void AddLine(Guid lineId, Guid productId, int count, decimal price) + { + // Business rule: Can only add lines to created orders + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotModifyOrder"); + + Lines.Add(new OrderLine(lineId, productId, count, price)); + } + + public void Complete() + { + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotCompleteOrder"); + + Status = OrderStatus.Completed; + + // Publish events for side effects + AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction + AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service + } +} +``` + +### Domain Events +- `AddLocalEvent()` - Handled within same transaction, can access full entity +- `AddDistributedEvent()` - Handled asynchronously, use ETOs (Event Transfer Objects) + +### Entity Best Practices +- **Encapsulation**: Private setters, public methods that enforce rules +- **Primary constructor**: Enforce invariants, accept `id` parameter +- **Protected parameterless constructor**: Required for ORM +- **Initialize collections**: In primary constructor +- **Virtual members**: For ORM proxy compatibility +- **Reference by Id**: Don't add navigation properties to other aggregates +- **Don't generate GUID in constructor**: Use `IGuidGenerator` externally + +## Repository Pattern + +### When to Use Custom Repository +- **Generic repository** (`IRepository`): Sufficient for simple CRUD operations +- **Custom repository**: Only when you need custom query methods + +### Interface (Domain Layer) +```csharp +// Define custom interface only when custom queries are needed +public interface IOrderRepository : IRepository +{ + Task FindByOrderNumberAsync(string orderNumber, bool includeDetails = false); + Task> GetListByCustomerAsync(Guid customerId, bool includeDetails = false); +} +``` + +### Repository Best Practices +- **One repository per aggregate root only** - Never create repositories for child entities +- Child entities must be accessed/modified only through their aggregate root +- Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules) +- In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this +- Define custom repository only when custom queries are needed +- ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control +- Single entity methods: `includeDetails = true` by default +- List methods: `includeDetails = false` by default +- Don't return projection classes +- Interface in Domain, implementation in data layer + +```csharp +// ✅ Correct: Repository for aggregate root (Order) +public interface IOrderRepository : IRepository { } + +// ❌ Wrong: Repository for child entity (OrderLine) +// OrderLine should only be accessed through Order aggregate +public interface IOrderLineRepository : IRepository { } // Don't do this! +``` + +## Domain Services + +Use domain services for business logic that: +- Spans multiple aggregates +- Requires repository queries to enforce rules + +```csharp +public class OrderManager : DomainService +{ + private readonly IOrderRepository _orderRepository; + private readonly IProductRepository _productRepository; + + public OrderManager( + IOrderRepository orderRepository, + IProductRepository productRepository) + { + _orderRepository = orderRepository; + _productRepository = productRepository; + } + + public async Task CreateAsync(string orderNumber, Guid customerId) + { + // Business rule: Order number must be unique + var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber); + if (existing != null) + { + throw new BusinessException("Orders:OrderNumberAlreadyExists") + .WithData("OrderNumber", orderNumber); + } + + return new Order(GuidGenerator.Create(), orderNumber, customerId); + } + + public async Task AddProductAsync(Order order, Guid productId, int count) + { + var product = await _productRepository.GetAsync(productId); + order.AddLine(productId, count, product.Price); + } +} +``` + +### Domain Service Best Practices +- Use `*Manager` suffix naming +- No interface by default (create only if needed) +- Accept/return domain objects, not DTOs +- Don't depend on authenticated user - pass values from application layer +- Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services + +## Domain Events + +### Local Events +```csharp +// In aggregate +AddLocalEvent(new OrderCompletedEvent(Id)); + +// Handler +public class OrderCompletedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCompletedEvent eventData) + { + // Handle within same transaction + } +} +``` + +### Distributed Events (ETO) +For inter-module/microservice communication: +```csharp +// In Domain.Shared +[EventName("Orders.OrderCompleted")] +public class OrderCompletedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} +``` + +## Specifications + +Reusable query conditions: +```csharp +public class CompletedOrdersSpec : Specification +{ + public override Expression> ToExpression() + { + return o => o.Status == OrderStatus.Completed; + } +} + +// Usage +var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec()); +``` diff --git a/.agents/skills/abp-dependency-rules/SKILL.md b/.agents/skills/abp-dependency-rules/SKILL.md new file mode 100644 index 0000000000..025e6b707f --- /dev/null +++ b/.agents/skills/abp-dependency-rules/SKILL.md @@ -0,0 +1,150 @@ +--- +name: abp-dependency-rules +description: ABP project layer dependency rules - which projects can reference which, domain/application/infrastructure separation, cross-layer violations to avoid. Use when reviewing project structure, adding new project references, or checking if a dependency direction is correct. +--- + +# ABP Dependency Rules + +## Core Principles (All Templates) + +These principles apply regardless of solution structure: + +1. **Domain logic never depends on infrastructure** (no DbContext in domain/application) +2. **Use abstractions** (interfaces) for dependencies +3. **Higher layers depend on lower layers**, never the reverse +4. **Data access through repositories**, not direct DbContext + +## Layered Template Structure + +> **Note**: This section applies to layered templates (app, module). Single-layer and microservice templates have different structures. + +``` +Domain.Shared → Constants, enums, localization keys + ↑ + Domain → Entities, repository interfaces, domain services + ↑ +Application.Contracts → App service interfaces, DTOs + ↑ + Application → App service implementations + ↑ + HttpApi → REST controllers (optional) + ↑ + Host → Final application with DI and middleware +``` + +### Layered Dependency Direction + +| Project | Can Reference | Referenced By | +|---------|---------------|---------------| +| Domain.Shared | Nothing | All | +| Domain | Domain.Shared | Application, Data layer | +| Application.Contracts | Domain.Shared | Application, HttpApi, Clients | +| Application | Domain, Contracts | Host | +| EntityFrameworkCore/MongoDB | Domain | Host only | +| HttpApi | Contracts only | Host | + +## Critical Rules + +### ❌ Never Do +```csharp +// Application layer accessing DbContext directly +public class BookAppService : ApplicationService +{ + private readonly MyDbContext _dbContext; // ❌ WRONG +} + +// Domain depending on application layer +public class BookManager : DomainService +{ + private readonly IBookAppService _appService; // ❌ WRONG +} + +// HttpApi depending on Application implementation +public class BookController : AbpController +{ + private readonly BookAppService _bookAppService; // ❌ WRONG - Use interface +} +``` + +### ✅ Always Do +```csharp +// Application layer using repository abstraction +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// Domain service using domain abstractions +public class BookManager : DomainService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// HttpApi depending on contracts only +public class BookController : AbpController +{ + private readonly IBookAppService _bookAppService; // ✅ CORRECT +} +``` + +## Repository Pattern Enforcement + +### Interface Location +```csharp +// In Domain project +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### Implementation Location +```csharp +// In EntityFrameworkCore project +public class BookRepository : EfCoreRepository, IBookRepository +{ + // Implementation +} + +// In MongoDB project +public class BookRepository : MongoDbRepository, IBookRepository +{ + // Implementation +} +``` + +## Multi-Application Scenarios + +When you have multiple applications (e.g., Admin + Public API): + +### Vertical Separation +``` +MyProject.Admin.Application - Admin-specific services +MyProject.Public.Application - Public-specific services +MyProject.Domain - Shared domain (both reference this) +``` + +### Rules +- Admin and Public application layers **MUST NOT** reference each other +- Share domain logic, not application logic +- Each vertical can have its own DTOs even if similar + +## Enforcement Checklist (Layered Templates) + +When adding a new feature: +1. **Entity changes?** → Domain project +2. **Constants/enums?** → Domain.Shared project +3. **Repository interface?** → Domain project (only if custom queries needed) +4. **Repository implementation?** → EntityFrameworkCore/MongoDB project +5. **DTOs and service interface?** → Application.Contracts project +6. **Service implementation?** → Application project +7. **API endpoint?** → HttpApi project (if not using auto API controllers) + +## Common Violations to Watch + +| Violation | Impact | Fix | +|-----------|--------|-----| +| DbContext in Application | Breaks DB independence | Use repository | +| Entity in DTO | Exposes internals | Map to DTO | +| IQueryable in interface | Breaks abstraction | Return concrete types | +| Cross-module app service call | Tight coupling | Use events or domain | diff --git a/.agents/skills/abp-development-flow/SKILL.md b/.agents/skills/abp-development-flow/SKILL.md new file mode 100644 index 0000000000..ad6abe3373 --- /dev/null +++ b/.agents/skills/abp-development-flow/SKILL.md @@ -0,0 +1,261 @@ +--- +name: abp-development-flow +description: ABP development workflow - step-by-step guide for adding new entities, migrations, application services, localization, permissions, and tests. Use when adding new features or entities to an ABP project. +--- + +# ABP Development Workflow + +> **Tutorials**: https://abp.io/docs/latest/tutorials + +## Adding a New Entity (Full Flow) + +### 1. Domain Layer +Create entity (location varies by template: `*.Domain/Entities/` for layered, `Entities/` for single-layer/microservice): + +```csharp +public class Book : AggregateRoot +{ + public string Name { get; private set; } + public decimal Price { get; private set; } + public Guid AuthorId { get; private set; } + + protected Book() { } + + public Book(Guid id, string name, decimal price, Guid authorId) : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + SetPrice(price); + AuthorId = authorId; + } + + public void SetPrice(decimal price) + { + Price = Check.Range(price, nameof(price), 0, 9999); + } +} +``` + +### 2. Domain.Shared +Add constants and enums in `*.Domain.Shared/`: + +```csharp +public static class BookConsts +{ + public const int MaxNameLength = 128; +} + +public enum BookType +{ + Novel, + Science, + Biography +} +``` + +### 3. Repository Interface (Optional) +Define custom repository in `*.Domain/` only if you need custom query methods. For simple CRUD, use generic `IRepository` directly: + +```csharp +// Only if custom queries are needed +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### 4. EF Core Configuration +In `*.EntityFrameworkCore/`: + +**DbContext:** +```csharp +public DbSet Books { get; set; } +``` + +**OnModelCreating:** +```csharp +builder.Entity(b => +{ + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); + b.Property(x => x.Name).IsRequired().HasMaxLength(BookConsts.MaxNameLength); + b.HasIndex(x => x.Name); +}); +``` + +**Repository Implementation (only if custom interface defined):** +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + return await (await GetDbSetAsync()) + .FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### 5. Run Migration +See `abp-ef-core` skill for migration commands. Recommended: use `DbMigrator` project to apply migrations and seed data. + +### 6. Application.Contracts +Create DTOs and service interface: + +```csharp +// DTOs +public class BookDto : EntityDto +{ + public string Name { get; set; } + public decimal Price { get; set; } + public Guid AuthorId { get; set; } +} + +public class CreateBookDto +{ + [Required] + [StringLength(BookConsts.MaxNameLength)] + public string Name { get; set; } + + [Range(0, 9999)] + public decimal Price { get; set; } + + [Required] + public Guid AuthorId { get; set; } +} + +// Service Interface +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(PagedAndSortedResultRequestDto input); + Task CreateAsync(CreateBookDto input); +} +``` + +### 7. Object Mapping (Mapperly / AutoMapper) +ABP supports both Mapperly and AutoMapper. Prefer the provider already used in the solution. + +If the solution uses **Mapperly**, create a mapper in the Application project: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +context.Services.AddSingleton(); +``` + +### 8. Application Service +Implement service (using generic repository - use `IBookRepository` if you defined custom interface in step 3): + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; // Or IBookRepository + private readonly BookMapper _bookMapper; + + public BookAppService( + IRepository bookRepository, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(MyProjectPermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = new Book( + GuidGenerator.Create(), + input.Name, + input.Price, + input.AuthorId + ); + + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +### 9. Add Localization +In `*.Domain.Shared/Localization/*/en.json`: + +```json +{ + "Book": "Book", + "Books": "Books", + "BookName": "Name", + "BookPrice": "Price" +} +``` + +### 10. Add Permissions (if needed) +```csharp +public static class MyProjectPermissions +{ + public static class Books + { + public const string Default = "MyProject.Books"; + public const string Create = Default + ".Create"; + } +} +``` + +### 11. Add Tests +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + var result = await _bookAppService.CreateAsync(new CreateBookDto + { + Name = "Test Book", + Price = 19.99m + }); + + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("Test Book"); + } +} +``` + +## Checklist for New Features + +- [ ] Entity created with proper constructors +- [ ] Constants in Domain.Shared +- [ ] Custom repository interface in Domain (only if custom queries needed) +- [ ] EF Core configuration added +- [ ] Custom repository implementation (only if interface defined) +- [ ] Migration generated and applied (use DbMigrator) +- [ ] Mapperly mapper created and registered +- [ ] DTOs created in Application.Contracts +- [ ] Service interface defined +- [ ] Service implementation with authorization +- [ ] Localization keys added +- [ ] Permissions defined (if applicable) +- [ ] Tests written diff --git a/.agents/skills/abp-ef-core/SKILL.md b/.agents/skills/abp-ef-core/SKILL.md new file mode 100644 index 0000000000..d255042b83 --- /dev/null +++ b/.agents/skills/abp-ef-core/SKILL.md @@ -0,0 +1,262 @@ +--- +name: abp-ef-core +description: ABP Entity Framework Core - DbContext, entity configuration, EfCoreRepository implementation, migrations (dotnet ef migrations add), data seeding. Use when working in EntityFrameworkCore projects, adding migrations, or implementing EF Core repositories. +--- + +# ABP Entity Framework Core + +> **Docs**: https://abp.io/docs/latest/framework/data/entity-framework-core + +## Never Do + +| Don't | Do Instead | +|-------|-----------| +| Skip `b.ConfigureByConvention()` | Always call it first in entity config | +| `AddDefaultRepositories(includeAllEntities: true)` | Use `AddDefaultRepositories()` only for aggregate roots | +| Inject `DbContext` in application/domain services | Use `IRepository` or custom repository interface | +| Use `DbContext` directly outside the EF Core project | Access via `GetDbContextAsync()` inside repository only | + +## DbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectDbContext : AbpDbContext +{ + public DbSet Books { get; set; } + public DbSet Authors { get; set; } + + public MyProjectDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Configure all entities + builder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectDbContextModelCreatingExtensions +{ + public static void ConfigureMyProject(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); // ABP conventions (audit, soft-delete, etc.) + + // Property configurations + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(BookConsts.MaxNameLength); + + b.Property(x => x.Price) + .HasColumnType("decimal(18,2)"); + + // Indexes + b.HasIndex(x => x.Name); + + // Relationships + b.HasOne() + .WithMany() + .HasForeignKey(x => x.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public override async Task> WithDetailsAsync() + { + return (await GetQueryableAsync()) + .Include(b => b.Reviews); + } +} +``` + +## Extension Method for Include +```csharp +public static class BookEfCoreQueryableExtensions +{ + public static IQueryable IncludeDetails( + this IQueryable queryable, + bool include = true) + { + if (!include) + { + return queryable; + } + + return queryable + .Include(b => b.Reviews); + } +} +``` + +## Migration Commands + +```bash +# Navigate to EF Core project +cd src/MyProject.EntityFrameworkCore + +# Add migration +dotnet ef migrations add MigrationName + +# Apply migration (choose one): +dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data +dotnet ef database update # EF Core command only + +# Remove last migration (if not applied) +dotnet ef migrations remove + +# Generate SQL script +dotnet ef migrations script +``` + +> **Note**: ABP templates include `IDesignTimeDbContextFactory` in the EF Core project, so `-s` (startup project) parameter is not needed. + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpEntityFrameworkCoreModule))] +public class MyProjectEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - it creates repositories for child entities, + // allowing them to be modified without going through the aggregate root, + // which breaks data consistency + }); + + Configure(options => + { + options.UseSqlServer(); // or UseNpgsql(), UseMySql(), etc. + }); + } +} +``` + +## Best Practices + +### Repositories for Aggregate Roots Only +Don't use `includeAllEntities: true` in `AddDefaultRepositories()`. This creates repositories for child entities, allowing direct modification without going through the aggregate root - breaking DDD data consistency rules. + +```csharp +// ✅ Correct - Only aggregate roots get repositories +options.AddDefaultRepositories(); + +// ❌ Avoid - Creates repositories for ALL entities including child entities +options.AddDefaultRepositories(includeAllEntities: true); +``` + +### Always Call ConfigureByConvention +```csharp +builder.Entity(b => +{ + b.ConfigureByConvention(); // Don't forget this! + // Other configurations... +}); +``` + +### Use Table Prefix +```csharp +public static class MyProjectConsts +{ + public const string DbTablePrefix = "App"; + public const string DbSchema = null; // Or "myschema" +} +``` + +### Performance Tips +- Add explicit indexes for frequently queried fields +- Use `AsNoTracking()` for read-only queries +- Avoid N+1 queries with `.Include()` or specifications +- ABP handles cancellation automatically; use `GetCancellationToken(cancellationToken)` only in custom repository methods +- Consider query splitting for complex queries with multiple collections + +### Accessing Raw DbContext +```csharp +public async Task CustomOperationAsync() +{ + var dbContext = await GetDbContextAsync(); + + // Raw SQL + await dbContext.Database.ExecuteSqlRawAsync( + "UPDATE Books SET IsPublished = 1 WHERE AuthorId = {0}", + authorId + ); +} +``` + +## Data Seeding + +```csharp +public class MyProjectDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book(_guidGenerator.Create(), "Sample Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` diff --git a/.agents/skills/abp-infrastructure/SKILL.md b/.agents/skills/abp-infrastructure/SKILL.md new file mode 100644 index 0000000000..3d48675bfc --- /dev/null +++ b/.agents/skills/abp-infrastructure/SKILL.md @@ -0,0 +1,243 @@ +--- +name: abp-infrastructure +description: ABP infrastructure services - ISettingProvider, IFeatureChecker, IDistributedCache, ILocalEventBus, IDistributedEventBus, IBackgroundJobManager, localization resource. Use when working with settings, feature flags, caching, event bus, or background jobs in ABP. +--- + +# ABP Infrastructure Services + +> **Docs**: https://abp.io/docs/latest/framework/infrastructure + +## Settings + +### Define Settings +```csharp +public class MySettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add( + new SettingDefinition("MyApp.MaxItemCount", "10"), + new SettingDefinition("MyApp.EnableFeature", "false"), + new SettingDefinition("MyApp.SecretKey", isEncrypted: true) + ); + } +} +``` + +### Read Settings +```csharp +public class MyService : ITransientDependency +{ + private readonly ISettingProvider _settingProvider; + + public async Task DoSomethingAsync() + { + var maxCount = await _settingProvider.GetAsync("MyApp.MaxItemCount"); + var isEnabled = await _settingProvider.IsTrueAsync("MyApp.EnableFeature"); + } +} +``` + +### Setting Value Providers (Priority Order) +1. User settings (highest) +2. Tenant settings +3. Global settings +4. Configuration (appsettings.json) +5. Default value (lowest) + +## Features + +### Define Features +```csharp +public class MyFeatureDefinitionProvider : FeatureDefinitionProvider +{ + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + myGroup.AddFeature( + "MyApp.PdfReporting", + defaultValue: "false", + valueType: new ToggleStringValueType() + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)) + ); + } +} +``` + +### Check Features +```csharp +[RequiresFeature("MyApp.PdfReporting")] +public async Task GetPdfReportAsync() +{ + // Only executes if feature is enabled +} + +// Or programmatically +if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) +{ + // Feature is enabled for current tenant +} + +var maxCount = await _featureChecker.GetAsync("MyApp.MaxProductCount"); +``` + +## Distributed Caching + +### Typed Cache +```csharp +public class BookService : ITransientDependency +{ + private readonly IDistributedCache _cache; + private readonly IClock _clock; + + public BookService(IDistributedCache cache, IClock clock) + { + _cache = cache; + _clock = clock; + } + + public async Task GetAsync(Guid bookId) + { + return await _cache.GetOrAddAsync( + bookId.ToString(), + async () => await GetBookFromDatabaseAsync(bookId), + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = _clock.Now.AddHours(1) + } + ); + } +} + +[CacheName("Books")] +public class BookCacheItem +{ + public string Name { get; set; } + public decimal Price { get; set; } +} +``` + +## Event Bus + +### Local Events (Same Process) +```csharp +// Event class +public class OrderCreatedEvent +{ + public Order Order { get; set; } +} + +// Handler +public class OrderCreatedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEvent eventData) + { + // Handle within same transaction + } +} + +// Publish +await _localEventBus.PublishAsync(new OrderCreatedEvent { Order = order }); +``` + +### Distributed Events (Cross-Service) +```csharp +// Event Transfer Object (in Domain.Shared) +[EventName("MyApp.Order.Created")] +public class OrderCreatedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} + +// Handler +public class OrderCreatedEtoHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEto eventData) + { + // Handle distributed event + } +} + +// Publish +await _distributedEventBus.PublishAsync(new OrderCreatedEto { ... }); +``` + +### When to Use Which +- **Local**: Within same module/bounded context +- **Distributed**: Cross-module or microservice communication + +## Background Jobs + +### Define Job +```csharp +public class EmailSendingArgs +{ + public string EmailAddress { get; set; } + public string Subject { get; set; } + public string Body { get; set; } +} + +public class EmailSendingJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly IEmailSender _emailSender; + + public EmailSendingJob(IEmailSender emailSender) + { + _emailSender = emailSender; + } + + public override async Task ExecuteAsync(EmailSendingArgs args) + { + await _emailSender.SendAsync(args.EmailAddress, args.Subject, args.Body); + } +} +``` + +### Enqueue Job +```csharp +await _backgroundJobManager.EnqueueAsync( + new EmailSendingArgs + { + EmailAddress = "user@example.com", + Subject = "Hello", + Body = "..." + }, + delay: TimeSpan.FromMinutes(5) // Optional delay +); +``` + +## Localization + +### Define Resource +```csharp +[LocalizationResourceName("MyModule")] +public class MyModuleResource { } +``` + +### JSON Structure +```json +{ + "culture": "en", + "texts": { + "HelloWorld": "Hello World!", + "Menu:Books": "Books" + } +} +``` + +### Usage +- In `ApplicationService`: Use `L["Key"]` property (already available from base class) +- In other services: Inject `IStringLocalizer` + +> **Tip**: ABP base classes already provide commonly used services as properties. Check before injecting: +> - `StringLocalizer` (L), `Clock`, `CurrentUser`, `CurrentTenant`, `GuidGenerator` +> - `AuthorizationService`, `FeatureChecker`, `DataFilter` +> - `LoggerFactory`, `Logger` +> - Methods like `CheckPolicyAsync()` for authorization checks diff --git a/.agents/skills/abp-microservice/SKILL.md b/.agents/skills/abp-microservice/SKILL.md new file mode 100644 index 0000000000..e122789728 --- /dev/null +++ b/.agents/skills/abp-microservice/SKILL.md @@ -0,0 +1,209 @@ +--- +name: abp-microservice +description: ABP Microservice solution template - service structure, Integration Services ([IntegrationService]), inter-service HTTP proxies, distributed events with Outbox/Inbox, Entity Cache, RabbitMQ/Redis/YARP setup. Use when working with the ABP microservice solution template or inter-service communication patterns. +--- + +# ABP Microservice Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/microservice + +## Solution Structure + +``` +MyMicroservice/ +├── apps/ # UI applications +│ ├── web/ # Web application +│ ├── public-web/ # Public website +│ └── auth-server/ # Authentication server (OpenIddict) +├── gateways/ # BFF pattern - one gateway per UI +│ └── web-gateway/ # YARP reverse proxy +├── services/ # Microservices +│ ├── administration/ # Permissions, settings, features +│ ├── identity/ # Users, roles +│ └── [your-services]/ # Your business services +└── etc/ + ├── docker/ # Docker compose for local infra + └── helm/ # Kubernetes deployment +``` + +## Microservice Structure (NOT Layered!) + +Each microservice has simplified structure - everything in one project: + +``` +services/ordering/ +├── OrderingService/ # Main project +│ ├── Entities/ +│ ├── Services/ +│ ├── IntegrationServices/ # For inter-service communication +│ ├── Data/ # DbContext (implements IHasEventInbox, IHasEventOutbox) +│ └── OrderingServiceModule.cs +├── OrderingService.Contracts/ # Interfaces, DTOs, ETOs (shared) +└── OrderingService.Tests/ +``` + +## Inter-Service Communication + +### 1. Integration Services (Synchronous HTTP) + +For synchronous calls, use **Integration Services** - NOT regular application services. + +#### Step 1: Provider Service - Create Integration Service + +```csharp +// In CatalogService.Contracts project +[IntegrationService] +public interface IProductIntegrationService : IApplicationService +{ + Task> GetProductsByIdsAsync(List ids); +} + +// In CatalogService project +[IntegrationService] +public class ProductIntegrationService : ApplicationService, IProductIntegrationService +{ + public async Task> GetProductsByIdsAsync(List ids) + { + var products = await _productRepository.GetListAsync(p => ids.Contains(p.Id)); + return ObjectMapper.Map, List>(products); + } +} +``` + +#### Step 2: Provider Service - Expose Integration Services + +```csharp +// In CatalogServiceModule.cs +Configure(options => +{ + options.ExposeIntegrationServices = true; +}); +``` + +#### Step 3: Consumer Service - Add Package Reference + +Add reference to provider's Contracts project (via ABP Studio or manually): +- Right-click OrderingService → Add Package Reference → Select `CatalogService.Contracts` + +#### Step 4: Consumer Service - Generate Proxies + +```bash +# Run ABP CLI in consumer service folder +abp generate-proxy -t csharp -u http://localhost:44361 -m catalog --without-contracts +``` + +Or use ABP Studio: Right-click service → ABP CLI → Generate Proxy → C# + +#### Step 5: Consumer Service - Register HTTP Client Proxies + +```csharp +// In OrderingServiceModule.cs +[DependsOn(typeof(CatalogServiceContractsModule))] // Add module dependency +public class OrderingServiceModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Register static HTTP client proxies + context.Services.AddStaticHttpClientProxies( + typeof(CatalogServiceContractsModule).Assembly, + "CatalogService"); + } +} +``` + +#### Step 6: Consumer Service - Configure Remote Service URL + +```json +// appsettings.json +"RemoteServices": { + "CatalogService": { + "BaseUrl": "http://localhost:44361" + } +} +``` + +#### Step 7: Use Integration Service + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IProductIntegrationService _productIntegrationService; + + public async Task> GetListAsync() + { + var orders = await _orderRepository.GetListAsync(); + var productIds = orders.Select(o => o.ProductId).Distinct().ToList(); + + // Call remote service via generated proxy + var products = await _productIntegrationService.GetProductsByIdsAsync(productIds); + // ... + } +} +``` + +> **Why Integration Services?** Application services are for UI - they have different authorization, validation, and optimization needs. Integration services are designed specifically for inter-service communication. + +**When to use:** Need immediate response, data required to complete current operation (e.g., get product details to display in order list). + +### 2. Distributed Events (Asynchronous) + +Use RabbitMQ-based events for loose coupling. + +**When to use:** +- Notifying other services about state changes (e.g., "order placed", "stock updated") +- Operations that don't need immediate response +- When services should remain independent and decoupled + +```csharp +// Define ETO in Contracts project +[EventName("Product.StockChanged")] +public class StockCountChangedEto +{ + public Guid ProductId { get; set; } + public int NewCount { get; set; } +} + +// Publish +await _distributedEventBus.PublishAsync(new StockCountChangedEto { ... }); + +// Subscribe in another service +public class StockChangedHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(StockCountChangedEto eventData) { ... } +} +``` + +DbContext must implement `IHasEventInbox`, `IHasEventOutbox` for Outbox/Inbox pattern. + +## Performance: Entity Cache + +For frequently accessed data from other services, use Entity Cache: + +```csharp +// Register +context.Services.AddEntityCache(); + +// Use - auto-invalidates on entity changes +private readonly IEntityCache _productCache; + +public async Task GetProductAsync(Guid id) +{ + return await _productCache.GetAsync(id); +} +``` + +## Pre-Configured Infrastructure + +- **RabbitMQ** - Distributed events with Outbox/Inbox +- **Redis** - Distributed cache and locking +- **YARP** - API Gateway +- **OpenIddict** - Auth server + +## Best Practices + +- **Choose communication wisely** - Synchronous for queries needing immediate data, asynchronous for notifications and state changes +- **Use Integration Services** - Not application services for inter-service calls +- **Cache remote data** - Use Entity Cache or IDistributedCache for frequently accessed data +- **Share only Contracts** - Never share implementations +- **Idempotent handlers** - Events may be delivered multiple times +- **Database per service** - Each service owns its database diff --git a/.agents/skills/abp-module/SKILL.md b/.agents/skills/abp-module/SKILL.md new file mode 100644 index 0000000000..def061f3cb --- /dev/null +++ b/.agents/skills/abp-module/SKILL.md @@ -0,0 +1,234 @@ +--- +name: abp-module +description: ABP reusable Module solution template - EF Core + MongoDB dual support, virtual methods for extensibility, DbTablePrefix, module options pattern, entity extension, separate connection string. Use when building or reviewing reusable ABP modules that will be distributed or consumed by other solutions. +--- + +# ABP Module Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/application-module + +This template is for developing reusable ABP modules. Key requirement: **extensibility** - consumers must be able to override and customize module behavior. + +## Solution Structure + +``` +MyModule/ +├── src/ +│ ├── MyModule.Domain.Shared/ # Constants, enums, localization +│ ├── MyModule.Domain/ # Entities, repository interfaces, domain services +│ ├── MyModule.Application.Contracts/ # DTOs, service interfaces +│ ├── MyModule.Application/ # Service implementations +│ ├── MyModule.EntityFrameworkCore/ # EF Core implementation +│ ├── MyModule.MongoDB/ # MongoDB implementation +│ ├── MyModule.HttpApi/ # REST controllers +│ ├── MyModule.HttpApi.Client/ # Client proxies +│ ├── MyModule.Web/ # MVC/Razor Pages UI +│ └── MyModule.Blazor/ # Blazor UI +├── test/ +│ └── MyModule.Tests/ +└── host/ + └── MyModule.HttpApi.Host/ # Test host application +``` + +## Database Independence + +Support both EF Core and MongoDB: + +### Repository Interface (Domain) +```csharp +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); + Task> GetListByAuthorAsync(Guid authorId); +} +``` + +### EF Core Implementation +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### MongoDB Implementation +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var queryable = await GetQueryableAsync(); + return await queryable.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +## Table/Collection Prefix + +Allow customization to avoid naming conflicts: + +```csharp +// Domain.Shared +public static class MyModuleDbProperties +{ + public static string DbTablePrefix { get; set; } = "MyModule"; + public static string DbSchema { get; set; } = null; + + public const string ConnectionStringName = "MyModule"; +} +``` + +Usage: +```csharp +builder.Entity(b => +{ + b.ToTable(MyModuleDbProperties.DbTablePrefix + "Books", MyModuleDbProperties.DbSchema); +}); +``` + +## Module Options + +Provide configuration options: + +```csharp +// Domain +public class MyModuleOptions +{ + public bool EnableFeatureX { get; set; } = true; + public int MaxItemCount { get; set; } = 100; +} +``` + +Usage in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.EnableFeatureX = true; + }); +} +``` + +Usage in service: +```csharp +public class MyService : ITransientDependency +{ + private readonly MyModuleOptions _options; + + public MyService(IOptions options) + { + _options = options.Value; + } +} +``` + +## Extensibility Points + +### Virtual Methods (Critical for Modules!) +When developing a reusable module, **all public and protected methods must be virtual** to allow consumers to override behavior: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + // ✅ Public methods MUST be virtual + public virtual async Task CreateAsync(CreateBookDto input) + { + var book = await CreateBookEntityAsync(input); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + // ✅ Use protected virtual for helper methods (not private) + protected virtual Task CreateBookEntityAsync(CreateBookDto input) + { + return Task.FromResult(new Book( + GuidGenerator.Create(), + input.Name, + input.Price + )); + } + + // ❌ WRONG for modules - private methods cannot be overridden + // private Book CreateBook(CreateBookDto input) { ... } +} +``` + +This allows module consumers to: +- Override specific methods without copying entire class +- Extend functionality while preserving base behavior +- Customize module behavior for their needs + +### Entity Extension +Support object extension system: +```csharp +public class MyModuleModuleExtensionConfigurator +{ + public static void Configure() + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance.Modules() + .ConfigureMyModule(module => + { + module.ConfigureBook(book => + { + book.AddOrUpdateProperty("CustomProperty"); + }); + }); + }); + } +} +``` + +## Localization + +```csharp +// Domain.Shared +[LocalizationResourceName("MyModule")] +public class MyModuleResource +{ +} + +// Module configuration +Configure(options => +{ + options.Resources + .Add("en") + .AddVirtualJson("/Localization/MyModule"); +}); +``` + +## Permission Definition + +```csharp +public class MyModulePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var myGroup = context.AddGroup( + MyModulePermissions.GroupName, + L("Permission:MyModule")); + + myGroup.AddPermission( + MyModulePermissions.Books.Default, + L("Permission:Books")); + } +} +``` + +## Best Practices + +1. **Virtual methods** - All public/protected methods must be `virtual` for extensibility +2. **Protected virtual helpers** - Use `protected virtual` instead of `private` for helper methods +3. **Database agnostic** - Support both EF Core and MongoDB +4. **Configurable** - Use options pattern for customization +5. **Localizable** - Use localization for all user-facing text +6. **Table prefix** - Allow customization to avoid conflicts +7. **Separate connection string** - Support dedicated database +8. **No dependencies on host** - Module should be self-contained +9. **Test with host app** - Include a host application for testing diff --git a/.agents/skills/abp-mongodb/SKILL.md b/.agents/skills/abp-mongodb/SKILL.md new file mode 100644 index 0000000000..42ef94517c --- /dev/null +++ b/.agents/skills/abp-mongodb/SKILL.md @@ -0,0 +1,202 @@ +--- +name: abp-mongodb +description: ABP MongoDB patterns - AbpMongoDbContext, IMongoCollection, MongoDbRepository, no migrations, embedded documents vs references, manual UpdateAsync required. Use when working in MongoDB projects or implementing MongoDB repositories in ABP. +--- + +# ABP MongoDB + +> **Docs**: https://abp.io/docs/latest/framework/data/mongodb + +## MongoDbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectMongoDbContext : AbpMongoDbContext +{ + public IMongoCollection Books => Collection(); + public IMongoCollection Authors => Collection(); + + protected override void CreateModel(IMongoModelBuilder modelBuilder) + { + base.CreateModel(modelBuilder); + + modelBuilder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectMongoDbContextExtensions +{ + public static void ConfigureMyProject(this IMongoModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.CollectionName = MyProjectConsts.DbTablePrefix + "Books"; + }); + + builder.Entity(b => + { + b.CollectionName = MyProjectConsts.DbTablePrefix + "Authors"; + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public BookRepository(IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} +``` + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpMongoDbModule))] +public class MyProjectMongoDbModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMongoDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - breaks DDD data consistency + }); + } +} +``` + +## Connection String + +In `appsettings.json`: +```json +{ + "ConnectionStrings": { + "Default": "mongodb://localhost:27017/MyProjectDb" + } +} +``` + +## Key Differences from EF Core + +### No Migrations +MongoDB is schema-less; no migrations needed. Changes to entity structure are handled automatically. + +### includeDetails Parameter +Often ignored in MongoDB because documents typically embed related data: + +```csharp +public async Task> GetListAsync( + bool includeDetails = false, // Usually ignored + CancellationToken cancellationToken = default) +{ + // MongoDB documents already include nested data + return await (await GetQueryableAsync()) + .ToListAsync(GetCancellationToken(cancellationToken)); +} +``` + +### Embedded Documents vs References +```csharp +// Embedded (stored in same document) +public class Order : AggregateRoot +{ + public List Lines { get; set; } // Embedded +} + +// Reference (separate collection, store ID only) +public class Order : AggregateRoot +{ + public Guid CustomerId { get; set; } // Reference by ID +} +``` + +### No Change Tracking +MongoDB doesn't track entity changes automatically: + +```csharp +public async Task UpdateBookAsync(Guid id, string newName) +{ + var book = await _bookRepository.GetAsync(id); + book.SetName(newName); + + // Must explicitly update + await _bookRepository.UpdateAsync(book); +} +``` + +## Direct Collection Access + +```csharp +public async Task CustomOperationAsync() +{ + var collection = await GetCollectionAsync(); + + // Use MongoDB driver directly + var filter = Builders.Filter.Eq(b => b.AuthorId, authorId); + var update = Builders.Update.Set(b => b.IsPublished, true); + + await collection.UpdateManyAsync(filter, update); +} +``` + +## Indexing + +Configure indexes in repository or via MongoDB driver: + +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public override async Task> GetQueryableAsync() + { + var collection = await GetCollectionAsync(); + + // Ensure index exists + var indexKeys = Builders.IndexKeys.Ascending(b => b.Name); + await collection.Indexes.CreateOneAsync(new CreateIndexModel(indexKeys)); + + return await base.GetQueryableAsync(); + } +} +``` + +## Best Practices + +- Design documents for query patterns (denormalize when needed) +- Use references for frequently changing data +- Use embedding for data that's always accessed together +- Add indexes for frequently queried fields +- Use `GetCancellationToken(cancellationToken)` for proper cancellation +- Remember: ABP data filters (soft-delete, multi-tenancy) work with MongoDB too diff --git a/.agents/skills/abp-multi-tenancy/SKILL.md b/.agents/skills/abp-multi-tenancy/SKILL.md new file mode 100644 index 0000000000..3ad892ef15 --- /dev/null +++ b/.agents/skills/abp-multi-tenancy/SKILL.md @@ -0,0 +1,161 @@ +--- +name: abp-multi-tenancy +description: ABP Multi-Tenancy - IMultiTenant interface, CurrentTenant, CurrentTenant.Change(), DataFilter.Disable(IMultiTenant), tenant resolution order, database-per-tenant. Use when working with multi-tenant features, tenant-specific data isolation, or switching tenant context. +--- + +# ABP Multi-Tenancy + +> **Docs**: https://abp.io/docs/latest/framework/architecture/multi-tenancy + +## Making Entities Multi-Tenant + +Implement `IMultiTenant` interface to make entities tenant-aware: + +```csharp +public class Product : AggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // Required by IMultiTenant + + public string Name { get; private set; } + public decimal Price { get; private set; } + + protected Product() { } + + public Product(Guid id, string name, decimal price) : base(id) + { + Name = name; + Price = price; + // TenantId is automatically set from CurrentTenant.Id + } +} +``` + +**Key points:** +- `TenantId` is **nullable** - `null` means entity belongs to Host +- ABP **automatically filters** queries by current tenant +- ABP **automatically sets** `TenantId` when creating entities + +## Accessing Current Tenant + +Use `CurrentTenant` property (available in base classes) or inject `ICurrentTenant`: + +```csharp +public class ProductAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Available from base class + var tenantId = CurrentTenant.Id; // Guid? - null for host + var tenantName = CurrentTenant.Name; // string? + var isAvailable = CurrentTenant.IsAvailable; // true if Id is not null + } +} + +// In other services +public class MyService : ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant; +} +``` + +## Switching Tenant Context + +Use `CurrentTenant.Change()` to temporarily switch tenant (useful in host context): + +```csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public async Task GetProductCountAsync(Guid? tenantId) + { + // Switch to specific tenant + using (CurrentTenant.Change(tenantId)) + { + return await _productRepository.GetCountAsync(); + } + // Automatically restored to previous tenant after using block + } + + public async Task DoHostOperationAsync() + { + // Switch to host context + using (CurrentTenant.Change(null)) + { + // Operations here are in host context + } + } +} +``` + +> **Important**: Always use `Change()` with a `using` statement. + +## Disabling Multi-Tenant Filter + +To query all tenants' data (only works with single database): + +```csharp +public class ProductManager : DomainService +{ + public async Task GetAllProductCountAsync() + { + // DataFilter is available from base class + using (DataFilter.Disable()) + { + return await _productRepository.GetCountAsync(); + // Returns count from ALL tenants + } + } +} +``` + +> **Note**: This doesn't work with separate databases per tenant. + +## Database Architecture Options + +| Approach | Description | Use Case | +|----------|-------------|----------| +| Single Database | All tenants share one database | Simple, cost-effective | +| Database per Tenant | Each tenant has dedicated database | Data isolation, compliance | +| Hybrid | Mix of shared and dedicated | Flexible, premium tenants | + +Connection strings are configured per tenant in Tenant Management module. + +## Best Practices + +1. **Always implement `IMultiTenant`** for tenant-specific entities +2. **Never manually filter by `TenantId`** - ABP does it automatically +3. **Don't change `TenantId` after creation** - it moves entity between tenants +4. **Use `Change()` scope carefully** - nested scopes are supported +5. **Test both host and tenant contexts** - ensure proper data isolation +6. **Consider nullable `TenantId`** - entity may be host-only or shared + +## Enabling Multi-Tenancy + +```csharp +Configure(options => +{ + options.IsEnabled = true; // Enabled by default in ABP templates +}); +``` + +Check `MultiTenancyConsts.IsEnabled` in your solution for centralized control. + +## Tenant Resolution + +ABP resolves current tenant from (in order): +1. Current user's claims +2. Query string (`?__tenant=...`) +3. Route (`/{__tenant}/...`) +4. HTTP header (`__tenant`) +5. Cookie (`__tenant`) +6. Domain/subdomain (if configured) + +For subdomain-based resolution: +```csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.mydomain.com"); +}); +``` diff --git a/.agents/skills/abp-mvc/SKILL.md b/.agents/skills/abp-mvc/SKILL.md new file mode 100644 index 0000000000..f7e4cc0bff --- /dev/null +++ b/.agents/skills/abp-mvc/SKILL.md @@ -0,0 +1,257 @@ +--- +name: abp-mvc +description: ABP MVC and Razor Pages UI - AbpPageModel, abp tag helpers (abp-card, abp-dynamic-form, abp-modal), JavaScript abp.ajax/abp.auth/abp.notify, DataTables integration, bundle/minification. Use when working on MVC or Razor Pages UI in ABP projects. +--- + +# ABP MVC / Razor Pages UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall + +## Razor Page Model +```csharp +public class IndexModel : AbpPageModel +{ + private readonly IBookAppService _bookAppService; + + public List Books { get; set; } + + public IndexModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + Books = result.Items.ToList(); + } +} +``` + +## Razor Page View +```html +@page +@model IndexModel + + + + + +

@L["Books"]

+
+ + + +
+
+ + + + + @L["Name"] + @L["Price"] + @L["Actions"] + + + + @foreach (var book in Model.Books) + { + + @book.Name + @book.Price + + + + + } + + + +
+``` + +## ABP Tag Helpers + +### Cards +```html + + Header + Content + Footer + +``` + +### Buttons +```html + + +``` + +### Forms +```html + + + + + + + + + +``` + +### Tables +```html + + + +``` + +## Localization +```html +@* In Razor views/pages *@ +

@L["Books"]

+ +@* With parameters *@ +

@L["WelcomeMessage", Model.UserName]

+``` + +## JavaScript API +```javascript +// Localization +var text = abp.localization.getResource('BookStore')('Books'); + +// Authorization +if (abp.auth.isGranted('BookStore.Books.Create')) { + // Show create button +} + +// Settings +var maxCount = abp.setting.get('BookStore.MaxItemCount'); + +// Ajax with automatic error handling +abp.ajax({ + url: '/api/app/book', + type: 'POST', + data: JSON.stringify(bookData) +}).then(function(result) { + // Success +}); + +// Notifications +abp.notify.success('Book created successfully!'); +abp.notify.error('An error occurred!'); + +// Confirmation +abp.message.confirm('Are you sure?').then(function(confirmed) { + if (confirmed) { + // User confirmed + } +}); +``` + +## DataTables Integration +```javascript +var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + ajax: abp.libs.datatables.createAjax(bookService.getList), + columnDefs: [ + { + title: l('Name'), + data: 'name' + }, + { + title: l('Price'), + data: 'price', + render: function(data) { + return data.toFixed(2); + } + }, + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + visible: abp.auth.isGranted('BookStore.Books.Edit'), + action: function(data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + visible: abp.auth.isGranted('BookStore.Books.Delete'), + confirmMessage: function(data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function(data) { + bookService.delete(data.record.id).then(function() { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + } + ] + }) +); +``` + +## Modal Pages +**CreateModal.cshtml:** +```html +@page +@model CreateModalModel + + + + + + + + + + +``` + +**CreateModal.cshtml.cs:** +```csharp +public class CreateModalModel : AbpPageModel +{ + [BindProperty] + public CreateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } +} +``` + +## Bundle & Minification +```csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundle => bundle.AddFiles("/styles/my-styles.css") + ); +}); +``` diff --git a/.agents/skills/abp-testing/SKILL.md b/.agents/skills/abp-testing/SKILL.md new file mode 100644 index 0000000000..bd41ef4a32 --- /dev/null +++ b/.agents/skills/abp-testing/SKILL.md @@ -0,0 +1,269 @@ +--- +name: abp-testing +description: ABP testing patterns - integration tests over unit tests, GetRequiredService, IDataSeedContributor, Shouldly assertions, AddAlwaysAllowAuthorization, NSubstitute mocking, WithUnitOfWorkAsync. Use when writing or reviewing tests for application services, domain services, or repositories in ABP projects. +--- + +# ABP Testing Patterns + +> **Docs**: https://abp.io/docs/latest/testing + +## Test Project Structure + +| Project | Purpose | Base Class | +|---------|---------|------------| +| `*.Domain.Tests` | Domain logic, entities, domain services | `*DomainTestBase` | +| `*.Application.Tests` | Application services | `*ApplicationTestBase` | +| `*.EntityFrameworkCore.Tests` | Repository implementations | `*EntityFrameworkCoreTestBase` | + +## Integration Test Approach + +ABP recommends integration tests over unit tests: +- Tests run with real services and database (SQLite in-memory) +- No mocking of internal services +- Each test gets a fresh database instance + +## Application Service Test + +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + 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"); + } + + [Fact] + public async Task Should_Create_Book() + { + // Arrange + var input = new CreateBookDto + { + Name = "New Book", + Price = 19.99m + }; + + // Act + var result = await _bookAppService.CreateAsync(input); + + // Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New Book"); + result.Price.ShouldBe(19.99m); + } + + [Fact] + public async Task Should_Not_Create_Book_With_Invalid_Name() + { + // Arrange + var input = new CreateBookDto + { + Name = "", // Invalid + Price = 10m + }; + + // Act & Assert + await Should.ThrowAsync(async () => + { + await _bookAppService.CreateAsync(input); + }); + } +} +``` + +## Domain Service Test + +```csharp +public class BookManager_Tests : MyProjectDomainTestBase +{ + private readonly BookManager _bookManager; + private readonly IBookRepository _bookRepository; + + public BookManager_Tests() + { + _bookManager = GetRequiredService(); + _bookRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + // Act + var book = await _bookManager.CreateAsync("Test Book", 29.99m); + + // Assert + book.ShouldNotBeNull(); + book.Name.ShouldBe("Test Book"); + book.Price.ShouldBe(29.99m); + } + + [Fact] + public async Task Should_Not_Allow_Duplicate_Book_Name() + { + // Arrange + await _bookManager.CreateAsync("Existing Book", 10m); + + // Act & Assert + var exception = await Should.ThrowAsync(async () => + { + await _bookManager.CreateAsync("Existing Book", 20m); + }); + + exception.Code.ShouldBe("MyProject:BookNameAlreadyExists"); + } +} +``` + +## Test Naming Convention + +Use descriptive names: +```csharp +// Pattern: Should_ExpectedBehavior_When_Condition +public async Task Should_Create_Book_When_Input_Is_Valid() +public async Task Should_Throw_BusinessException_When_Name_Already_Exists() +public async Task Should_Return_Empty_List_When_No_Books_Exist() +``` + +## Arrange-Act-Assert (AAA) + +```csharp +[Fact] +public async Task Should_Update_Book_Price() +{ + // Arrange + var bookId = await CreateTestBookAsync(); + var newPrice = 39.99m; + + // Act + var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto + { + Price = newPrice + }); + + // Assert + result.Price.ShouldBe(newPrice); +} +``` + +## Assertions with Shouldly + +ABP uses Shouldly library: +```csharp +result.ShouldNotBeNull(); +result.Name.ShouldBe("Expected Name"); +result.Price.ShouldBeGreaterThan(0); +result.Items.ShouldContain(x => x.Id == expectedId); +result.Items.ShouldBeEmpty(); +result.Items.Count.ShouldBe(5); + +// Exception assertions +await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); + +var ex = await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); +ex.Code.ShouldBe("MyProject:ErrorCode"); +``` + +## Test Data Seeding + +```csharp +public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + public static readonly Guid TestBookId = Guid.Parse("..."); + + private readonly IBookRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(TestBookId, "Test Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` + +## Disabling Authorization in Tests + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAlwaysAllowAuthorization(); +} +``` + +## Mocking External Services + +Use NSubstitute when needed: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var emailSender = Substitute.For(); + emailSender.SendAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + context.Services.AddSingleton(emailSender); +} +``` + +## Testing with Specific User + +```csharp +[Fact] +public async Task Should_Get_Current_User_Books() +{ + // Login as specific user + await WithUnitOfWorkAsync(async () => + { + using (CurrentUser.Change(TestData.UserId)) + { + var result = await _bookAppService.GetMyBooksAsync(); + result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId); + } + }); +} +``` + +## Testing Multi-Tenancy + +```csharp +[Fact] +public async Task Should_Filter_Books_By_Tenant() +{ + using (CurrentTenant.Change(TestData.TenantId)) + { + var result = await _bookAppService.GetListAsync(new GetBookListDto()); + // Results should be filtered by tenant + } +} +``` + +## Best Practices + +- Each test should be independent +- Don't share state between tests +- Use meaningful test data +- Test edge cases and error conditions +- Keep tests focused on single behavior +- Use test data seeders for common data +- Avoid testing framework internals diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1eff521897..4b5b456a93 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,8 @@ "permissions": { "allow": [ "Bash(yarn nx g:*)", - "Bash(npx vitest:*)" + "Bash(npx vitest:*)", + "Bash(git show:*)" ] } } diff --git a/.github/workflows/update-studio-docs.yml b/.github/workflows/update-studio-docs.yml index 541ceeaf30..9e16b07280 100644 --- a/.github/workflows/update-studio-docs.yml +++ b/.github/workflows/update-studio-docs.yml @@ -18,9 +18,9 @@ on: description: 'Release URL' required: true target_branch: - description: 'Target branch (default: dev)' + description: 'Target branch (leave empty to auto-detect from latest stable ABP release)' required: false - default: 'dev' + default: '' jobs: update-docs: @@ -41,7 +41,7 @@ jobs: echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT echo "name=${{ github.event.client_payload.name }}" >> $GITHUB_OUTPUT echo "url=${{ github.event.client_payload.url }}" >> $GITHUB_OUTPUT - echo "target_branch=${{ github.event.client_payload.target_branch || 'dev' }}" >> $GITHUB_OUTPUT + echo "target_branch=${{ github.event.client_payload.target_branch }}" >> $GITHUB_OUTPUT # Save notes to environment variable (multiline) { @@ -53,7 +53,7 @@ jobs: echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT echo "name=${{ github.event.inputs.name }}" >> $GITHUB_OUTPUT echo "url=${{ github.event.inputs.url }}" >> $GITHUB_OUTPUT - echo "target_branch=${{ github.event.inputs.target_branch || 'dev' }}" >> $GITHUB_OUTPUT + echo "target_branch=${{ github.event.inputs.target_branch }}" >> $GITHUB_OUTPUT # Save notes to environment variable (multiline) { @@ -63,12 +63,55 @@ jobs: } >> $GITHUB_ENV fi + # ------------------------------------------------- + # Resolve target branch (auto-detect from latest stable ABP if not provided) + # ------------------------------------------------- + - name: Resolve target branch + id: resolve_branch + run: | + TARGET_BRANCH="${{ steps.payload.outputs.target_branch }}" + + if [ -z "$TARGET_BRANCH" ]; then + echo "🔍 No target_branch provided - fetching latest stable ABP release..." + + RELEASES=$(curl -fsS \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/abpframework/abp/releases?per_page=20") + + ABP_VERSION=$(echo "$RELEASES" | jq -r ' + [.[] | select( + (.prerelease == false) and + (.tag_name | test("preview|rc|beta|dev"; "i") | not) + )] | first | .tag_name + ') + + if [ -z "$ABP_VERSION" ] || [ "$ABP_VERSION" = "null" ]; then + echo "❌ Could not determine latest stable ABP version" + exit 1 + fi + + # Derive rel-X.Y from X.Y.Z (e.g., 10.1.1 -> rel-10.1) + TARGET_BRANCH=$(echo "$ABP_VERSION" | grep -oE '^[0-9]+\.[0-9]+' | sed 's/^/rel-/') + + if [ -z "$TARGET_BRANCH" ]; then + echo "❌ Could not derive target branch from version: $ABP_VERSION" + exit 1 + fi + + echo "✅ Auto-detected target branch: $TARGET_BRANCH (from ABP $ABP_VERSION)" + else + echo "✅ Using provided target branch: $TARGET_BRANCH" + fi + + echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT + - name: Validate payload env: VERSION: ${{ steps.payload.outputs.version }} NAME: ${{ steps.payload.outputs.name }} URL: ${{ steps.payload.outputs.url }} - TARGET_BRANCH: ${{ steps.payload.outputs.target_branch }} + TARGET_BRANCH: ${{ steps.resolve_branch.outputs.target_branch }} run: | if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then echo "❌ Missing: version" @@ -98,7 +141,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ steps.payload.outputs.target_branch }} + ref: ${{ steps.resolve_branch.outputs.target_branch }} fetch-depth: 0 - name: Configure git @@ -563,7 +606,7 @@ jobs: VERSION: ${{ steps.payload.outputs.version }} NAME: ${{ steps.payload.outputs.name }} URL: ${{ steps.payload.outputs.url }} - TARGET_BRANCH: ${{ steps.payload.outputs.target_branch }} + TARGET_BRANCH: ${{ steps.resolve_branch.outputs.target_branch }} run: | # Check for existing PR EXISTING_PR=$(gh pr list \ @@ -593,7 +636,8 @@ jobs: gh pr edit "$EXISTING_PR" \ --title "docs(studio): release $VERSION - $NAME" \ - --body "$PR_BODY" + --body "$PR_BODY" \ + --add-reviewer skoc10 echo "PR_NUMBER=$EXISTING_PR" >> $GITHUB_ENV else @@ -605,7 +649,8 @@ jobs: --title "docs(studio): release $VERSION - $NAME" \ --body "$PR_BODY" \ --base "$TARGET_BRANCH" \ - --head "$BRANCH") + --head "$BRANCH" \ + --reviewer skoc10) PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV @@ -643,7 +688,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Version**: $VERSION" >> $GITHUB_STEP_SUMMARY echo "**Release**: ${{ steps.payload.outputs.name }}" >> $GITHUB_STEP_SUMMARY - echo "**Target Branch**: ${{ steps.payload.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY + echo "**Target Branch**: ${{ steps.resolve_branch.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.changes.outputs.has_changes }}" = "true" ]; then diff --git a/.gitignore b/.gitignore index d3bc52cfb8..9c6c3aa5a0 100644 --- a/.gitignore +++ b/.gitignore @@ -270,6 +270,7 @@ modules/blogging/app/Volo.BloggingTestApp/Logs/*.* modules/blogging/app/Volo.BloggingTestApp/wwwroot/files/*.* modules/docs/app/VoloDocs.Web/Logs/*.* modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/Logs/*.* +modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/** templates/module/app/MyCompanyName.MyProjectName.DemoApp/Logs/*.* templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Logs/logs.txt templates/mvc/src/MyCompanyName.MyProjectName.Web/Logs/*.* @@ -328,4 +329,5 @@ deploy/_run_all_log.txt # No commit yarn.lock files in the subfolders of templates directory templates/**/yarn.lock templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Logs/logs.txt -templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json \ No newline at end of file +templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json +**/.abpstudio/** diff --git a/Directory.Packages.props b/Directory.Packages.props index 3b43d18c26..d4e2d6a70a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -117,12 +117,12 @@ - - - - + + + + - + @@ -132,11 +132,11 @@ - - - - - + + + + + @@ -183,10 +183,10 @@ - - - - + + + + diff --git a/common.props b/common.props index 082dbf3f84..15fafa9120 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 10.2.0-rc.4 - 5.2.0-rc.4 + 10.3.0-preview + 5.3.0-preview $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/POST.md b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/POST.md new file mode 100644 index 0000000000..5855c9977a --- /dev/null +++ b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/POST.md @@ -0,0 +1,242 @@ +# ABP Platform 10.2 RC Has Been Released + +We are happy to release [ABP](https://abp.io) version **10.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. + +Try this version and provide feedback for a more stable version of ABP v10.2! Thanks to you in advance. + +## Get Started with the 10.2 RC + +You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). + +By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: + +![studio-switch-to-preview.png](studio-switch-to-preview.png) + +## Migration Guide + +There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v10.1 or earlier: [ABP Version 10.2 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/abp-10-2). + +## What's New with ABP v10.2? + +In this section, I will introduce some major features released in this version. +Here is a brief list of titles explained in the next sections: + +- Multi-Tenant Account Usage: Shared User Accounts +- Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions +- `ClientResourcePermissionValueProvider` for OAuth/OpenIddict +- Angular: Hybrid Localization Support +- Angular: Extensible Table Row Detail +- Angular: CMS Kit Module Features +- Blazor: Upgrade to Blazorise 2.0 +- Identity: Single Active Token Providers +- TickerQ Package Upgrade to 10.1.1 +- AI Management: MCP (Model Context Protocol) Support +- AI Management: RAG with File Upload +- AI Management: OpenAI-Compatible Chat Endpoint +- File Management: Resource-Based Authorization + +### Multi-Tenant Account Usage: Shared User Accounts + +ABP v10.2 introduces **Shared User Accounts**: a single user account can belong to multiple tenants, and the user can choose or switch the active tenant when signing in. This enables a "one account, multiple tenants" experience — for example, inviting the same email address into multiple tenants. + +When you use Shared User Accounts: + +- Username/email uniqueness becomes **global** (Host + all tenants) +- Users are prompted to select the tenant at login if they belong to multiple tenants +- Users can switch between tenants using the tenant switcher in the user menu +- Tenant administrators can invite existing or new users to join a tenant + +Enable shared accounts by configuring `UserSharingStrategy`: + +```csharp +Configure(options => +{ + options.IsEnabled = true; + options.UserSharingStrategy = TenantUserSharingStrategy.Shared; +}); +``` + +> See the [Shared User Accounts](https://abp.io/docs/10.2/modules/account/shared-user-accounts) documentation for details. + +### Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions + +ABP v10.2 implements a unified **privilege escalation prevention** model to address security vulnerabilities where users could assign themselves or others roles or permissions they do not possess. + +**Role Assignment Restriction:** Users can only assign or remove roles they currently have. Users cannot add new roles to themselves (removal only) and cannot assign or remove roles they do not possess. + +**Permission Grant/Revoke Authorization:** Users can only grant or revoke permissions they currently have. Validation applies to both grant and revoke operations. + +**Incremental Permission Protection:** When updating user or role permissions, permissions the current user does not have are treated as non-editable and are preserved as-is during updates. + +Users with the `admin` role can assign any role and grant/revoke any permission. All validations are enforced on the backend — the UI is not a security boundary. + +> See [#24775](https://github.com/abpframework/abp/pull/24775) for more details. + +### `ClientResourcePermissionValueProvider` for OAuth/OpenIddict + +ABP v10.2 adds **ClientResourcePermissionValueProvider**, extending resource-based authorization to OAuth clients. When using IdentityServer or OpenIddict, clients can now have resource permissions aligned with the standard user and role permission model. + +This allows you to control which OAuth clients can access which resources, providing fine-grained authorization for API consumers. The implementation integrates with ABP's existing resource permission infrastructure. + +> See [#24515](https://github.com/abpframework/abp/pull/24515) for more details. + +### Angular: Hybrid Localization Support + +ABP v10.2 introduces **Hybrid Localization** for Angular applications, combining server-side and client-side localization strategies. This gives you flexibility in how translations are loaded and resolved — you can use server-provided localization, client-side fallbacks, or a mix of both. + +This feature is useful when you want to reduce initial load time, support offline scenarios, or have environment-specific localization behavior. The Angular packages have been updated to support the hybrid approach seamlessly. + +> See the [Hybrid Localization](https://abp.io/docs/10.2/framework/ui/angular/hybrid-localization) documentation and [#24731](https://github.com/abpframework/abp/pull/24731). + +### Angular: Extensible Table Row Detail + +ABP v10.2 adds the **ExtensibleTableRowDetailComponent** for expandable row details in extensible tables. You can now display additional information for each row in a collapsible detail section. + +The feature supports row detail templates via both direct input and content child component. It adds toggle logic and emits `rowDetailToggle` events, making it easy to customize the behavior and appearance of expandable rows in your data tables. + +> See [#24636](https://github.com/abpframework/abp/pull/24636) for more details. + +### Angular: CMS Kit Module Features + +ABP v10.2 brings **CMS Kit features to Angular**, completing the cross-platform UI coverage for the CMS Kit module. The Angular implementation includes: Blogs, Blog Posts, Comments, Menus, Pages, Tags, Global Resources, and CMS Settings. + +Together with the CMS Kit Pro Angular implementation (FAQ, Newsletters, Page Feedbacks, Polls, Url forwarding), ABP now provides full Angular UI coverage for both the open-source CMS Kit and CMS Kit Pro modules. + +> See [#24234](https://github.com/abpframework/abp/pull/24234) for more details. + +### Blazor: Upgrade to Blazorise 2.0 + +ABP v10.2 upgrades the [Blazorise](https://blazorise.com/) library to **version 2.0** for Blazor UI. If you are upgrading your project to v10.2 RC, please ensure that all Blazorise-related packages are updated to v2.0 in your application. + +Blazorise 2.0 includes various improvements and changes. Please refer to the [Blazorise 2.0 Release Notes](https://blazorise.com/news/release-notes/200) and the [ABP Blazorise 2.0 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/blazorise-2-0-migration) for upgrade instructions. + +> See [#24906](https://github.com/abpframework/abp/pull/24906) for more details. + +### Identity: Single Active Token Providers + +ABP v10.2 introduces a **single active token** policy for password reset, email confirmation, and change-email flows. Three new token providers are available: `AbpPasswordResetTokenProvider`, `AbpEmailConfirmationTokenProvider`, and `AbpChangeEmailTokenProvider`. + +When a new token is generated, it invalidates any previously issued tokens for that purpose. This improves security by ensuring that only the most recently issued token is valid. Token lifespan can be customized via the respective options classes for each provider. + +> See [#24926](https://github.com/abpframework/abp/pull/24926) for more details. + +### TickerQ Package Upgrade to 10.1.1 + +**If you are using the TickerQ integration packages** (`Volo.Abp.TickerQ`, `Volo.Abp.BackgroundJobs.TickerQ`, or `Volo.Abp.BackgroundWorkers.TickerQ`), you need to apply breaking changes when upgrading to ABP 10.2. TickerQ has been upgraded from 2.5.3 to 10.1.1, which only targets .NET 10.0 and contains several API changes. + +Key changes include: + +- `UseAbpTickerQ` moved from `IApplicationBuilder` to `IHost` — use `context.GetHost().UseAbpTickerQ()` in your module +- Entity types renamed: `TimeTicker` → `TimeTickerEntity`, `CronTicker` → `CronTickerEntity` +- Scheduler and dashboard configuration APIs have changed +- New helpers: `context.GetHost()`, `GetWebApplication()`, `GetEndpointRouteBuilder()` + +> **Important:** Do **not** resolve `IHost` from `context.ServiceProvider.GetRequiredService()`. Always use `context.GetHost()`. See the [ABP Version 10.2 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/abp-10-2) for the complete list of changes. + +### AI Management: MCP (Model Context Protocol) Support + +_This is a **PRO** feature available for ABP Commercial customers._ + +The [AI Management Module](https://abp.io/docs/10.2/modules/ai-management) now supports [MCP (Model Context Protocol)](https://modelcontextprotocol.io/), enabling AI workspaces to use external MCP servers as tools. MCP allows AI models to interact with external services, databases, APIs, and more through a standardized protocol. + +![mcp-servers](mcp-servers.png) + +You can create and manage MCP servers via the AI Management UI. Each MCP server supports one of the following transport types: **Stdio** (runs a local command), **SSE** (Server-Sent Events), or **StreamableHttp**. For HTTP-based transports, you can configure authentication (API Key, Bearer token, or custom headers). Once MCP servers are defined, you can associate them with workspaces. When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations — tool calls and results are displayed in the chat interface. + +You can test the connection to an MCP server after creating it to verify connectivity and list available tools before use: + +![test-connection](test-connection.png) + +When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations. Tool calls and results are displayed in the chat interface. + +![chat-playground](chat-playground.png) + +> See the [AI Management documentation](https://abp.io/docs/10.2/modules/ai-management#mcp-servers) for details. + +### AI Management: RAG with File Upload + +_This is a **PRO** feature available for ABP Commercial customers._ + +The AI Management module supports **RAG (Retrieval-Augmented Generation)** with file upload, which enables workspaces to answer questions based on the content of uploaded documents. When RAG is configured, the AI model searches the uploaded documents for relevant information before generating a response. + +To enable RAG, configure an **embedder** (e.g., OpenAI, Ollama) and a **vector store** (e.g., PgVector) on the workspace: + +| Embedder | Vector Store | +| --- | --- | +| ![rag-embedder](rag-embedder.png) | ![rag-vector-store](rag-vector-store.png) | + +You can then upload documents (PDF, Markdown, or text files, max 10 MB) through the workspace management UI. Uploaded documents are automatically processed — their content is chunked, embedded, and stored in the configured vector store: + +![rag-file-upload](rag-file-upload.png) + +When you ask questions in the chat interface, the AI model uses the uploaded documents as context for accurate, grounded responses. + +> See the [AI Management — RAG with File Upload](https://abp.io/docs/10.2/modules/ai-management#rag-with-file-upload) documentation for configuration details. + +### AI Management: OpenAI-Compatible Chat Endpoint + +_This is a **PRO** feature available for ABP Commercial customers._ + +The AI Management module exposes an **OpenAI-compatible REST API** at the `/v1` path. This allows any application or tool that supports the OpenAI API format — such as [AnythingLLM](https://anythingllm.com/), [Open WebUI](https://openwebui.com/), [Dify](https://dify.ai/), or custom scripts using the OpenAI SDK — to connect directly to your AI Management instance. + +**Example configuration from AnythingLLM**: + +![anythingllm](ai-management-openai-anythingllm.png) + +Each AI Management **workspace** appears as a selectable model in the client application. The workspace's configured AI provider handles the actual inference transparently. Available endpoints include `/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/files`, and more. All endpoints require authentication via a Bearer token in the `Authorization` header. + +> See the [AI Management — OpenAI-Compatible API](https://abp.io/docs/10.2/modules/ai-management#openai-compatible-api) documentation for usage examples. + +### File Management: Resource-Based Authorization + +_This is a **PRO** feature available for ABP Commercial customers._ + +The **File Management Module** now supports **resource-based authorization**. You can control access to individual files and folders per user, role, or client. Permissions can be granted at the resource level via the UI, and the feature integrates with ABP's resource permission infrastructure. + +![file-management-resource-based-authorization](file-management-rba.png) + +This feature is **implemented for all three supported UIs: MVC/Razor Pages, Blazor, and Angular**, providing a consistent experience across your application regardless of the UI framework you use. + +### Other Improvements and Enhancements + +- **Angular signal APIs**: ABP Angular packages migrated to signal queries, output functions, and signal input functions for alignment with Angular 21 ([#24765](https://github.com/abpframework/abp/pull/24765), [#24766](https://github.com/abpframework/abp/pull/24766), [#24777](https://github.com/abpframework/abp/pull/24777)). +- **Angular Vitest**: ABP Angular templates now use Vitest as the default testing framework instead of Karma/Jasmine ([#24725](https://github.com/abpframework/abp/pull/24725)). +- **Ambient auditing**: Programmatic disable/enable of auditing via `IAuditingHelper.DisableAuditing()` and `IsAuditingEnabled()` ([#24718](https://github.com/abpframework/abp/pull/24718)). +- **Complex property auditing**: Entity History and ModifierId now support EF Core complex properties ([#24767](https://github.com/abpframework/abp/pull/24767)). +- **RabbitMQ correlation ID**: Correlation ID support added to RabbitMQ JobQueue for distributed tracing ([#24755](https://github.com/abpframework/abp/pull/24755)). +- **Concurrent config retrieval**: `MvcCachedApplicationConfigurationClient` now fetches configuration and localization concurrently for faster startup ([#24838](https://github.com/abpframework/abp/pull/24838)). +- **Environment localization fallback**: Angular can use `environment.defaultResourceName` when the backend does not provide it ([#24589](https://github.com/abpframework/abp/pull/24589)). +- **JS proxy namespace fix**: Resolved namespace mismatch for multi-segment company names in generated proxies ([#24877](https://github.com/abpframework/abp/pull/24877)). +- **Audit Logging max length**: Entity/property type full names increased to 512 characters to reduce truncation ([#24846](https://github.com/abpframework/abp/pull/24846)). +- **AI guidelines**: Cursor and Copilot AI guideline documents added for ABP development ([#24563](https://github.com/abpframework/abp/pull/24563), [#24593](https://github.com/abpframework/abp/pull/24593)). + +## Community News + +### New ABP Community Articles + +As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: + +- [Enis Necipoğlu](https://abp.io/community/members/enisn) has published 2 new posts: + - [ABP Framework's Hidden Magic: Things That Just Work Without You Knowing](https://abp.io/community/articles/hidden-magic-things-that-just-work-without-you-knowing-vw6osmyt) + - [Implementing Multiple Global Query Filters with Entity Framework Core](https://abp.io/community/articles/implementing-multiple-global-query-filters-with-entity-ugnsmf6i) +- [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa) has published 2 new posts: + - [.NET 11 Preview 1 Highlights: Faster Runtime, Smarter JIT, and AI-Ready Improvements](https://abp.io/community/articles/dotnet-11-preview-1-highlights-hspp3o5x) + - [TOON vs JSON for LLM Prompts in ABP: Token-Efficient Structured Context](https://abp.io/community/articles/toon-vs-json-b4rn2avd) +- [Fahri Gedik](https://abp.io/community/members/fahrigedik) has published 2 new posts: + - [Building a Multi-Agent AI System with A2A, MCP, and ADK in .NET](https://abp.io/community/articles/building-a-multiagent-ai-system-with-a2a-mcp-iefdehyx) + - [Async Chain of Persistence Pattern: Designing for Failure in Event-Driven Systems](https://abp.io/community/articles/async-chain-of-persistence-pattern-wzjuy4gl) +- [Alper Ebiçoğlu](https://abp.io/community/members/alper) has published 2 new posts: + - [NDC London 2026: From a Developer's Perspective and My Personal Notes about AI](https://abp.io/community/articles/ndc-london-2026-a-.net-conf-from-a-developers-perspective-07wp50yl) + - [Which Open-Source PDF Libraries Are Recently Popular? A Data-Driven Look At PDF Topic](https://abp.io/community/articles/which-opensource-pdf-libraries-are-recently-popular-a-g68q78it) +- [Stop Spam and Toxic Users in Your App with AI](https://abp.io/community/articles/stop-spam-and-toxic-users-in-your-app-with-ai-3i0xxh0y) by [Engincan Veske](https://abp.io/community/members/EngincanV) +- [How AI Is Changing Developers](https://abp.io/community/articles/how-ai-is-changing-developers-e8y4a85f) by [Liming Ma](https://abp.io/community/members/maliming) +- [JetBrains State of Developer Ecosystem Report 2025 — Key Insights](https://abp.io/community/articles/jetbrains-state-of-developer-ecosystem-report-2025-key-z0638q5e) by [Tarık Özdemir](https://abp.io/community/members/mtozdemir) +- [Integrating AI into ABP.IO Applications: The Complete Guide to Volo.Abp.AI and AI Management Module](https://abp.io/community/articles/integrating-ai-into-abp.io-applications-the-complete-guide-jc9fbjq0) by [Adnan Ali](https://abp.io/community/members/adnanaldaim) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.2/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.2 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/ai-management-openai-anythingllm.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/ai-management-openai-anythingllm.png new file mode 100644 index 0000000000..b8b8fe109b Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/ai-management-openai-anythingllm.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/chat-playground.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/chat-playground.png new file mode 100644 index 0000000000..e1ab32f314 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/chat-playground.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/cover-image.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/cover-image.png new file mode 100644 index 0000000000..f4bf16c1d3 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/file-management-rba.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/file-management-rba.png new file mode 100644 index 0000000000..46e5506f17 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/file-management-rba.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/mcp-servers.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/mcp-servers.png new file mode 100644 index 0000000000..cbb93403d1 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/mcp-servers.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-embedder.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-embedder.png new file mode 100644 index 0000000000..378ba8f4e5 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-embedder.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-file-upload.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-file-upload.png new file mode 100644 index 0000000000..c4d4391a15 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-file-upload.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-vector-store.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-vector-store.png new file mode 100644 index 0000000000..83d38aeb2a Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-vector-store.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/studio-switch-to-preview.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/studio-switch-to-preview.png new file mode 100644 index 0000000000..62fd4d165e Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/studio-switch-to-preview.png differ diff --git a/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/test-connection.png b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/test-connection.png new file mode 100644 index 0000000000..3a92ccd9f2 Binary files /dev/null and b/docs/en/Blog-Posts/2026-02-25 v10_2_Preview/test-connection.png differ diff --git a/docs/en/Community-Articles/2026-03-09-Automate-Localhost-Access-for-Expo/POST.md b/docs/en/Community-Articles/2026-03-09-Automate-Localhost-Access-for-Expo/POST.md new file mode 100644 index 0000000000..cb5214a675 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-09-Automate-Localhost-Access-for-Expo/POST.md @@ -0,0 +1,227 @@ +# Automate Localhost Access for Expo: A Guide to Dynamic Cloudflare Tunnels & Dev Builds + +Every mobile developer eventually hits the "Localhost Wall." You have built a brilliant API on your machine, and your React Native app works perfectly in the iOS Simulator or Android Emulator. But the moment you pick up a physical device to test real-world performance or camera features, everything breaks. + +### The Problem: Why Your Phone Can’t See localhost + +When you run a backend server on your computer, localhost refers to the "loopback" address and essentially, the computer talking to itself. Your physical iPhone or Android device is a separate node on the network. From its perspective, localhost is itself, not your development machine. Without a direct bridge, your mobile app is shouting into a void, unable to reach the API sitting just inches away on your desk. + +### The Conflict: The Fragility of Local IP Addresses + +The traditional workaround is to find the local IP address of your device and hardcode it into your app. However, this approach has many obstacles that make it difficult to use: + +- **Network Volatility:** Your router might assign you a new IP address tomorrow, forcing you to update your code constantly. +- **The SSL Headache:** Modern mobile operating systems and many OAuth providers (like Google or Auth0) strictly require **HTTPS**. Running a local development server with valid SSL certificates is a notorious configuration nightmare. +- **Broken OAuth flows:** Most authentication providers refuse to redirect to a non-secure `http` address or a random local IP, effectively locking you out of testing login features on a real device. + +### The Solution: Cloudflare Tunnel as a Secure Bridge + +This is where **Cloudflare Tunnel** changes the game. Instead of poking holes in your firewall or wrestling with self-signed certificates, Cloudflare Tunnel creates a secure, outbound-only connection between your local machine and the Cloudflare edge. + +It provides you with a **public, HTTPS-enabled URL** (e.g., `https://random-word.trycloudflare.com`) that automatically points to your local port. To your mobile device, your local backend looks like a standard, secure production API. It bypasses network restrictions, satisfies SSL requirements, and—when paired with a simple automation script—makes "localhost" development on physical devices completely seamless. + +### 1. Architecture Overview + +In order to understand why this setup is so effective, it is better to visualize the data flow. Traditionally, your mobile device would try to ping your laptop directly over Wi-Fi that is often blocked by firewalls or complicated by internal IP routing. + +#### Workflow Summary: The Secure "Middleman" + +The Cloudflare Tunnel acts as a persistent, encrypted bridge between your local environment and the public internet. Here is how the traffic flows in a standard development session: + +1. **The Connector:** You run a small `cloudflared` daemon on your development machine. It establishes an **outbound** connection to Cloudflare’s nearest edge server. Because it is outbound, you don't need to open any ports on your home or office router. +2. **The Public Endpoint:** Cloudflare provides a temporary, unique HTTPS URL (e.g., `https://example-tunnel.trycloudflare.com`). This URL is globally accessible. +3. **The Mobile Request:** Your React Native app that is running on a physical iPhone or Android sends an API request to that HTTPS URL. To the phone, this looks like any other secure production website. +4. **The Local Handoff:** Cloudflare receives the request and "tunnels" it down the active connection to your machine. The `cloudflared` tool then forwards that request to your local backend whether it's running on `.NET` at port `44358`, `Node.js` at `3000`, or `Rails` at `3000`. +5. **The Response:** Your backend processes the request and sends the data back through the same tunnel to the phone. + +By sitting in the middle, Cloudflare handles the **SSL termination** and the **Global Routing**, ensuring your backend is reachable regardless of whether your phone is on the same Wi-Fi as your laptop. + +### 2. Prerequisites + +Before we bridge the gap between your mobile device and your local machine, ensure your development environment is equipped with the following core components. + +To follow this guide, you will need: + +- **Node.js & Package Manager:** A stable version of Node.js (LTS recommended) and either **npm** or **yarn** to manage dependencies and run the automation scripts. +- **Expo CLI:** Ensure you have the latest version of `expo` installed globally or within your project. We will be using this to manage the development server and build the application. +- **Cloudflared CLI:** This is the critical "connector" tool from Cloudflare. You’ll need it installed on your local machine to establish the tunnel. + - *Quick Tip:* You don't need a paid Cloudflare account; the **Quick Tunnels** used in this guide are free and require no login. +- **A Running Backend API:** Your local server (e.g., .NET, Node.js, Django, or Rails) should be active and listening on a specific port (like `44358` or `3000`). + +### 3. Step-by-Step Implementation + +Now, let’s configure the automation that makes this workflow "set it and forget it." + +#### Phase A: Backend Configuration (The OAuth Handshake) + +Modern mobile authentication often relies on **OAuth 2.0** or **OpenID Connect**. For the login flow to succeed, your backend must "trust" the redirect URI sent by the mobile app. ABP applications are an example for such handshake. + +Even though we are using a Cloudflare URL for the API calls, the `auth-session` of Expo typically generates a `localhost` redirect for development. You must update your backend configuration (e.g., `appsettings.json` in a .NET TemplateTwo setup) to allow this: + +**File:** `src/YourProject.DbMigrator/appsettings.json` + +```json +{ + "OpenIddict": { + "Applications": { + "Mobile_App": { + "ClientId": "Mobile_App", + "RootUrl": "exp://localhost:19000" + } + } + } +} +``` + +**Note:** By setting the `RootUrl` to `exp://localhost:19000`, you ensure that once the user authenticates via the tunnel's secure page, the mobile OS knows exactly how to hand the token back to your running Expo instance. + +#### Phase B: The "Magic" Script (Automating the Tunnel) + +The primary headache with free Cloudflare Tunnels is that they generate a **random URL** every time you restart the service. Manually copying `https://shiny-new-url.trycloudflare.com` into your frontend code every morning is a productivity killer. + +We solve this with a **Node.js automation script** that launches the tunnel, "listens" to the terminal output to find the new URL, and automatically injects it into your project's configuration. + +**File:** `react-native/scripts/tunnel.js` + +```js +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// Target files for automation +const tunnelConfigFile = path.join(__dirname, '..', 'tunnel-config.json'); +const environmentFile = path.join(__dirname, '..', 'Environment.ts'); + +// 1. Launch the Cloudflare Tunnel pointing to your local API port +const cloudflared = spawn('cloudflared', ['tunnel', '--url', 'http://localhost:44358']); + +let domainCaptured = false; + +cloudflared.stdout.on('data', data => { + const output = data.toString(); + console.log(output); // Keep logs visible for debugging + + if (!domainCaptured) { + // 2. Regex to catch the dynamic "trycloudflare" URL + const urlMatch = output.match(/https:\/\/([a-z0-9-]+\.trycloudflare\.com)/); + if (urlMatch) { + const domain = urlMatch[1]; + + // 3. Save to a JSON file for the app to read + fs.writeFileSync(tunnelConfigFile, JSON.stringify({ domain }, null, 2)); + + // 4. Update the fallback value in Environment.ts directly + let envContent = fs.readFileSync(environmentFile, 'utf8'); + envContent = envContent.replace( + /let tunnelDomain = '[^']*'; \/\/ fallback/, + `let tunnelDomain = '${domain}'; // fallback`, + ); + fs.writeFileSync(environmentFile, envContent, 'utf8'); + + console.log(`\n✅ Tunnel Synchronized: ${domain}`); + domainCaptured = true; + } + } +}); +``` + +By capturing the trycloudflare.com domain programmatically, we treat the tunnel like a dynamic environment variable. This ensures that your mobile app, your backend OAuth settings, and your API client stay in perfect sync without a single keystroke from you. + +#### Phase C: Environment Integration + +To make this work within your React Native code, your `Environment.ts` file needs to be "smart" enough to look for the generated config file. We use a `try/catch` block so the app doesn't crash if the tunnel isn't running. + +**File:** `react-native/Environment.ts` + +```tsx +let tunnelDomain = 'your-default-fallback.com'; // fallback + +try { + // Pull the latest domain from the script's output + const tunnelConfig = require('./tunnel-config.json'); + if (tunnelConfig?.domain) { + tunnelDomain = tunnelConfig.domain; + } +} catch (e) { + console.warn('⚠️ No active tunnel config found. Using fallback.'); +} + +const apiUrl = `https://${tunnelDomain}`; + +export const getEnvVars = () => { + return { + apiUrl, + // Other environment variables... + }; +}; +``` + +This setup creates a **"Single Source of Truth."** When you run the script, it updates `tunnel-config.json`, and your app instantly points to the correct secure endpoint. + +### 4. Integration with Expo Development Builds + +While you can technically use the standard **Expo Go** app for basic API testing, professional React Native workflows, especially those involving secure authentication and custom networking, rely on **Expo Development Builds**. + +#### Why Development Builds are Essential for This Workflow + +Standard Expo Go is a "one-size-fits-all" sandbox. However, as your app grows, it needs to behave more like a real, standalone binary. Development Builds are preferred for two main reasons: + +- **Custom URL Schemes:** For OAuth flows (like the one configured in Phase A), your app needs to handle specific deep links (e.g., `myapp://`). Expo Go has its own internal URL handling that can sometimes conflict with complex redirect logic. A Development Build allows you to define your own scheme, ensuring the Cloudflare-tunneled backend knows exactly where to send the user back after login. +- **Native Dependency Control:** If your app uses native modules for secure storage, biometrics, or advanced networking, Expo Go won't support them. A Development Build includes your project's specific native code while still giving you the "hot reloading" developer experience of Expo. + +#### Configuring the Build for Tunnelling + +To ensure your development build is ready for the Cloudflare tunnel, you'll typically use the `expo-dev-client` package. This transforms your app into a powerful developer tool that can switch between different local or tunneled environments on the fly. + +> **Pro Tip:** When you run `npx expo start`, your Development Build will look for the `apiUrl` we configured in `Environment.ts`. Since our script has already injected the Cloudflare URL, the physical device will connect to your local backend through the tunnel the moment the app loads. + +### 5. Execution Workflow + +To get your entire stack synchronized, follow this specific launch order. This ensures the tunnel is active and the configuration files are updated before the React Native app attempts to read them. + +#### Step 1: Start the Backend + +Fire up your API (e.g., `.NET`, `Node`, `Go`). Ensure it is listening on the port defined in your `tunnel.js` (e.g., `44358`). + +#### Step 2: Launch the Tunnel + +In a new terminal, run your automation script. + +Wait for the message: `✅ Tunnel Synchronized`. This confirms `tunnel-config.json` has been updated with the new `trycloudflare.com` domain. + +#### Step 3: Start Expo + +Finally, start your Expo development server: + +```bash +npx expo start +``` + +Open the app on your physical device by scanning the QR code. Your app is now communicating with your local machine over a secure, global HTTPS bridge. + +### 6. Troubleshooting & Best Practices + +Even with automation, networking can be finicky. If your app isn't reaching the API, check these common roadblocks: + +#### Common Pitfalls + +- **Port Mismatches:** Ensure the port in your `tunnel.js` script (e.g., `44358`) exactly matches the port your backend is listening on. If your backend uses HTTPS locally, ensure the tunnel command reflects that (e.g., `https://localhost:port`). +- **Firewall & Ghost Processes:** Sometimes a previous `cloudflared` process hangs in the background. If you can't start a new tunnel, kill existing processes or check if your local firewall is blocking `cloudflared` from making outbound connections. +- **Expired Sessions:** Free "Quick Tunnels" are temporary. If you leave your computer on overnight, the tunnel might disconnect. Simply restart the script to generate a fresh, synced URL. + +#### Security Note + +Cloudflare Tunnels create a **publicly accessible URL**. While the random strings in `trycloudflare.com` provide "security through obscurity," anyone with that link can hit your local API. + +- **Development Data Only:** Never use this setup with production databases or sensitive PII (Personally Identifiable Information). +- **Disable When Idle:** Close the tunnel terminal when you aren't actively developing to shut the "bridge" to your machine. + +### 7. Conclusion & Future-Proofing + +By replacing hardcoded local IPs with a dynamic Cloudflare Tunnel, you’ve transformed a clunky, manual process into a **"Set it and forget it"** workflow. You no longer have to worry about shifting Wi-Fi addresses or SSL certificate errors on physical devices. Your development environment now mirrors the behavior of a production app, providing more accurate testing and faster debugging. + +#### The Road to Production: EAS + +This tunneling strategy is the perfect companion for **EAS (Expo Application Services)**. As you move toward testing internal distributions, you can use these same environment patterns to point your EAS-built binaries to various staging or development endpoints. + +With a secure bridge and an automated config, you are no longer tethered to a simulator. Grab your phone, head to a coffee shop, and keep building—your backend is now globally (and securely) following you. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/POST.md b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/POST.md new file mode 100644 index 0000000000..555a74a7d7 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/POST.md @@ -0,0 +1,201 @@ +# Resource-Based Authorization in ABP Framework + +ABP has a built-in permission system that supports role-based access control (RBAC). You define permissions, assign them to roles, and assign roles to users — once a user logs in, they automatically have the corresponding access. This covers the vast majority of real-world scenarios and is simple, straightforward, and easy to maintain. + +However, there is one class of requirements it cannot handle: **different access rights for different instances of the same resource type**. + +Take a bookstore application as an example. You define a `Books.Edit` permission and assign it to an editor role, so every editor can modify every book. But reality is often more nuanced: + +- A specific book should only be editable by its assigned editor +- Certain books are only visible to specific users +- Different users have different levels of access to the same book + +Standard permissions cannot address this, because their granularity is the *permission type*, not a *specific record*. The traditional approach requires designing your own database tables, writing query logic, and building a management UI from scratch — all of which is costly. + +ABP Framework now ships with **Resource-Based Authorization** to solve exactly this problem. The core idea is to bind permissions to specific resource instances rather than just resource types. For example, you can grant a user permission to edit the price of *1984* specifically, while they have no access to any other book. + +More importantly, the entire permission management workflow is handled through a built-in UI dialog — **no custom code needed for the management side**. + +## How It Works + +Each resource instance (e.g. a book) can have its own permission management dialog. Users who hold the `ManagePermissions` permission can open it and grant or revoke access for users, roles, or OAuth clients — all from the UI. + +A **Permissions** action appears in each book's action menu: + +![book-list](./book-list.png) + +Clicking it opens the resource permission management dialog for that specific book. You can see who currently has access and click **Add permission** to grant more: + +![resource-permission-dialog](./resource-permission-dialog.png) + +The **Add permission** dialog lets you select a user, role, or OAuth client, then choose which permissions to grant: + +![add-permission-dialog](./add-permission-dialog.png) + +After saving, the new entry appears in the list immediately. + +Each entry in the list also supports **Edit** and **Delete** actions. Clicking **Edit** opens the update dialog where you can adjust the granted permissions: + +![update-permission-dialog](./update-permission-dialog.png) + +Clicking **Delete** shows a confirmation prompt — confirming removes all permissions for that user, role, or OAuth client on this book: + +![delete-permission-confirm](./delete-permission-confirm.png) + +## Setting It Up + +To get this working, you need to define your resource permissions and wire up the dialog. + +### Defining Resource Permissions + +```csharp +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string ManagePermissions = Default + ".ManagePermissions"; + + public static class Resources + { + public const string Name = "Acme.BookStore.Books.Book"; + public const string View = Name + ".View"; + public const string Edit = Name + ".Edit"; + public const string Delete = Name + ".Delete"; + } + } +} +``` + +```csharp +public override void Define(IPermissionDefinitionContext context) +{ + var group = context.AddGroup(BookStorePermissions.GroupName); + + var bookPermission = group.AddPermission(BookStorePermissions.Books.Default); + + // Users with this permission can open the resource permission dialog + bookPermission.AddChild(BookStorePermissions.Books.ManagePermissions); + + context.AddResourcePermission( + name: BookStorePermissions.Books.Resources.View, + resourceName: BookStorePermissions.Books.Resources.Name, + managementPermissionName: BookStorePermissions.Books.ManagePermissions + ); + + context.AddResourcePermission( + name: BookStorePermissions.Books.Resources.Edit, + resourceName: BookStorePermissions.Books.Resources.Name, + managementPermissionName: BookStorePermissions.Books.ManagePermissions + ); + + context.AddResourcePermission( + name: BookStorePermissions.Books.Resources.Delete, + resourceName: BookStorePermissions.Books.Resources.Name, + managementPermissionName: BookStorePermissions.Books.ManagePermissions + ); +} +``` + +The `managementPermissionName` acts as a gate: only users who hold `ManagePermissions` will see the resource permission dialog for a book. + +### Wiring Up the Dialog (MVC) + +Add the required script to your page and open the dialog using `abp.ModalManager`: + +```html +@section scripts +{ + + +} +``` + +```javascript +var _permissionsModal = new abp.ModalManager({ + viewUrl: abp.appPath + 'AbpPermissionManagement/ResourcePermissionManagementModal', + modalClass: 'ResourcePermissionManagement' +}); + +function openPermissionsModal(bookId, bookName) { + _permissionsModal.open({ + resourceName: 'Acme.BookStore.Books.Book', + resourceKey: bookId, + resourceDisplayName: bookName + }); +} +``` + +> For Blazor and Angular applications, ABP provides the equivalent `ResourcePermissionManagementModal` component and `ResourcePermissionManagementComponent`. See the [Permission Management Module](https://abp.io/docs/latest/modules/permission-management) documentation for details. + +## Checking Permissions in Code + +The UI manages the permission assignments; the code enforces them at runtime. In your application service, use `AuthorizationService.CheckAsync` to verify that the current user holds a specific permission on a given resource instance. + +All ABP entities implement `IKeyedObject`, which the framework uses to extract the resource key automatically — so you can pass the entity object directly without building the key manually: + +```csharp +public virtual async Task GetAsync(Guid id) +{ + var book = await _bookRepository.GetAsync(id); + + // Throws AbpAuthorizationException if the current user has no View permission on this book + await AuthorizationService.CheckAsync(book, BookStorePermissions.Books.Resources.View); + + return ObjectMapper.Map(book); +} + +public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) +{ + var book = await _bookRepository.GetAsync(id); + + await AuthorizationService.CheckAsync(book, BookStorePermissions.Books.Resources.Edit); + + book.Name = input.Name; + await _bookRepository.UpdateAsync(book); + + return ObjectMapper.Map(book); +} +``` + +If you want to check a permission without throwing an exception — for example, to conditionally show or hide a button — use `IsGrantedAsync` instead, which returns a `bool`: + +```csharp +var canEdit = await AuthorizationService.IsGrantedAsync(book, BookStorePermissions.Books.Resources.Edit); +``` + +## Don't Forget to Clean Up + +Every resource permission grant is stored as a record in the database. When a book is deleted, those records are not removed automatically — orphaned permission data accumulates over time. + +Make sure to clean up resource permissions whenever a resource is deleted: + +```csharp +public virtual async Task DeleteAsync(Guid id) +{ + await _bookRepository.DeleteAsync(id); + + // Clean up all resource permissions for this book + await _resourcePermissionManager.DeleteAsync( + resourceName: BookStorePermissions.Books.Resources.Name, + resourceKey: id.ToString() + ); +} +``` + +## Summary + +Resource-Based Authorization fills the gap between "everyone can do this" and "only specific users can do this on specific resources." In practice, most of the work comes down to two things: + +- Define resource permissions and wire up the built-in UI dialog so administrators can assign access through the interface +- Call `AuthorizationService.CheckAsync` in your application services to enforce those permissions at runtime + +Storing permission grants, rendering the dialog, searching for users, roles, and OAuth clients — ABP handles all of that for you. + +## References + +- [Resource-Based Authorization](https://abp.io/docs/latest/framework/fundamentals/authorization/resource-based-authorization) +- [Authorization](https://abp.io/docs/latest/framework/fundamentals/authorization) +- [Permission Management Module](https://abp.io/docs/latest/modules/permission-management) diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/add-permission-dialog.png b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/add-permission-dialog.png new file mode 100644 index 0000000000..f12d69a66e Binary files /dev/null and b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/add-permission-dialog.png differ diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/book-list.png b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/book-list.png new file mode 100644 index 0000000000..2c65b597c8 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/book-list.png differ diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/cover.jpeg b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/cover.jpeg new file mode 100644 index 0000000000..831dfe8718 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/cover.jpeg differ diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/delete-permission-confirm.png b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/delete-permission-confirm.png new file mode 100644 index 0000000000..646be2897a Binary files /dev/null and b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/delete-permission-confirm.png differ diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/resource-permission-dialog.png b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/resource-permission-dialog.png new file mode 100644 index 0000000000..73fae47b7d Binary files /dev/null and b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/resource-permission-dialog.png differ diff --git a/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/update-permission-dialog.png b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/update-permission-dialog.png new file mode 100644 index 0000000000..041c16d538 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/update-permission-dialog.png differ diff --git a/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md b/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md new file mode 100644 index 0000000000..d3a851247f --- /dev/null +++ b/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md @@ -0,0 +1,314 @@ +# Operation Rate Limiting in ABP + +Almost every user-facing system eventually runs into the same problem: **some operations cannot be allowed to run without limits**. + +Sometimes it's a cost issue — sending an SMS costs money, and generating a report hammers the database. Sometimes it's security — a login endpoint with no attempt limit is an open invitation for brute-force attacks. And sometimes it's a matter of fairness — your paid plan says "up to 100 data exports per month," and you need to actually enforce that. + +What all these cases have in common is that the thing being limited isn't an HTTP request — it's a *business operation*, performed by a specific *who*, doing a specific *what*, against a specific *resource*. + +ASP.NET Core ships with a built-in [rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) that sits in the HTTP pipeline. It's excellent for broad API protection — throttling requests per IP to fend off bots or DDoS traffic. But it only sees HTTP requests. It can tell you how many requests came from an IP address; it cannot tell you: + +- **"How many verification codes has this phone number received today?"** The moment the user switches networks, the counter resets — completely useless +- **"How many reports has this user exported today?"** Switching from mobile to desktop gives them a fresh counter +- **"How many times has someone tried to log in as `alice`?"** An attacker rotating through dozens of IPs will never hit the per-IP limit + +There's another gap: some rate-limiting logic has no corresponding HTTP endpoint at all — it lives inside an application service method called by multiple endpoints, or triggered by a background job. HTTP middleware has no place to hook in. + +Real-world requirements tend to look like this: + +- The same phone number can receive at most 3 verification codes per hour, regardless of which device or IP the request comes from +- Each user can generate at most 2 monthly sales reports per day, because a single report query scans millions of records +- Login attempts are limited to 5 failures per username per 5 minutes, *and* 20 failures per IP per hour — two independent counters, both enforced simultaneously +- Free-tier users get 50 AI calls per month, paid users get 500 — this is a product-defined quota, not a security measure +- Your system integrates with an LLM provider (OpenAI, Azure OpenAI, etc.) where every call has a real dollar cost. Without per-user or per-tenant limits, a single user can exhaust your monthly budget overnight + +The pattern is clear: the identity being throttled is a **business identity** — a user, a phone number, a resource ID — not an IP address. And the action being throttled is a **business operation**, not an HTTP request. + +ABP's **Operation Rate Limiting** module is built for exactly this. It lets you enforce limits directly in your application or domain layer, with full awareness of who is doing what. + +This module is used by the Account (Pro) modules internally and comes pre-installed in the latest startup templates. You must have an [ABP Team or a higher license](https://abp.io/pricing) to use this module. + +## Defining a Policy + +The model is straightforward: define a named policy in `ConfigureServices`, then call `CheckAsync` wherever you need to enforce it. + +Name your policies after the business action they protect — `"SendSmsCode"`, `"GenerateReport"`, `"CallAI"`. A clear name makes the intent obvious at the call site, and avoids the mystery of something like `"policy1"`. + +```csharp +Configure(options => +{ + options.AddPolicy("SendSmsCode", policy => + { + policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByParameter(); + }); +}); +``` + +- `WithFixedWindow` sets the time window and maximum count — here, at most 1 call per minute +- `PartitionByParameter` means each distinct value you pass at call time (such as a phone number) gets its own independent counter + +Then inject `IOperationRateLimitingChecker` and call `CheckAsync` at the top of the method you want to protect: + +```csharp +public class SmsAppService : ApplicationService +{ + private readonly IOperationRateLimitingChecker _rateLimitChecker; + + public SmsAppService(IOperationRateLimitingChecker rateLimitChecker) + { + _rateLimitChecker = rateLimitChecker; + } + + public virtual async Task SendCodeAsync(string phoneNumber) + { + await _rateLimitChecker.CheckAsync("SendSmsCode", phoneNumber); + + // Limit not exceeded — proceed with sending the SMS + } +} +``` + +`CheckAsync` checks the current usage against the limit and throws `AbpOperationRateLimitingException` (HTTP 429) if the limit is already exceeded. If the check passes, it then increments the counter and proceeds. ABP's exception pipeline catches this automatically and returns a standard error response. Put `CheckAsync` first — the rate limit check is the gate, and everything else only runs if it passes. + +## Declarative Usage with `[OperationRateLimiting]` + +The explicit `CheckAsync` approach is useful when you need fine-grained control — for example, when you want to check the limit conditionally, or when the parameter value comes from somewhere other than a method argument. But for the common case where you simply want to enforce a policy on every invocation of a specific method, there's a cleaner way: the `[OperationRateLimiting]` attribute. + +```csharp +public class SmsAppService : ApplicationService +{ + [OperationRateLimiting("SendSmsCode")] + public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber) + { + // Rate limit is enforced automatically — no manual CheckAsync needed. + await _smsSender.SendAsync(phoneNumber, GenerateCode()); + } +} +``` + +The attribute works on both **Application Service methods** (via ABP's interceptor) and **MVC Controller actions** (via an action filter). No manual injection of `IOperationRateLimitingChecker` required. + +### Providing the Partition Key + +When using the attribute, the partition key is resolved from the method's parameters automatically: + +- Mark a parameter with `[RateLimitingParameter]` to use its `ToString()` value as the key — this is the most common case when the key is a single primitive like a phone number or email. +- Have your input DTO implement `IHasOperationRateLimitingParameter` and provide a `GetPartitionParameter()` method — useful when the key is a property buried inside a complex input object. + +```csharp +public class SendSmsCodeInput : IHasOperationRateLimitingParameter +{ + public string PhoneNumber { get; set; } + public string Language { get; set; } + + public string? GetPartitionParameter() => PhoneNumber; +} + +[OperationRateLimiting("SendSmsCode")] +public virtual async Task SendCodeAsync(SendSmsCodeInput input) +{ + // input.GetPartitionParameter() = input.PhoneNumber is used as the partition key. +} +``` + +If neither is provided, `Parameter` is `null` — which is perfectly valid for policies that use `PartitionByCurrentUser`, `PartitionByClientIp`, or similar partition types that don't rely on an explicit value. + +```csharp +// Policy uses PartitionByCurrentUser — no partition key needed. +[OperationRateLimiting("GenerateReport")] +public virtual async Task GenerateMonthlyReportAsync() +{ + // Rate limit is checked per current user, automatically. +} +``` + +> The resolution order is: `[RateLimitingParameter]` first, then `IHasOperationRateLimitingParameter`, then `null`. If the method has parameters but none is resolved, a warning is logged to help you catch the misconfiguration early. + +You can also place `[OperationRateLimiting]` on the class itself to apply the policy to all public methods: + +```csharp +[OperationRateLimiting("MyServiceLimit")] +public class MyAppService : ApplicationService +{ + public virtual async Task MethodAAsync([RateLimitingParameter] string key) { ... } + + public virtual async Task MethodBAsync([RateLimitingParameter] string key) { ... } +} +``` + +A method-level attribute always takes precedence over the class-level one. + +## Choosing a Partition Type + +The partition type controls **how counters are isolated from each other** — it's the most important decision when setting up a policy, because it determines *what dimension you're counting across*. + +Getting this wrong can make your rate limiting completely ineffective. Using `PartitionByClientIp` for SMS verification? An attacker just needs to switch networks. Using `PartitionByCurrentUser` for a login endpoint? There's no current user before login, so the counter has nowhere to land. + +- **`PartitionByParameter`** — uses the value you explicitly pass as the partition key. This is the most flexible option. Pass a phone number, an email address, a resource ID, or any business identifier you have at hand. It's the right choice whenever you know exactly what the "who" is. +- **`PartitionByCurrentUser`** — uses the authenticated user's ID, with no value to pass. Perfect for "each user gets N per day" scenarios where user identity is all you need. +- **`PartitionByClientIp`** — uses the client's IP address. Don't rely on this alone — it's too easy to rotate. Use it as a secondary layer alongside another partition type, as in the login example below. +- **`PartitionByEmail`** and **`PartitionByPhoneNumber`** — designed for pre-authentication flows where the user isn't logged in yet. They prefer the `Parameter` value you explicitly pass, and fall back to the current user's email or phone number if none is provided. +- **`PartitionBy`** — a named custom resolver that can produce any partition key you need. Register a resolver function under a unique name via `options.AddPartitionKeyResolver("MyResolver", ctx => ...)`, then reference it by name: `.PartitionBy("MyResolver")`. You can also register and reference in one step: `.PartitionBy("MyResolver", ctx => ...)`. When the built-in options don't fit, you're free to implement whatever logic makes sense: look up a resource's owner in the database, derive a key from the user's subscription tier, partition by tenant — anything that returns a string. Because the resolver is stored by name (not as an anonymous delegate), it can be serialized and managed from a UI or database. + +> The rule of thumb: partition by the identity of whoever's behavior you're trying to limit. + +## Combining Rules in One Policy + +A single rule covers most cases, but sometimes you need to enforce limits across multiple dimensions simultaneously. Login protection is the textbook example: throttling by username alone doesn't stop an attacker from targeting many accounts; throttling by IP alone doesn't stop an attacker with a botnet. You need both, at the same time. + +```csharp +options.AddPolicy("Login", policy => +{ + // Rule 1: at most 5 attempts per username per 5-minute window + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 5) + .PartitionByParameter()); + + // Rule 2: at most 20 attempts per IP per hour, counted independently + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20) + .PartitionByClientIp()); +}); +``` + +The two counters are completely independent. If `alice` fails 5 times, her account is locked — but other accounts from the same IP are unaffected. If an IP accumulates 20 failures, it's blocked — but `alice` can still be targeted from other IPs until their own counters fill up. + +When multiple rules are present, the module uses a two-phase approach: it checks all rules first, and only increments counters if every rule passes. This prevents a rule from consuming quota on a request that would have been rejected by another rule anyway. + +## Customizing Policies from Reusable Modules + +ABP modules (including your own) can ship with built-in rate limiting policies. For example, an Account module might define a `"Account.SendPasswordResetCode"` policy with conservative defaults that make sense for most applications. When you need different rules in your specific application, you have two options. + +**Complete replacement with `AddPolicy`:** call `AddPolicy` with the same name and the second registration wins, replacing all rules from the module: + +```csharp +Configure(options => +{ + options.AddPolicy("Account.SendPasswordResetCode", policy => + { + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3) + .PartitionByEmail()); + }); +}); +``` + +**Partial modification with `ConfigurePolicy`:** when you only want to tweak part of a policy — change the error code, add a secondary rule, or tighten the window — use `ConfigurePolicy`. The builder starts pre-populated with the module's existing rules, so you only express what changes. + +For example, keep the module's default rules but assign your own localized error code: + +```csharp +Configure(options => +{ + options.ConfigurePolicy("Account.SendPasswordResetCode", policy => + { + policy.WithErrorCode("MyApp:PasswordResetLimit"); + }); +}); +``` + +Or add a secondary IP-based rule on top of what the module already defined, without touching it: + +```csharp +Configure(options => +{ + options.ConfigurePolicy("Account.SendPasswordResetCode", policy => + { + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20) + .PartitionByClientIp()); + }); +}); +``` + +If you want a clean slate, call `ClearRules()` first and then define entirely new rules — this gives you the same result as `AddPolicy` but makes the intent explicit: + +```csharp +Configure(options => +{ + options.ConfigurePolicy("Account.SendPasswordResetCode", policy => + { + policy.ClearRules() + .WithFixedWindow(TimeSpan.FromMinutes(10), maxCount: 5) + .PartitionByEmail(); + }); +}); +``` + +`ConfigurePolicy` throws if the policy name doesn't exist — which catches typos at startup rather than silently doing nothing. + +The general rule: use `AddPolicy` for full replacements, `ConfigurePolicy` for surgical modifications. + +## Beyond Just Checking + +Not every scenario calls for throwing an exception. `IOperationRateLimitingChecker` provides three additional methods for more nuanced control. + +**`IsAllowedAsync`** performs a read-only check — it returns `true` or `false` without touching any counter. The most common use case is UI pre-checking: when a user opens the "send verification code" page, check the limit first. If they've already hit it, disable the button and show a countdown immediately, rather than making them click and get an error. That's a meaningfully better experience. + +```csharp +var isAllowed = await _rateLimitChecker.IsAllowedAsync("SendSmsCode", phoneNumber); +``` + +**`GetStatusAsync`** also reads without incrementing, but returns richer data: `RemainingCount`, `RetryAfter`, and `CurrentCount`. This is what you need to build quota displays — "You have 2 exports remaining today" or "Please try again in 47 seconds" — which are far friendlier than a raw 429. + +```csharp +var status = await _rateLimitChecker.GetStatusAsync("SendSmsCode", phoneNumber); +// status.RemainingCount, status.RetryAfter, status.IsAllowed ... +``` + +**`ResetAsync`** clears the counter for a given policy and context. Useful in admin panels where support staff can manually unblock a user, or in test environments where you need to reset state between runs. + +```csharp +await _rateLimitChecker.ResetAsync("SendSmsCode", phoneNumber); +``` + +## When the Limit Is Hit + +When `CheckAsync` triggers, it throws `AbpOperationRateLimitingException`, which: + +- Inherits from `BusinessException` and maps to HTTP **429 Too Many Requests** +- Is handled automatically by ABP's exception pipeline +- Carries useful metadata: `RetryAfterSeconds`, `RemainingCount`, `MaxCount`, `CurrentCount` + +By default, the error code sent to the client is a generic one from the module. If you want each operation to produce its own localized message — "Too many verification code requests, please wait before trying again" instead of a generic error — assign a custom error code to the policy: + +```csharp +options.AddPolicy("SendSmsCode", policy => +{ + policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByParameter() + .WithErrorCode("App:SmsCodeLimit"); +}); +``` + +> For details on mapping error codes to localized messages, see [Exception Handling](https://abp.io/docs/latest/framework/fundamentals/exception-handling) in the ABP docs. + +## Turning It Off in Development + +Rate limiting and local development don't mix well. When you're iterating quickly and calling the same endpoint a dozen times to test something, getting blocked by a 429 every few seconds is genuinely painful. Disable the module in your development environment: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var hostEnvironment = context.Services.GetHostingEnvironment(); + + Configure(options => + { + if (hostEnvironment.IsDevelopment()) + { + options.IsEnabled = false; + } + }); +} +``` + +## Summary + +ABP's Operation Rate Limiting fills the gap that ASP.NET Core's HTTP middleware can't: rate limiting with real awareness of *who* is doing *what*. Define a named policy, pick a time window, a max count, and a partition type. Then either call `CheckAsync` explicitly, or just add `[OperationRateLimiting]` to your method and let the framework handle the rest. Counter storage, distributed locking, and exception handling are all taken care of. + +## References + +- [Operation Rate Limiting (Pro)](https://abp.io/docs/latest/modules/operation-rate-limiting) +- [ASP.NET Core Rate Limiting Middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) +- [Exception Handling](https://abp.io/docs/latest/framework/fundamentals/exception-handling) diff --git a/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/cover.jpeg b/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/cover.jpeg new file mode 100644 index 0000000000..c9deca9026 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/cover.jpeg differ diff --git a/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/article.md b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/article.md new file mode 100644 index 0000000000..43dee7c712 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/article.md @@ -0,0 +1,113 @@ +# Automatically Validate Your Documentation: How We Built a Tutorial Validator + +Writing a tutorial is difficult. Keeping technical documentation accurate over time is even harder. +If you maintain developer documentation, you probably know the problem: a tutorial that worked a few months ago can silently break after a framework update, dependency change, or a small missing line in a code snippet. +New developers follow the guide, encounter an error, and quickly lose trust in the documentation. +To solve this problem, we built the tutorial validator — an open-source AI-powered tutorial validator that automatically verifies whether a software tutorial actually works from start to finish. +Instead of manually reviewing documentation, the tutorial validator behaves like a real developer following your guide step by step. +It reads instructions, runs commands, writes files, executes the application, and verifies expected results. +We initially created it to automatically validate ABP Framework tutorials, then released it as an open-source tool so anyone can use it to test their own documentation. + + +![the tutorial validator Orchestrator](docs/images/image.png) + + +## The Problem: Broken Tutorials in Technical Documentation + +Many documentation issues are difficult to catch during normal reviews. +Common problems include: + +- A command assumes a file already exists + +- A code snippet misses a namespace or import + +- A tutorial step relies on hidden context + +- An endpoint is expected to respond but fails + +- A dependency version changed and breaks the project + + +Traditional proofreading tools only check grammar or wording. +**The tutorial validator focuses on execution correctness.** +It treats tutorials like testable workflows, ensuring that every step works exactly as written. + +## How the Tutorial Validator Works? + +the tutorial validator validates tutorials using a three-stage pipeline: + +1. **Analyst**: Scrapes tutorial pages and converts instructions into a structured test plan +2. **Executor**: Follows the plan step by step in a clean environment +3. **Reporter**: Produces a clear result summary and optional notifications + +![the tutorial validator Analyst](docs/images/image-1.png) + +It identifies commands, code edits, HTTP requests, and expected outcomes. +The key idea is simple: if a developer would need to do it, the validator does it too. +That includes running terminal commands, editing files, checking HTTP responses, and validating build outcomes. + +![the tutorial validator Executor](docs/images/image-2.png) + +## Why Automated Tutorial Validation Matters? + +The tutorial validator is designed for practical documentation quality, not just technical experimentation. + +- **Catches real-world breakages early** before readers report them +- **Creates repeatable validation** instead of one-off manual checks +- **Works well in teams** through report outputs, logs, and CI-friendly behavior +- **Supports different strictness levels** with developer personas (`junior`, `mid`, `senior`) + +For example, `junior` and `mid` personas are great for spotting unclear documentation, while `senior` helps identify issues an experienced developer could work around. + +## Built for ABP, Open for Everyone + +Although TutorialValidator was originally built to validate **ABP Framework tutorials**, it works with **any publicly accessible software tutorial**. + +It supports validating any publicly accessible software tutorial and can run in: + +- **Docker mode** for clean, isolated execution (recommended) +- **Local mode** for faster feedback when your environment is already prepared + +It also supports multiple AI providers, including OpenAI, Azure OpenAI, and OpenAI-compatible endpoints. + +## Open Source and Easily Extensible + +The tutorial validator is designed with a modular architecture. +The project consists of multiple focused components: + +- **Core** – shared models and contracts +- **Analyst** – tutorial scraping and step extraction +- **Executor** – step-by-step execution engine +- **Orchestrator** – workflow coordination +- **Reporter** – notifications and result summaries + +This architecture makes it easy to extend the validator with: + +- new step types +- additional AI providers +- custom reporting integrations + +This architecture keeps the project easy to understand and extend. Teams can add new step types, plugins, or reporting channels based on their own workflow. + +## Final Thoughts + +Documentation is a critical part of the product experience. +When tutorials break, developer trust breaks too. +TutorialValidator helps teams move from: + +> We believe this tutorial works 🙄 + +to + +> We verified this tutorial works ✅ + +If your team maintains **technical tutorials, developer guides, or framework documentation**, automated tutorial validation can provide a powerful safety net. + +Documentation is part of the product experience. When tutorials fail, trust fails. +If your team maintains technical tutorials, this project can give you a practical safety net and a repeatable quality process. + +--- + +You can find the source-code of the tutorial validator at this repo 👉 https://github.com/abpframework/tutorial-validator + +We would love to hear your feedback, ideas and waiting PRs to improve this application. diff --git a/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-1.png b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-1.png new file mode 100644 index 0000000000..6f533e3e6c Binary files /dev/null and b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-1.png differ diff --git a/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-2.png b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-2.png new file mode 100644 index 0000000000..fdb5b394be Binary files /dev/null and b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image-2.png differ diff --git a/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image.png b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image.png new file mode 100644 index 0000000000..69be373a32 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/docs/images/image.png differ diff --git a/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/POST.md b/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/POST.md new file mode 100644 index 0000000000..a12779289c --- /dev/null +++ b/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/POST.md @@ -0,0 +1,185 @@ +# Secure Client Authentication with private_key_jwt in ABP 10.3 + +If you've built a confidential client with ABP's OpenIddict module, you know the drill: create an application in the management UI, set a `client_id`, generate a `client_secret`, and paste that secret into your client's `appsettings.json` or environment variables. It works. It's familiar. And for a lot of projects, it's perfectly fine. + +But `client_secret` is a **shared secret** — and shared secrets carry an uncomfortable truth: the same value exists in two places at once. The authorization server stores a hash of it in the database, and your client stores the raw value in configuration. That means two potential leak points. Worse, the secret has no inherent identity. Anyone who obtains the string can impersonate your client and the server has no way to tell the difference. + +For many teams, this tradeoff is acceptable. But certain scenarios make it hard to ignore: + +- **Microservice-to-microservice calls**: A backend mesh of a dozen services, each with its own `client_secret` scattered across deployment configs and CI/CD pipelines. Rotating them across environments without missing one becomes a coordination problem. +- **Multi-tenant SaaS platforms**: Every tenant's client application deserves truly isolated credentials. With shared secrets, the database holds hashed copies for all tenants — a breach of that table is a breach of everyone's credentials. +- **Financial-grade API (FAPI) compliance**: Standards like [FAPI 2.0](https://openid.net/specs/fapi-2_0-security-profile.html) explicitly require asymmetric client authentication. `client_secret` doesn't make the cut. +- **Zero-trust architectures**: In a zero-trust model, identity must be cryptographically provable, not based on a string that can be copied and pasted. + +The underlying problem is that a shared secret is just a password. It can be stolen, replicated, and used without leaving a trace. The fix has existed in cryptography for decades: **asymmetric keys**. + +With asymmetric key authentication, the client generates a key pair. The public key is registered with the authorization server. The private key never leaves the client. Each time the client needs a token, it signs a short-lived JWT — called a _client assertion_ — with the private key. The server verifies the signature using the registered public key. There is no secret on the server side that could be used to forge a request, because the private key is never transmitted or stored remotely. + +This is exactly what the **`private_key_jwt`** client authentication method, defined in [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication), provides. ABP's OpenIddict module now supports it end-to-end: you register a **JSON Web Key Set (JWKS)** containing your public key through the application management UI (ABP Commercial), and your client authenticates using the corresponding private key. The key generation tooling (`abp generate-jwks`) ships as part of the open-source ABP CLI. + +> This feature is available starting from **ABP Framework 10.3**. + +## How It Works + +The flow is straightforward: + +1. The client holds an RSA key pair — **private key** (kept locally) and **public key** (registered on the authorization server as a JWKS). +2. On each token request, the client uses the private key to sign a JWT with a short expiry and a unique `jti` claim. +3. The authorization server verifies the signature against the registered public key and issues a token if it checks out. + +The private key never leaves the client. Even if someone obtains the authorization server's database, there's nothing there that can be used to generate a valid client assertion. + +## Generating a Key Pair + +ABP CLI includes a `generate-jwks` command that creates an RSA key pair in the right formats: + +```bash +abp generate-jwks +``` + +This produces two files in the current directory: + +- `jwks.json` — the public key in JWKS format, to be uploaded to the server +- `jwks-private.pem` — the private key in PKCS#8 PEM format, to be kept on the client + +You can customize the output directory, key size, and signing algorithm: + +```bash +abp generate-jwks --alg RS512 --key-size 4096 -o ./keys -f myapp +``` + +> Supported algorithms: `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`. The default is `RS256` with a 2048-bit key. + +The command also prints the contents of `jwks.json` to the console so you can copy it directly. + +## Registering the JWKS in the Management UI + +Open **OpenIddict → Applications** in the ABP admin panel and create or edit a confidential application (Client Type: `Confidential`). + +In the **Client authentication method** section, you'll find the new **JSON Web Key Set** field. + +![](./create-edit-ui.png) + +Paste the contents of `jwks.json` into the **JSON Web Key Set** field: + +```json +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "6444...", + "alg": "RS256", + "n": "tx...", + "e": "AQAB" + } + ] +} +``` + +Save the application. It's now configured for `private_key_jwt` authentication. You can set either `client_secret` or a JWKS, or both — ABP enforces that a confidential application always has at least one credential. + +## Requesting a Token with the Private Key + +On the client side, each token request requires building a _client assertion_ JWT signed with the private key. Here's a complete `client_credentials` example: + +```csharp +// Discover the authorization server endpoints (including the issuer URI). +var client = new HttpClient(); +var configuration = await client.GetDiscoveryDocumentAsync("https://your-auth-server/"); + +// Load the private key generated by `abp generate-jwks`. +using var rsaKey = RSA.Create(); +rsaKey.ImportFromPem(await File.ReadAllTextAsync("jwks-private.pem")); + +// Read the kid from jwks.json so it stays in sync with the server-registered public key. +string? signingKid = null; +if (File.Exists("jwks.json")) +{ + using var jwksDoc = JsonDocument.Parse(await File.ReadAllTextAsync("jwks.json")); + if (jwksDoc.RootElement.TryGetProperty("keys", out var keysElem) && + keysElem.GetArrayLength() > 0 && + keysElem[0].TryGetProperty("kid", out var kidElem)) + { + signingKid = kidElem.GetString(); + } +} + +var signingKey = new RsaSecurityKey(rsaKey) { KeyId = signingKid }; +var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256); + +// Build the client assertion JWT. +var now = DateTime.UtcNow; +var jwtHandler = new JsonWebTokenHandler(); +var clientAssertionToken = jwtHandler.CreateToken(new SecurityTokenDescriptor +{ + // OpenIddict requires typ = "client-authentication+jwt" for client assertion JWTs. + TokenType = "client-authentication+jwt", + Issuer = "MyClientId", + // aud must equal the authorization server's issuer URI from the discovery document, + // not the token endpoint URL. + Audience = configuration.Issuer, + Subject = new ClaimsIdentity(new[] + { + new Claim(JwtRegisteredClaimNames.Sub, "MyClientId"), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }), + IssuedAt = now, + NotBefore = now, + Expires = now.AddMinutes(5), + SigningCredentials = signingCredentials, +}); + +// Request a token using the client_credentials flow. +var tokenResponse = await client.RequestClientCredentialsTokenAsync( + new ClientCredentialsTokenRequest + { + Address = configuration.TokenEndpoint, + ClientId = "MyClientId", + ClientCredentialStyle = ClientCredentialStyle.PostBody, + ClientAssertion = new ClientAssertion + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertionToken, + }, + Scope = "MyAPI", + }); +``` + +A few things worth paying attention to: + +- **`TokenType`** must be `"client-authentication+jwt"`. OpenIddict rejects client assertion JWTs that don't carry this header. +- **`Audience`** must match the authorization server's issuer URI exactly — use `configuration.Issuer` from the discovery document, not the token endpoint URL. +- **`Jti`** must be unique per request to prevent replay attacks. +- Keep **`Expires`** short (five minutes or less). A client assertion is a one-time proof of identity, not a long-lived credential. + +This example uses [IdentityModel](https://github.com/IdentityModel/IdentityModel) for the token request helpers and [Microsoft.IdentityModel.JsonWebTokens](https://www.nuget.org/packages/Microsoft.IdentityModel.JsonWebTokens) for JWT creation. + +## Key Rotation Without Downtime + +One of the practical advantages of JWKS is that it can hold multiple public keys simultaneously. This makes **zero-downtime key rotation** straightforward: + +1. Run `abp generate-jwks` to produce a new key pair. +2. Append the new public key to the `keys` array in your existing `jwks.json` and update the JWKS in the management UI. +3. Switch the client to sign assertions with the new private key. +4. Once the transition is complete, remove the old public key from the JWKS. + +During the transition window, both the old and new public keys are registered on the server, so any in-flight requests signed with either key will still validate correctly. + +## Summary + +To use `private_key_jwt` authentication in an ABP Pro application: + +1. Run `abp generate-jwks` to generate an RSA key pair. +2. Paste the `jwks.json` contents into the **JSON Web Key Set** field in the OpenIddict application management UI. +3. On the client side, sign a short-lived _client assertion_ JWT with the private key — making sure to set the correct `typ`, `aud` (from the discovery document), and a unique `jti` — then use it to request a token. + +ABP handles public key storage and validation automatically. OpenIddict handles the signature verification on the token endpoint. As a developer, you only need to keep the private key file secure — there's no shared secret to synchronize between client and server. + +## References + +- [OpenID Connect Core — Client Authentication](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication) +- [RFC 7523 — JWT Profile for Client Authentication](https://datatracker.ietf.org/doc/html/rfc7523) +- [ABP OpenIddict Module Documentation](https://abp.io/docs/latest/modules/openiddict) +- [ABP CLI Documentation](https://abp.io/docs/latest/cli) +- [OpenIddict Documentation](https://documentation.openiddict.com/) diff --git a/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/cover.png b/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/cover.png new file mode 100644 index 0000000000..e268703fb6 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/cover.png differ diff --git a/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/create-edit-ui.png b/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/create-edit-ui.png new file mode 100644 index 0000000000..1ca04b12bd Binary files /dev/null and b/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/create-edit-ui.png differ diff --git a/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/POST.md b/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/POST.md new file mode 100644 index 0000000000..6bf5ed5774 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/POST.md @@ -0,0 +1,151 @@ +# One Endpoint, Many AI Clients: Turning ABP Workspaces into OpenAI-Compatible Models + +ABP's AI Management module already makes it easy to define and manage AI workspaces (provider, model, API key/base URL, system prompt, permissions, MCP tools, RAG settings, and more). With **ABP v10.2**, there is a major addition: you can now expose those workspaces through **OpenAI-compatible endpoints** under `/v1`. + +That changes the integration story in a practical way. Instead of wiring every external tool directly to a provider, you can point those tools to ABP and keep runtime decisions centralized in one place. + +In this post, we will walk through a practical setup with **AnythingLLM** and show why this pattern is useful in real projects. + +Before we get into the details, here's a quick look at the full flow in action: + +## See It in Action: AnythingLLM + ABP + +The demo below shows the full flow: connecting an OpenAI-compatible client to ABP, selecting a workspace-backed model, and sending a successful chat request through `/v1`. + +![ABP AI Management OpenAI-compatible endpoints demo](./openai-compatible-endpoints-demo.gif) + +## Why This Is a Big Deal + +Many teams end up with AI configuration spread across multiple clients and services. Updating providers, rotating keys, or changing model behavior can become operationally messy. + +With ABP in front of your AI traffic: + +- Clients keep speaking the familiar OpenAI contract. +- ABP resolves the requested `model` to a workspace. +- The workspace decides which provider/model settings are actually used. + +This gives you a clean split: standardized client integration outside, governed AI configuration inside. + +## Key Concept: Workspace = Model + +OpenAI-compatible clients send a `model` value. +In ABP AI Management, that `model` maps to a **workspace name**. + +**For example:** + +- Workspace name: `SupportAgent` +- Client request model: `SupportAgent` + +When the client calls `/v1/chat/completions` with `"model": "SupportAgent"`, ABP routes the request to that workspace and applies that workspace's provider (OpenAI, Ollama etc.) and model configuration. + +This is the main mental model to keep in mind while integrating any OpenAI-compatible tool with ABP. + +## Endpoints Exposed by ABP v10.2 + +The AI Management module exposes OpenAI-compatible REST endpoints at `/v1`. + +| Endpoint | Method | Description | +| ---------------------------- | ------ | ---------------------------------------------- | +| `/v1/chat/completions` | POST | Chat completions (streaming and non-streaming) | +| `/v1/completions` | POST | Legacy text completions | +| `/v1/models` | GET | List available models (workspaces) | +| `/v1/models/{modelId}` | GET | Get a single model (workspace) | +| `/v1/embeddings` | POST | Generate embeddings | +| `/v1/files` | GET | List files | +| `/v1/files` | POST | Upload a file | +| `/v1/files/{fileId}` | GET | Get file metadata | +| `/v1/files/{fileId}` | DELETE | Delete a file | +| `/v1/files/{fileId}/content` | GET | Download file content | + +All endpoints require `Authorization: Bearer `. + +## Quick Setup with AnythingLLM + +Before configuration, ensure: + +1. AI Management is installed and running in your ABP app. +2. At least one workspace is created and **active**. +3. You have a valid Bearer token for your ABP application. + +### 1) Get an access token + +Use any valid token accepted by your app. In a demo-style setup, token retrieval can look like this: + +```bash +curl -X POST http://localhost:44337/connect/token \ + -d "grant_type=password&username=admin&password=1q2w3E*&client_id=DemoApp_API&client_secret=1q2w3e*&scope=DemoApp" +``` + +Use the returned `access_token` as the API key value in your OpenAI-compatible client. + +### 2) Configure AnythingLLM as Generic OpenAI + +In **AnythingLLM -> Settings -> LLM Preference**, select **Generic OpenAI** and set: + +| Setting | Value | +| -------------------- | --------------------------- | +| Base URL | `http://localhost:44337/v1` | +| API Key | `` | +| Chat Model Selection | Select an active workspace | + +In most OpenAI-compatible UIs, the app adds `Bearer` automatically, so the API key field should contain only the raw token string. + +### 3) Optional: configure embeddings + +If you want RAG flows through ABP, go to **Settings -> Embedding Preference** and use the same Base URL/API key values. +Then select a workspace that has embedder settings configured. + +## Validate the Flow + +### List models (workspaces) + +```bash +curl http://localhost:44337/v1/models \ + -H "Authorization: Bearer " +``` + +### Chat completion + +```bash +curl -X POST http://localhost:44337/v1/chat/completions \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "model": "MyWorkspace", + "messages": [ + { "role": "user", "content": "Hello from ABP OpenAI-compatible endpoint!" } + ] + }' +``` + +### Optional SDK check (Python) + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:44337/v1", + api_key="" +) + +response = client.chat.completions.create( + model="MyWorkspace", + messages=[{"role": "user", "content": "Hello!"}] +) + +print(response.choices[0].message.content) +``` + +## Where This Fits in Real Projects + +This approach is a strong fit when you want to: + +- Keep ABP as the central control plane for AI workspaces. +- Let client tools integrate through a standard OpenAI contract. +- Switch providers or model settings without rewriting client-side integration. + +If your team uses multiple AI clients, this pattern keeps integration simple while preserving control where it matters. + +## Learn More + +- [ABP AI Management Documentation](https://abp.io/docs/10.2/modules/ai-management) diff --git a/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/cover-image.png b/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/cover-image.png new file mode 100644 index 0000000000..3024f341b4 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/cover-image.png differ diff --git a/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/openai-compatible-endpoints-demo.gif b/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/openai-compatible-endpoints-demo.gif new file mode 100644 index 0000000000..e1c830087b Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-OpenAI-Compatible-Endpoints/openai-compatible-endpoints-demo.gif differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/POST.md b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/POST.md new file mode 100644 index 0000000000..dcb69c289d --- /dev/null +++ b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/POST.md @@ -0,0 +1,167 @@ +# Shared User Accounts in ABP Multi-Tenancy + +Multi-tenancy is built on **isolation** — isolated data, isolated permissions, isolated users. ABP's default behavior has always followed this assumption: one user belongs to exactly one tenant. Clean, simple, no ambiguity. For most SaaS applications, that's exactly what you want. (The new `TenantUserSharingStrategy` enum formally names this default behavior `Isolated`.) + +But isolation is **the system's** concern, not **the user's**. In practice, people's work doesn't always line up neatly with tenant boundaries. + +Think about a financial consultant who works with three different companies — each one a tenant in your system. Under the Isolated model, she needs three separate accounts, three passwords. Forgot which password goes with which company? Good luck. Worse, the system sees three unrelated people — there's nothing linking those accounts to the same human being. + +This comes up more often than you'd think: + +- In a **corporate group**, an IT admin manages multiple subsidiaries, each running as its own tenant. Every day means logging out, logging back in with different credentials, over and over +- A **SaaS platform's ops team** needs to hop into different customer tenants to debug issues. Each time they create a throwaway account, then delete it — or just share one account and lose all audit trail +- Some users resort to email aliases (`alice+company1@example.com`) to work around uniqueness constraints — that's not a solution, that's a hack + +The common thread here: the user's **identity** is global, but their **working context** is per-tenant. The problem isn't a technical limitation — it's that the Isolated assumption ("one user, one tenant") simply doesn't hold in these scenarios. + +What's needed is not "one account per tenant" but "one account, multiple tenants." + +ABP's **Shared User Accounts** (`TenantUserSharingStrategy.Shared`) does exactly this. It makes user identity global and turns tenants into workspaces that a user can join and switch between — similar to how one person can belong to multiple workspaces in Slack. + +> This is a **commercial** feature, available starting from **ABP 10.2**, provided by the Account.Pro and Identity.Pro modules. + +## Enabling the Shared Strategy + +A single configuration is all it takes: + +```csharp +Configure(options => +{ + options.IsEnabled = true; + options.UserSharingStrategy = TenantUserSharingStrategy.Shared; +}); +``` + +The most important behavior change after switching to Shared: **username and email uniqueness become global** instead of per-tenant. This follows naturally — if the same account needs to be recognized across tenants, its identifiers must be unique across the entire system. + +Security-related settings (2FA, account lockout, password policies, captcha, etc.) are also managed at the **Host** level. This makes sense too: if user identity is global, the security rules around it should be global as well. + +## One Account, Multiple Tenants + +With the Shared strategy enabled, the day-to-day user experience changes fundamentally. + +When a user is associated with only one tenant, the system recognizes it automatically and signs them in directly — the user doesn't even notice that tenants exist. When the user belongs to multiple tenants, the login flow presents a tenant selection screen after credentials are verified: + +![tenant-selection](./tenant-selection.png) + +After signing into a tenant, a tenant switcher appears in the user menu — click it anytime to jump to another tenant without signing out. ABP re-issues the authentication ticket (with the new `TenantId` in the claims) on each switch, so the permission system is fully independent per tenant. + +![switch-tenant](./switch-tenant.png) + +Users can also leave a tenant. Leaving doesn't delete the association record — it marks it as inactive. This preserves foreign key relationships with other entities. If the user is invited back later, the association is simply reactivated instead of recreated. + +Back to our earlier scenario: the financial consultant now has one account, one password. She picks which company to work in at login, switches between them during the day. The system knows it's the same person, and the audit log can trace her actions across every tenant. + +## Invitations + +Users don't just appear in a tenant — someone has to invite them. This is the core operation from the administrator's perspective. + +A tenant admin opens the invitation dialog, enters one or more email addresses (batch invitations are supported), and can pre-assign roles — so the user gets the right permissions the moment they join, no extra setup needed: + +![invite-user](./invite-user.png) + +The invited person receives an email with a link. What happens next depends on whether they already have an account. + +If they **already have an account**, they see a confirmation page and can join the tenant with a single click: + +![exist-user-accept](./exist-user-accept.png) + +If they **don't have an account yet**, the link takes them to a registration form. Once they register, they're automatically added to the tenant: + +![new-user-accept](./new-user-accept.png) + +Admins can also manage pending invitations at any time — resend emails or revoke invitations. + +> The invitation feature is also available under the Isolated strategy, but invited users can only join a single tenant. + +## Setting Up a New Tenant + +There's a notable shift in how new tenants are bootstrapped. + +Under the Isolated model, creating a tenant typically seeds an `admin` user automatically. With Shared, this no longer happens — because users are global, and it doesn't make sense to create one out of thin air for a specific tenant. + +Instead, you create the tenant first, then invite someone in and grant them the admin role. + +![invite-admin-user-to-join-tenant](./invite-admin-user-to-join-tenant.png) + +![invite-admin-user-to-join-tenant-modal](./invite-admin-user-to-join-tenant-modal.png) + +This is a natural fit — the admin is just a global user who happens to hold the admin role in this particular tenant. + +## Where Do Newly Registered Users Go? + +Under the Shared strategy, self-registration runs into an interesting problem: the system doesn't know which tenant the user wants to join. Without being signed in, tenant context is usually determined by subdomain or a tenant switcher on the login page — but for a brand-new user, those signals might not exist at all. + +So ABP's approach is: **don't establish any tenant association at registration time**. A newly registered user doesn't belong to any tenant, and doesn't belong to the Host either — this is an entirely new state. ABP still lets these users sign in, change their password, and manage their account, but they can't access any permission-protected features within a tenant. + +`AbpIdentityPendingTenantUserOptions.Strategy` controls what happens in this "pending" state. + +**CreateTenant** — automatically creates a tenant for the new user. This fits the "sign up and get your own workspace" pattern, like how Slack or Notion handles registration: you register, the system spins up a workspace for you. + +```csharp +Configure(options => +{ + options.Strategy = AbpIdentityPendingTenantUserStrategy.CreateTenant; +}); +``` + +![new-user-join-strategy-create-tenant](./new-user-join-strategy-create-tenant.png) + +**Inform** (the default) — shows a message telling the user to contact an administrator to join a tenant. This is the right choice for invite-only platforms where users must be brought in by an existing tenant admin. + +```csharp +Configure(options => +{ + options.Strategy = AbpIdentityPendingTenantUserStrategy.Inform; +}); +``` + +![new-user-join-strategy-inform](./new-user-join-strategy-inform.png) + +There's also a **Redirect** strategy that sends the user to a custom URL for more complex flows. + +> See the [official documentation](https://abp.io/docs/latest/modules/account/shared-user-accounts) for full configuration details. + +## Database Considerations + +The Shared strategy introduces some mechanisms and constraints at the database level that are worth understanding. + +### Global Uniqueness: Enforced in Code, Not by Database Indexes + +Username and email uniqueness checks must span all tenants. ABP disables the tenant filter (`TenantFilter.Disable()`) during validation and searches globally for conflicts. + +A notable design choice here: **global uniqueness is enforced at the application level, not through database unique indexes**. The reason is practical — in a database-per-tenant setup, users live in separate physical databases, so a cross-database unique index simply isn't possible. Even in a shared database, soft-delete complicates unique indexes (you'd need a composite index on "username + deletion time"). So ABP handles this in application code instead. + +To keep things safe under concurrency — say two tenant admins invite the same email address at the same time — ABP uses a **distributed lock** to serialize uniqueness validation. This means your production environment needs a distributed lock provider configured (such as Redis). + +The uniqueness check goes beyond just "no duplicate usernames." ABP also checks for **cross-field conflicts**: a user's username can't match another user's email, and vice versa. This prevents identity confusion in edge cases. + +### Tenants with Separate Databases + +If some of your tenants use their own database (database-per-tenant), the Shared strategy requires extra attention. + +The login flow and tenant selection happen on the **Host side**. This means the Host database's `AbpUsers` table must contain records for all users — even those originally created in a tenant's separate database. ABP's approach is replication: it saves the primary user record in the Host context and creates a copy in the tenant context. In a shared-database setup, both records live in the same table; in a database-per-tenant setup, they live in different physical databases. Updates and deletes are kept in sync automatically. + +If your application uses social login or passkeys, the `AbpUserLogins` and `AbpUserPasskeys` tables also need to be synced in the Host database. + +### Migrating from the Isolated Strategy + +If you're moving an existing multi-tenant application from Isolated to Shared, ABP automatically runs a global uniqueness check when you switch the strategy and reports any conflicts. + +The most common conflict: the same email address registered as separate users in different tenants. You'll need to resolve these first — merge the accounts or change one side's email — before the Shared strategy can be enabled. + +## Summary + +ABP's Shared User Accounts addresses a real-world need in multi-tenant systems: one person working across multiple tenants. + +- One configuration switch to `TenantUserSharingStrategy.Shared` +- User experience: pick a tenant at login, switch between tenants anytime, one password for everything +- Admin experience: invite users by email, pre-assign roles on invitation +- Database notes: configure a distributed lock provider for production; tenants with separate databases need user records replicated in the Host database + +ABP takes care of global uniqueness validation, tenant association management, and login flow adaptation under the hood. + +## References + +- [Shared User Accounts](https://abp.io/docs/latest/modules/account/shared-user-accounts) +- [ABP Multi-Tenancy](https://abp.io/docs/latest/framework/architecture/multi-tenancy) diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/cover.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/cover.png new file mode 100644 index 0000000000..33cbea2f52 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/cover.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/exist-user-accept.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/exist-user-accept.png new file mode 100644 index 0000000000..23f35c0904 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/exist-user-accept.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant-modal.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant-modal.png new file mode 100644 index 0000000000..8fa9d2fee9 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant-modal.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant.png new file mode 100644 index 0000000000..edfb5bedb0 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-user.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-user.png new file mode 100644 index 0000000000..67a3f04073 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-user.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-accept.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-accept.png new file mode 100644 index 0000000000..ffc887f1ed Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-accept.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-create-tenant.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-create-tenant.png new file mode 100644 index 0000000000..7d4a64c7c0 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-create-tenant.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-inform.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-inform.png new file mode 100644 index 0000000000..a6a62e1c96 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-inform.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/switch-tenant.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/switch-tenant.png new file mode 100644 index 0000000000..6f19de1da7 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/switch-tenant.png differ diff --git a/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/tenant-selection.png b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/tenant-selection.png new file mode 100644 index 0000000000..e40bf6aaeb Binary files /dev/null and b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/tenant-selection.png differ diff --git a/docs/en/Community-Articles/2026-03-21-Dynamic-Background-Jobs-and-Workers-in-ABP/POST.md b/docs/en/Community-Articles/2026-03-21-Dynamic-Background-Jobs-and-Workers-in-ABP/POST.md new file mode 100644 index 0000000000..ed02df79e6 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-21-Dynamic-Background-Jobs-and-Workers-in-ABP/POST.md @@ -0,0 +1,226 @@ +# Dynamic Background Jobs and Workers in ABP + +> This feature is available since ABP 10.3. + +ABP's Background Jobs and Background Workers are two well-established infrastructure pieces. Background jobs handle fire-and-forget async tasks — sending emails, generating reports, processing orders. Background workers handle continuously running periodic tasks — syncing inventory, cleaning up expired data, pushing scheduled notifications. + +This works great, but it has one assumption: **you know all your job and worker types at compile time**. + +In practice, that assumption breaks down more often than you'd expect: + +- You're building a **plugin system** where third-party plugins need to register their own background processing logic at runtime — you can't pre-define an `IBackgroundJob` implementation in the host project for every possible plugin +- Your system needs to execute background tasks based on **external configuration** (database, API responses) — the task types and parameters are entirely unknown at compile time +- Your **multi-tenant SaaS platform** needs different sync intervals for different tenants — some every 30 seconds, some every 5 minutes — and you need to adjust these without restarting the application +- You're building a **low-code/no-code platform** where end users define automation workflows through a visual designer, and those workflows need to run as background jobs or scheduled tasks — the job types and scheduling parameters are entirely determined by end users at runtime, unknowable to developers at compile time + +ABP's **Dynamic Background Jobs** (`IDynamicBackgroundJobManager`) and **Dynamic Background Workers** (`IDynamicBackgroundWorkerManager`) are designed for exactly these scenarios. They let you register, enqueue, schedule, and manage background tasks by name at runtime, with no compile-time type binding required. + +## Dynamic Background Jobs + +`IDynamicBackgroundJobManager` offers two usage patterns, covering different levels of runtime flexibility. + +### Enqueue an Existing Typed Job by Name + +If you already have a typed background job (say, an `EmailSendingJob` registered via `[BackgroundJobName("emails")]`), you can enqueue it by name without referencing its args type: + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IDynamicBackgroundJobManager _dynamicJobManager; + + public OrderAppService(IDynamicBackgroundJobManager dynamicJobManager) + { + _dynamicJobManager = dynamicJobManager; + } + + public async Task PlaceOrderAsync(PlaceOrderInput input) + { + // Business logic... + + // Enqueue a confirmation email — no reference to EmailSendingJobArgs needed + await _dynamicJobManager.EnqueueAsync("emails", new + { + EmailAddress = input.CustomerEmail, + Subject = "Order Confirmed", + Body = $"Your order {input.OrderId} has been placed." + }); + } +} +``` + +The framework looks up the typed job configuration by name, serializes the anonymous object, deserializes it into the correct args type, and feeds it through the standard typed job pipeline. The caller doesn't need to `using` any specific project namespace. + +### Register a Runtime Dynamic Handler + +When you don't even have a job type — say a plugin decides at startup what processing logic to register — you can register a handler directly: + +```csharp +public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) +{ + var dynamicJobManager = context.ServiceProvider + .GetRequiredService(); + + // A plugin registers its own processing logic at startup + dynamicJobManager.RegisterHandler("SyncExternalCatalog", async (jobContext, ct) => + { + using var doc = JsonDocument.Parse(jobContext.JsonData); + var catalogUrl = doc.RootElement.GetProperty("url").GetString(); + + var httpClient = jobContext.ServiceProvider + .GetRequiredService() + .CreateClient(); + + var catalog = await httpClient.GetStringAsync(catalogUrl, ct); + // Process catalog data... + }); + + // Now you can enqueue jobs for this handler + await dynamicJobManager.EnqueueAsync("SyncExternalCatalog", new + { + Url = "https://partner-api.example.com/catalog" + }); +} +``` + +The handler receives a context object containing `JsonData` (the raw JSON string) and `ServiceProvider` (a scoped container). Resolving dependencies from `ServiceProvider` is the recommended approach — avoid capturing external state in the handler closure. + +There's one priority rule to keep in mind: **if a name matches both a typed job and a dynamic handler, the typed job wins**. Dynamic handlers never accidentally override existing typed jobs. + +> Dynamic jobs ultimately go through the standard typed job pipeline, so they **work with every background job provider** — Default, Hangfire, Quartz, RabbitMQ, TickerQ — without any provider-specific code. + +## Dynamic Background Workers + +`IDynamicBackgroundWorkerManager` lets you register periodic tasks at runtime and manage their full lifecycle: add, remove, update schedule. + +```csharp +public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) +{ + var workerManager = context.ServiceProvider + .GetRequiredService(); + + await workerManager.AddAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 30000 // 30 seconds + }, + async (workerContext, cancellationToken) => + { + var syncService = workerContext.ServiceProvider + .GetRequiredService(); + + await syncService.SyncAsync(cancellationToken); + } + ); +} +``` + +If you're using Hangfire or Quartz as your provider, you can use a cron expression instead of a fixed interval: + +```csharp +await workerManager.AddAsync( + "DailyReportWorker", + new DynamicBackgroundWorkerSchedule + { + CronExpression = "0 2 * * *" // Every day at 2:00 AM + }, + async (workerContext, cancellationToken) => + { + var reportService = workerContext.ServiceProvider + .GetRequiredService(); + + await reportService.GenerateDailyReportAsync(cancellationToken); + } +); +``` + +### Runtime Schedule Management + +Adding a worker is just the beginning. The real value of dynamic workers is that the entire lifecycle is controllable at runtime: + +```csharp +// Check if a worker is currently registered +bool exists = workerManager.IsRegistered("InventorySyncWorker"); + +// A tenant upgrades their plan — speed up sync from 30s to 10s +await workerManager.UpdateScheduleAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule { Period = 10000 } +); + +// Tenant disables the sync feature — remove the worker entirely +await workerManager.RemoveAsync("InventorySyncWorker"); +``` + +`UpdateScheduleAsync` only changes the schedule — the handler itself stays the same. For persistent providers like Hangfire and Quartz, `UpdateScheduleAsync` and `RemoveAsync` can operate on the persistent scheduling record even after an application restart, when the handler is no longer in memory. + +### Stopping All Workers + +When you need to stop all dynamic workers at once (e.g., as part of a graceful shutdown), call `StopAllAsync`: + +```csharp +await workerManager.StopAllAsync(cancellationToken); +``` + +All registered workers are stopped and cleaned up, and the handler registry is cleared. Calling `AddAsync` or `UpdateScheduleAsync` after this throws `ObjectDisposedException` — this is intentional, preventing new workers from being added during a shutdown sequence. + +## Provider Support + +Dynamic background jobs and dynamic background workers have different levels of provider support. + +**Dynamic background jobs** are compatible with all providers because they reuse the standard typed job pipeline: + +| Provider | Supported | +|---|---| +| Default (In-Memory) | ✅ | +| Hangfire | ✅ | +| Quartz | ✅ | +| RabbitMQ | ✅ | +| TickerQ | ✅ | + +**Dynamic background workers** have per-provider implementations: + +| Provider | AddAsync | RemoveAsync | UpdateScheduleAsync | Period | CronExpression | +|---|---|---|---|---|---| +| Default (In-Memory) | ✅ | ✅ | ✅ | ✅ | ❌ | +| Hangfire | ✅ | ✅ | ✅ | ✅ | ✅ | +| Quartz | ✅ | ✅ | ✅ | ✅ | ✅ | +| TickerQ | ❌ | ❌ | ❌ | — | — | + +TickerQ uses `FrozenDictionary` for function registration, which requires all functions to be registered before the application starts. Runtime dynamic registration is not possible. + +## Restart Behavior + +Dynamic handlers are stored **in memory** and are not persisted across application restarts. This is a deliberate design choice — handlers are code logic (delegates), and code logic is inherently not serializable. + +For persistent providers (Hangfire, Quartz), this means: enqueued jobs and recurring job entries survive a restart in the database, but the handlers need to be re-registered. If a handler is not re-registered, the job executor throws an exception (background jobs) or skips the execution with a warning log (background workers). + +The recommended approach is to register handlers in `OnApplicationInitializationAsync`, so they are automatically restored on every startup: + +```csharp +public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) +{ + var dynamicJobManager = context.ServiceProvider + .GetRequiredService(); + + // Re-registered on every startup — persistent jobs will find their handler + dynamicJobManager.RegisterHandler("SyncExternalCatalog", async (jobContext, ct) => + { + // handler logic... + }); +} +``` + +## Summary + +`IDynamicBackgroundJobManager` lets you enqueue jobs and register handlers by name at runtime, compatible with all background job providers, no compile-time types required. `IDynamicBackgroundWorkerManager` lets you add, remove, and update the schedule of periodic workers at runtime — Hangfire and Quartz providers also support cron expressions. Register handlers in `OnApplicationInitializationAsync` to ensure automatic recovery on every startup. + +## References + +- [Background Jobs](https://abp.io/docs/latest/framework/infrastructure/background-jobs) +- [Background Workers](https://abp.io/docs/latest/framework/infrastructure/background-workers) +- [Hangfire Background Job Manager](https://abp.io/docs/latest/framework/infrastructure/background-jobs/hangfire) +- [Quartz Background Job Manager](https://abp.io/docs/latest/framework/infrastructure/background-jobs/quartz) diff --git a/docs/en/Community-Articles/2026-03-21-Dynamic-Background-Jobs-and-Workers-in-ABP/cover.jpg b/docs/en/Community-Articles/2026-03-21-Dynamic-Background-Jobs-and-Workers-in-ABP/cover.jpg new file mode 100644 index 0000000000..f1c85ceb58 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-21-Dynamic-Background-Jobs-and-Workers-in-ABP/cover.jpg differ diff --git a/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md new file mode 100644 index 0000000000..cdc1a03f58 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md @@ -0,0 +1,205 @@ +# Dynamic Events in ABP + +> This feature is available since ABP 10.3. + +ABP's Event Bus is a core infrastructure piece. The **Local Event Bus** handles in-process communication between services. The **Distributed Event Bus** handles cross-service communication over message brokers like RabbitMQ, Kafka, Azure Service Bus, and Rebus. + +Both are fully type-safe — you define event types at compile time, register handlers via DI, and everything is wired up automatically. This works great, but it has one assumption: **you know all your event types at compile time**. + +In practice, that assumption breaks down in several scenarios: + +- You're building a **plugin system** where third-party modules register their own event types at runtime — you can't pre-define an `IDistributedEventHandler` for every possible plugin event +- Your system receives events from **external systems** (webhooks, IoT devices, partner APIs) where the event schema is defined by the external party, not by your codebase +- You're building a **low-code platform** where end users define event-driven workflows through a visual designer — the event names and payloads are entirely determined at runtime + +ABP's **Dynamic Events** extend the existing `IEventBus` and `IDistributedEventBus` interfaces with string-based publishing and subscription. You can publish events by name, subscribe to events by name, and handle payloads without any compile-time type binding — all while coexisting seamlessly with the existing typed event system. + +## Publishing Events by Name + +The most straightforward use case: publish an event using a string name and an arbitrary payload. + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IDistributedEventBus _eventBus; + + public OrderAppService(IDistributedEventBus eventBus) + { + _eventBus = eventBus; + } + + public async Task PlaceOrderAsync(PlaceOrderInput input) + { + // Business logic... + + // Publish a dynamic event — no event class needed + await _eventBus.PublishAsync( + "OrderPlaced", + new { OrderId = input.Id, CustomerEmail = input.Email } + ); + } +} +``` + +The payload can be any serializable object — an anonymous type, a `Dictionary`, or even an existing typed class. The event bus serializes the payload and sends it to the broker with the string name as the routing key. + +### What If a Typed Event Already Exists? + +If the string name matches an existing typed event (via `EventNameAttribute`), the framework automatically converts the payload to the typed class and routes it through the **typed pipeline**. Both typed handlers and dynamic handlers are triggered. + +```csharp +[EventName("OrderPlaced")] +public class OrderPlacedEto +{ + public Guid OrderId { get; set; } + public string CustomerEmail { get; set; } +} + +// This handler will still receive the event, with auto-converted data +public class OrderEmailHandler : IDistributedEventHandler +{ + public Task HandleEventAsync(OrderPlacedEto eventData) + { + // eventData.OrderId and eventData.CustomerEmail are populated + return Task.CompletedTask; + } +} +``` + +Publishing by name with `new { OrderId = ..., CustomerEmail = ... }` triggers this typed handler — the framework handles the serialization round-trip. This is especially useful for scenarios where a service needs to emit events without taking a dependency on the project that defines the event type. + +## Subscribing to Dynamic Events + +Dynamic subscription lets you register event handlers at runtime, using a string event name. + +```csharp +public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) +{ + var eventBus = context.ServiceProvider + .GetRequiredService(); + + // Subscribe to a dynamic event — no event class needed + eventBus.Subscribe("PartnerOrderReceived", + new PartnerOrderHandler(context.ServiceProvider)); +} +``` + +The handler implements `IDistributedEventHandler`: + +```csharp +public class PartnerOrderHandler : IDistributedEventHandler +{ + private readonly IServiceProvider _serviceProvider; + + public PartnerOrderHandler(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task HandleEventAsync(DynamicEventData eventData) + { + // eventData.EventName = "PartnerOrderReceived" + // eventData.Data = the raw payload from the broker + + var orderProcessor = _serviceProvider + .GetRequiredService(); + + await orderProcessor.ProcessAsync(eventData.EventName, eventData.Data); + } +} +``` + +`DynamicEventData` is a simple POCO with two properties: + +- **`EventName`** — the string name that identifies the event +- **`Data`** — the raw event data payload (the deserialized `object` from the broker) + +> `Subscribe` returns an `IDisposable`. Call `Dispose()` to unsubscribe the handler at runtime. + +## Mixed Typed and Dynamic Handlers + +Typed and dynamic handlers coexist naturally. When both are registered for the same event name, **both are triggered** — the framework automatically converts the data to the appropriate format for each handler. + +```csharp +// Typed handler — receives OrderPlacedEto +eventBus.Subscribe(); + +// Dynamic handler — receives DynamicEventData for the same event +eventBus.Subscribe("OrderPlaced", new AuditLogHandler()); +``` + +When `OrderPlacedEto` is published (by type or by name), both handlers fire. The typed handler receives a fully deserialized `OrderPlacedEto` object. The dynamic handler receives a `DynamicEventData` wrapping the raw payload. + +This enables a powerful pattern: the core business logic uses typed handlers for safety, while infrastructure concerns (auditing, logging, plugin hooks) use dynamic handlers for flexibility. + +## Outbox Support + +Dynamic events go through the same **outbox/inbox pipeline** as typed events. If you have outbox configured, dynamic events benefit from the same reliability guarantees — they are stored in the outbox table within the same database transaction as your business data, then reliably delivered to the broker by the background worker. + +No additional configuration is needed. The outbox works transparently for both typed and dynamic events: + +```csharp +// This dynamic event goes through the outbox if configured +using var uow = _unitOfWorkManager.Begin(); +await _eventBus.PublishAsync( + "OrderPlaced", + new { OrderId = orderId }, + onUnitOfWorkComplete: true, + useOutbox: true +); +await uow.CompleteAsync(); +``` + +## Local Event Bus + +Dynamic events work on the local event bus too, not just the distributed bus. The API is the same: + +```csharp +var localEventBus = context.ServiceProvider + .GetRequiredService(); + +// Subscribe dynamically +localEventBus.Subscribe("UserActivityTracked", + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + // Handle the event + return Task.CompletedTask; + }))); + +// Publish dynamically +await localEventBus.PublishAsync("UserActivityTracked", new +{ + UserId = currentUser.Id, + Action = "PageView", + Url = "/products/42" +}); +``` + +## Provider Support + +Dynamic events work with all distributed event bus providers: + +| Provider | Dynamic Subscribe | Dynamic Publish | +|---|---|---| +| LocalDistributedEventBus (default) | ✅ | ✅ | +| RabbitMQ | ✅ | ✅ | +| Kafka | ✅ | ✅ | +| Rebus | ✅ | ✅ | +| Azure Service Bus | ✅ | ✅ | +| Dapr | ❌ | ❌ | + +Dapr requires topic subscriptions to be declared at application startup and cannot add subscriptions at runtime. Calling `Subscribe(string, ...)` on the Dapr provider throws an `AbpException`. + +## Summary + +`IEventBus.PublishAsync(string, object)` and `IEventBus.Subscribe(string, handler)` let you publish and subscribe to events by name at runtime — no compile-time types required. If the event name matches a typed event, the framework auto-converts the payload and triggers both typed and dynamic handlers. Dynamic events go through the same outbox/inbox pipeline as typed events, so reliability guarantees are preserved. This works across all providers except Dapr, and coexists seamlessly with the existing typed event system. + +## References + +- [Local Event Bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/local) +- [Distributed Event Bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed) +- [RabbitMQ Integration](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/rabbitmq) +- [Kafka Integration](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/kafka) +- [Dynamic Distributed Events Sample](https://github.com/abpframework/abp-samples/tree/master/DynamicDistributedEvents) diff --git a/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png new file mode 100644 index 0000000000..4776c8485b Binary files /dev/null and b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png differ diff --git a/docs/en/cli/index.md b/docs/en/cli/index.md index e376887baf..bdbe9493b9 100644 --- a/docs/en/cli/index.md +++ b/docs/en/cli/index.md @@ -75,6 +75,7 @@ Here is the list of all available commands before explaining their details: * **[`install-old-cli`](../cli#install-old-cli)**: Installs old ABP CLI. * **[`mcp-studio`](../cli#mcp-studio)**: Starts ABP Studio MCP bridge for AI tools (requires ABP Studio running). * **[`generate-razor-page`](../cli#generate-razor-page)**: Generates a page class that you can use it in the ASP NET Core pipeline to return an HTML page. +* **[`generate-jwks`](../cli#generate-jwks)**: Generates an RSA key pair (JWKS public key + PEM private key) for OpenIddict `private_key_jwt` client authentication. ### help @@ -1127,6 +1128,99 @@ app.Use(async (httpContext, next) => * ```--version``` or ```-v```: Specifies the version for ABP CLI to be installed. +### generate-jwks + +Generates an RSA key pair for use with OpenIddict `private_key_jwt` client authentication. + +The command produces two files: + +| File | Description | +|---|---| +| `.json` | JWKS (JSON Web Key Set) containing the **public key**. Paste this into the **JSON Web Key Set** field of your OpenIddict application in the ABP management UI. | +| `-private.pem` | PKCS#8 PEM **private key**. Store this securely in your client application and use it to sign JWT client assertions. | + +> **Security notice:** Never commit the private key file to source control. Add it to `.gitignore`. Only the JWKS (public key) needs to be shared with the authorization server. + +Usage: + +```bash +abp generate-jwks [options] +``` + +#### Options + +* `--output` or `-o`: Output directory. Defaults to the current directory. +* `--key-size` or `-s`: RSA key size in bits. Supported values: `2048` (default), `4096`. +* `--alg`: Signing algorithm. Supported values: `RS256` (default), `RS384`, `RS512`, `PS256`, `PS384`, `PS512`. +* `--kid`: Custom Key ID. Auto-generated if not specified. +* `--file` or `-f`: Output file name prefix. Defaults to `jwks`. Generates `.json` and `-private.pem`. + +#### Examples + +```bash +# Generate with defaults (2048-bit RS256, current directory) +abp generate-jwks + +# Generate with RS512 and 4096-bit key +abp generate-jwks --alg RS512 --key-size 4096 + +# Output to a specific directory with a custom file prefix +abp generate-jwks -o ./keys -f myapp +``` + +#### Workflow + +1. Run `abp generate-jwks` to generate the key pair. + +2. Open the ABP OpenIddict application management UI, select your **Confidential** application, choose **JWKS (private_key_jwt)** as the authentication method, and paste the contents of `jwks.json` into the **JSON Web Key Set** field. + +3. In your client application, load the private key from the PEM file and sign JWT client assertions: + +```csharp +// Load private key from PEM file +using var rsa = RSA.Create(); +rsa.ImportFromPem(await File.ReadAllTextAsync("jwks-private.pem")); + +// The kid must match the "kid" field in the JWKS registered on the server +var signingKey = new RsaSecurityKey(rsa) { KeyId = "" }; +var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256); + +var now = DateTime.UtcNow; +var jwtHandler = new JsonWebTokenHandler(); +var clientAssertion = jwtHandler.CreateToken(new SecurityTokenDescriptor +{ + // OpenIddict requires typ = "client-authentication+jwt" + TokenType = "client-authentication+jwt", + // iss and sub must both equal the client_id + Issuer = "", + Audience = "", + Subject = new ClaimsIdentity(new[] + { + new Claim(JwtRegisteredClaimNames.Sub, ""), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }), + IssuedAt = now, + NotBefore = now, + Expires = now.AddMinutes(5), + SigningCredentials = signingCredentials, +}); + +// Use the assertion in the token request +var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync( + new ClientCredentialsTokenRequest + { + Address = "", + ClientId = "", + ClientCredentialStyle = ClientCredentialStyle.PostBody, + ClientAssertion = new ClientAssertion + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertion, + }, + Scope = "", + }); +``` + ## See Also * [Examples for the new command](./new-command-samples.md) diff --git a/docs/en/contribution/angular-ui.md b/docs/en/contribution/angular-ui.md index e3445b12bd..783ed64a62 100644 --- a/docs/en/contribution/angular-ui.md +++ b/docs/en/contribution/angular-ui.md @@ -7,58 +7,168 @@ # Contribution Guide for the Angular UI +This guide explains how to set up the ABP Angular UI workspace, run the demo app, and prepare your environment to contribute UI changes. It assumes that you are already familiar with basic Angular and .NET development. + +> Before sending a pull request for Angular UI changes, please also read the main [Contribution Guide](index.md). + ## Pre-requirements -- Dotnet core SDK https://dotnet.microsoft.com/en-us/download -- Nodejs LTS https://nodejs.org/en/ -- Docker https://docs.docker.com/engine/install -- Angular CLI. https://angular.dev/tools/cli -- Abp CLI https://docs.abp.io/en/abp/latest/cli -- A code editor +Make sure you have the following tools installed: + +- [.NET SDK](https://dotnet.microsoft.com/en-us/download) +- [Node.js LTS](https://nodejs.org/en/) (recommended: use the version supported by the Angular CLI used in this repository) +- [Docker Engine](https://docs.docker.com/engine/install/) (required if you use the sample SQL Server and Redis containers) +- [Angular CLI](https://angular.dev/tools/cli) +- [ABP CLI](https://docs.abp.io/en/abp/latest/cli) +- A code editor (for example, Visual Studio Code or Visual Studio) -Note: This article prepare Windows OS. You may change the path type of your OS. +> This article uses Windows-style paths in examples. On Unix-like systems, replace backslashes (`\`) with forward slashes (`/`). Examples: -* Windows: `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json` -* Unix: `templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/appsettings.json` +- Windows: `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json` +- Unix: `templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/appsettings.json` -## Sample docker commands +## Sample Docker Commands -You need to install SQL Server and Redis. You can install these programs without docker, but my example uses docker containers. Your computer should have Docker Engine. Then open the terminal and execute the commands one by one. -For the SQL Server +You need SQL Server and Redis. You can install these programs without Docker, but the examples below use Docker containers. Your computer should have Docker Engine running. Then open a terminal and execute the commands. -```cmd -docker run -v sqlvolume:/var/opt/mssql -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=yourpassword" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-CU3-ubuntu-18.04 +### SQL Server + +```bash +docker run -v sqlvolume:/var/opt/mssql \ + -e 'ACCEPT_EULA=Y' \ + -e 'SA_PASSWORD=YourStrong!Passw0rd' \ + -p 1433:1433 \ + -d mcr.microsoft.com/mssql/server:2019-CU3-ubuntu-18.04 ``` -For the Redis +- Replace `YourStrong!Passw0rd` with a strong password that satisfies SQL Server password requirements. +- The `sqlvolume` named volume is used to persist database files. + +### Redis -```cmd -docker run -p 6379:6379 -d redis +```bash +docker run -p 6379:6379 -d redis:latest ``` -Then we are ready to download and execute the code. +After running the commands, you can use `docker ps` to verify that both containers are running. + +Once the containers are ready, you can download the ABP source code and run the apps. ## Folder Structure -The app has a backend written in .net core (c#) and an angular app. It would help if you ran both of them. +The sample application has: + +- A backend built with ASP.NET Core (C#). +- An Angular workspace managed by Nx. + +You will run both the backend and the Angular dev app during development. + +## Running the Backend App + +The backend root path is `templates\app\aspnet-core`. + +### 1. Configure the Connection Strings -## Running Backend App +If you are using the Dockerized SQL Server, update the connection strings to point to your Docker container. The configuration file is: -The path of the Backend app is “templates\app\aspnet-core.” If you want to work with dockerized SQL Server, you should change connection strings for running with docker. The path of the connection string is -`templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json`. +- `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json` + +Ensure that the connection string uses the correct server name (`localhost,1433` by default), user (`sa`), and your password. + +### 2. Run the DbMigrator + +The DbMigrator project creates the initial database schema and seed data. + +```bash +cd templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator +dotnet run +``` + +Wait until the migration completes successfully. + +### 3. Install Client-side Libraries + +Before running the backend host, install the client-side libraries: + +```bash +cd templates/app/aspnet-core +abp install-libs +``` + +This command restores the required client-side libraries for the backend. + +### 4. Run the Backend Host + +Go to the backend HTTP API host project folder. The exact project name may differ based on your template, but it will be similar to: + +- `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.HttpApi.HostWithIds` + +Run the host: + +```bash +cd templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds +dotnet run +``` + +After it starts, the backend API will be available on a localhost URL defined in the project (for example, `https://localhost:44305`, depending on your template). + +## Running the Frontend (Angular Dev App) + +The Angular workspace is under `npm\ng-packs`. It is an Nx workspace that contains both the dev app and the Angular UI packages. + +- Dev app path: `npm\ng-packs\apps\dev-app` +- Package path: `npm\ng-packs\packages\` + +The dev app uses local references to the packages under `packages`, so your library changes will be reflected immediately while the dev server is running. + +### 1. Install Dependencies + +From the dev app folder: + +```bash +cd npm/ng-packs/apps/dev-app +yarn +# or, if you prefer npm: +# npm install +``` + +Choose one package manager (preferably `yarn` if that is what the repository uses) and stick with it. + +### 2. Start the Dev Server + +```bash +yarn start +# or: +# npm start +``` -Before running the backend, you should run the Db migrator project. The DbMigrator created initial tables and values. The path of DbMigrator is `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator`. Open a terminal in the path and execute the command `dotnet run` in terminal +This will start the Angular dev server (via Nx) and open the dev app in your browser. Ensure that the backend API is running so the dev app can connect to it. -One last step before the running the backend is installing client-side libraries. Go to `templates\app\aspnet-core`. Open a terminal in the path and execute the command `abp install-libs` in terminal +## Typical Contribution Workflow -Next step you should go to path of backend host project. The path is `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.HttpApi.HostWithIds`. Open a terminal in the path and execute the command `dotnet run` in terminal +1. Start SQL Server and Redis (for example, using the Docker commands above). +2. Run DbMigrator to create and seed the database. +3. Run `abp install-libs` and start the backend HTTP API host. +4. Install dependencies and start the Angular dev app. +5. Make changes in the Angular UI packages under `npm\ng-packs\packages\`. +6. Run any relevant tests for the affected packages (for example, via Nx). +7. Commit your changes and open a pull request on GitHub, referencing the related issue. -Your backend should be running successfully +## Troubleshooting -## Running Frontend App +- **Backend cannot connect to SQL Server** + - Check that the SQL Server container is running (`docker ps`). + - Verify the connection string server/port and `SA_PASSWORD` value. +- **Angular app cannot reach the backend API** + - Confirm that the backend host is running and listening on the expected URL. + - Check the API base URL configuration in the dev app’s environment files. +- **Node or package manager version issues** + - Use an LTS version of Node.js. + - Consider using a version manager (like `nvm`) to match the version used in the project. -There is a demo app. The path of the demo app is `npm\ng-packs\apps\dev-app`. The demo app is connected to the packages with local references. Open the terminal in `npm\ng-packs\apps\dev-app` and execute `yarn` or `npm i` in terminal. After the package installed run `npm start` or `yarn start`. +## See Also -The repo uses Nx and packages connected with `local references`. The packages path is `npm\ng-packs\packages` +- [Contribution Guide](index.md) +- [ABP CLI](https://docs.abp.io/en/abp/latest/cli) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index bcef20914d..b0a76124b4 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -2573,6 +2573,10 @@ "text": "Language Management (Pro)", "path": "modules/language-management.md" }, + { + "text": "Operation Rate Limiting (Pro)", + "path": "modules/operation-rate-limiting.md" + }, { "text": "OpenIddict", "isLazyExpandable": true, diff --git a/docs/en/framework/infrastructure/background-jobs/index.md b/docs/en/framework/infrastructure/background-jobs/index.md index 0d5e19442c..2f27055f46 100644 --- a/docs/en/framework/infrastructure/background-jobs/index.md +++ b/docs/en/framework/infrastructure/background-jobs/index.md @@ -346,6 +346,87 @@ If you don't want to use a distributed lock provider, you may go with the follow * Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs). * Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance. +## Dynamic Background Jobs + +ABP provides `IDynamicBackgroundJobManager` for scenarios where you need to enqueue jobs by name at runtime, without requiring a strongly-typed job args class at compile time. This is useful for plugin systems, dynamic workflows, or any case where job types are not known ahead of time. + +### Enqueue by Job Name (Typed Job) + +If a typed job is already registered (e.g., via `[BackgroundJobName("emails")]`), you can enqueue it by name: + +````csharp +public class MyService : ApplicationService +{ + private readonly IDynamicBackgroundJobManager _dynamicJobManager; + + public MyService(IDynamicBackgroundJobManager dynamicJobManager) + { + _dynamicJobManager = dynamicJobManager; + } + + public async Task DoSomethingAsync() + { + await _dynamicJobManager.EnqueueAsync("emails", new + { + EmailAddress = "user@abp.io", + Subject = "Hello", + Body = "World" + }); + } +} +```` + +The `IDynamicBackgroundJobManager` will look up the typed job configuration, deserialize the args to the expected type, and enqueue through the standard typed pipeline. + +### Dynamic Job Handlers + +You can also register dynamic handlers at runtime for jobs that don't have a pre-defined typed job class: + +````csharp +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + var dynamicJobManager = context.ServiceProvider + .GetRequiredService(); + + dynamicJobManager.RegisterHandler("ProcessOrder", async (context, ct) => + { + var json = context.JsonData; + var serviceProvider = context.ServiceProvider; + // Process the order using JsonData and resolved services... + }); +} +```` + +Then enqueue jobs using the registered name: + +````csharp +await _dynamicJobManager.EnqueueAsync("ProcessOrder", new +{ + OrderId = "ORD-001", + Amount = 99.99 +}); +```` + +### Handler Management + +````csharp +// Check if a handler is registered +bool exists = _dynamicJobManager.IsHandlerRegistered("ProcessOrder"); + +// Unregister a handler +bool removed = _dynamicJobManager.UnregisterHandler("ProcessOrder"); +```` + +### How It Works + +- **Typed job path**: When the job name matches a registered typed job configuration, the args are serialized to JSON and deserialized to the expected args type, then enqueued through `IBackgroundJobManager.EnqueueAsync`. +- **Dynamic handler path**: When the job name matches a registered dynamic handler, the args are wrapped as `DynamicBackgroundJobArgs` (a public transport type used internally by the framework) and enqueued through `IBackgroundJobManager.EnqueueAsync`. When the job executes, the framework looks up the handler by name and invokes it. +- All dynamic jobs go through the **standard typed job pipeline**, which means they work with all providers (Default, Hangfire, Quartz, RabbitMQ, TickerQ) without any provider-specific changes. + +> **Note:** If the job name matches both a registered typed job configuration and a dynamic handler, **the typed job takes priority** and the dynamic handler is ignored. To avoid confusion, use distinct names for dynamic handlers that do not conflict with existing typed job names. + +> **Important:** Dynamic job handlers are stored **in memory only** and are not persisted across application restarts. When using a persistent provider (Hangfire, Quartz, RabbitMQ, TickerQ), enqueued jobs survive a restart but if no handler is re-registered, the job executor will throw an exception when the job is picked up. To ensure handlers are always available, register them in `OnApplicationInitialization` so they are re-registered on every startup. + ## Integrations Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations. diff --git a/docs/en/framework/infrastructure/background-jobs/tickerq.md b/docs/en/framework/infrastructure/background-jobs/tickerq.md index de7b3631f2..81dd021607 100644 --- a/docs/en/framework/infrastructure/background-jobs/tickerq.md +++ b/docs/en/framework/infrastructure/background-jobs/tickerq.md @@ -95,13 +95,13 @@ public class CleanupJobs public override Task OnPreApplicationInitializationAsync(ApplicationInitializationContext context) { var abpTickerQFunctionProvider = context.ServiceProvider.GetRequiredService(); - abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) => + abpTickerQFunctionProvider.AddFunction(nameof(CleanupJobs), async (cancellationToken, serviceProvider, tickerFunctionContext) => { var service = new CleanupJobs(); // Or get it from the serviceProvider var request = await TickerRequestProvider.GetRequestAsync(tickerFunctionContext, cancellationToken); var genericContext = new TickerFunctionContext(tickerFunctionContext, request); await service.CleanupLogsAsync(genericContext, cancellationToken); - }))); + }, TickerTaskPriority.Normal); abpTickerQFunctionProvider.RequestTypes.TryAdd(nameof(CleanupJobs), (typeof(string).FullName, typeof(string))); return Task.CompletedTask; } diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 6204857d8c..a8e558cea1 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -120,6 +120,66 @@ So, it resolves the given background worker and adds to the `IBackgroundWorkerMa While we generally add workers in `OnApplicationInitializationAsync`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. +### Dynamic Workers (Runtime Registration) + +You can add a runtime worker without pre-defining a dedicated worker class. Inject `IDynamicBackgroundWorkerManager` and pass a handler directly: + +````csharp +public class MyModule : AbpModule +{ + public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) + { + var dynamicWorkerManager = context.ServiceProvider + .GetRequiredService(); + + await dynamicWorkerManager.AddAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 30000 //30 seconds + //CronExpression = "*/30 * * * *" //Every 30 minutes. Only for Hangfire or Quartz integration. + }, + async (workerContext, cancellationToken) => + { + var inventorySyncAppService = workerContext + .ServiceProvider + .GetRequiredService(); + + await inventorySyncAppService.SyncAsync(cancellationToken); + } + ); + } +} +```` + +You can also **remove** a dynamic worker or **update its schedule** at runtime: + +````csharp +//Remove a dynamic worker +var removed = await dynamicWorkerManager.RemoveAsync("InventorySyncWorker"); + +//Update the schedule of a dynamic worker +var updated = await dynamicWorkerManager.UpdateScheduleAsync( + "InventorySyncWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 60000 //Change to 60 seconds + } +); +```` + +* `IDynamicBackgroundWorkerManager` is a **separate interface** from `IBackgroundWorkerManager`, dedicated to runtime (non-type-safe) worker management. +* `workerName` is the runtime identifier of the dynamic worker. If a worker with the same name already exists, it will be **replaced**. +* The `handler` receives a `DynamicBackgroundWorkerExecutionContext` containing the worker name and a scoped `IServiceProvider`. It is a good practice to **resolve dependencies** from the `workerContext.ServiceProvider` instead of constructor injection. +* At least one of `Period` or `CronExpression` must be set in `DynamicBackgroundWorkerSchedule`. +* **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md)).** The default in-memory provider requires `Period` and does not support `CronExpression` alone. +* **[TickerQ](./tickerq.md) does not support dynamic background workers** because it uses `FrozenDictionary` for function registration, which requires all functions to be registered before the application starts. +* `RemoveAsync` stops and removes a dynamic worker. Returns `true` if the worker was found and removed. The exact semantics are provider-dependent — for persistent providers (Hangfire, Quartz), the persistent scheduling record is always cleaned up, but the return value may only reflect the in-memory registry state. +* `UpdateScheduleAsync` changes the schedule of an existing dynamic worker. The handler itself is not changed. Returns `true` if the schedule was updated. The exact semantics are provider-dependent — for persistent providers (Hangfire, Quartz), this also works correctly after an application restart, updating the persistent scheduling record even if the handler is no longer registered in memory. + +> **Important:** Dynamic worker handlers are stored **in memory only** and are not persisted across application restarts. When using a persistent scheduler provider (Hangfire or Quartz), the recurring job entries remain in the database after a restart, but the handlers will no longer be registered. Until the handler is re-registered, each scheduled execution will be **skipped with a warning log**. To ensure handlers are always available, register them in `OnApplicationInitializationAsync` so they are re-registered on every startup. + ## Options `AbpBackgroundWorkerOptions` class is used to [set options](../../fundamentals/options.md) for the background workers. Currently, there is only one option: diff --git a/docs/en/framework/infrastructure/background-workers/tickerq.md b/docs/en/framework/infrastructure/background-workers/tickerq.md index d4ddde3cd9..4547b85b85 100644 --- a/docs/en/framework/infrastructure/background-workers/tickerq.md +++ b/docs/en/framework/infrastructure/background-workers/tickerq.md @@ -83,13 +83,13 @@ public class CleanupJobs public override Task OnPreApplicationInitializationAsync(ApplicationInitializationContext context) { var abpTickerQFunctionProvider = context.ServiceProvider.GetRequiredService(); - abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) => + abpTickerQFunctionProvider.AddFunction(nameof(CleanupJobs), async (cancellationToken, serviceProvider, tickerFunctionContext) => { var service = new CleanupJobs(); // Or get it from the serviceProvider var request = await TickerRequestProvider.GetRequestAsync(tickerFunctionContext, cancellationToken); var genericContext = new TickerFunctionContext(tickerFunctionContext, request); await service.CleanupLogsAsync(genericContext, cancellationToken); - }))); + }, TickerTaskPriority.Normal); abpTickerQFunctionProvider.RequestTypes.TryAdd(nameof(CleanupJobs), (typeof(string).FullName, typeof(string))); return Task.CompletedTask; } @@ -112,11 +112,11 @@ await cronTickerManager.AddAsync(new CronTickerEntity You can specify a cron expression instead of using `ICronTickerManager` to add a worker: ```csharp -abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) => +abpTickerQFunctionProvider.AddFunction(nameof(CleanupJobs), async (cancellationToken, serviceProvider, tickerFunctionContext) => { var service = new CleanupJobs(); var request = await TickerRequestProvider.GetRequestAsync(tickerFunctionContext, cancellationToken); var genericContext = new TickerFunctionContext(tickerFunctionContext, request); await service.CleanupLogsAsync(genericContext, cancellationToken); -}))); +}, TickerTaskPriority.Normal); ``` diff --git a/docs/en/framework/infrastructure/entity-cache.md b/docs/en/framework/infrastructure/entity-cache.md index 50f1bc1b3c..aed1f33c6e 100644 --- a/docs/en/framework/infrastructure/entity-cache.md +++ b/docs/en/framework/infrastructure/entity-cache.md @@ -26,7 +26,7 @@ public class Product : AggregateRoot public string Name { get; set; } public string Description { get; set; } - public float Price { get; set; } + public decimal Price { get; set; } public int StockCount { get; set; } } ``` @@ -72,7 +72,7 @@ public class ProductDto : EntityDto { public string Name { get; set; } public string Description { get; set; } - public float Price { get; set; } + public decimal Price { get; set; } public int StockCount { get; set; } } ``` @@ -147,6 +147,115 @@ context.Services.AddEntityCache( * Entity classes should be serializable/deserializable to/from JSON to be cached (because it's serialized to JSON when saving in the [Distributed Cache](../fundamentals/caching.md)). If your entity class is not serializable, you can consider using a cache-item/DTO class instead, as explained before. * Entity Caching System is designed as **read-only**. You should use the standard [repository](../architecture/domain-driven-design/repositories.md) methods to manipulate the entity if you need to. If you need to manipulate (update) the entity, do not get it from the entity cache. Instead, read it from the repository, change it and update using the repository. +## Getting Multiple Entities + +In addition to the single-entity methods `FindAsync` and `GetAsync`, the `IEntityCache` service provides batch retrieval methods for retrieving multiple entities at once. + +### List-Based Batch Retrieval + +`FindManyAsync` and `GetManyAsync` return results as a list, preserving the order of the given IDs (including duplicates): + +```csharp +public class ProductAppService : ApplicationService, IProductAppService +{ + private readonly IEntityCache _productCache; + + public ProductAppService(IEntityCache productCache) + { + _productCache = productCache; + } + + public async Task> GetManyAsync(List ids) + { + return await _productCache.GetManyAsync(ids); + } + + public async Task> FindManyAsync(List ids) + { + return await _productCache.FindManyAsync(ids); + } +} +``` + +* `GetManyAsync` throws `EntityNotFoundException` if any entity is not found for the given IDs. +* `FindManyAsync` returns a list where each entry corresponds to the given ID in the same order; an entry will be `null` if the entity was not found. + +### Dictionary-Based Batch Retrieval + +`FindManyAsDictionaryAsync` and `GetManyAsDictionaryAsync` return results as a dictionary keyed by entity ID, which is convenient when you need fast lookup by ID: + +```csharp +public async Task> FindManyAsDictionaryAsync(List ids) +{ + return await _productCache.FindManyAsDictionaryAsync(ids); +} + +public async Task> GetManyAsDictionaryAsync(List ids) +{ + return await _productCache.GetManyAsDictionaryAsync(ids); +} +``` + +* `GetManyAsDictionaryAsync` throws `EntityNotFoundException` if any entity is not found for the given IDs. +* `FindManyAsDictionaryAsync` returns a dictionary where the value is `null` if the entity was not found for the corresponding key. + +All batch methods internally use `IDistributedCache.GetOrAddManyAsync` to batch-fetch only the cache-missed entities from the database, making them more efficient than calling `FindAsync` or `GetAsync` in a loop. + +## Custom Object Mapping + +When you need full control over how an entity is mapped to a cache item, you can derive from `EntityCacheWithObjectMapper` and override the `MapToValue` method: + +First, define the cache item class: + +```csharp +public class ProductCacheDto +{ + public Guid Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } +} +``` + +Then, derive from `EntityCacheWithObjectMapper` and override `MapToValue`: + +```csharp +public class ProductEntityCache : + EntityCacheWithObjectMapper +{ + public ProductEntityCache( + IReadOnlyRepository repository, + IDistributedCache, Guid> cache, + IUnitOfWorkManager unitOfWorkManager, + IObjectMapper objectMapper) + : base(repository, cache, unitOfWorkManager, objectMapper) + { + } + + protected override ProductCacheDto MapToValue(Product entity) + { + // Custom mapping logic here + return new ProductCacheDto + { + Id = entity.Id, + Name = entity.Name.ToUpperInvariant(), + Price = entity.Price + }; + } +} +``` + +Register your custom cache class in the `ConfigureServices` method of your [module class](../architecture/modularity/basics.md): + +```csharp +context.Services.ReplaceEntityCache( + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) + }); +``` + +> If no prior `AddEntityCache` registration exists for the same cache item type, `ReplaceEntityCache` will simply add the service instead of throwing an error. + ## See Also * [Distributed caching](../fundamentals/caching.md) diff --git a/docs/en/framework/infrastructure/event-bus/distributed/index.md b/docs/en/framework/infrastructure/event-bus/distributed/index.md index e5f96bba10..5f2c7b11ec 100644 --- a/docs/en/framework/infrastructure/event-bus/distributed/index.md +++ b/docs/en/framework/infrastructure/event-bus/distributed/index.md @@ -721,6 +721,82 @@ Configure(options => }); ```` +## Dynamic (String-Based) Events + +In addition to the type-safe event system described above, ABP also supports **dynamic events** that are identified by a string name rather than a CLR type. This is useful for scenarios where event types are not known at compile time, such as integrating with external systems or building plugin architectures. + +> **Note:** Dynamic event subscriptions are supported by RabbitMQ, Kafka, Azure Service Bus, and Rebus providers. The **Dapr provider does not support dynamic events** because Dapr requires topic subscriptions to be declared at application startup and cannot add subscriptions at runtime. Attempting to call `Subscribe(string, ...)` on the Dapr provider will throw an `AbpException`. + +### Publishing Dynamic Events + +Use the `PublishAsync` overload that accepts a string event name: + +````csharp +await distributedEventBus.PublishAsync( + "MyDynamicEvent", + new Dictionary + { + ["UserId"] = 42, + ["Name"] = "John" + } +); +```` + +If a typed event exists with the given name (via `EventNameAttribute` or convention), the data is automatically deserialized and routed to the typed handler. Otherwise, it is delivered as a `DynamicEventData` to dynamic handlers. + +You can also control `onUnitOfWorkComplete` and `useOutbox` parameters: + +````csharp +await distributedEventBus.PublishAsync( + "MyDynamicEvent", + new { UserId = 42, Name = "John" }, + onUnitOfWorkComplete: true, + useOutbox: true +); +```` + +### Subscribing to Dynamic Events + +Use the `Subscribe` overload that accepts a string event name: + +````csharp +var subscription = distributedEventBus.Subscribe( + "MyDynamicEvent", + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + // Access the event name and raw data + var name = eventData.EventName; + var data = eventData.Data; + + return Task.CompletedTask; + }))); + +// Unsubscribe when done +subscription.Dispose(); +```` + +You can also subscribe using a typed distributed event handler: + +````csharp +distributedEventBus.Subscribe("MyDynamicEvent", myDistributedEventHandler); +```` + +Where `myDistributedEventHandler` implements `IDistributedEventHandler`. + +### Mixed Typed and Dynamic Handlers + +When both a typed handler and a dynamic handler are registered for the same event name, **both** handlers are triggered. The typed handler receives the converted typed data, while the dynamic handler receives a `DynamicEventData` wrapper. + +### DynamicEventData Class + +The `DynamicEventData` class is a simple data object that wraps the event payload: + +- **`EventName`**: The string name that identifies the event. +- **`Data`**: The raw event data payload. + +> If a typed handler exists for the same event name, the framework automatically converts the data to the expected type using the event bus serialization pipeline. Dynamic handlers receive the raw `Data` as-is. + ## See Also * [Local Event Bus](../local) diff --git a/docs/en/framework/infrastructure/event-bus/local/index.md b/docs/en/framework/infrastructure/event-bus/local/index.md index b20718c51f..bc8af41a06 100644 --- a/docs/en/framework/infrastructure/event-bus/local/index.md +++ b/docs/en/framework/infrastructure/event-bus/local/index.md @@ -249,6 +249,59 @@ If you set it to `false`, the `EntityUpdatedEventData` will not be published > This option is only used for the EF Core. +## Dynamic (String-Based) Events + +In addition to the type-safe event system described above, ABP also supports **dynamic events** that are identified by a string name rather than a CLR type. This is useful for scenarios where event types are not known at compile time. + +### Publishing Dynamic Events + +Use the `PublishAsync` overload that accepts a string event name: + +````csharp +await localEventBus.PublishAsync( + "MyDynamicEvent", + new Dictionary + { + ["UserId"] = 42, + ["Name"] = "John" + } +); +```` + +If a typed event exists with the given name (via `EventNameAttribute` or convention), the data is automatically converted and routed to the typed handler. Otherwise, it is delivered as a `DynamicEventData` to dynamic handlers. + +### Subscribing to Dynamic Events + +Use the `Subscribe` overload that accepts a string event name: + +````csharp +var subscription = localEventBus.Subscribe( + "MyDynamicEvent", + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + // Access the event name and raw data + var name = eventData.EventName; + var data = eventData.Data; + + return Task.CompletedTask; + }))); + +// Unsubscribe when done +subscription.Dispose(); +```` + +The `DynamicEventData` class is a simple data object with two properties: + +- **`EventName`**: The string name that identifies the event. +- **`Data`**: The raw event data payload. + +> If a typed handler exists for the same event name, the framework automatically converts the data to the expected type. Dynamic handlers receive the raw `Data` as-is. + +### Mixed Typed and Dynamic Handlers + +When both a typed handler and a dynamic handler are registered for the same event name, **both** handlers are triggered. The typed handler receives the converted typed data, while the dynamic handler receives a `DynamicEventData` wrapper. + ## See Also * [Distributed Event Bus](../distributed) diff --git a/docs/en/framework/infrastructure/luckypenny-automapper.md b/docs/en/framework/infrastructure/luckypenny-automapper.md new file mode 100644 index 0000000000..d3d516f1ce --- /dev/null +++ b/docs/en/framework/infrastructure/luckypenny-automapper.md @@ -0,0 +1,151 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to use the Volo.Abp.LuckyPenny.AutoMapper package to integrate the commercial AutoMapper (LuckyPenny) with ABP Framework." +} +``` + +# LuckyPenny AutoMapper Integration + +## Introduction + +[AutoMapper](https://automapper.org/) became a commercial product starting from version 15.x. The free open-source version (14.x) contains a [security vulnerability (GHSA-rvv3-g6hj-g44x)](https://github.com/advisories/GHSA-rvv3-g6hj-g44x) — a DoS (Denial of Service) vulnerability — and no patch will be released for the 14.x series. The patched version is only available in the commercial editions (15.x and later). + +The existing [Volo.Abp.AutoMapper](https://www.nuget.org/packages/Volo.Abp.AutoMapper) package uses AutoMapper 14.x and remains available for existing users. If you hold a valid [LuckyPenny AutoMapper commercial license](https://automapper.io/), the `Volo.Abp.LuckyPenny.AutoMapper` package provides the same ABP AutoMapper integration built on the patched commercial version. + +> If you don't need to use AutoMapper, you can migrate to [Mapperly](object-to-object-mapping.md#mapperly-integration), which is free and open-source. See the [AutoMapper to Mapperly migration guide](../../release-info/migration-guides/AutoMapper-To-Mapperly.md). + +## Installation + +Install the `Volo.Abp.LuckyPenny.AutoMapper` NuGet package to your project: + +````bash +dotnet add package Volo.Abp.LuckyPenny.AutoMapper +```` + +Then add `AbpLuckyPennyAutoMapperModule` to your module's `[DependsOn]` attribute, replacing the existing `AbpAutoMapperModule`: + +````csharp +[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] +public class MyModule : AbpModule +{ + // ... +} +```` + +> **Note:** `Volo.Abp.LuckyPenny.AutoMapper` and `Volo.Abp.AutoMapper` should **not** be used together in the same application. They are mutually exclusive — choose one or the other. + +## Usage + +`Volo.Abp.LuckyPenny.AutoMapper` is a drop-in replacement for `Volo.Abp.AutoMapper`. All the same APIs, options, and extension methods are available. Refer to the [AutoMapper Integration](object-to-object-mapping.md#automapper-integration) section of the Object to Object Mapping documentation for full usage details. + +The only difference from a user perspective is the module class name: + +| | Package | Module class | +|---|---|---| +| Free (14.x, has security vulnerability) | `Volo.Abp.AutoMapper` | `AbpAutoMapperModule` | +| Commercial (patched) | `Volo.Abp.LuckyPenny.AutoMapper` | `AbpLuckyPennyAutoMapperModule` | + +## License Configuration + +The commercial AutoMapper uses an honor-system license. Without a configured key, everything works normally but a warning is written to the logs under the `LuckyPennySoftware.AutoMapper.License` category. To configure your license key, use `AbpAutoMapperOptions.Configurators`: + +````csharp +[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.LicenseKey = "YOUR_LICENSE_KEY"; + }); + }); + } +} +```` + +It is recommended to read the key from configuration rather than hardcoding it: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var licenseKey = context.Configuration["AutoMapper:LicenseKey"]; + + Configure(options => + { + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.LicenseKey = licenseKey; + }); + }); +} +```` + +````json +{ + "AutoMapper": { + "LicenseKey": "YOUR_LICENSE_KEY" + } +} +```` + +To suppress the license warning in non-production environments (e.g. unit tests or local development), filter the log category in `Program.cs`: + +````csharp +builder.Logging.AddFilter("LuckyPennySoftware.AutoMapper.License", LogLevel.None); +```` + +Or in `appsettings.Development.json`: + +````json +{ + "Logging": { + "LogLevel": { + "LuckyPennySoftware.AutoMapper.License": "None" + } + } +} +```` + +> **Client-side applications** (Blazor WebAssembly, MAUI, WPF, etc.) should **not** set the license key to avoid exposing it on the client. Use the log filter above to silence the warning instead. + +## Obtaining a License + +AutoMapper offers a **free Community License** and several paid plans. + +### Community License (Free) + +A free license is available to organizations that meet **all** of the following criteria: + +- Annual gross revenue under **$5,000,000 USD** +- Never received more than **$10,000,000 USD** in outside capital (private equity or venture capital) +- Registered non-profits with an annual budget under **$5,000,000 USD** also qualify + +> Government and quasi-government agencies do **not** qualify for the Community License. + +Register for the Community License at: [https://luckypennysoftware.com/community](https://luckypennysoftware.com/community) + +### Paid Plans + +For organizations that do not meet the Community License criteria, paid plans are available at [https://luckypennysoftware.com/purchase](https://luckypennysoftware.com/purchase). For questions, contact [sales@luckypennysoftware.com](mailto:sales@luckypennysoftware.com). + +## Migration from Volo.Abp.AutoMapper + +To migrate an existing project from `Volo.Abp.AutoMapper` to `Volo.Abp.LuckyPenny.AutoMapper`: + +1. Replace the NuGet package reference in all `*.csproj` files: + ````diff + - + + + ```` + +2. Replace the module dependency in all `*.cs` files: + ````diff + -[DependsOn(typeof(AbpAutoMapperModule))] + +[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] + ```` + +3. No other code changes are required. All types (`AbpAutoMapperOptions`, `IMapperAccessor`, `AutoMapperExpressionExtensions`, etc.) remain in the same namespaces. diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 39218e7c86..5dcf526266 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -224,6 +224,8 @@ public class MyProfile : Profile } ```` +> AutoMapper 14.x contains a [known vulnerability (GHSA-rvv3-g6hj-g44x)](https://github.com/advisories/GHSA-rvv3-g6hj-g44x). ABP Framework has applied a code-level mitigation (`MaxDepth = 64`) to address this. If you hold a commercial AutoMapper license, you can use [Volo.Abp.LuckyPenny.AutoMapper](luckypenny-automapper.md) to upgrade to the officially patched version. Alternatively, you can migrate to [Mapperly](../../../release-info/migration-guides/AutoMapper-To-Mapperly.md). + ## Mapperly Integration [Mapperly](https://github.com/riok/mapperly) is a .NET source generator for generating object mappings. [Volo.Abp.Mapperly](https://www.nuget.org/packages/Volo.Abp.Mapperly) package defines the Mapperly integration for the `IObjectMapper`. diff --git a/docs/en/framework/ui/angular/extensions-overall.md b/docs/en/framework/ui/angular/extensions-overall.md index 287d62b38e..03971a46c4 100644 --- a/docs/en/framework/ui/angular/extensions-overall.md +++ b/docs/en/framework/ui/angular/extensions-overall.md @@ -32,10 +32,78 @@ Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible t /> ```` - * ` actionsText : ` ** Column name of action column. **Type** : string - * ` data : ` Items shows in your table. **Type** : Array - * ` list : ` Instance of ListService. **Type** : ListService - * `actionsColumnWidth : ` Width of your action column. **Type** : number - * ` actionsTemplate : ` Template of the action when "click this button" or whatever. Generally ng-template. **Type** : TemplateRef - * ` recordsTotal : ` Count of the record total. **Type** : number - * ` tableActivate : ` The Output(). A cell or row was focused via the keyboard or a mouse click. **Type** : EventEmitter() +| Input / Output | Description | Type | +|---------------------|----------------------------------------------------------------------------------------------|---------------------| +| `actionsText` | Column name of the action column. | `string` | +| `data` | Items shown in your table. | `Array` | +| `list` | Instance of `ListService`. | `ListService` | +| `actionsColumnWidth`| Width of your action column. | `number` | +| `actionsTemplate` | Template of the action (for example, an `ng-template`). | `TemplateRef` | +| `recordsTotal` | Total count of records. | `number` | +| `tableActivate` | Output fired when a cell or row is focused via keyboard or mouse click. | `EventEmitter` | + +### Multiple Selection + +The extensible table supports both single-row and multi-row selection. Use the `selectable` input to enable selection, and `selectionType` to control the selection mode. + +````ts + +```` + +When `selectionType` is `'single'`, each row displays a **radio button** and the header does not show a "select all" checkbox. For all other selection types (e.g. `'multiClick'`, `'checkbox'`), each row shows a **checkbox** and the header includes a "select all" checkbox. + +| Input / Output | Description | Type | Default | +|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------|----------------| +| `selectable` | Enables the row selection column. | `boolean` | `false` | +| `selectionType` | Controls the selection mode. Accepts `SelectionType` values such as `'single'`, `'multi'`, `'multiClick'`, `'checkbox'`, or `'cell'`. | `SelectionType | string`| `'multiClick'` | +| `selected` | The currently selected rows. | `any[]` | `[]` | +| `selectionChange` | Output fired when the selection changes. | `EventEmitter` | - | + +### Infinite Scroll + +The extensible table supports infinite scrolling as an alternative to pagination. When enabled, the table emits a `loadMore` event as the user scrolls near the bottom, allowing you to load additional data on demand. Pagination is hidden while infinite scroll is active. + +````ts + +```` + +In your component, append newly fetched data to the existing `items` array when `loadMore` fires: + +````ts +onLoadMore(): void { + if (this.isLoading) return; + this.isLoading = true; + // fetch next page and append results + this.myService.getList({ skipCount: this.items.length, maxResultCount: 10 }).subscribe(result => { + this.items = [...this.items, ...result.items]; + this.isLoading = false; + }); +} +```` + +> **Note:** When `infiniteScroll` is `true`, set a fixed `tableHeight` so the table has a scrollable viewport. Pagination is automatically hidden. + +| Input / Output | Description | Type | Default | +|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------|-----------| +| `infiniteScroll` | Enables infinite scroll mode (hides pagination). | `boolean` | `false` | +| `isLoading` | Indicates that more data is being fetched. Prevents duplicate `loadMore` events and shows a loading indicator. | `boolean` | `false` | +| `tableHeight` | Fixed height of the table in pixels when `infiniteScroll` is enabled. | `number` | - | +| `scrollThreshold` | Distance from the bottom (in pixels) at which `loadMore` is triggered. | `number` | `10` | +| `loadMore` | Output fired when the user scrolls near the bottom of the table (only when `infiniteScroll` is `true` and `isLoading` is `false`). | `EventEmitter` | - | diff --git a/docs/en/framework/ui/angular/pwa-configuration.md b/docs/en/framework/ui/angular/pwa-configuration.md index ea63f60b96..4c10fb6d27 100644 --- a/docs/en/framework/ui/angular/pwa-configuration.md +++ b/docs/en/framework/ui/angular/pwa-configuration.md @@ -284,11 +284,53 @@ In addition to updated icons, the library will generate splash screens. However, ## 3. Configure Service Worker -### 3.1 Modify Asset Groups +Once the PWA schematic is installed and the manifest is customized, you should review and tune the Angular service worker configuration. -Angular has defined some static files to be cached by the service worker, but they are not 100% accurate. Let's change it. +The configuration lives in `ngsw-config.json` and controls **what is cached**, **how it is cached**, and **for how long**. See Angular’s [service worker configuration](https://angular.dev/ecosystem/service-workers/config) for full details. -Open _ngsw-config.json_ file and replace its content with this: + + +### 3.1. Minimal starter configuration + +This is a **simple, safe default** that works well for most ABP Angular applications: + +```json +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/**", + "/*.(eot|svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" + ] + } + } + ] +} +``` + +- `app` group: prefetches the application shell (HTML, JS, CSS, manifest) so the UI loads quickly and works offline after first visit. +- `assets` group: lazily caches static assets (images, fonts, etc.) as they are requested. + +> **Note**: The `"/*.js"` pattern is intentionally generic to work with modern Angular build outputs. Always adapt patterns to match your actual `dist//browser` files if you customize the build. + + + +### 3.2. Advanced: separate lazy modules + +If your app uses many lazy‑loaded feature modules and you want more control over their caching, you can split them into a dedicated group: ```json { @@ -304,11 +346,10 @@ Open _ngsw-config.json_ file and replace its content with this: "/index.html", "/manifest.webmanifest", "/*.css", - "/common-es2015.*.js", - "/main-es2015.*.js", - "/polyfills-es2015.*.js", - "/runtime-es2015.*.js", - "/vendor-es2015.*.js" + "/main.*.js", + "/polyfills.*.js", + "/runtime.*.js", + "/vendor.*.js" ] } }, @@ -317,14 +358,7 @@ Open _ngsw-config.json_ file and replace its content with this: "installMode": "lazy", "updateMode": "prefetch", "resources": { - "files": [ - "/*-es2015.*.js", - "!/common-es2015.*.js", - "!/main-es2015.*.js", - "!/polyfills-es2015.*.js", - "!/runtime-es2015.*.js", - "!/vendor-es2015.*.js" - ] + "files": ["/*.*.js", "!/main.*.js", "!/polyfills.*.js", "!/runtime.*.js", "!/vendor.*.js"] } }, { @@ -334,7 +368,7 @@ Open _ngsw-config.json_ file and replace its content with this: "resources": { "files": [ "/assets/**", - "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" + "/*.(eot|svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" ] } } @@ -342,8 +376,76 @@ Open _ngsw-config.json_ file and replace its content with this: } ``` -In case you want to cache other static files, please refer to the [service worker configuration document](https://angular.dev/ecosystem/service-workers/config) on Angular.dev. +- `app`: core shell bundles that should always be prefetched. +- `modules`: lazy‑loaded feature bundles that are cached only when actually used, then updated in the background. +- `assets`: all static files. + +For ABP Angular apps that use `index.csr.html` (CSR/SSR setups), you can add it into the `app` group as well: + +```json +"/index.csr.html", +"/index.html", +``` + + + +### 3.3. Example `dataGroups` for API caching + +`dataGroups` control **HTTP request caching**. This is highly application‑specific, but a small, explicit example is very helpful: + +```json +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + // ... + ], + "dataGroups": [ + { + "name": "api", + "urls": ["/api/**"], + "cacheConfig": { + "strategy": "freshness", + "maxSize": 50, + "maxAge": "1h", + "timeout": "5s" + } + } + ] +} +``` + +- `urls`: which HTTP URLs are cached (`/api/**` is an example; narrow this to specific APIs in real apps). +- `strategy: "freshness"`: try network first, fall back to cache if the network is too slow (`timeout`) or offline. +- `maxSize`: maximum number of request entries stored. +- `maxAge`: how long a cached response is considered fresh. + +For endpoints where stale data is acceptable and you want faster responses, you can use `"strategy": "performance"` instead. + +> **Important**: Be careful not to cache authenticated or highly dynamic endpoints unless you fully understand the implications (stale user data, security, GDPR, etc.). + + + +### 3.4. Build and verify + +After changing `ngsw-config.json`: + +1. **Build with service worker enabled** (production config): + + ```bash + ng build --configuration production + ``` + +2. **Serve the built app over HTTP/HTTPS** and open it in the browser. + +3. In Chrome DevTools → **Application**: + - **Service Workers**: ensure `ngsw-worker.js` is _installed_ and _controlling the page_. + - **Manifest**: verify the manifest and that the app is installable. -### 3.2 Set Data Groups +4. **Test offline**: + - Load the app once while online. + - Enable “Offline” in DevTools → Network and reload. + - The shell and static assets configured in `assetGroups` should still work. -This part is unique to your project. We recommend being very careful about which endpoints to cache. Please refer to [service worker configuration document](https://angular.dev/ecosystem/service-workers/config) on Angular.dev for details. +For further customization, refer to the official Angular service worker docs: +[https://angular.dev/ecosystem/service-workers/config](https://angular.dev/ecosystem/service-workers/config). diff --git a/docs/en/low-code/custom-endpoints.md b/docs/en/low-code/custom-endpoints.md index b3af587707..ad0057df70 100644 --- a/docs/en/low-code/custom-endpoints.md +++ b/docs/en/low-code/custom-endpoints.md @@ -106,7 +106,7 @@ The full [Scripting API](scripting-api.md) (`db` object) is available for queryi "route": "/api/custom/products/stats", "method": "GET", "requireAuthentication": false, - "javascript": "var totalCount = await db.count('LowCodeDemo.Products.Product');\nvar productTable = await db.query('LowCodeDemo.Products.Product');\nvar avgPrice = totalCount > 0 ? await productTable.average(p => p.Price) : 0;\nreturn ok({ totalProducts: totalCount, averagePrice: avgPrice });" + "javascript": "var totalCount = await db.count('LowCodeDemo.Products.Product');\nvar avgPrice = totalCount > 0 ? await db.query('LowCodeDemo.Products.Product').average(p => p.Price) : 0;\nreturn ok({ totalProducts: totalCount, averagePrice: avgPrice });" } ``` @@ -118,7 +118,7 @@ The full [Scripting API](scripting-api.md) (`db` object) is available for queryi "route": "/api/custom/customers/search", "method": "GET", "requireAuthentication": true, - "javascript": "var searchTerm = query.q || '';\nvar customerTable = await db.query('LowCodeDemo.Customers.Customer');\nvar customers = await customerTable\n .where(c => c.Name.toLowerCase().includes(searchTerm.toLowerCase()))\n .take(10)\n .toList();\nreturn ok(customers.map(c => ({ id: c.Id, name: c.Name, email: c.EmailAddress })));" + "javascript": "var searchTerm = query.q || '';\nvar customers = await db.query('LowCodeDemo.Customers.Customer')\n .where(c => c.Name.toLowerCase().includes(searchTerm.toLowerCase()))\n .take(10)\n .toList();\nreturn ok(customers.map(c => ({ id: c.Id, name: c.Name, email: c.EmailAddress })));" } ``` diff --git a/docs/en/low-code/scripting-api.md b/docs/en/low-code/scripting-api.md index 5cd790b7aa..a08b1107ec 100644 --- a/docs/en/low-code/scripting-api.md +++ b/docs/en/low-code/scripting-api.md @@ -21,8 +21,7 @@ The `db` object is the main entry point for all data operations. ```javascript // Immutable pattern — each call creates a new builder -var entityTable = await db.query('Entity'); -var baseQuery = await entityTable.where(x => x.Active); +var baseQuery = db.query('Entity').where(x => x.Active); var cheap = baseQuery.where(x => x.Price < 100); // baseQuery unchanged var expensive = baseQuery.where(x => x.Price > 500); // baseQuery unchanged ``` @@ -32,15 +31,13 @@ var expensive = baseQuery.where(x => x.Price > 500); // baseQuery unchanged ### Basic Queries ```javascript -var productTable = await db.query('LowCodeDemo.Products.Product'); -var products = await productTable +var products = await db.query('LowCodeDemo.Products.Product') .where(x => x.Price > 100) .orderBy(x => x.Price) .take(10) .toList(); -var resultTable = await db.query('LowCodeDemo.Products.Product'); -var result = await resultTable +var result = await db.query('LowCodeDemo.Products.Product') .where(x => x.Price > 100 && x.Price < 500) .where(x => x.StockCount > 0) .orderByDescending(x => x.Price) @@ -66,12 +63,9 @@ var result = await resultTable | `all(x => condition)` | Check if all records match | `Promise` | | `isEmpty()` | Check if no results | `Promise` | | `isSingle()` | Check if exactly one result | `Promise` | -| `first()` | Return first match, throws if empty | `Promise` | -| `firstOrDefault()` | Return first match or null | `Promise` | -| `last()` | Return last match, throws if empty | `Promise` | -| `lastOrDefault()` | Return last match or null | `Promise` | -| `single()` | Return single match, throws if empty/multiple | `Promise` | -| `singleOrDefault()` | Return single match or null (throws if multiple) | `Promise` | +| `first()` / `firstOrDefault()` | Return first match or null | `Promise` | +| `last()` / `lastOrDefault()` | Return last match or null | `Promise` | +| `single()` / `singleOrDefault()` | Return single match or null | `Promise` | | `elementAt(index)` | Return element at index | `Promise` | | `select(x => projection)` | Project to custom shape | `QueryBuilder` | | `join(entity, alias, condition)` | Inner join | `QueryBuilder` | @@ -98,18 +92,16 @@ var minPrice = 100; var config = { minStock: 10 }; var nested = { range: { min: 50, max: 200 } }; -var entityTable = await db.query('Entity'); -var result = await entityTable.where(x => x.Price > minPrice).toList(); -var result2 = await entityTable.where(x => x.StockCount > config.minStock).toList(); -var result3 = await entityTable.where(x => x.Price >= nested.range.min).toList(); +var result = await db.query('Entity').where(x => x.Price > minPrice).toList(); +var result2 = await db.query('Entity').where(x => x.StockCount > config.minStock).toList(); +var result3 = await db.query('Entity').where(x => x.Price >= nested.range.min).toList(); ``` ### Contains / IN Operator ```javascript var targetPrices = [50, 100, 200]; -var entityTable = await db.query('Entity'); -var products = await entityTable +var products = await db.query('Entity') .where(x => targetPrices.includes(x.Price)) .toList(); ``` @@ -117,8 +109,7 @@ var products = await entityTable ### Select Projection ```javascript -var productTable = await db.query('LowCodeDemo.Products.Product'); -var projected = await productTable +var projected = await db.query('LowCodeDemo.Products.Product') .where(x => x.Price > 0) .select(x => ({ ProductName: x.Name, ProductPrice: x.Price })) .toList(); @@ -129,8 +120,7 @@ var projected = await productTable ### Explicit Joins ```javascript -var orderLineTable = await db.query('LowCodeDemo.Orders.OrderLine'); -var orderLines = await orderLineTable +var orderLines = await db.query('LowCodeDemo.Orders.OrderLine') .join('LowCodeDemo.Products.Product', 'p', (ol, p) => ol.ProductId === p.Id) .take(10) .toList(); @@ -145,8 +135,7 @@ orderLines.forEach(line => { ### Left Join ```javascript -var orderTable = await db.query('LowCodeDemo.Orders.Order'); -var orders = await orderTable +var orders = await db.query('LowCodeDemo.Orders.Order') .leftJoin('LowCodeDemo.Products.Product', 'p', (o, p) => o.CustomerId === p.Id) .toList(); @@ -160,22 +149,18 @@ orders.forEach(order => { ### LINQ-Style Join ```javascript -var orderTable = await db.query('Order'); -await orderTable.join( - 'LowCodeDemo.Products.Product', - o => o.ProductId, - p => p.Id -); +db.query('Order') + .join('LowCodeDemo.Products.Product', + o => o.ProductId, + p => p.Id) ``` ### Join with Filtered Query ```javascript -var productTable = await db.query('Product'); -var expensiveProducts = await productTable.where(p => p.Price > 100); +var expensiveProducts = db.query('Product').where(p => p.Price > 100); -var orderLineTable = await db.query('OrderLine'); -var orders = await orderLineTable +var orders = await db.query('OrderLine') .join(expensiveProducts, ol => ol.ProductId, p => p.Id) @@ -194,9 +179,8 @@ Set operations execute at the database level using SQL: | `except(query)` | `EXCEPT` | Elements in first, not second | ```javascript -var productTable = await db.query('Product'); -var cheap = await productTable.where(x => x.Price <= 100); -var popular = await productTable.where(x => x.Rating > 4); +var cheap = db.query('Product').where(x => x.Price <= 100); +var popular = db.query('Product').where(x => x.Rating > 4); var bestDeals = await cheap.intersect(popular).toList(); var underrated = await cheap.except(popular).toList(); @@ -216,17 +200,15 @@ All aggregations execute as SQL statements: | `groupBy(x => x.Property)` | `GROUP BY ...` | `Promise` | ```javascript -var productTable = await db.query('Product'); -var totalValue = await productTable.sum(x => x.Price); -var avgPrice = await (await productTable.where(x => x.InStock)).average(x => x.Price); -var cheapest = await productTable.min(x => x.Price); +var totalValue = await db.query('Product').sum(x => x.Price); +var avgPrice = await db.query('Product').where(x => x.InStock).average(x => x.Price); +var cheapest = await db.query('Product').min(x => x.Price); ``` ### GroupBy with Select ```javascript -var productTable = await db.query('Product'); -var grouped = await productTable +var grouped = await db.query('Product') .groupBy(x => x.Category) .select(g => ({ Category: g.Key, @@ -255,8 +237,7 @@ var grouped = await productTable ### GroupBy with Items ```javascript -var productTable = await db.query('Product'); -var grouped = await productTable +var grouped = await db.query('Product') .groupBy(x => x.Category) .select(g => ({ Category: g.Key, @@ -277,12 +258,11 @@ var grouped = await productTable Math functions translate to SQL functions (ROUND, FLOOR, CEILING, ABS, etc.): ```javascript -var productTable = await db.query('Product'); -var products = await productTable +var products = await db.query('Product') .where(x => Math.round(x.Price) > 100) .toList(); -var result = await productTable +var result = await db.query('Product') .where(x => Math.abs(x.Balance) < 10 && Math.floor(x.Rating) >= 4) .toList(); ``` @@ -400,8 +380,7 @@ All values are parameterized: ```javascript var malicious = "'; DROP TABLE Products;--"; // Safely treated as a literal string — no injection -var entityTable = await db.query('Entity'); -var result = await (await entityTable.where(x => x.Name.includes(malicious))).count(); +var result = await db.query('Entity').where(x => x.Name.includes(malicious)).count(); ``` ### Blocked Features @@ -418,8 +397,7 @@ if (!context.commandArgs.getValue('Email').includes('@')) { // Try-catch for safe execution try { - var entityTable = await db.query('Entity'); - var products = await entityTable.where(x => x.Price > 0).toList(); + var products = await db.query('Entity').where(x => x.Price > 0).toList(); } catch (error) { context.log('Query failed: ' + error.message); } @@ -444,8 +422,7 @@ try { var productId = context.commandArgs.getValue('ProductId'); var quantity = context.commandArgs.getValue('Quantity'); -var productTable = await db.query('LowCodeDemo.Products.Product'); -var product = await productTable +var product = await db.query('LowCodeDemo.Products.Product') .where(x => x.Id === productId) .first(); @@ -458,10 +435,11 @@ context.commandArgs.setValue('TotalAmount', product.Price * quantity); ### Sales Dashboard (Custom Endpoint) ```javascript -var orderTable = await db.query('LowCodeDemo.Orders.Order'); -var totalOrders = await orderTable.count(); -var delivered = await (await orderTable.where(x => x.IsDelivered === true)).count(); -var revenue = await (await orderTable.where(x => x.IsDelivered === true)).sum(x => x.TotalAmount); +var totalOrders = await db.query('LowCodeDemo.Orders.Order').count(); +var delivered = await db.query('LowCodeDemo.Orders.Order') + .where(x => x.IsDelivered === true).count(); +var revenue = await db.query('LowCodeDemo.Orders.Order') + .where(x => x.IsDelivered === true).sum(x => x.TotalAmount); return ok({ orders: totalOrders, diff --git a/docs/en/modules/operation-rate-limiting.md b/docs/en/modules/operation-rate-limiting.md new file mode 100644 index 0000000000..f285793695 --- /dev/null +++ b/docs/en/modules/operation-rate-limiting.md @@ -0,0 +1,741 @@ +````json +//[doc-seo] +{ + "Description": "Learn how to use the Operation Rate Limiting module (Pro) in ABP to control the frequency of specific operations like SMS sending, login attempts, and resource-intensive tasks." +} +```` + +# Operation Rate Limiting Module (Pro) + +> You must have an [ABP Team or a higher license](https://abp.io/pricing) to use this module. + +ABP provides an operation rate limiting system that allows you to control the frequency of specific operations in your application. You may need operation rate limiting for several reasons: + +* Do not allow sending an SMS verification code to the same phone number more than 3 times in an hour. +* Do not allow generating a "monthly sales report" more than 2 times per day for each user (if generating the report is resource-intensive). +* Restrict login attempts per IP address to prevent brute-force attacks. + +> This is not for [ASP.NET Core's built-in rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) which works at the HTTP request pipeline level. This module works at the **application/domain code level** and is called explicitly from your services. See the [Combining with ASP.NET Core Rate Limiting](#combining-with-aspnet-core-rate-limiting) section for a comparison. + +## How to Install + +This module is used by the [Account (Pro)](account-pro.md) module internally and comes pre-installed in the latest [startup templates](../solution-templates). So, no need to manually install it. + +## Quick Start + +This section shows the basic usage of the operation rate limiting system with a simple example. + +### Defining a Policy + +First, define a rate limiting policy in the `ConfigureServices` method of your [module class](../framework/architecture/modularity/basics.md): + +````csharp +Configure(options => +{ + options.AddPolicy("SendSmsCode", policy => + { + policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByParameter(); + }); +}); +```` + +* `"SendSmsCode"` is a unique name for this policy. +* `WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1)` means at most **1 request per minute**. +* `PartitionByParameter()` means the counter is keyed by the parameter you pass at check time (e.g., a phone number), so different phone numbers have independent counters. + +### Checking the Limit + +Then inject `IOperationRateLimitingChecker` and call `CheckAsync` in your service: + +````csharp +public class SmsAppService : ApplicationService +{ + private readonly IOperationRateLimitingChecker _rateLimitChecker; + + public SmsAppService(IOperationRateLimitingChecker rateLimitChecker) + { + _rateLimitChecker = rateLimitChecker; + } + + public virtual async Task SendCodeAsync(string phoneNumber) + { + await _rateLimitChecker.CheckAsync("SendSmsCode", phoneNumber); + + // If we reach here, the limit was not exceeded. + // Send the SMS code... + } +} +```` + +* `CheckAsync` increments the counter and throws `AbpOperationRateLimitingException` (HTTP 429) if the limit is exceeded. +* Each phone number has its own counter because we used `PartitionByParameter()`. +* Passing `phoneNumber` directly is a shortcut for `new OperationRateLimitingContext { Parameter = phoneNumber }`. Extension methods are provided for all four methods (`CheckAsync`, `IsAllowedAsync`, `GetStatusAsync`, `ResetAsync`) when you only need to pass a `parameter` string. + +That's the basic usage. The following sections explain each concept in detail. + +## Declarative Usage (Attribute) + +Instead of injecting `IOperationRateLimitingChecker` manually, you can use the `[OperationRateLimiting]` attribute to enforce a policy declaratively on Application Service methods or MVC Controller actions. + +> **Application Services** are handled by the ABP interceptor (built into the Domain layer). +> **MVC Controllers** are handled by `AbpOperationRateLimitingActionFilter`, which is automatically registered when you reference the `Volo.Abp.OperationRateLimiting.AspNetCore` package. + +### Applying to an Application Service + +````csharp +public class SmsAppService : ApplicationService +{ + [OperationRateLimiting("SendSmsCode")] + public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber) + { + // Rate limit is checked automatically before this line executes. + await _smsSender.SendAsync(phoneNumber, GenerateCode()); + } +} +```` + +### Applying to an MVC Controller + +````csharp +[Route("api/account")] +public class AccountController : AbpController +{ + [HttpPost("send-sms-code")] + [OperationRateLimiting("SendSmsCode")] + public async Task SendSmsCodeAsync([RateLimitingParameter] string phoneNumber) + { + // Rate limit is checked automatically before this line executes. + await _smsSender.SendAsync(phoneNumber, GenerateCode()); + return Ok(); + } +} +```` + +### Resolving the Parameter Value + +The `[OperationRateLimiting]` attribute resolves `OperationRateLimitingContext.Parameter` automatically using the following priority order: + +1. **`[RateLimitingParameter]`** — a method parameter marked with this attribute. Its `ToString()` value is used as the partition key. +2. **`IHasOperationRateLimitingParameter`** — a method parameter whose type implements this interface. The value returned by `GetPartitionParameter()` is used as the partition key. +3. **`null`** — no parameter is resolved; suitable for policies that use `PartitionByCurrentUser`, `PartitionByClientIp`, etc. + +#### Using `[RateLimitingParameter]` + +Mark a single parameter to use its value as the partition key: + +````csharp +[OperationRateLimiting("SendSmsCode")] +public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber) +{ + // partition key = phoneNumber +} +```` + +#### Using `IHasOperationRateLimitingParameter` + +Implement the interface on an input DTO when the partition key is a property of the DTO: + +````csharp +public class SendSmsCodeInput : IHasOperationRateLimitingParameter +{ + public string PhoneNumber { get; set; } + public string Language { get; set; } + + public string? GetPartitionParameter() => PhoneNumber; +} +```` + +````csharp +[OperationRateLimiting("SendSmsCode")] +public virtual async Task SendCodeAsync(SendSmsCodeInput input) +{ + // partition key = input.GetPartitionParameter() = input.PhoneNumber +} +```` + +#### No Partition Parameter + +If no parameter is marked and no DTO implements the interface, the policy is checked without a `Parameter` value. This is appropriate for policies that use `PartitionByCurrentUser`, `PartitionByClientIp`, or `PartitionByCurrentTenant`: + +````csharp +// Policy uses PartitionByCurrentUser — no explicit parameter needed. +[OperationRateLimiting("GenerateReport")] +public virtual async Task GenerateMonthlyReportAsync() +{ + // Rate limit is checked per current user automatically. +} +```` + +> If the method has parameters but none is resolved, a **warning log** is emitted to help you catch misconfigured usages early. + +### Applying to a Class + +You can also place `[OperationRateLimiting]` on the class to apply it to **all public methods** of that class: + +````csharp +[OperationRateLimiting("MyServiceLimit")] +public class MyAppService : ApplicationService +{ + public virtual async Task MethodAAsync([RateLimitingParameter] string key) { ... } + + public virtual async Task MethodBAsync([RateLimitingParameter] string key) { ... } +} +```` + +> A method-level attribute takes precedence over the class-level attribute. + +## Defining Policies + +Policies are defined using `AbpOperationRateLimitingOptions` in the `ConfigureServices` method of your [module class](../framework/architecture/modularity/basics.md). Each policy has a unique name, one or more rules, and a partition strategy. + +### Single-Rule Policies + +For simple scenarios, use the `WithFixedWindow` shortcut directly on the policy builder: + +````csharp +options.AddPolicy("SendSmsCode", policy => +{ + policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByParameter(); +}); +```` + +### Multi-Rule Policies + +Use `AddRule` to combine multiple rules. All rules are checked together (**AND** logic) — a request is allowed only when **all** rules pass: + +````csharp +options.AddPolicy("Login", policy => +{ + // Rule 1: Max 5 attempts per 5 minutes per username + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 5) + .PartitionByParameter()); + + // Rule 2: Max 20 attempts per hour per IP + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20) + .PartitionByClientIp()); +}); +```` + +> When multiple rules are present, the module uses a **two-phase check**: it first verifies all rules without incrementing counters, then increments only if all rules pass. This prevents wasted quota when one rule would block the request. + +### Overriding an Existing Policy + +If a reusable module (e.g., ABP's Account module) defines a policy with default rules, you have two ways to customize it in your own module's `ConfigureServices`. + +**Option 1 — Full replacement with `AddPolicy`:** + +Call `AddPolicy` with the same name. The last registration wins and completely replaces all rules: + +````csharp +// In your application module — runs after the Account module +Configure(options => +{ + options.AddPolicy("Account.SendPasswordResetCode", policy => + { + // Replaces all rules defined by the Account module for this policy + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3) + .PartitionByEmail()); + }); +}); +```` + +> `AddPolicy` stores policies in a dictionary keyed by name, so calling it again with the same name fully replaces the previous policy and all its rules. + +**Option 2 — Partial modification with `ConfigurePolicy`:** + +Use `ConfigurePolicy` to modify an existing policy without replacing it entirely. The builder is pre-populated with the existing rules, so you only need to express what changes: + +````csharp +Configure(options => +{ + // Only override the error code, keeping the module's original rules + options.ConfigurePolicy("Account.SendPasswordResetCode", policy => + { + policy.WithErrorCode("MyApp:SmsCodeLimit"); + }); +}); +```` + +You can also add a rule on top of the existing ones: + +````csharp +options.ConfigurePolicy("Account.SendPasswordResetCode", policy => +{ + // Keep the module's per-email rule and add a per-IP rule on top + policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20) + .PartitionByClientIp()); +}); +```` + +Or clear all inherited rules first and define entirely new ones using `ClearRules()`: + +````csharp +options.ConfigurePolicy("Account.SendPasswordResetCode", policy => +{ + policy.ClearRules() + .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3) + .PartitionByEmail(); +}); +```` + +`ConfigurePolicy` returns `AbpOperationRateLimitingOptions`, so you can chain multiple calls: + +````csharp +options + .ConfigurePolicy("Account.SendPasswordResetCode", p => p.WithErrorCode("MyApp:SmsLimit")) + .ConfigurePolicy("Account.Login", p => p.WithErrorCode("MyApp:LoginLimit")); +```` + +> `ConfigurePolicy` throws `AbpException` if the policy name is not found. Use `AddPolicy` first (in the module that owns the policy), then `ConfigurePolicy` in downstream modules to customize it. + +### Custom Error Code + +By default, the exception uses the error code `Volo.Abp.OperationRateLimiting:010001`. You can override it per policy: + +````csharp +options.AddPolicy("SendSmsCode", policy => +{ + policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByParameter() + .WithErrorCode("App:SmsCodeLimit"); +}); +```` + +## Partition Types + +Each rule must specify a **partition type** that determines how requests are grouped. Requests with different partition keys have independent counters. + +### PartitionByParameter + +Uses the `Parameter` value from the context you pass to `CheckAsync`: + +````csharp +policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByParameter(); + +// Each phone number has its own counter +await checker.CheckAsync("SendSmsCode", + new OperationRateLimitingContext { Parameter = phoneNumber }); +```` + +### PartitionByCurrentUser + +Uses `ICurrentUser.Id` as the partition key. The user must be authenticated: + +````csharp +policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 10) + .PartitionByCurrentUser(); +```` + +> If you need to check rate limits for a specific user (e.g., admin checking another user's limit), use `PartitionByParameter()` and pass the user ID as the `Parameter`. + +### PartitionByCurrentTenant + +Uses `ICurrentTenant.Id` as the partition key. Uses `"host"` for the host side when no tenant is active: + +````csharp +policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionByCurrentTenant(); +```` + +### PartitionByClientIp + +Uses `IWebClientInfoProvider.ClientIpAddress` as the partition key: + +````csharp +policy.WithFixedWindow(TimeSpan.FromMinutes(15), maxCount: 10) + .PartitionByClientIp(); +```` + +> This requires an ASP.NET Core environment. In non-web scenarios, the IP address cannot be determined and an exception will be thrown. Use `PartitionByParameter()` if you need to pass the IP explicitly. + +### PartitionByEmail + +Resolves from `context.Parameter` first, then falls back to `ICurrentUser.Email`: + +````csharp +policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1) + .PartitionByEmail(); + +// For unauthenticated users, pass the email explicitly: +await checker.CheckAsync("SendEmailCode", + new OperationRateLimitingContext { Parameter = email }); +```` + +### PartitionByPhoneNumber + +Works the same way as `PartitionByEmail`: resolves from `context.Parameter` first, then falls back to `ICurrentUser.PhoneNumber`. + +### Custom Partition (PartitionBy) + +You can register a named custom resolver to generate the partition key. The resolver is an async function, so you can perform database queries or other I/O operations. Because the resolver is stored by name (not as an anonymous delegate), it can be serialized and managed from a UI or database. + +**Step 1 — Register the resolver by name:** + +````csharp +Configure(options => +{ + options.AddPartitionKeyResolver("ByDevice", ctx => + Task.FromResult($"{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); +}); +```` + +**Step 2 — Reference it in a policy:** + +````csharp +policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionBy("ByDevice"); +```` + +You can also register and reference in one step (inline): + +````csharp +policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionBy("ByDevice", ctx => + Task.FromResult($"{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); +```` + +> If you call `PartitionBy("name")` with a resolver name that hasn't been registered, an exception is thrown at configuration time (not at runtime), so typos are caught early. + +To replace an existing resolver (e.g., in a downstream module), use `ReplacePartitionKeyResolver`: + +````csharp +options.ReplacePartitionKeyResolver("ByDevice", ctx => + Task.FromResult($"v2:{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); +```` + +### Named Rules (WithName) + +By default, a rule's store key is derived from its `Duration`, `MaxCount`, and `PartitionType`. This means that if you change a rule's parameters (e.g., increase `maxCount` from 5 to 10), the counter resets because the key changes. + +To keep a stable key across parameter changes, give the rule a name: + +````csharp +policy.AddRule(rule => rule + .WithName("HourlyLimit") + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionByCurrentUser()); +```` + +When a name is set, it is used as the store key instead of the content-based descriptor. This is particularly useful when rules are managed from a database or UI — changing the `maxCount` or `duration` will not reset existing counters. + +> Rule names must be unique within a policy. Duplicate names cause an exception at build time. + +## Multi-Tenancy + +By default, partition keys do not include tenant information — for partition types like `PartitionByParameter`, `PartitionByCurrentUser`, `PartitionByClientIp`, etc., counters are shared across tenants unless you call `WithMultiTenancy()`. Note that `PartitionByCurrentTenant()` is inherently per-tenant since the partition key is the tenant ID itself, and `PartitionByClientIp()` is typically kept global since the same IP should share a counter regardless of tenant. + +You can enable tenant isolation for a rule by calling `WithMultiTenancy()`: + +````csharp +policy.AddRule(rule => rule + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 5) + .WithMultiTenancy() + .PartitionByParameter()); +```` + +When multi-tenancy is enabled, the cache key includes the tenant ID, so each tenant has independent counters: + +* **Global key format:** `orl:{PolicyName}:{RuleKey}:{PartitionKey}` +* **Tenant-isolated key format:** `orl:t:{TenantId}:{PolicyName}:{RuleKey}:{PartitionKey}` + +## Checking the Limit + +Inject `IOperationRateLimitingChecker` to interact with rate limits. It provides four methods: + +### CheckAsync + +The primary method. It checks the rate limit and **increments the counter** if allowed. Throws `AbpOperationRateLimitingException` (HTTP 429) if the limit is exceeded: + +````csharp +await checker.CheckAsync("SendSmsCode", + new OperationRateLimitingContext { Parameter = phoneNumber }); +```` + +### IsAllowedAsync + +A read-only check that returns `true` or `false` **without incrementing** the counter. Useful for UI pre-checks (e.g., disabling a button before the user clicks): + +````csharp +var isAllowed = await checker.IsAllowedAsync("SendSmsCode", + new OperationRateLimitingContext { Parameter = phoneNumber }); +```` + +### GetStatusAsync + +Returns detailed status information **without incrementing** the counter: + +````csharp +var status = await checker.GetStatusAsync("SendSmsCode", + new OperationRateLimitingContext { Parameter = phoneNumber }); + +// status.IsAllowed - whether the next request would be allowed +// status.RemainingCount - how many requests are left in this window +// status.RetryAfter - time until the window resets +// status.MaxCount - maximum allowed count +// status.CurrentCount - current usage count +```` + +### ResetAsync + +Resets the counter for a specific policy and context. This can be useful for administrative operations: + +````csharp +await checker.ResetAsync("SendSmsCode", + new OperationRateLimitingContext { Parameter = phoneNumber }); +```` + +## The Exception + +When a rate limit is exceeded, `CheckAsync` throws `AbpOperationRateLimitingException`. This exception: + +* Extends `BusinessException` and implements `IHasHttpStatusCode` with status code **429** (Too Many Requests). +* Is automatically handled by ABP's exception handling pipeline and serialized into the HTTP response. + +The exception uses one of two error codes depending on the policy type: + +| Error Code | Constant | When Used | +|---|---|---| +| `Volo.Abp.OperationRateLimiting:010001` | `AbpOperationRateLimitingErrorCodes.ExceedLimit` | Regular rate limit exceeded (has a retry-after window) | +| `Volo.Abp.OperationRateLimiting:010002` | `AbpOperationRateLimitingErrorCodes.ExceedLimitPermanently` | Ban policy (`maxCount: 0`, permanently denied) | + +You can override the error code per policy using `WithErrorCode()`. When a custom code is set, it is always used regardless of the policy type. + +The exception includes the following data properties: + +| Key | Type | Description | +|-----|------|-------------| +| `PolicyName` | string | Name of the triggered policy | +| `MaxCount` | int | Maximum allowed count | +| `CurrentCount` | int | Current usage count | +| `RemainingCount` | int | Remaining allowed count | +| `RetryAfterSeconds` | int | Seconds until the window resets (`0` for ban policies) | +| `RetryAfterMinutes` | int | Minutes until the window resets, rounded down (`0` for ban policies) | +| `RetryAfter` | string | Localized retry-after description (e.g., "5 minutes"); absent for ban policies | +| `WindowDurationSeconds` | int | Total window duration in seconds | +| `WindowDescription` | string | Localized window description | +| `RuleDetails` | List | Per-rule details (for multi-rule policies) | + +## Configuration + +### AbpOperationRateLimitingOptions + +`AbpOperationRateLimitingOptions` is the main options class for the operation rate limiting system: + +````csharp +Configure(options => +{ + options.IsEnabled = true; + options.LockTimeout = TimeSpan.FromSeconds(5); +}); +```` + +* **`IsEnabled`** (`bool`, default: `true`): Global switch to enable or disable rate limiting. When set to `false`, all `CheckAsync` calls pass through without checking. This is useful for disabling rate limiting in development (see [below](#disabling-in-development)). +* **`LockTimeout`** (`TimeSpan`, default: `5 seconds`): Timeout for acquiring the distributed lock during counter increment operations. + +## Advanced Usage + +### Disabling in Development + +You may want to disable rate limiting during development to avoid being blocked while testing: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var hostEnvironment = context.Services.GetHostingEnvironment(); + + Configure(options => + { + if (hostEnvironment.IsDevelopment()) + { + options.IsEnabled = false; + } + }); +} +```` + +### Ban Policy (maxCount: 0) + +Setting `maxCount` to `0` creates a ban policy that permanently denies all requests regardless of the window duration. The `RetryAfter` value will be `null` since there is no window to wait for. The exception uses the error code `Volo.Abp.OperationRateLimiting:010002` (`AbpOperationRateLimitingErrorCodes.ExceedLimitPermanently`) with the message "Operation rate limit exceeded. This request is permanently denied.": + +````csharp +options.AddPolicy("BlockedUser", policy => +{ + policy.WithFixedWindow(TimeSpan.FromHours(24), maxCount: 0) + .PartitionByParameter(); +}); +```` + +### Passing Extra Properties + +Use `ExtraProperties` on `OperationRateLimitingContext` to pass additional context data. These values are available in custom partition resolvers and are included in the exception data when the limit is exceeded: + +````csharp +await checker.CheckAsync("ApiCall", new OperationRateLimitingContext +{ + Parameter = apiEndpoint, + ExtraProperties = + { + ["DeviceId"] = deviceId, + ["ClientVersion"] = clientVersion + } +}); +```` + +### Pre-checking Before Expensive Operations + +Use `IsAllowedAsync` or `GetStatusAsync` to check the limit **before** performing expensive work (e.g., validating input or querying the database): + +````csharp +public async Task SendCodeAsync(string phoneNumber) +{ + var context = new OperationRateLimitingContext { Parameter = phoneNumber }; + + // Check limit before doing any work + var status = await _rateLimitChecker.GetStatusAsync("SendSmsCode", context); + + if (!status.IsAllowed) + { + return new SendCodeResultDto + { + Success = false, + RetryAfterSeconds = (int)(status.RetryAfter?.TotalSeconds ?? 0) + }; + } + + // Now do the actual work and increment the counter + await _rateLimitChecker.CheckAsync("SendSmsCode", context); + + await _smsSender.SendAsync(phoneNumber, GenerateCode()); + return new SendCodeResultDto { Success = true }; +} +```` + +> `IsAllowedAsync` and `GetStatusAsync` are read-only — they do not increment the counter. Only `CheckAsync` increments. + +### Checking on Behalf of Another User + +`PartitionByCurrentUser()`, `PartitionByCurrentTenant()`, and `PartitionByClientIp()` always resolve from their respective services (`ICurrentUser`, `ICurrentTenant`, `IWebClientInfoProvider`) and do not accept explicit overrides. This design avoids partition key conflicts in [composite policies](#multi-rule-policies) where `Parameter` is shared across all rules. + +If you need to check or enforce rate limits for a **specific user, tenant, or IP**, define the policy with `PartitionByParameter()` and pass the value explicitly: + +````csharp +// Policy definition: use PartitionByParameter for explicit control +options.AddPolicy("UserApiLimit", policy => +{ + policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionByParameter(); +}); +```` + +````csharp +// Check current user's limit +await checker.CheckAsync("UserApiLimit", + new OperationRateLimitingContext { Parameter = CurrentUser.Id.ToString() }); + +// Admin checking another user's limit +await checker.CheckAsync("UserApiLimit", + new OperationRateLimitingContext { Parameter = targetUserId.ToString() }); + +// Check a specific IP in a background job +await checker.CheckAsync("UserApiLimit", + new OperationRateLimitingContext { Parameter = ipAddress }); +```` + +This approach gives you full flexibility while keeping the API simple — `PartitionByCurrentUser()` is a convenience shortcut for "always use the current authenticated user", and `PartitionByParameter()` is for "I want to specify the value explicitly". + +### Combining with ASP.NET Core Rate Limiting + +This module and ASP.NET Core's built-in [rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) serve different purposes and can be used together: + +| | ASP.NET Core Rate Limiting | Operation Rate Limiting | +|---|---|---| +| **Level** | HTTP request pipeline | Application/domain code | +| **Scope** | All incoming requests | Specific business operations | +| **Usage** | Middleware (automatic) | `[OperationRateLimiting]` attribute or explicit `CheckAsync` calls | +| **Typical use** | API throttling, DDoS protection | Business logic limits (SMS, reports) | + +A common pattern is to use ASP.NET Core middleware for broad API protection and this module for fine-grained business operation limits. + +## Extensibility + +### Custom Store + +The default store uses ABP's `IDistributedCache`. You can replace it by implementing `IOperationRateLimitingStore`: + +````csharp +public class MyCustomStore : IOperationRateLimitingStore, ITransientDependency +{ + public Task IncrementAsync( + string key, TimeSpan duration, int maxCount) + { + // Your custom implementation (e.g., Redis Lua script for atomicity) + } + + public Task GetAsync( + string key, TimeSpan duration, int maxCount) + { + // Read-only check + } + + public Task ResetAsync(string key) + { + // Reset the counter + } +} +```` + +ABP's [dependency injection](../framework/fundamentals/dependency-injection.md) system will automatically use your implementation since it replaces the default one. + +### Custom Rule + +You can implement custom rate limiting algorithms (e.g., sliding window, token bucket) by implementing `IOperationRateLimitingRule` and registering it with `AddRule()`: + +````csharp +policy.AddRule(); +```` + +### Custom Formatter + +Replace `IOperationRateLimitingFormatter` to customize how time durations are displayed in error messages (e.g., "5 minutes", "2 hours 30 minutes"). + +### Custom Policy Provider + +Replace `IOperationRateLimitingPolicyProvider` to load policies from a database or external configuration source instead of the in-memory options. + +When loading pre-built policies from an external source, use the `AddPolicy` overload that accepts an `OperationRateLimitingPolicy` object directly (bypassing the builder): + +````csharp +options.AddPolicy(new OperationRateLimitingPolicy +{ + Name = "DynamicPolicy", + Rules = + [ + new OperationRateLimitingRuleDefinition + { + Name = "HourlyLimit", + Duration = TimeSpan.FromHours(1), + MaxCount = 100, + PartitionType = OperationRateLimitingPartitionType.CurrentUser + } + ] +}); +```` + +To remove a policy (e.g., when it is deleted from the database), use `RemovePolicy`: + +````csharp +options.RemovePolicy("DynamicPolicy"); +```` + +## See Also + +* [ASP.NET Core Rate Limiting Middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) +* [Distributed Caching](../framework/fundamentals/caching.md) +* [Exception Handling](../framework/fundamentals/exception-handling.md) diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index 90ae4ffab1..6b453dc1bd 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -1,21 +1,42 @@ +```json +//[doc-seo] +{ + "Description": "Explore the latest package version changes in ABP Framework, detailing updates and improvements for enhanced development efficiency." +} +``` + # Package Version Changes -## 10.2.0-rc.3 +## 10.3.0-rc.1 | Package | Old Version | New Version | PR | |---------|-------------|-------------|-----| -| Scriban | 6.3.0 | 6.6.0 | #25122 | +| Microsoft.IdentityModel.JsonWebTokens | 8.14.0 | 8.16.0 | #25068 | +| Microsoft.IdentityModel.Protocols.OpenIdConnect | 8.14.0 | 8.16.0 | #25068 | +| Microsoft.IdentityModel.Tokens | 8.14.0 | 8.16.0 | #25068 | +| MongoDB.Driver | 3.7.0 | 3.7.1 | #25114 | +| System.IdentityModel.Tokens.Jwt | 8.14.0 | 8.16.0 | #25068 | +| TickerQ | 10.1.1 | 10.2.0 | #25091 | +| TickerQ.Dashboard | 10.1.1 | 10.2.0 | #25091 | +| TickerQ.EntityFrameworkCore | 10.1.1 | 10.2.0 | #25091 | +| TickerQ.Utilities | 10.1.1 | 10.2.0 | #25091 | +| OpenIddict.Abstractions | 7.2.0 | 7.3.0 | #25053 | +| OpenIddict.Core | 7.2.0 | 7.3.0 | #25053 | +| OpenIddict.Server.AspNetCore | 7.2.0 | 7.3.0 | #25053 | +| OpenIddict.Validation.AspNetCore | 7.2.0 | 7.3.0 | #25053 | +| OpenIddict.Validation.ServerIntegration | 7.2.0 | 7.3.0 | #25053 | -## 10.2.0-rc.1 +## 10.2.0-rc.3 | Package | Old Version | New Version | PR | |---------|-------------|-------------|-----| -| MongoDB.Driver | 3.6.0 | 3.7.0 | #25003 | +| Scriban | 6.3.0 | 6.6.0 | #25122 | -## 10.2.0-preview +## 10.2.0-rc.1 | Package | Old Version | New Version | PR | |---------|-------------|-------------|-----| +| MongoDB.Driver | 3.6.0 | 3.7.0 | #25003 | | Blazorise | 1.8.8 | 2.0.0 | #24906 | | Blazorise.Components | 1.8.8 | 2.0.0 | #24906 | | Blazorise.DataGrid | 1.8.8 | 2.0.0 | #24906 | @@ -31,4 +52,3 @@ |---------|-------------|-------------|-----| | Microsoft.SemanticKernel | 1.67.1 | 1.71.0 | [#24891](https://github.com/abpframework/abp/pull/24891) | | Microsoft.SemanticKernel.Abstractions | 1.67.1 | 1.71.0 | [#24891](https://github.com/abpframework/abp/pull/24891) | - diff --git a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md index c293dd9a37..f19449d04f 100644 --- a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md +++ b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md @@ -6,6 +6,8 @@ The AutoMapper library is **no longer free for commercial use**. For more detail ABP Framework provides both AutoMapper and Mapperly integrations. If your project currently uses AutoMapper and you don't have a commercial license, you can switch to Mapperly by following the steps outlined below. +> **Already have a commercial AutoMapper license?** Use the [Volo.Abp.LuckyPenny.AutoMapper](../../framework/infrastructure/luckypenny-automapper.md) package instead. It is a drop-in replacement for `Volo.Abp.AutoMapper` built on the patched commercial version of AutoMapper, requiring only two changes to your project. + ## Migration Steps Please open your project in an IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform the following global search and replace operations: diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png new file mode 100644 index 0000000000..5941de9284 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png new file mode 100644 index 0000000000..df98abdec4 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png new file mode 100644 index 0000000000..69834ec643 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png new file mode 100644 index 0000000000..e45e3248a5 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-blazor-webapp.png new file mode 100644 index 0000000000..5aaabf4ef9 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png new file mode 100644 index 0000000000..07dc46fc76 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png new file mode 100644 index 0000000000..172d06f4d0 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png new file mode 100644 index 0000000000..3dd079b399 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/catalog-module-vs-code-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/catalog-module-vs-code-blazor-webapp.png new file mode 100644 index 0000000000..0ad75a81bf Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/catalog-module-vs-code-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/images/vscode-catalog-index-razor-blazor-webapp.png b/docs/en/tutorials/modular-crm/images/vscode-catalog-index-razor-blazor-webapp.png new file mode 100644 index 0000000000..674b575527 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/vscode-catalog-index-razor-blazor-webapp.png differ diff --git a/docs/en/tutorials/modular-crm/index.md b/docs/en/tutorials/modular-crm/index.md index 5cd7baaecf..b56d059d39 100644 --- a/docs/en/tutorials/modular-crm/index.md +++ b/docs/en/tutorials/modular-crm/index.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { diff --git a/docs/en/tutorials/modular-crm/part-01.md b/docs/en/tutorials/modular-crm/part-01.md index d3b5d64380..00d42fad3c 100644 --- a/docs/en/tutorials/modular-crm/part-01.md +++ b/docs/en/tutorials/modular-crm/part-01.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -35,7 +42,7 @@ In this first part of this tutorial, we will create a new ABP solution with modu Follow the *[Get Started](../../get-started/single-layer-web-application.md)* guide to create a single layer web application with the following configuration: * **Solution name**: `ModularCrm` -* **UI Framework**: {{if UI == "MVC"}}ASP.NET Core MVC / Razor Pages{{else if UI == "NG"}}Angular{{end}} +* **UI Framework**: {{if UI == "MVC"}}ASP.NET Core MVC / Razor Pages{{else if UI == "BlazorWebApp"}}Blazor WebApp{{end}} * **Database Provider**: Entity Framework Core {{if UI == "NG"}}> **Note:** Angular users can continue with the Angular UI steps in the upcoming parts while following the same modularity flow. @@ -72,12 +79,16 @@ Initially, you see a `ModularCrm` solution with two solution folders: If you expand it, you can see the .NET projects (ABP Studio Packages) of the `ModularCrm.Catalog` module: +{{if UI == "MVC"}} ![abp-studio-catalog-module-expanded-in-solution-explorer](images/abp-studio-catalog-module-expanded-in-solution-explorer.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-catalog-module-expanded-in-solution-explorer](images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png) +{{end}} - `ModularCrm.Catalog`: The main module project that contains your [entities](../../framework/architecture/domain-driven-design/entities.md), [application service](../../framework/architecture/domain-driven-design/application-services.md) implementations and other business objects - `ModularCrm.Catalog.Contracts`: Basically contains [application service](../../framework/architecture/domain-driven-design/application-services.md) interfaces and [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md). These interfaces then can be used by client modules for integration purposes or by the user interface to perform use cases related to that module - `ModularCrm.Catalog.Tests`: Unit and integration tests (if you selected the _Include Tests_ option) for that module -- `ModularCrm.Catalog.UI`: Contains user interface pages and components for the module +- {{if UI == "MVC"}}`ModularCrm.Catalog.UI`: Contains user interface pages and components for the module{{else if UI == "BlazorWebApp"}}`ModularCrm.Catalog.Blazor`: Contains Blazor WebApp user interface pages and components for the module{{end}} ## Summary diff --git a/docs/en/tutorials/modular-crm/part-02.md b/docs/en/tutorials/modular-crm/part-02.md index 76c23d5e2e..ef38a09227 100644 --- a/docs/en/tutorials/modular-crm/part-02.md +++ b/docs/en/tutorials/modular-crm/part-02.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -32,7 +39,7 @@ In this part, you will install the `ModularCrm.Catalog` module to the main appli ## Installing the Catalog Module to the Main Application -A module does not contain an executable application inside. The `ModularCrm.Catalog.UI` project is just a class library project, not an executable web application. A module should be installed in an executable application to run it. +A module does not contain an executable application inside. The {{if UI == "MVC"}}`ModularCrm.Catalog.UI`{{else if UI == "BlazorWebApp"}}`ModularCrm.Catalog.Blazor`{{end}} project is just a class library project, not an executable web application. A module should be installed in an executable application to run it. > **Ensure that the web application is not running in [Solution Runner](../../studio/running-applications.md) or in your IDE. Installing a module to a running application will produce errors.** @@ -48,9 +55,13 @@ Select the `ModularCrm.Catalog` module and check the *Install this module* optio When you click the *OK* button, ABP Studio opens the *Install Module* dialog: +{{if UI == "MVC"}} ![abp-studio-module-installation-dialog-for-catalog](images/abp-studio-module-installation-dialog-for-catalog.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-module-installation-dialog-for-catalog](images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png) +{{end}} -Select the `ModularCrm.Catalog` and `ModularCrm.Catalog.UI` packages from the left area and ensure the `ModularCrm` package from the middle area was checked as shown in the preceding figure. Finally, click _OK_. +Select the `ModularCrm.Catalog` and {{if UI == "MVC"}}`ModularCrm.Catalog.UI`{{else if UI == "BlazorWebApp"}}`ModularCrm.Catalog.Blazor`{{end}} packages from the left area. {{if UI == "MVC"}}Ensure `ModularCrm` was checked in the middle area as shown in the preceding figure.{{else if UI == "BlazorWebApp"}}For `ModularCrm.Catalog`, ensure `ModularCrm` is checked. For `ModularCrm.Catalog.Blazor`, ensure both `ModularCrm` and `ModularCrm.Client` are checked in the middle area as shown in the preceding figure.{{end}} Finally, click _OK_. ## Building the Main Application diff --git a/docs/en/tutorials/modular-crm/part-03.md b/docs/en/tutorials/modular-crm/part-03.md index 43457a9cca..52e5098e60 100644 --- a/docs/en/tutorials/modular-crm/part-03.md +++ b/docs/en/tutorials/modular-crm/part-03.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -42,7 +49,11 @@ Open the `ModularCrm.Catalog` module in your favorite IDE. You can right-click t The `ModularCrm.Catalog` .NET solution should look like the following figure: +{{if UI == "MVC"}} ![catalog-module-vs-code](images/catalog-module-vs-code.png) +{{else if UI == "BlazorWebApp"}} +![catalog-module-vs-code](images/catalog-module-vs-code-blazor-webapp.png) +{{end}} Add a new `Product` class under the `ModularCrm.Catalog` project: @@ -351,6 +362,8 @@ public partial class ProductToProductDtoMapper : MapperBase ### Exposing Application Services as HTTP API Controllers +{{if UI == "MVC"}} + > This application doesn't need to expose any functionality as HTTP API, because all the module integration and communication will be done in the same process as a natural aspect of a monolith modular application. However, in this section, we will create HTTP APIs because; > > 1. We will use these HTTP API endpoints in development to create some example data. @@ -358,6 +371,8 @@ public partial class ProductToProductDtoMapper : MapperBase > > So, follow the instructions in this section and expose the product application service as an HTTP API endpoint. +{{end}} + To create HTTP API endpoints for the catalog module, you have two options: * You can create a regular ASP.NET Core Controller class in the `ModularCrm.Catalog` project, inject `IProductAppService` and create wrapper methods for each public method of the product application service. You will do this later while you create the Ordering module. (Also, you can check the `SampleController` class under the **Samples** folder in the `ModularCrm.Catalog` project for an example) @@ -379,6 +394,34 @@ This will tell the ABP framework to create API controllers for the application s Now, ABP will automatically expose the application services defined in the `ModularCrm.Catalog` project as API controllers. The next section will use these API controllers to create some example products. +{{if UI == "BlazorWebApp"}} + +### Configuring Client Proxies for the Catalog Module + +Since the Blazor WebApp template has a separate `ModularCrm.Client` project, configure HTTP client proxies for the Catalog contracts in the `ModularCrmClientModule` class: + +````csharp +using ModularCrm.Catalog; + +[DependsOn( + typeof(CatalogContractsModule) + // ...other dependencies +)] +public class ModularCrmClientModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + ... + context.Services.AddHttpClientProxies(typeof(ModularCrmContractsModule).Assembly); + context.Services.AddHttpClientProxies(typeof(CatalogContractsModule).Assembly); // NEW: ADD HttpClientProxies + } +} +```` + +Also ensure the `ModularCrm.Catalog.Blazor` package is installed for both the `ModularCrm` and `ModularCrm.Client` projects. + +{{end}} + ### Creating Example Products This section will create a few example products using the [Swagger UI](../../framework/api-development/swagger.md). Thus, you will have some sample products to show on the UI. @@ -415,6 +458,8 @@ As a first step, you can stop the application on ABP Studio's Solution Runner if ### Creating the Products Page +{{if UI == "MVC"}} + Open the `ModularCrm.Catalog` .NET solution in your IDE, and find the `Pages/Catalog/Index.cshtml` file under the `ModularCrm.Catalog.UI` project: ![vscode-catalog-cshtml](images/vscode-catalog-cshtml.png) @@ -470,7 +515,50 @@ Here, you simply use the `IProductAppService` to get a list of all products and ```` -Right-click the `ModularCrm` application on ABP Studio's solution runner and select the *Start* command: +{{else if UI == "BlazorWebApp"}} + +Open the `ModularCrm.Catalog` .NET solution in your IDE, and find the `Pages/Catalog/Index.razor` file under the `ModularCrm.Catalog.Blazor` project. +![vscode-catalog-index-razor-blazor-webapp](images/vscode-catalog-index-razor-blazor-webapp.png) + +Replace the `Index.razor` file with the following content: + +````razor +@page "/catalog" +@using System.Collections.Generic +@using System.Threading.Tasks +@using ModularCrm.Catalog +@inject IProductAppService ProductAppService + +

Products

+ + + + + @foreach (var product in Products) + { + + @product.Name (stock: @product.StockCount) + + } + + + + +@code { + private List Products { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Products = await ProductAppService.GetListAsync(); + } +} +```` + +Here, you inject `IProductAppService`, get all products in `OnInitializedAsync`, and then render the result in a simple list. + +{{end}} + +Right-click the `ModularCrm` application on ABP Studio's Solution Runner and select the *Start* command: ![abp-studio-build-and-restart-application](images/abp-studio-build-and-restart-application.png) diff --git a/docs/en/tutorials/modular-crm/part-04.md b/docs/en/tutorials/modular-crm/part-04.md index 26e312f05a..db119eed31 100644 --- a/docs/en/tutorials/modular-crm/part-04.md +++ b/docs/en/tutorials/modular-crm/part-04.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -47,10 +54,12 @@ That command opens a dialog to define the properties of the new module: Set `ModularCrm.Ordering` as the *Module name*, leave the *Output folder* as is and click the *Next* button. {{if UI == "MVC"}} - ![abp-studio-add-new-standard-module-ui-dialog](images/abp-studio-add-new-standard-module-ui-dialog.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-add-new-standard-module-ui-dialog](images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png) +{{end}} -You can choose the type of UI you want to support in your module or select *No UI* if you don't need a user interface. In this example, we'll select the *MVC* option and click *Next*. +You can choose the type of UI you want to support in your module or select *No UI* if you don't need a user interface. In this example, we'll select the {{if UI == "MVC"}}*MVC*{{else if UI == "BlazorWebApp"}}*Blazor WebApp*{{end}} option and click *Next*. {{else if UI == "NG"}} @@ -68,7 +77,11 @@ You can include or not include unit tests for the new module here. We are unchec Here is the final solution structure after adding the `ModularCrm.Ordering` module: +{{if UI == "MVC"}} ![abp-studio-modular-crm-with-standard-module](images/abp-studio-modular-crm-with-standard-module.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-modular-crm-with-standard-module](images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png) +{{end}} ## Installing into the Main Application @@ -86,9 +99,13 @@ That command opens the *Import Module* dialog: Select the `ModularCrm.Ordering` module and check the *Install this module* option as shown in the preceding figure. When you click the OK button, a new dialog is shown to select the packages to install: +{{if UI == "MVC"}} ![abp-studio-install-module-dialog](images/abp-studio-install-module-dialog-v2.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-install-module-dialog](images/abp-studio-install-module-dialog-blazor-webapp.png) +{{end}} -Select the `ModularCrm.Ordering` and `ModularCrm.Ordering.UI` packages from the left area and ensure the `ModularCrm` package from the middle area was checked as shown in the preceding figure. Finally, click _OK_. +Select the `ModularCrm.Ordering` and {{if UI == "MVC"}}`ModularCrm.Ordering.UI`{{else if UI == "BlazorWebApp"}}`ModularCrm.Ordering.Blazor`{{end}} packages from the left area. {{if UI == "MVC"}}Ensure `ModularCrm` was checked in the middle area as shown in the preceding figure.{{else if UI == "BlazorWebApp"}}For `ModularCrm.Ordering`, ensure `ModularCrm` is checked. For `ModularCrm.Ordering.Blazor`, ensure both `ModularCrm` and `ModularCrm.Client` are checked in the middle area as shown in the preceding figure.{{end}} Finally, click _OK_. {{if UI == "NG"}} diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index ffce44fda2..c5582ec530 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -364,6 +371,35 @@ Configure(options => This will tell the ABP framework to create API controllers for the application services in the `ModularCrm.Ordering` assembly. +{{if UI == "BlazorWebApp"}} + +### Configuring Client Proxies for the Ordering Module + +In the `ModularCrm.Client` project, configure HTTP client proxies for the Ordering contracts in the `ModularCrmClientModule` class: + +````csharp +using ModularCrm.Ordering; + +[DependsOn( + typeof(OrderingContractsModule) + // ...other dependencies +)] +public class ModularCrmClientModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + ... + context.Services.AddHttpClientProxies(typeof(ModularCrmContractsModule).Assembly); + context.Services.AddHttpClientProxies(typeof(CatalogContractsModule).Assembly); + context.Services.AddHttpClientProxies(typeof(OrderingContractsModule).Assembly); // NEW: ADD HttpClientProxies + } +} +```` + +Also ensure the `ModularCrm.Ordering.Blazor` package is installed for both the `ModularCrm` and `ModularCrm.Client` projects. + +{{end}} + ### Creating Example Orders This section will create a few example orders using the [Swagger UI](../../framework/api-development/swagger.md). Thus, you will have some sample orders to show on the UI. @@ -394,6 +430,8 @@ As a first step, you can stop the application on ABP Studio's Solution Runner if ### Creating the Orders Page +{{if UI == "MVC"}} + Replace the `Index.cshtml.cs` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.UI` project with the following code block: ````csharp @@ -490,6 +528,90 @@ public class OrderingMenuContributor : IMenuContributor > You can check the [menu documentation](../../framework/ui/mvc-razor-pages/navigation-menu.md) to learn more about manipulating menu items. +{{else if UI == "BlazorWebApp"}} + +Replace the `Index.razor` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.Blazor` project with the following code block: + +````razor +@page "/ordering" +@using System.Collections.Generic +@using System.Threading.Tasks +@using ModularCrm.Ordering +@inject IOrderAppService OrderAppService + +

Orders

+ + + + + @foreach (var order in Orders) + { + + Customer: @order.CustomerName
+ Product: @order.ProductId
+ State: @order.State +
+ } +
+
+
+ +@code { + private List Orders { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Orders = await OrderAppService.GetListAsync(); + } +} +```` + +This page shows a list of orders on the UI. You haven't created a UI to create new orders, and we will not do it to keep this tutorial simple. If you want to learn how to create advanced UIs with ABP, please follow the [Book Store tutorial](../book-store/index.md). + +### Editing the Menu Item + +ABP provides a modular navigation [menu system](../../framework/ui/blazor/navigation-menu.md) where each module can contribute to the main menu dynamically. + +Edit the `OrderingMenuContributor` class in the `ModularCrm.Ordering.Blazor` project: + +````csharp +using System.Threading.Tasks; +using Volo.Abp.UI.Navigation; + +namespace ModularCrm.Ordering.Blazor.Menus; + +public class OrderingMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + context.Menu.AddItem( + new ApplicationMenuItem( + OrderingMenus.Prefix, // Unique menu id + "Orders", // Menu display text + "/ordering", // URL + "fa-solid fa-basket-shopping" // Icon CSS class + ) + ); + + return Task.CompletedTask; + } +} +```` + +`OrderingMenuContributor` implements the `IMenuContributor` interface, which forces us to implement the `ConfigureMenuAsync` method. In that method, you can manipulate the menu items (add new menu items, remove existing menu items or change the properties of existing menu items). The `ConfigureMenuAsync` method is executed whenever the menu is rendered on the UI, so you can dynamically decide how to manipulate the menu items. + +> You can check the [menu documentation](../../framework/ui/blazor/navigation-menu.md) to learn more about manipulating menu items. + +{{end}} + ### Building the Application Now, you will run the application to see the result. Please stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm` application, and select the *Build* -> *Graph Build* command: diff --git a/docs/en/tutorials/modular-crm/part-06.md b/docs/en/tutorials/modular-crm/part-06.md index 40647a9ec8..7eb7c5faba 100644 --- a/docs/en/tutorials/modular-crm/part-06.md +++ b/docs/en/tutorials/modular-crm/part-06.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -145,7 +152,11 @@ Open the ABP Studio UI and stop the application if it is already running. Then o In the opening dialog, select the *This solution* tab, find and check the `ModularCrm.Catalog.Contracts` package and click the OK button: +{{if UI == "MVC"}} ![abp-studio-add-package-reference-dialog-3](images/abp-studio-add-package-reference-dialog-3.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-add-package-reference-dialog-3](images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png) +{{end}} ABP Studio adds the package reference and arranges the [module](../../framework/architecture/modularity/basics.md) dependency. @@ -258,7 +269,7 @@ Let's see what we've changed: {{if UI == "MVC"}} -Open the `Index.cshtml` file, and change the `@order.ProductId` part by `@order.ProductName` to write the product name instead of the product ID. The final `Index.cshtml` content should be the following: +Open the `Index.cshtml` file, and change the `@order.ProductId` part to `@order.ProductName` to write the product name instead of the product ID. The final `Index.cshtml` content should be the following: ````html @page @@ -282,6 +293,46 @@ Open the `Index.cshtml` file, and change the `@order.ProductId` part by `@order. ```` +{{else if UI == "BlazorWebApp"}} + +Open the `Index.razor` file, and change the `@order.ProductId` part to `@order.ProductName` to write the product name instead of the product ID. The final `Index.razor` content should be the following: + +````razor +@page "/ordering" +@using System.Collections.Generic +@using System.Threading.Tasks +@using ModularCrm.Ordering +@inject IOrderAppService OrderAppService + +

Orders

+ + + + + @foreach (var order in Orders) + { + + Customer: @order.CustomerName
+ Product: @order.ProductName
+ State: @order.State +
+ } +
+
+
+ +@code { + private List Orders { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Orders = await OrderAppService.GetListAsync(); + } +} +```` + +{{end}} + That's all. Now, you can graph build the main application and run it in ABP Studio to see the result: ![abp-studio-browser-list-of-orders-with-product-name](images/abp-studio-browser-list-of-orders-with-product-name.png) diff --git a/docs/en/tutorials/modular-crm/part-07.md b/docs/en/tutorials/modular-crm/part-07.md index 4cfd272a18..2967b94dfa 100644 --- a/docs/en/tutorials/modular-crm/part-07.md +++ b/docs/en/tutorials/modular-crm/part-07.md @@ -14,6 +14,13 @@ } ``` +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -165,13 +172,21 @@ In the opening dialog, find and select the `ModularCrm.Ordering` module, check t Once you click the OK button, the Ordering module is imported to the Catalog module, and an installation dialog is open: +{{if UI == "MVC"}} ![abp-studio-install-module-dialog-for-ordering](images/abp-studio-install-module-dialog-for-ordering-v2.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-install-module-dialog-for-ordering](images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png) +{{end}} Here, select the `ModularCrm.Ordering.Contracts` package on the left side (because we want to add that package reference) and `ModularCrm.Catalog` package on the middle area (because we want to add the package reference to that project). Also, select the `ModularCrm.Ordering` package on the right side, and unselect all packages on the middle area (we don't need the implementation or any other packages). Then, click the OK button to finish the installation operation. You can check the ABP Studio's *Solution Explorer* panel to see the module import and the project reference (dependency). +{{if UI == "MVC"}} ![abp-studio-imports-and-dependencies](images/abp-studio-imports-and-dependencies-v2.png) +{{else if UI == "BlazorWebApp"}} +![abp-studio-imports-and-dependencies](images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png) +{{end}} ### Handling the `OrderPlacedEto` Event diff --git a/docs/en/tutorials/modular-crm/part-08.md b/docs/en/tutorials/modular-crm/part-08.md index 476edb7f6d..b7254b0c1f 100644 --- a/docs/en/tutorials/modular-crm/part-08.md +++ b/docs/en/tutorials/modular-crm/part-08.md @@ -14,6 +14,13 @@ # Integrating the Modules: Joining the Products and Orders Data +````json +//[doc-params] +{ + "UI": ["MVC","BlazorWebApp"] +} +```` + ````json //[doc-nav] { @@ -176,7 +183,7 @@ Now, you know the fundamental principles and mechanics of building sophisticated ## Download the Source Code -You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM). +You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM-BlazorWebApp). ## See Also diff --git a/framework/Volo.Abp.abpsln b/framework/Volo.Abp.abpsln index 2f34cdcfbe..5ad02f81ce 100644 --- a/framework/Volo.Abp.abpsln +++ b/framework/Volo.Abp.abpsln @@ -4,5 +4,6 @@ "Volo.Abp": { "path": "Volo.Abp.abpmdl" } - } + }, + "id": "9f9e3d5f-6a9a-4b00-ac5a-746c65981918" } \ No newline at end of file diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index 1302600c09..26d462fb4f 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -48,6 +48,7 @@ + @@ -189,6 +190,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Abstractions/Volo/Abp/AspNetCore/AbpAspNetCoreAbstractionsModule.cs b/framework/src/Volo.Abp.AspNetCore.Abstractions/Volo/Abp/AspNetCore/AbpAspNetCoreAbstractionsModule.cs index 6a15c5550f..603a578ef4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Abstractions/Volo/Abp/AspNetCore/AbpAspNetCoreAbstractionsModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Abstractions/Volo/Abp/AspNetCore/AbpAspNetCoreAbstractionsModule.cs @@ -10,6 +10,6 @@ public class AbpAspNetCoreAbstractionsModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddSingleton(); - context.Services.AddSingleton();; + context.Services.AddSingleton(); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs index 10f7e28811..1cf47ddb64 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Volo.Abp.Http.Modeling; namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; @@ -16,8 +17,8 @@ public class AbpApiDefinitionController : AbpController, IRemoteService } [HttpGet] - public virtual ApplicationApiDescriptionModel Get(ApplicationApiDescriptionModelRequestDto model) + public virtual async Task Get(ApplicationApiDescriptionModelRequestDto model) { - return ModelProvider.CreateApiModel(model); + return await ModelProvider.CreateApiModelAsync(model); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/IXmlDocumentationProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/IXmlDocumentationProvider.cs new file mode 100644 index 0000000000..fdc1138c27 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/IXmlDocumentationProvider.cs @@ -0,0 +1,22 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public interface IXmlDocumentationProvider +{ + Task GetSummaryAsync(Type type); + + Task GetRemarksAsync(Type type); + + Task GetSummaryAsync(MethodInfo method); + + Task GetRemarksAsync(MethodInfo method); + + Task GetReturnsAsync(MethodInfo method); + + Task GetParameterSummaryAsync(MethodInfo method, string parameterName); + + Task GetSummaryAsync(PropertyInfo property); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs new file mode 100644 index 0000000000..538a79a611 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDependency +{ + public ILogger Logger { get; set; } + + public XmlDocumentationProvider() + { + Logger = NullLogger.Instance; + } + + private static readonly Regex WhitespaceRegex = new(@"\s+", RegexOptions.Compiled); + + // Matches any remaining XML tags like , , , , etc. + private static readonly Regex XmlTagRegex = new(@"<[^>]+>", RegexOptions.Compiled); + + // Matches , , , + private static readonly Regex XmlRefTagRegex = new( + @"<(see|paramref|typeparamref)\s+(cref|name|langword)=""([TMFPE]:)?(?[^""]+)""\s*/?>", + RegexOptions.Compiled); + + private readonly ConcurrentDictionary>> _xmlDocCache = new(); + + public virtual async Task GetSummaryAsync(Type type) + { + var memberName = GetMemberNameForType(type); + return await GetDocumentationElementAsync(type.Assembly, memberName, "summary"); + } + + public virtual async Task GetRemarksAsync(Type type) + { + var memberName = GetMemberNameForType(type); + return await GetDocumentationElementAsync(type.Assembly, memberName, "remarks"); + } + + public virtual async Task GetSummaryAsync(MethodInfo method) + { + var memberName = GetMemberNameForMethod(method); + return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "summary"); + } + + public virtual async Task GetRemarksAsync(MethodInfo method) + { + var memberName = GetMemberNameForMethod(method); + return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "remarks"); + } + + public virtual async Task GetReturnsAsync(MethodInfo method) + { + var memberName = GetMemberNameForMethod(method); + return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "returns"); + } + + public virtual async Task GetParameterSummaryAsync(MethodInfo method, string parameterName) + { + var memberName = GetMemberNameForMethod(method); + var doc = await LoadXmlDocumentationAsync(method.DeclaringType!.Assembly); + if (doc == null) + { + return null; + } + + var memberNode = doc.XPathSelectElement($"//member[@name='{memberName}']"); + var paramNode = memberNode?.XPathSelectElement($"param[@name='{parameterName}']"); + return CleanXmlText(paramNode); + } + + public virtual async Task GetSummaryAsync(PropertyInfo property) + { + var memberName = GetMemberNameForProperty(property); + return await GetDocumentationElementAsync(property.DeclaringType!.Assembly, memberName, "summary"); + } + + protected virtual async Task GetDocumentationElementAsync(Assembly assembly, string memberName, string elementName) + { + var doc = await LoadXmlDocumentationAsync(assembly); + if (doc == null) + { + return null; + } + + var memberNode = doc.XPathSelectElement($"//member[@name='{memberName}']"); + var element = memberNode?.Element(elementName); + return CleanXmlText(element); + } + + protected virtual Task LoadXmlDocumentationAsync(Assembly assembly) + { + return _xmlDocCache.GetOrAdd( + assembly, + asm => new Lazy>( + () => LoadXmlDocumentationFromDiskAsync(asm), + LazyThreadSafetyMode.ExecutionAndPublication) + ).Value; + } + + protected virtual async Task LoadXmlDocumentationFromDiskAsync(Assembly assembly) + { + if (string.IsNullOrEmpty(assembly.Location)) + { + return null; + } + + var xmlFilePath = Path.ChangeExtension(assembly.Location, ".xml"); + if (!File.Exists(xmlFilePath)) + { + return null; + } + + try + { + await using var stream = new FileStream(xmlFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); + return await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to load XML documentation from {XmlFilePath}.", xmlFilePath); + return null; + } + } + + private static string? CleanXmlText(XElement? element) + { + if (element == null) + { + return null; + } + + // Convert to string first so we can process inline XML tags like + var raw = element.ToString(); + + // Strip the outer element tags (e.g. ...) + var start = raw.IndexOf('>') + 1; + var end = raw.LastIndexOf('<'); + if (start >= end) + { + return null; + } + + var inner = raw[start..end]; + + // Replace with the short name "Bar" + // Replace with "null" + // Replace and with the name + inner = XmlRefTagRegex.Replace(inner, m => + { + var display = m.Groups["display"].Value; + // For cref values like "T:Foo.Bar.Baz", return only "Baz" + var dot = display.LastIndexOf('.'); + return dot >= 0 ? display[(dot + 1)..] : display; + }); + + // Strip any remaining XML tags (e.g. , , , , etc.) + inner = XmlTagRegex.Replace(inner, string.Empty); + + if (string.IsNullOrWhiteSpace(inner)) + { + return null; + } + + return WhitespaceRegex.Replace(inner.Trim(), " "); + } + + private static string GetMemberNameForType(Type type) + { + return $"T:{GetTypeFullName(type)}"; + } + + private static string GetMemberNameForMethod(MethodInfo method) + { + var typeName = GetTypeFullName(method.DeclaringType!); + var parameters = method.GetParameters(); + if (parameters.Length == 0) + { + return $"M:{typeName}.{method.Name}"; + } + + var paramTypes = string.Join(",", + parameters.Select(p => GetParameterTypeName(p.ParameterType))); + return $"M:{typeName}.{method.Name}({paramTypes})"; + } + + private static string GetMemberNameForProperty(PropertyInfo property) + { + var typeName = GetTypeFullName(property.DeclaringType!); + return $"P:{typeName}.{property.Name}"; + } + + private static string GetTypeFullName(Type type) + { + return type.FullName?.Replace('+', '.') ?? type.Name; + } + + private static string GetParameterTypeName(Type type) + { + if (type.IsGenericType) + { + var genericDef = type.GetGenericTypeDefinition(); + var defName = genericDef.FullName!; + defName = defName[..defName.IndexOf('`')]; + var args = string.Join(",", type.GetGenericArguments().Select(GetParameterTypeName)); + return $"{defName}{{{args}}}"; + } + + if (type.IsArray) + { + return GetParameterTypeName(type.GetElementType()!) + "[]"; + } + + if (type.IsByRef) + { + return GetParameterTypeName(type.GetElementType()!) + "@"; + } + + return type.FullName ?? type.Name; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index 2df5dea048..464981cb3c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Asp.Versioning; using JetBrains.Annotations; using Microsoft.AspNetCore.Authorization; @@ -12,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc.ApiExploring; using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.Utils; using Volo.Abp.DependencyInjection; @@ -29,26 +33,30 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider; private readonly AbpAspNetCoreMvcOptions _abpAspNetCoreMvcOptions; private readonly AbpApiDescriptionModelOptions _modelOptions; + private readonly IXmlDocumentationProvider _xmlDocProvider; public AspNetCoreApiDescriptionModelProvider( IOptions options, IApiDescriptionGroupCollectionProvider descriptionProvider, IOptions abpAspNetCoreMvcOptions, - IOptions modelOptions) + IOptions modelOptions, + IXmlDocumentationProvider xmlDocProvider) { _options = options.Value; _descriptionProvider = descriptionProvider; _abpAspNetCoreMvcOptions = abpAspNetCoreMvcOptions.Value; _modelOptions = modelOptions.Value; + _xmlDocProvider = xmlDocProvider; Logger = NullLogger.Instance; } - public ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input) + public virtual async Task CreateApiModelAsync(ApplicationApiDescriptionModelRequestDto input) { //TODO: Can cache the model? var model = ApplicationApiDescriptionModel.Create(); + var populatedControllers = new HashSet(); foreach (var descriptionGroupItem in _descriptionProvider.ApiDescriptionGroups.Items) { @@ -59,7 +67,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide continue; } - AddApiDescriptionToModel(apiDescription, model, input); + await AddApiDescriptionToModelAsync(apiDescription, model, input, populatedControllers); } } @@ -80,10 +88,11 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide return model; } - private void AddApiDescriptionToModel( + private async Task AddApiDescriptionToModelAsync( ApiDescription apiDescription, ApplicationApiDescriptionModel applicationModel, - ApplicationApiDescriptionModelRequestDto input) + ApplicationApiDescriptionModelRequestDto input, + HashSet populatedControllers) { var controllerType = apiDescription .ActionDescriptor @@ -139,10 +148,21 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide var implementFrom = controllerType.FullName; - var interfaceType = controllerType.GetInterfaces().FirstOrDefault(i => i.GetMethods().Any(x => x.ToString() == method.ToString())); - if (interfaceType != null) + foreach (var iface in controllerType.GetInterfaces()) { - implementFrom = TypeHelper.GetFullNameHandlingNullableAndGenerics(interfaceType); + try + { + var map = controllerType.GetInterfaceMap(iface); + if (Array.IndexOf(map.TargetMethods, method) >= 0) + { + implementFrom = TypeHelper.GetFullNameHandlingNullableAndGenerics(iface); + break; + } + } + catch (ArgumentException) + { + // GetInterfaceMap is not supported for some generic interface edge cases + } } var actionModel = controllerModel.AddAction( @@ -161,10 +181,22 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide if (input.IncludeTypes) { - AddCustomTypesToModel(applicationModel, method); + await AddCustomTypesToModelAsync(applicationModel, method, input.IncludeDescriptions); } AddParameterDescriptionsToModel(actionModel, method, apiDescription); + + if (input.IncludeDescriptions) + { + if (populatedControllers.Add(controllerModel)) + { + await PopulateControllerDescriptionsAsync(controllerModel, controllerType); + } + + var interfaceMethod = GetInterfaceMethod(method); + await PopulateActionDescriptionsAsync(actionModel, method, interfaceMethod); + await PopulateParameterDescriptionsAsync(actionModel, method, interfaceMethod); + } } private static List GetSupportedVersions(Type controllerType, MethodInfo method, @@ -191,18 +223,18 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide return supportedVersions.Select(v => v.ToString()).Distinct().ToList(); } - private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, MethodInfo method) + private async Task AddCustomTypesToModelAsync(ApplicationApiDescriptionModel applicationModel, MethodInfo method, bool includeDescriptions) { foreach (var parameterInfo in method.GetParameters()) { - AddCustomTypesToModel(applicationModel, parameterInfo.ParameterType); + await AddCustomTypesToModelAsync(applicationModel, parameterInfo.ParameterType, includeDescriptions); } - AddCustomTypesToModel(applicationModel, method.ReturnType); + await AddCustomTypesToModelAsync(applicationModel, method.ReturnType, includeDescriptions); } - private static void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, - Type? type) + private async Task AddCustomTypesToModelAsync(ApplicationApiDescriptionModel applicationModel, + Type? type, bool includeDescriptions) { if (type == null) { @@ -229,14 +261,14 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) { - AddCustomTypesToModel(applicationModel, keyType); - AddCustomTypesToModel(applicationModel, valueType); + await AddCustomTypesToModelAsync(applicationModel, keyType, includeDescriptions); + await AddCustomTypesToModelAsync(applicationModel, valueType, includeDescriptions); return; } if (TypeHelper.IsEnumerable(type, out var itemType)) { - AddCustomTypesToModel(applicationModel, itemType); + await AddCustomTypesToModelAsync(applicationModel, itemType, includeDescriptions); return; } @@ -244,11 +276,11 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide { var genericTypeDefinition = type.GetGenericTypeDefinition(); - AddCustomTypesToModel(applicationModel, genericTypeDefinition); + await AddCustomTypesToModelAsync(applicationModel, genericTypeDefinition, includeDescriptions); foreach (var genericArgument in type.GetGenericArguments()) { - AddCustomTypesToModel(applicationModel, genericArgument); + await AddCustomTypesToModelAsync(applicationModel, genericArgument, includeDescriptions); } return; @@ -262,11 +294,16 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide applicationModel.Types[typeName] = TypeApiDescriptionModel.Create(type); - AddCustomTypesToModel(applicationModel, type.BaseType); + if (includeDescriptions) + { + await PopulateTypeDescriptionsAsync(applicationModel.Types[typeName], type); + } + + await AddCustomTypesToModelAsync(applicationModel, type.BaseType, includeDescriptions); foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type)) { - AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType); + await AddCustomTypesToModelAsync(applicationModel, propertyInfo.PropertyType, includeDescriptions); } } @@ -414,4 +451,149 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide return null; } + + protected virtual async Task PopulateControllerDescriptionsAsync(ControllerApiDescriptionModel controllerModel, Type controllerType) + { + controllerModel.Summary = await _xmlDocProvider.GetSummaryAsync(controllerType); + controllerModel.Remarks = await _xmlDocProvider.GetRemarksAsync(controllerType); + + if (controllerModel.Summary == null && controllerModel.Remarks == null) + { + foreach (var interfaceType in GetDirectInterfaces(controllerType).Where(i => !_modelOptions.IgnoredInterfaces.Contains(i))) + { + controllerModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceType); + controllerModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceType); + if (controllerModel.Summary != null || controllerModel.Remarks != null) + { + break; + } + } + } + + controllerModel.Description = controllerType.GetCustomAttribute()?.Description; + controllerModel.DisplayName = controllerType.GetCustomAttribute()?.Name; + } + + protected virtual async Task PopulateActionDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method, MethodInfo? interfaceMethod) + { + actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(method); + actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(method); + + if (actionModel.Summary == null && actionModel.Remarks == null && interfaceMethod != null) + { + actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceMethod); + actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceMethod); + } + + actionModel.Description = method.GetCustomAttribute()?.Description; + actionModel.DisplayName = method.GetCustomAttribute()?.Name; + + actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(method); + if (actionModel.ReturnValue.Summary == null && interfaceMethod != null) + { + actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(interfaceMethod); + } + } + + protected virtual async Task PopulateParameterDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method, MethodInfo? interfaceMethod) + { + var methodParameters = method.GetParameters(); + + foreach (var param in actionModel.ParametersOnMethod) + { + var paramInfo = methodParameters.FirstOrDefault(p => p.Name == param.Name); + if (paramInfo == null) + { + continue; + } + + param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(method, param.Name); + if (param.Summary == null && interfaceMethod != null) + { + param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(interfaceMethod, param.Name); + } + + param.Description = paramInfo.GetCustomAttribute()?.Description; + param.DisplayName = paramInfo.GetCustomAttribute()?.Name; + } + + foreach (var param in actionModel.Parameters) + { + // Skip expanded properties from complex types - their descriptions + // should come from type-level documentation (PopulateTypeDescriptionsAsync) + if (!string.IsNullOrEmpty(param.DescriptorName) && param.Name != param.NameOnMethod) + { + continue; + } + + param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(method, param.NameOnMethod); + if (param.Summary == null && interfaceMethod != null) + { + param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(interfaceMethod, param.NameOnMethod); + } + + var paramInfo = methodParameters.FirstOrDefault(p => p.Name == param.NameOnMethod); + if (paramInfo != null) + { + param.Description = paramInfo.GetCustomAttribute()?.Description; + param.DisplayName = paramInfo.GetCustomAttribute()?.Name; + } + } + } + + private MethodInfo? GetInterfaceMethod(MethodInfo method) + { + var declaringType = method.DeclaringType; + if (declaringType == null || declaringType.IsInterface) + { + return null; + } + + foreach (var interfaceType in GetDirectInterfaces(declaringType).Where(i => !_modelOptions.IgnoredInterfaces.Contains(i))) + { + var map = declaringType.GetInterfaceMap(interfaceType); + for (var i = 0; i < map.TargetMethods.Length; i++) + { + if (map.TargetMethods[i] == method) + { + return map.InterfaceMethods[i]; + } + } + } + + return null; + } + + private static IEnumerable GetDirectInterfaces(Type type) + { + var allInterfaces = type.GetInterfaces(); + var baseInterfaces = type.BaseType?.GetInterfaces() ?? Type.EmptyTypes; + return allInterfaces.Except(baseInterfaces); + } + + protected virtual async Task PopulateTypeDescriptionsAsync(TypeApiDescriptionModel typeModel, Type type) + { + typeModel.Summary = await _xmlDocProvider.GetSummaryAsync(type); + typeModel.Remarks = await _xmlDocProvider.GetRemarksAsync(type); + typeModel.Description = type.GetCustomAttribute()?.Description; + typeModel.DisplayName = type.GetCustomAttribute()?.Name; + + if (typeModel.Properties == null) + { + return; + } + + foreach (var propModel in typeModel.Properties) + { + var propInfo = type.GetProperty(propModel.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); + if (propInfo == null) + { + continue; + } + + propModel.Summary = await _xmlDocProvider.GetSummaryAsync(propInfo); + propModel.Description = propInfo.GetCustomAttribute()?.Description; + propModel.DisplayName = propInfo.GetCustomAttribute()?.Name; + } + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/AbpServiceProxyScriptController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/AbpServiceProxyScriptController.cs index ef7196fb0c..49881e103d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/AbpServiceProxyScriptController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/AbpServiceProxyScriptController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Volo.Abp.Auditing; using Volo.Abp.Http; @@ -29,11 +30,11 @@ public class AbpServiceProxyScriptController : AbpController [HttpGet] [Produces(MimeTypes.Application.Javascript, MimeTypes.Text.Plain)] - public virtual ActionResult GetAll(ServiceProxyGenerationModel model) + public virtual async Task GetAll(ServiceProxyGenerationModel model) { model.Normalize(); - var script = ProxyScriptManager.GetScript(model.CreateOptions()); + var script = await ProxyScriptManager.GetScriptAsync(model.CreateOptions()); return Content( Options.MinifyGeneratedScript == true diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AbpBackgroundJobOptions.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AbpBackgroundJobOptions.cs index 1a6cb6a9e9..8494f5df3e 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AbpBackgroundJobOptions.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AbpBackgroundJobOptions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -46,7 +46,7 @@ public class AbpBackgroundJobOptions public BackgroundJobConfiguration GetJob(string name) { - var jobConfiguration = _jobConfigurationsByName.GetOrDefault(name); + var jobConfiguration = GetJobOrNull(name); if (jobConfiguration == null) { @@ -56,6 +56,11 @@ public class AbpBackgroundJobOptions return jobConfiguration; } + public BackgroundJobConfiguration? GetJobOrNull(string name) + { + return _jobConfigurationsByName.GetOrDefault(name); + } + public IReadOnlyList GetJobs() { return _jobConfigurationsByArgsType.Values.ToImmutableList(); diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs new file mode 100644 index 0000000000..7028e0f858 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.BackgroundJobs; + +public class DefaultDynamicBackgroundJobManager : IDynamicBackgroundJobManager, ITransientDependency +{ + private static readonly ConcurrentDictionary>> EnqueueDelegateCache = new(); + + protected IBackgroundJobManager BackgroundJobManager { get; } + protected IDynamicBackgroundJobHandlerRegistry HandlerRegistry { get; } + protected AbpBackgroundJobOptions BackgroundJobOptions { get; } + protected IJsonSerializer JsonSerializer { get; } + public ILogger Logger { get; set; } + + public DefaultDynamicBackgroundJobManager( + IBackgroundJobManager backgroundJobManager, + IDynamicBackgroundJobHandlerRegistry handlerRegistry, + IOptions backgroundJobOptions, + IJsonSerializer jsonSerializer) + { + BackgroundJobManager = backgroundJobManager; + HandlerRegistry = handlerRegistry; + BackgroundJobOptions = backgroundJobOptions.Value; + JsonSerializer = jsonSerializer; + Logger = NullLogger.Instance; + } + + public virtual async Task EnqueueAsync( + string jobName, + object args, + BackgroundJobPriority priority = BackgroundJobPriority.Normal, + TimeSpan? delay = null) + { + Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + Check.NotNull(args, nameof(args)); + + var jobConfiguration = BackgroundJobOptions.GetJobOrNull(jobName); + if (jobConfiguration != null) + { + return await EnqueueTypedJobAsync(jobConfiguration, args, priority, delay); + } + + if (HandlerRegistry.IsRegistered(jobName)) + { + return await EnqueueDynamicHandlerJobAsync(jobName, args, priority, delay); + } + + throw new AbpException( + $"No typed job configuration or dynamic handler registered for job name: {jobName}"); + } + + public virtual void RegisterHandler( + string jobName, + DynamicBackgroundJobHandler handler) + { + HandlerRegistry.Register(jobName, handler); + } + + public virtual bool UnregisterHandler(string jobName) + { + return HandlerRegistry.Unregister(jobName); + } + + public virtual bool IsHandlerRegistered(string jobName) + { + return HandlerRegistry.IsRegistered(jobName); + } + + protected virtual async Task EnqueueTypedJobAsync( + BackgroundJobConfiguration jobConfiguration, + object args, + BackgroundJobPriority priority, + TimeSpan? delay) + { + var argsType = jobConfiguration.ArgsType; + + // Normalize args to the expected type via JSON round-trip + var json = JsonSerializer.Serialize(args); + var typedArgs = JsonSerializer.Deserialize(argsType, json); + + var enqueueDelegate = GetOrCreateEnqueueDelegate(argsType); + return await enqueueDelegate(BackgroundJobManager, typedArgs, priority, delay); + } + + protected virtual Task EnqueueDynamicHandlerJobAsync( + string jobName, + object args, + BackgroundJobPriority priority, + TimeSpan? delay) + { + var jsonData = JsonSerializer.Serialize(args); + var dynamicArgs = new DynamicBackgroundJobArgs(jobName, jsonData); + return BackgroundJobManager.EnqueueAsync(dynamicArgs, priority, delay); + } + + private static Func> GetOrCreateEnqueueDelegate(Type argsType) + { + return EnqueueDelegateCache.GetOrAdd(argsType, static type => + { + var method = typeof(IBackgroundJobManager) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => + m.Name == nameof(IBackgroundJobManager.EnqueueAsync) + && m.IsGenericMethodDefinition + && m.GetParameters() is { Length: 3 } p + && p[1].ParameterType == typeof(BackgroundJobPriority) + && p[2].ParameterType == typeof(TimeSpan?)); + + if (method == null) + { + throw new AbpException( + $"Could not find the generic EnqueueAsync method on {nameof(IBackgroundJobManager)}."); + } + + var genericMethod = method.MakeGenericMethod(type); + + // Build: (manager, args, priority, delay) => manager.EnqueueAsync((TArgs)args, priority, delay) + var managerParam = Expression.Parameter(typeof(IBackgroundJobManager), "manager"); + var argsParam = Expression.Parameter(typeof(object), "args"); + var priorityParam = Expression.Parameter(typeof(BackgroundJobPriority), "priority"); + var delayParam = Expression.Parameter(typeof(TimeSpan?), "delay"); + + var call = Expression.Call( + managerParam, + genericMethod, + Expression.Convert(argsParam, type), + priorityParam, + delayParam); + + return Expression.Lambda>>( + call, managerParam, argsParam, priorityParam, delayParam).Compile(); + }); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobArgs.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobArgs.cs new file mode 100644 index 0000000000..45066b7586 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobArgs.cs @@ -0,0 +1,24 @@ +namespace Volo.Abp.BackgroundJobs; + +[BackgroundJobName(JobNameConstant)] +public class DynamicBackgroundJobArgs +{ + public const string JobNameConstant = "Abp.DynamicJob"; + + public string JobName { get; private set; } + + public string JsonData { get; private set; } + + // For serializers that require a parameterless constructor (e.g. System.Text.Json) + private DynamicBackgroundJobArgs() + { + JobName = string.Empty; + JsonData = string.Empty; + } + + public DynamicBackgroundJobArgs(string jobName, string jsonData) + { + JobName = Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + JsonData = Check.NotNull(jsonData, nameof(jsonData)); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutionContext.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutionContext.cs new file mode 100644 index 0000000000..6b207cac97 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutionContext.cs @@ -0,0 +1,22 @@ +using System; + +namespace Volo.Abp.BackgroundJobs; + +public class DynamicBackgroundJobExecutionContext +{ + public string JobName { get; } + + public string JsonData { get; } + + public IServiceProvider ServiceProvider { get; } + + public DynamicBackgroundJobExecutionContext( + string jobName, + string jsonData, + IServiceProvider serviceProvider) + { + JobName = Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + JsonData = Check.NotNull(jsonData, nameof(jsonData)); + ServiceProvider = Check.NotNull(serviceProvider, nameof(serviceProvider)); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutorJob.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutorJob.cs new file mode 100644 index 0000000000..5818e6de70 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutorJob.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundJobs; + +public class DynamicBackgroundJobExecutorJob : AsyncBackgroundJob, ITransientDependency +{ + protected IDynamicBackgroundJobHandlerRegistry HandlerRegistry { get; } + protected IServiceProvider ServiceProvider { get; } + + public DynamicBackgroundJobExecutorJob( + IDynamicBackgroundJobHandlerRegistry handlerRegistry, + IServiceProvider serviceProvider) + { + HandlerRegistry = handlerRegistry; + ServiceProvider = serviceProvider; + } + + public override async Task ExecuteAsync(DynamicBackgroundJobArgs args) + { + Logger.LogDebug( + "Executing dynamic job. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}", + DynamicBackgroundJobArgs.JobNameConstant, + args.JobName + ); + + var handler = HandlerRegistry.Get(args.JobName); + if (handler == null) + { + throw new AbpException( + $"No dynamic job handler registered for: {args.JobName}. " + + $"The handler may have been unregistered or the application restarted since the job was enqueued."); + } + + var cancellationToken = ServiceProvider.GetRequiredService().Token; + var executionContext = new DynamicBackgroundJobExecutionContext(args.JobName, args.JsonData, ServiceProvider); + await handler(executionContext, cancellationToken); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandler.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandler.cs new file mode 100644 index 0000000000..f1f9d17602 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandler.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Represents a handler delegate for dynamic background jobs. +/// +public delegate Task DynamicBackgroundJobHandler(DynamicBackgroundJobExecutionContext context, CancellationToken cancellationToken); diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandlerRegistry.cs new file mode 100644 index 0000000000..101277b789 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandlerRegistry.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs; + +public class DynamicBackgroundJobHandlerRegistry : IDynamicBackgroundJobHandlerRegistry, ISingletonDependency +{ + protected ConcurrentDictionary Handlers { get; } + + public DynamicBackgroundJobHandlerRegistry() + { + Handlers = new ConcurrentDictionary(); + } + + public virtual void Register(string jobName, DynamicBackgroundJobHandler handler) + { + Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + Check.NotNull(handler, nameof(handler)); + + Handlers[jobName] = handler; + } + + public virtual bool Unregister(string jobName) + { + Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + return Handlers.TryRemove(jobName, out _); + } + + public virtual bool IsRegistered(string jobName) + { + Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + return Handlers.ContainsKey(jobName); + } + + public virtual DynamicBackgroundJobHandler? Get(string jobName) + { + Check.NotNullOrWhiteSpace(jobName, nameof(jobName)); + return Handlers.TryGetValue(jobName, out var handler) ? handler : null; + } + + public virtual IReadOnlyCollection GetAllNames() + { + return Handlers.Keys.ToList().AsReadOnly(); + } + + public virtual void Clear() + { + Handlers.Clear(); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobHandlerRegistry.cs new file mode 100644 index 0000000000..57e75d1607 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobHandlerRegistry.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Volo.Abp.BackgroundJobs; + +public interface IDynamicBackgroundJobHandlerRegistry +{ + void Register(string jobName, DynamicBackgroundJobHandler handler); + + bool Unregister(string jobName); + + bool IsRegistered(string jobName); + + DynamicBackgroundJobHandler? Get(string jobName); + + IReadOnlyCollection GetAllNames(); + + void Clear(); +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobManager.cs b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobManager.cs new file mode 100644 index 0000000000..a94a71cd02 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobManager.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Manages dynamic background jobs that can be enqueued by name +/// without requiring a strongly-typed job args class at compile time. +/// Also supports registering dynamic job handlers at runtime. +/// +public interface IDynamicBackgroundJobManager +{ + /// + /// Enqueues a job by name with dynamic payload. + /// If a typed job configuration exists for this name, the args will be + /// deserialized to the configured args type and enqueued through the typed pipeline. + /// If a dynamic handler is registered, the args will be wrapped as + /// and enqueued through the standard typed pipeline. + /// + /// Name of the background job. + /// Job arguments (will be serialized to JSON). + /// Job priority. + /// Job delay (wait duration before first try). + /// Unique identifier of a background job. + Task EnqueueAsync( + string jobName, + object args, + BackgroundJobPriority priority = BackgroundJobPriority.Normal, + TimeSpan? delay = null); + + /// + /// Registers a dynamic job handler at runtime. + /// + /// Unique name for the dynamic job. + /// The handler delegate to execute when the job runs. + void RegisterHandler(string jobName, DynamicBackgroundJobHandler handler); + + /// + /// Unregisters a previously registered dynamic job handler. + /// + /// Name of the dynamic job to unregister. + /// True if the handler was found and removed; false otherwise. + bool UnregisterHandler(string jobName); + + /// + /// Checks whether a dynamic handler is registered for the given job name. + /// + /// Name of the dynamic job. + /// True if registered; false otherwise. + bool IsHandlerRegistered(string jobName); +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/AbpDashboardOptionsProvider.cs b/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/AbpDashboardOptionsProvider.cs index e68489290a..2f861a33b5 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/AbpDashboardOptionsProvider.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/AbpDashboardOptionsProvider.cs @@ -24,7 +24,14 @@ public class AbpDashboardOptionsProvider : ITransientDependency var jobName = job.ToString(); if (job.Args.Count == 3 && job.Args.Last() is CancellationToken) { - jobName = AbpBackgroundJobOptions.GetJob(job.Args[1].GetType()).JobName; + if (job.Args[1] is DynamicBackgroundJobArgs dynamicJobArgs) + { + jobName = dynamicJobArgs.JobName; + } + else + { + jobName = AbpBackgroundJobOptions.GetJob(job.Args[1].GetType()).JobName; + } } return jobName; diff --git a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs index 1e2280b78a..ddc2613b47 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs @@ -176,10 +176,10 @@ public class JobQueue : IJobQueue CorrelationId = CorrelationIdProvider.Get() }; - if (delay.HasValue) + if (delay.HasValue && delay.Value > TimeSpan.Zero) { routingKey = QueueConfiguration.DelayedQueueName; - basicProperties.Expiration = delay.Value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + basicProperties.Expiration = ((long)Math.Ceiling(delay.Value.TotalMilliseconds)).ToString(CultureInfo.InvariantCulture); } if (ChannelAccessor != null) diff --git a/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs b/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs index 3d93fc68a1..157f0cc050 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs @@ -23,26 +23,14 @@ public class AbpBackgroundJobsTickerQModule : AbpModule { var abpBackgroundJobOptions = context.ServiceProvider.GetRequiredService>(); var abpBackgroundJobsTickerQOptions = context.ServiceProvider.GetRequiredService>(); - var tickerFunctionDelegates = new Dictionary(); - var requestTypes = new Dictionary(); + var abpTickerQFunctionProvider = context.ServiceProvider.GetRequiredService(); foreach (var jobConfiguration in abpBackgroundJobOptions.Value.GetJobs()) { var genericMethod = GetTickerFunctionDelegateMethod.MakeGenericMethod(jobConfiguration.ArgsType); var tickerFunctionDelegate = (TickerFunctionDelegate)genericMethod.Invoke(null, [jobConfiguration.ArgsType])!; var config = abpBackgroundJobsTickerQOptions.Value.GetConfigurationOrNull(jobConfiguration.JobType); - tickerFunctionDelegates.TryAdd(jobConfiguration.JobName, (string.Empty, config?.Priority ?? TickerTaskPriority.Normal, tickerFunctionDelegate)); - requestTypes.TryAdd(jobConfiguration.JobName, (jobConfiguration.ArgsType.FullName, jobConfiguration.ArgsType)!); - } - - var abpTickerQFunctionProvider = context.ServiceProvider.GetRequiredService(); - foreach (var functionDelegate in tickerFunctionDelegates) - { - abpTickerQFunctionProvider.Functions.TryAdd(functionDelegate.Key, functionDelegate.Value); - } - - foreach (var requestType in requestTypes) - { - abpTickerQFunctionProvider.RequestTypes.TryAdd(requestType.Key, requestType.Value); + abpTickerQFunctionProvider.AddFunction(jobConfiguration.JobName, tickerFunctionDelegate, config?.Priority ?? TickerTaskPriority.Normal, config?.MaxConcurrency ?? 0); + abpTickerQFunctionProvider.RequestTypes.TryAdd(jobConfiguration.JobName, (jobConfiguration.ArgsType.FullName, jobConfiguration.ArgsType)!); } } diff --git a/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTimeTickerConfiguration.cs b/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTimeTickerConfiguration.cs index ecceaeb28a..1f2c6d32ac 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTimeTickerConfiguration.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTimeTickerConfiguration.cs @@ -10,5 +10,7 @@ public class AbpBackgroundJobsTimeTickerConfiguration public TickerTaskPriority? Priority { get; set; } + public int? MaxConcurrency { get; set; } + public RunCondition? RunCondition { get; set; } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs index 64a4a1be64..e23737d2a1 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs @@ -162,19 +162,23 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet if (time.TotalSeconds <= 59) { - cron = $"*/{time.TotalSeconds} * * * * *"; + var seconds = Math.Max(1, (int)Math.Round(time.TotalSeconds)); + cron = $"*/{seconds} * * * * *"; } else if (time.TotalMinutes <= 59) { - cron = $"*/{time.TotalMinutes} * * * *"; + var minutes = Math.Max(1, (int)Math.Round(time.TotalMinutes)); + cron = $"*/{minutes} * * * *"; } else if (time.TotalHours <= 23) { - cron = $"0 */{time.TotalHours} * * *"; + var hours = Math.Max(1, (int)Math.Round(time.TotalHours)); + cron = $"0 */{hours} * * *"; } else if(time.TotalDays <= 31) { - cron = $"0 0 0 1/{time.TotalDays} * *"; + var days = Math.Max(1, (int)Math.Round(time.TotalDays)); + cron = $"0 0 0 1/{days} * *"; } else { diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs new file mode 100644 index 0000000000..56a5d7a5c8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerAdapter.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; + +namespace Volo.Abp.BackgroundWorkers.Hangfire; + +public class HangfireDynamicBackgroundWorkerAdapter : ITransientDependency +{ + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + protected IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; set; } + + public HangfireDynamicBackgroundWorkerAdapter( + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry, + IServiceProvider serviceProvider) + { + HandlerRegistry = handlerRegistry; + ServiceProvider = serviceProvider; + Logger = NullLogger.Instance; + } + + public virtual async Task DoWorkAsync(string workerName, CancellationToken cancellationToken = default) + { + var handler = HandlerRegistry.Get(workerName); + if (handler == null) + { + Logger.LogWarning("No handler registered for dynamic worker: {WorkerName}", workerName); + return; + } + + try + { + await handler(new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), cancellationToken); + } + catch (Exception ex) + { + // Swallow the exception to match the behavior of AsyncPeriodicBackgroundWorkerBase, + // which catches, notifies and logs without rethrowing. This prevents Hangfire from + // treating a single failed execution as a job failure and triggering retries. + await ServiceProvider.GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..a9b9026606 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs @@ -0,0 +1,183 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Hangfire.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Hangfire; + +namespace Volo.Abp.BackgroundWorkers.Hangfire; + +[Dependency(ReplaceServices = true)] +public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + protected IServiceProvider ServiceProvider { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + public ILogger Logger { get; set; } + + public HangfireDynamicBackgroundWorkerManager( + IServiceProvider serviceProvider, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + { + ServiceProvider = serviceProvider; + HandlerRegistry = handlerRegistry; + Logger = NullLogger.Instance; + } + + public virtual Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + var cronExpression = schedule.CronExpression; + if (cronExpression.IsNullOrWhiteSpace()) + { + var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + cronExpression = GetCron(period); + } + + // Register the handler first so it is available the moment the recurring job fires. + HandlerRegistry.Register(workerName, handler); + try + { + ScheduleRecurringJob(workerName, cronExpression, cancellationToken); + } + catch + { + HandlerRegistry.Unregister(workerName); + throw; + } + + return Task.CompletedTask; + } + + public virtual Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + // Always remove the persistent recurring job regardless of in-memory registry state. + // This ensures cleanup works correctly after an application restart, when the registry + // is empty but the Hangfire recurring job may still exist in the database. + var recurringJobId = $"DynamicWorker:{workerName}"; + RecurringJob.RemoveIfExists(recurringJobId); + var wasRegistered = HandlerRegistry.Unregister(workerName); + + return Task.FromResult(wasRegistered); + } + + public virtual Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + var cronExpression = schedule.CronExpression; + if (cronExpression.IsNullOrWhiteSpace()) + { + var period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + cronExpression = GetCron(period); + } + + // Always update the persistent recurring job regardless of in-memory registry state. + // This ensures UpdateScheduleAsync works correctly after an application restart, + // when the registry is empty but the Hangfire recurring job may still exist in the database. + ScheduleRecurringJob(workerName, cronExpression, cancellationToken); + + return Task.FromResult(true); + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return HandlerRegistry.IsRegistered(workerName); + } + + public virtual Task StopAllAsync(CancellationToken cancellationToken = default) + { + HandlerRegistry.Clear(); + return Task.CompletedTask; + } + + protected virtual void ScheduleRecurringJob(string workerName, string cronExpression, CancellationToken cancellationToken) + { + var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; + var queueName = abpHangfireOptions.DefaultQueue; + var recurringJobId = $"DynamicWorker:{workerName}"; + + if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) + { + Logger.LogWarning( + "Current storage doesn't support specifying queues ({QueueName}) directly for a specific job. Please use the QueueAttribute instead.", + queueName); + + RecurringJob.AddOrUpdate( + recurringJobId, + adapter => adapter.DoWorkAsync(workerName, CancellationToken.None), + cronExpression, + new RecurringJobOptions + { + TimeZone = TimeZoneInfo.Utc + }); + } + else + { + RecurringJob.AddOrUpdate( + recurringJobId, + queueName, + adapter => adapter.DoWorkAsync(workerName, CancellationToken.None), + cronExpression, + new RecurringJobOptions + { + TimeZone = TimeZoneInfo.Utc + }); + } + } + + protected virtual string GetCron(int period) + { + var time = TimeSpan.FromMilliseconds(period); + string cron; + + if (time.TotalSeconds <= 59) + { + var seconds = Math.Max(1, (int)Math.Round(time.TotalSeconds)); + cron = $"*/{seconds} * * * * *"; + } + else if (time.TotalMinutes <= 59) + { + var minutes = Math.Max(1, (int)Math.Round(time.TotalMinutes)); + cron = $"*/{minutes} * * * *"; + } + else if (time.TotalHours <= 23) + { + var hours = Math.Max(1, (int)Math.Round(time.TotalHours)); + cron = $"0 */{hours} * * *"; + } + else if (time.TotalDays <= 31) + { + var days = Math.Max(1, (int)Math.Round(time.TotalDays)); + cron = $"0 0 0 1/{days} * *"; + } + else + { + throw new AbpException($"Cannot convert period: {period} to cron expression."); + } + + return cron; + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs new file mode 100644 index 0000000000..d27395bab2 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerAdapter.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Quartz; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; + +namespace Volo.Abp.BackgroundWorkers.Quartz; + +public class QuartzDynamicBackgroundWorkerAdapter : IJob, ITransientDependency +{ + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + protected IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; set; } + + public QuartzDynamicBackgroundWorkerAdapter( + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry, + IServiceProvider serviceProvider) + { + HandlerRegistry = handlerRegistry; + ServiceProvider = serviceProvider; + Logger = NullLogger.Instance; + } + + public virtual async Task Execute(IJobExecutionContext context) + { + var rawWorkerName = context.MergedJobDataMap.GetString(QuartzDynamicBackgroundWorkerManager.DynamicWorkerNameKey); + if (string.IsNullOrWhiteSpace(rawWorkerName)) + { + return; + } + + var workerName = rawWorkerName!; + var handler = HandlerRegistry.Get(workerName); + if (handler == null) + { + Logger.LogWarning("No handler registered for dynamic worker: {WorkerName}", workerName); + return; + } + + try + { + await handler( + new DynamicBackgroundWorkerExecutionContext(workerName, ServiceProvider), + context.CancellationToken); + } + catch (Exception ex) + { + // Swallow the exception to match the behavior of AsyncPeriodicBackgroundWorkerBase, + // which catches, notifies and logs without rethrowing. This prevents Quartz from + // treating a single failed execution as a job failure and triggering retries. + await ServiceProvider.GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..5a729ad974 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Quartz; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers.Quartz; + +[Dependency(ReplaceServices = true)] +public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + public const string DynamicWorkerNameKey = "AbpDynamicWorkerName"; + + protected IScheduler Scheduler { get; } + protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; } + public ILogger Logger { get; set; } + + public QuartzDynamicBackgroundWorkerManager( + IScheduler scheduler, + IDynamicBackgroundWorkerHandlerRegistry handlerRegistry) + { + Scheduler = scheduler; + HandlerRegistry = handlerRegistry; + Logger = NullLogger.Instance; + } + + public virtual async Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + var jobKey = new JobKey($"DynamicWorker:{workerName}"); + var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); + var jobDetail = JobBuilder.Create() + .WithIdentity(jobKey) + .UsingJobData(DynamicWorkerNameKey, workerName) + .Build(); + + var trigger = BuildTrigger(schedule, jobDetail, triggerKey); + + // Register the handler first so it is available the moment the job fires. + HandlerRegistry.Register(workerName, handler); + try + { + // Use replace=true to avoid TOCTOU race between CheckExists and ScheduleJob. + await Scheduler.ScheduleJobs( + new Dictionary> + { + { jobDetail, new[] { trigger } } + }, + replace: true, + cancellationToken); + } + catch + { + HandlerRegistry.Unregister(workerName); + throw; + } + } + + public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + // Always delete the persistent Quartz job regardless of in-memory registry state. + // This ensures cleanup works correctly after an application restart, when the registry + // is empty but the Quartz job may still exist in the scheduler store. + var jobKey = new JobKey($"DynamicWorker:{workerName}"); + var deleted = await Scheduler.DeleteJob(jobKey, cancellationToken); + var wasRegistered = HandlerRegistry.Unregister(workerName); + + return deleted || wasRegistered; + } + + public virtual async Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); + var jobKey = new JobKey($"DynamicWorker:{workerName}"); + var jobDetail = JobBuilder.Create() + .WithIdentity(jobKey) + .UsingJobData(DynamicWorkerNameKey, workerName) + .Build(); + + var trigger = BuildTrigger(schedule, jobDetail, triggerKey); + + // Always attempt to reschedule the persistent job regardless of in-memory registry state. + // This ensures UpdateScheduleAsync works correctly after an application restart, + // when the registry is empty but the Quartz job may still exist in the scheduler store. + // RescheduleJob returns null if the trigger was not found, indicating the job did not exist. + var result = await Scheduler.RescheduleJob(triggerKey, trigger, cancellationToken); + return result != null; + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return HandlerRegistry.IsRegistered(workerName); + } + + public virtual Task StopAllAsync(CancellationToken cancellationToken = default) + { + HandlerRegistry.Clear(); + return Task.CompletedTask; + } + + protected virtual ITrigger BuildTrigger(DynamicBackgroundWorkerSchedule schedule, IJobDetail jobDetail, TriggerKey triggerKey) + { + var triggerBuilder = TriggerBuilder.Create() + .ForJob(jobDetail) + .WithIdentity(triggerKey); + + if (!schedule.CronExpression.IsNullOrWhiteSpace()) + { + triggerBuilder.WithCronSchedule(schedule.CronExpression); + } + else + { + triggerBuilder.WithSimpleSchedule(builder => + builder.WithInterval(TimeSpan.FromMilliseconds(schedule.Period!.Value)).RepeatForever()); + } + + return triggerBuilder.Build(); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersCronTickerConfiguration.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersCronTickerConfiguration.cs index 0e8ed89a14..662c05a58a 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersCronTickerConfiguration.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersCronTickerConfiguration.cs @@ -9,4 +9,6 @@ public class AbpBackgroundWorkersCronTickerConfiguration public int[]? RetryIntervals { get; set; } public TickerTaskPriority? Priority { get; set; } + + public int? MaxConcurrency { get; set; } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs index 922cad294d..cc6c847197 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQBackgroundWorkerManager.cs @@ -53,11 +53,11 @@ public class AbpTickerQBackgroundWorkerManager : BackgroundWorkerManager, ISingl var name = BackgroundWorkerNameAttribute.GetNameOrNull(worker.GetType()) ?? worker.GetType().FullName; var config = Options.GetConfigurationOrNull(ProxyHelper.GetUnProxiedType(worker)); - AbpTickerQFunctionProvider.Functions.TryAdd(name!, (string.Empty, config?.Priority ?? TickerTaskPriority.LongRunning, async (tickerQCancellationToken, serviceProvider, tickerFunctionContext) => + AbpTickerQFunctionProvider.AddFunction(name!, async (tickerQCancellationToken, serviceProvider, tickerFunctionContext) => { var workerInvoker = new AbpTickerQPeriodicBackgroundWorkerInvoker(worker, serviceProvider); await workerInvoker.DoWorkAsync(tickerFunctionContext, tickerQCancellationToken); - })); + }, config?.Priority ?? TickerTaskPriority.LongRunning, config?.MaxConcurrency ?? 0); AbpTickerQBackgroundWorkersProvider.BackgroundWorkers.Add(name!, new AbpTickerQCronBackgroundWorker { diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..6976f49cb5 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs @@ -0,0 +1,49 @@ +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers.TickerQ; + +[Dependency(ReplaceServices = true)] +public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + public virtual Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler, + CancellationToken cancellationToken = default) + { + throw new AbpException( + "TickerQ does not support dynamic background worker registration at runtime. " + + "TickerQ uses FrozenDictionary for function registration, which requires all functions to be registered before the application starts. " + + "Please use Hangfire or Quartz provider for dynamic background workers."); + } + + public virtual Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + throw new AbpException( + "TickerQ does not support dynamic background worker registration at runtime. " + + "Please use Hangfire or Quartz provider for dynamic background workers."); + } + + public virtual Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + throw new AbpException( + "TickerQ does not support dynamic background worker registration at runtime. " + + "Please use Hangfire or Quartz provider for dynamic background workers."); + } + + public virtual bool IsRegistered(string workerName) + { + // TickerQ does not support runtime registration, so there are never any registered workers. + return false; + } + + public virtual Task StopAllAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs index 3b1b18e8b3..1d944817bf 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs @@ -49,6 +49,10 @@ public class AbpBackgroundWorkersModule : AbpModule await context.ServiceProvider .GetRequiredService() .StopAsync(cancellationToken); + + await context.ServiceProvider + .GetRequiredService() + .StopAllAsync(cancellationToken); } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..439e61de21 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundWorkers; + +public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency +{ + protected IServiceProvider ServiceProvider { get; } + public ILogger Logger { get; set; } + + private readonly ConcurrentDictionary _dynamicWorkers; + private readonly SemaphoreSlim _semaphore; + private volatile bool _isDisposed; + + public DefaultDynamicBackgroundWorkerManager(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + Logger = NullLogger.Instance; + _dynamicWorkers = new ConcurrentDictionary(); + _semaphore = new SemaphoreSlim(1, 1); + } + + public virtual async Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + Check.NotNull(handler, nameof(handler)); + + schedule.Validate(); + + if (schedule.Period == null) + { + throw new AbpException( + $"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " + + "Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ)."); + } + + await _semaphore.WaitAsync(cancellationToken); + try + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(DefaultDynamicBackgroundWorkerManager)); + } + + if (_dynamicWorkers.TryRemove(workerName, out var existingWorker)) + { + await existingWorker.StopAsync(cancellationToken); + Logger.LogInformation("Replaced existing dynamic worker: {WorkerName}", workerName); + } + + var worker = CreateDynamicWorker(workerName, schedule, handler); + _dynamicWorkers[workerName] = worker; + + await worker.StartAsync(cancellationToken); + } + finally + { + _semaphore.Release(); + } + } + + public virtual async Task RemoveAsync(string workerName, CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + + await _semaphore.WaitAsync(cancellationToken); + try + { + if (!_dynamicWorkers.TryRemove(workerName, out var worker)) + { + return false; + } + + await worker.StopAsync(cancellationToken); + return true; + } + finally + { + _semaphore.Release(); + } + } + + public virtual async Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + + schedule.Validate(); + + if (schedule.Period == null) + { + throw new AbpException( + $"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " + + "Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ)."); + } + + await _semaphore.WaitAsync(cancellationToken); + try + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(DefaultDynamicBackgroundWorkerManager)); + } + + if (!_dynamicWorkers.TryGetValue(workerName, out var worker)) + { + return false; + } + + worker.UpdateSchedule(schedule); + return true; + } + finally + { + _semaphore.Release(); + } + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return _dynamicWorkers.ContainsKey(workerName); + } + + public virtual async Task StopAllAsync(CancellationToken cancellationToken = default) + { + if (_isDisposed) + { + return; + } + + await _semaphore.WaitAsync(CancellationToken.None); + try + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + foreach (var kvp in _dynamicWorkers) + { + try + { + await kvp.Value.StopAsync(cancellationToken); + } + catch (Exception ex) + { + Logger.LogException(ex); + } + } + + _dynamicWorkers.Clear(); + } + finally + { + _semaphore.Release(); + } + } + + protected virtual InMemoryDynamicBackgroundWorker CreateDynamicWorker( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler) + { + var timer = ServiceProvider.GetRequiredService(); + var serviceScopeFactory = ServiceProvider.GetRequiredService(); + + var worker = new InMemoryDynamicBackgroundWorker( + workerName, schedule, handler, timer, serviceScopeFactory); + + worker.ServiceProvider = ServiceProvider; + worker.LazyServiceProvider = ServiceProvider.GetRequiredService(); + + return worker; + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs new file mode 100644 index 0000000000..edb810d105 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerExecutionContext.cs @@ -0,0 +1,16 @@ +using System; + +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerExecutionContext +{ + public string WorkerName { get; } + + public IServiceProvider ServiceProvider { get; } + + public DynamicBackgroundWorkerExecutionContext(string workerName, IServiceProvider serviceProvider) + { + WorkerName = Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + ServiceProvider = Check.NotNull(serviceProvider, nameof(serviceProvider)); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs new file mode 100644 index 0000000000..28f5c933d8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandler.cs @@ -0,0 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +public delegate Task DynamicBackgroundWorkerHandler(DynamicBackgroundWorkerExecutionContext context, CancellationToken cancellationToken); \ No newline at end of file diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs new file mode 100644 index 0000000000..55e8617915 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerHandlerRegistry.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerHandlerRegistry : IDynamicBackgroundWorkerHandlerRegistry, ISingletonDependency +{ + protected ConcurrentDictionary Handlers { get; } + + public DynamicBackgroundWorkerHandlerRegistry() + { + Handlers = new ConcurrentDictionary(); + } + + public virtual void Register(string workerName, DynamicBackgroundWorkerHandler handler) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(handler, nameof(handler)); + + Handlers[workerName] = handler; + } + + public virtual bool Unregister(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return Handlers.TryRemove(workerName, out _); + } + + public virtual bool IsRegistered(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return Handlers.ContainsKey(workerName); + } + + public virtual DynamicBackgroundWorkerHandler? Get(string workerName) + { + Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + return Handlers.TryGetValue(workerName, out var handler) ? handler : null; + } + + public virtual IReadOnlyCollection GetAllNames() + { + return Handlers.Keys.ToList().AsReadOnly(); + } + + public virtual void Clear() + { + Handlers.Clear(); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs new file mode 100644 index 0000000000..5fedbf1360 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManagerExtensions.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +public static class DynamicBackgroundWorkerManagerExtensions +{ + /// + /// Adds a dynamic worker with the default schedule (). + /// + public static Task AddAsync( + this IDynamicBackgroundWorkerManager manager, + string workerName, + DynamicBackgroundWorkerHandler handler, + CancellationToken cancellationToken = default) + { + return manager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = DynamicBackgroundWorkerSchedule.DefaultPeriod + }, + handler, + cancellationToken); + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs new file mode 100644 index 0000000000..6505492e44 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerSchedule.cs @@ -0,0 +1,28 @@ +using System; + +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerSchedule +{ + public const int DefaultPeriod = 60000; + + public int? Period { get; set; } + + public string? CronExpression { get; set; } + + public virtual void Validate() + { + if (Period.HasValue && Period.Value <= 0) + { + throw new ArgumentException( + $"Period must be greater than 0 when provided. Given value: {Period.Value}.", + nameof(Period)); + } + + if (Period == null && string.IsNullOrWhiteSpace(CronExpression)) + { + throw new ArgumentException( + "At least one of 'Period' or 'CronExpression' must be set."); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs new file mode 100644 index 0000000000..0b7d6757dc --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerHandlerRegistry.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Volo.Abp.BackgroundWorkers; + +public interface IDynamicBackgroundWorkerHandlerRegistry +{ + void Register(string workerName, DynamicBackgroundWorkerHandler handler); + + bool Unregister(string workerName); + + bool IsRegistered(string workerName); + + DynamicBackgroundWorkerHandler? Get(string workerName); + + IReadOnlyCollection GetAllNames(); + + void Clear(); +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs new file mode 100644 index 0000000000..7e625e7c9c --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs @@ -0,0 +1,52 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.BackgroundWorkers; + +/// +/// Manages dynamic background workers that are registered at runtime +/// without requiring a strongly-typed worker class. +/// +public interface IDynamicBackgroundWorkerManager +{ + /// + /// Adds a dynamic worker by name, schedule and handler. + /// If a worker with the same name already exists, it will be replaced. + /// + Task AddAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler, + CancellationToken cancellationToken = default); + + /// + /// Removes a previously added dynamic worker by name. + /// Always attempts to remove both the in-memory handler and any persistent scheduling record + /// (Hangfire RecurringJob or Quartz job), regardless of current in-memory state. + /// Returns true if the handler was registered in memory at the time of the call, or if + /// the persistent scheduling record was found and deleted (provider-dependent). + /// May return false after an application restart even if a persistent record was cleaned up, + /// when the provider cannot report whether a persistent record existed (e.g. Hangfire). + /// + Task RemoveAsync(string workerName, CancellationToken cancellationToken = default); + + /// + /// Updates the schedule of a previously added dynamic worker. + /// Returns true if the worker was found and updated; false otherwise. + /// + Task UpdateScheduleAsync( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + CancellationToken cancellationToken = default); + + /// + /// Checks whether a dynamic worker with the given name is registered. + /// + bool IsRegistered(string workerName); + + /// + /// Stops all dynamic workers and releases resources. + /// Called during application shutdown. + /// + Task StopAllAsync(CancellationToken cancellationToken = default); +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs new file mode 100644 index 0000000000..bab468a655 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/InMemoryDynamicBackgroundWorker.cs @@ -0,0 +1,51 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundWorkers; + +public class InMemoryDynamicBackgroundWorker : AsyncPeriodicBackgroundWorkerBase +{ + public string WorkerName { get; } + + private readonly DynamicBackgroundWorkerHandler _handler; + + public InMemoryDynamicBackgroundWorker( + string workerName, + DynamicBackgroundWorkerSchedule schedule, + DynamicBackgroundWorkerHandler handler, + AbpAsyncTimer timer, + IServiceScopeFactory serviceScopeFactory) + : base(timer, serviceScopeFactory) + { + WorkerName = Check.NotNullOrWhiteSpace(workerName, nameof(workerName)); + Check.NotNull(schedule, nameof(schedule)); + _handler = Check.NotNull(handler, nameof(handler)); + + Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + CronExpression = schedule.CronExpression; + } + + public virtual void UpdateSchedule(DynamicBackgroundWorkerSchedule schedule) + { + Check.NotNull(schedule, nameof(schedule)); + + Timer.Stop(); + Timer.Period = schedule.Period ?? DynamicBackgroundWorkerSchedule.DefaultPeriod; + CronExpression = schedule.CronExpression; + Timer.Start(StartCancellationToken); + } + + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + { + await _handler( + new DynamicBackgroundWorkerExecutionContext(WorkerName, workerContext.ServiceProvider), + workerContext.CancellationToken); + } + + public override string ToString() + { + return $"DynamicWorker:{WorkerName}"; + } +} 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 a188137ea2..b8af22ff19 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 @@ -80,6 +80,7 @@ public class AbpCliCoreModule : AbpModule options.Commands[RecreateInitialMigrationCommand.Name] = typeof(RecreateInitialMigrationCommand); options.Commands[GenerateRazorPage.Name] = typeof(GenerateRazorPage); options.Commands[McpCommand.Name] = typeof(McpCommand); + options.Commands[GenerateJwksCommand.Name] = typeof(GenerateJwksCommand); options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Pro"); options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Lite"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs index 52bbac8e43..9dd10558f7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs @@ -165,7 +165,7 @@ public class CliService : ITransientDependency promptInput = GetPromptInput(); - } while (promptInput?.ToLower() != "exit"); + } while (promptInput?.ToLowerInvariant() != "exit"); } private async Task RunBatchAsync(CommandLineArgs commandLineArgs) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BundleCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BundleCommand.cs index c0b6c0f965..ff56b5b414 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BundleCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BundleCommand.cs @@ -75,7 +75,7 @@ public class BundleCommand : IConsoleCommand, ITransientDependency var projectType = commandLineArgs.Options.GetOrNull(Options.ProjectType.Short, Options.ProjectType.Long); projectType ??= BundlingConsts.WebAssembly; - return projectType.ToLower() switch { + return projectType.ToLowerInvariant() switch { "webassembly" => BundlingConsts.WebAssembly, "maui-blazor" => BundlingConsts.MauiBlazor, _ => throw new CliUsageException(ExceptionMessageHelper.GetInvalidOptionExceptionMessage("Project Type")) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateJwksCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateJwksCommand.cs new file mode 100644 index 0000000000..f8474fea46 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateJwksCommand.cs @@ -0,0 +1,173 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +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.Commands; + +public class GenerateJwksCommand : IConsoleCommand, ITransientDependency +{ + public const string Name = "generate-jwks"; + + public ILogger Logger { get; set; } + + public GenerateJwksCommand() + { + Logger = NullLogger.Instance; + } + + public Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + var outputDir = commandLineArgs.Options.GetOrNull("output", "o") + ?? Directory.GetCurrentDirectory(); + var keySizeStr = commandLineArgs.Options.GetOrNull("key-size", "s") ?? "2048"; + var alg = commandLineArgs.Options.GetOrNull("alg") ?? "RS256"; + var kid = commandLineArgs.Options.GetOrNull("kid") ?? Guid.NewGuid().ToString("N"); + var filePrefix = commandLineArgs.Options.GetOrNull("file", "f") ?? "jwks"; + + if (!int.TryParse(keySizeStr, out var keySize) || (keySize != 2048 && keySize != 4096)) + { + Logger.LogError("Invalid key size '{0}'. Supported values: 2048, 4096.", keySizeStr); + return Task.CompletedTask; + } + + if (!IsValidAlgorithm(alg)) + { + Logger.LogError("Invalid algorithm '{0}'. Supported values: RS256, RS384, RS512, PS256, PS384, PS512.", alg); + return Task.CompletedTask; + } + + if (!Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + + Logger.LogInformation("Generating RSA {0}-bit key pair (algorithm: {1})...", keySize, alg); + + using var rsa = RSA.Create(); + rsa.KeySize = keySize; + + var jwksJson = BuildJwksJson(rsa, alg, kid); + var privateKeyPem = ExportPrivateKeyPem(rsa); + + var jwksFilePath = Path.Combine(outputDir, $"{filePrefix}.json"); + var privateKeyFilePath = Path.Combine(outputDir, $"{filePrefix}-private.pem"); + + File.WriteAllText(jwksFilePath, jwksJson, Encoding.UTF8); + File.WriteAllText(privateKeyFilePath, privateKeyPem, Encoding.UTF8); + + Logger.LogInformation(""); + Logger.LogInformation("Generated files:"); + Logger.LogInformation(" JWKS (public key) : {0}", jwksFilePath); + Logger.LogInformation(" Private key (PEM) : {0}", privateKeyFilePath); + Logger.LogInformation(""); + Logger.LogInformation("JWKS content (paste this into the ABP OpenIddict application's 'JSON Web Key Set' field):"); + Logger.LogInformation(""); + Logger.LogInformation("{0}", jwksJson); + Logger.LogInformation(""); + Logger.LogInformation("IMPORTANT: Keep the private key file safe. Never share it or commit it to source control."); + Logger.LogInformation(" The JWKS file contains only the public key and is safe to share."); + + return Task.CompletedTask; + } + + private static string BuildJwksJson(RSA rsa, string alg, string kid) + { + var parameters = rsa.ExportParameters(false); + + var n = Base64UrlEncode(parameters.Modulus); + var e = Base64UrlEncode(parameters.Exponent); + + using var stream = new System.IO.MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + + writer.WriteStartObject(); + writer.WriteStartArray("keys"); + writer.WriteStartObject(); + writer.WriteString("kty", "RSA"); + writer.WriteString("use", "sig"); + writer.WriteString("kid", kid); + writer.WriteString("alg", alg); + writer.WriteString("n", n); + writer.WriteString("e", e); + writer.WriteEndObject(); + writer.WriteEndArray(); + writer.WriteEndObject(); + writer.Flush(); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + + private static string ExportPrivateKeyPem(RSA rsa) + { +#if NET5_0_OR_GREATER + return rsa.ExportPkcs8PrivateKeyPem(); +#elif NETSTANDARD2_0 + // RSA.ExportPkcs8PrivateKey() was introduced in .NET Standard 2.1. + // The ABP CLI always runs on .NET 5+, so this path is never reached at runtime. + throw new PlatformNotSupportedException("Private key export requires .NET Standard 2.1 or later."); +#else + var privateKeyBytes = rsa.ExportPkcs8PrivateKey(); + var base64 = Convert.ToBase64String(privateKeyBytes, Base64FormattingOptions.InsertLineBreaks); + return $"-----BEGIN PRIVATE KEY-----\n{base64}\n-----END PRIVATE KEY-----"; +#endif + } + + private static string Base64UrlEncode(byte[] input) + { + return Convert.ToBase64String(input) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + + private static bool IsValidAlgorithm(string alg) + { + return alg == "RS256" || alg == "RS384" || alg == "RS512" || + alg == "PS256" || alg == "PS384" || alg == "PS512"; + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(" abp generate-jwks [options]"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine(" -o|--output Output directory (default: current directory)"); + sb.AppendLine(" -s|--key-size RSA key size: 2048 or 4096 (default: 2048)"); + sb.AppendLine(" --alg Algorithm: RS256, RS384, RS512, PS256, PS384, PS512 (default: RS256)"); + sb.AppendLine(" --kid Key ID (kid) - auto-generated if not specified"); + sb.AppendLine(" -f|--file Output file name prefix (default: jwks)"); + sb.AppendLine(" Generates: .json (JWKS) and -private.pem (private key)"); + sb.AppendLine(""); + sb.AppendLine("Examples:"); + sb.AppendLine(" abp generate-jwks"); + sb.AppendLine(" abp generate-jwks --alg RS512 --key-size 4096"); + sb.AppendLine(" abp generate-jwks -o ./keys -f myapp"); + sb.AppendLine(""); + sb.AppendLine("Description:"); + sb.AppendLine(" Generates an RSA key pair for use with OpenIddict private_key_jwt client authentication."); + sb.AppendLine(" The JWKS file (public key) should be pasted into the ABP OpenIddict application's"); + sb.AppendLine(" 'JSON Web Key Set' field in the management UI."); + sb.AppendLine(" The private key PEM file should be kept secure and used by the client application"); + sb.AppendLine(" to sign JWT assertions when authenticating to the token endpoint."); + sb.AppendLine(""); + sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli"); + + return sb.ToString(); + } + + public static string GetShortDescription() + { + return "Generates an RSA key pair (JWKS + private key) for OpenIddict private_key_jwt authentication."; + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProjectCreationCommandBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProjectCreationCommandBase.cs index 51ade9be0c..d1f29ef14d 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProjectCreationCommandBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProjectCreationCommandBase.cs @@ -687,7 +687,7 @@ public abstract class ProjectCreationCommandBase protected virtual Theme? GetThemeByTemplateOrNull(CommandLineArgs commandLineArgs, string template = "app") { - var theme = commandLineArgs.Options.GetOrNull(Options.Theme.Long)?.ToLower(); + var theme = commandLineArgs.Options.GetOrNull(Options.Theme.Long)?.ToLowerInvariant(); return template switch { @@ -725,7 +725,7 @@ public abstract class ProjectCreationCommandBase return null; } - var themeStyle = commandLineArgs.Options.GetOrNull(Options.ThemeStyle.Long)?.ToLower(); + var themeStyle = commandLineArgs.Options.GetOrNull(Options.ThemeStyle.Long)?.ToLowerInvariant(); return themeStyle switch { @@ -803,9 +803,9 @@ public abstract class ProjectCreationCommandBase var commandBuilder = new StringBuilder($"npx ng g @abp/ng.schematics:create-lib --package-name {libraryName}"); - commandBuilder.Append($" --is-secondary-entrypoint {isSecondaryEndpoint.ToString().ToLower()}"); - commandBuilder.Append($" --is-module-template {isModuleTemplate.ToString().ToLower()}"); - commandBuilder.Append($" --override {isOverride.ToString().ToLower()}"); + commandBuilder.Append($" --is-secondary-entrypoint {isSecondaryEndpoint.ToString().ToLowerInvariant()}"); + commandBuilder.Append($" --is-module-template {isModuleTemplate.ToString().ToLowerInvariant()}"); + commandBuilder.Append($" --override {isOverride.ToString().ToLowerInvariant()}"); var result = CmdHelper.RunCmdAndGetOutput(commandBuilder.ToString(), workingDirectory); return await Task.FromResult(result); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs index 425242ed70..b7b61e6a90 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs @@ -33,7 +33,7 @@ public abstract class ProxyCommandBase : IConsoleCommand, ITransientDependenc public async Task ExecuteAsync(CommandLineArgs commandLineArgs) { - var generateType = commandLineArgs.Options.GetOrNull(Options.GenerateType.Short, Options.GenerateType.Long)?.ToUpper(); + var generateType = commandLineArgs.Options.GetOrNull(Options.GenerateType.Short, Options.GenerateType.Long)?.ToUpperInvariant(); if (string.IsNullOrWhiteSpace(generateType)) { @@ -75,9 +75,9 @@ public abstract class ProxyCommandBase : IConsoleCommand, ITransientDependenc ServiceType? serviceType = null; if (!serviceTypeArg.IsNullOrWhiteSpace()) { - serviceType = serviceTypeArg.ToLower() == "application" + serviceType = serviceTypeArg.ToLowerInvariant() == "application" ? ServiceType.Application - : serviceTypeArg.ToLower() == "integration" + : serviceTypeArg.ToLowerInvariant() == "integration" ? ServiceType.Integration : ServiceType.All; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SuiteAppSettingsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SuiteAppSettingsService.cs index 665fcbb6cd..197e2b9bb5 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SuiteAppSettingsService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/SuiteAppSettingsService.cs @@ -110,7 +110,7 @@ public class SuiteAppSettingsService : ITransientDependency var dotnetToolList = CmdHelper.RunCmdAndGetOutput("dotnet tool list -g", out int exitCode); var suiteLine = dotnetToolList.Split(Environment.NewLine) - .FirstOrDefault(l => l.ToLower().StartsWith("volo.abp.suite ")); + .FirstOrDefault(l => l.ToLowerInvariant().StartsWith("volo.abp.suite ")); if (string.IsNullOrEmpty(suiteLine)) { 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 80d5a5a13b..f5ce341bf8 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 @@ -252,7 +252,7 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency var dotnetToolList = CmdHelper.RunCmdAndGetOutput("dotnet tool list -g", out int exitCode); var suiteLine = dotnetToolList.Split(Environment.NewLine) - .FirstOrDefault(l => l.ToLower().StartsWith("volo.abp.suite ")); + .FirstOrDefault(l => l.ToLowerInvariant().StartsWith("volo.abp.suite ")); if (string.IsNullOrEmpty(suiteLine)) { @@ -542,7 +542,7 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency private IEnumerable GetProcessesRelatedWithSuite() { return (from p in Process.GetProcesses() - where p.ProcessName.ToLower().Contains("abp-suite") + where p.ProcessName.ToLowerInvariant().Contains("abp-suite") select p); } 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 411be841ed..a594613b44 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 @@ -356,7 +356,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency private static bool IsNetworkSource(string source) { - return source.ToLower().StartsWith("http"); + return source.ToLowerInvariant().StartsWith("http"); } private List<(string TemplateName, string Version)> GetLocalTemplates() diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/SolutionRenamer.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/SolutionRenamer.cs index e5c950f082..f1b8220f01 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/SolutionRenamer.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/SolutionRenamer.cs @@ -59,7 +59,7 @@ public class SolutionRenamer RenameHelper.RenameAll(_entries, _projectNamePlaceHolder.ToCamelCase(), ToCamelCaseWithNamespace(_projectName)); RenameHelper.RenameAll(_entries, _projectNamePlaceHolder.ToKebabCase(), _projectName.ToKebabCase()); RenameHelper.RenameAll(_entries, _projectNamePlaceHolder.ToLowerInvariant(), _projectName.ToLowerInvariant()); - RenameHelper.RenameAll(_entries, _projectNamePlaceHolder.ToSnakeCase().ToUpper(), _projectName.ToSnakeCase().ToUpper()); + RenameHelper.RenameAll(_entries, _projectNamePlaceHolder.ToSnakeCase().ToUpperInvariant(), _projectName.ToSnakeCase().ToUpperInvariant()); } private static string ToCamelCaseWithNamespace(string name) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs index c83a5fc29d..8246aa34ea 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs @@ -241,7 +241,7 @@ public abstract class AppNoLayersTemplateBase : TemplateInfo if (context.BuildArgs.Theme != Theme.NotSpecified) { - context.Symbols.Add(context.BuildArgs.Theme.Value.ToString().ToUpper()); + context.Symbols.Add(context.BuildArgs.Theme.Value.ToString().ToUpperInvariant()); } if (context.BuildArgs.Theme == Theme.LeptonX) 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 ff656d7598..170664dae1 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 @@ -228,7 +228,7 @@ public abstract class AppTemplateBase : TemplateInfo if (context.BuildArgs.Theme != Theme.NotSpecified) { - context.Symbols.Add(context.BuildArgs.Theme.Value.ToString().ToUpper()); + context.Symbols.Add(context.BuildArgs.Theme.Value.ToString().ToUpperInvariant()); } if (context.BuildArgs.Theme == Theme.LeptonX) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceTemplateBase.cs index ace0abacb9..b0c57ce0c9 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceTemplateBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceTemplateBase.cs @@ -41,7 +41,7 @@ public abstract class MicroserviceTemplateBase : TemplateInfo if (context.BuildArgs.Theme != Theme.NotSpecified) { - context.Symbols.Add(context.BuildArgs.Theme.Value.ToString().ToUpper()); + context.Symbols.Add(context.BuildArgs.Theme.Value.ToString().ToUpperInvariant()); } if (context.BuildArgs.Theme == Theme.LeptonX) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs index 3a29358350..31dd1a76d4 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs @@ -85,7 +85,7 @@ public class AngularServiceProxyGenerator : ServiceProxyGeneratorBase : ILocalEventHandler> where TEntity : Entity where TEntityCacheItem : class + where TKey : notnull { protected IReadOnlyRepository Repository { get; } protected IDistributedCache, TKey> Cache { get; } @@ -44,6 +47,20 @@ public abstract class EntityCacheBase : }))?.Value; } + public virtual async Task> FindManyAsync(IEnumerable ids) + { + var idArray = ids.ToArray(); + var cacheItemDict = await GetCacheItemDictionaryAsync(idArray.Distinct().ToArray()); + return idArray + .Select(id => cacheItemDict.TryGetValue(id, out var item) ? item : null) + .ToList(); + } + + public virtual async Task> FindManyAsDictionaryAsync(IEnumerable ids) + { + return await GetCacheItemDictionaryAsync(ids.Distinct().ToArray()); + } + public virtual async Task GetAsync(TKey id) { return (await Cache.GetOrAddAsync( @@ -59,6 +76,75 @@ public abstract class EntityCacheBase : }))!.Value!; } + public virtual async Task> GetManyAsync(IEnumerable ids) + { + var idArray = ids.ToArray(); + var cacheItemDict = await GetCacheItemDictionaryAsync(idArray.Distinct().ToArray()); + return idArray + .Select(id => + { + if (!cacheItemDict.TryGetValue(id, out var item) || item == null) + { + throw new EntityNotFoundException(id); + } + return item; + }) + .ToList(); + } + + public virtual async Task> GetManyAsDictionaryAsync(IEnumerable ids) + { + var distinctIds = ids.Distinct().ToArray(); + var cacheItemDict = await GetCacheItemDictionaryAsync(distinctIds); + var result = new Dictionary(); + foreach (var id in distinctIds) + { + if (!cacheItemDict.TryGetValue(id, out var item) || item == null) + { + throw new EntityNotFoundException(id); + } + result[id] = item; + } + return result; + } + + protected virtual async Task> GetCacheItemDictionaryAsync(TKey[] distinctIds) + { + var cacheItems = await GetOrAddManyCacheItemsAsync(distinctIds); + return cacheItems.ToDictionary(x => x.Key, x => x.Value?.Value); + } + + protected virtual async Task?>[]> GetOrAddManyCacheItemsAsync(TKey[] ids) + { + return await Cache.GetOrAddManyAsync( + ids, + async missingKeys => + { + if (HasObjectExtensionInfo()) + { + Repository.EnableTracking(); + } + + var missingKeyArray = missingKeys.ToArray(); + var entities = await Repository.GetListAsync( + x => missingKeyArray.Contains(x.Id), + includeDetails: true + ); + var entityDict = entities.ToDictionary(e => e.Id); + + return missingKeyArray + .Select(key => + { + entityDict.TryGetValue(key, out var entity); + return new KeyValuePair>( + key, + MapToCacheItem(entity)! + ); + }) + .ToList(); + }); + } + protected virtual bool HasObjectExtensionInfo() { return typeof(IHasExtraProperties).IsAssignableFrom(typeof(TEntity)) && diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs index c35406b45b..febdbd07f9 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ public static class EntityCacheServiceCollectionExtensions this IServiceCollection services, DistributedCacheEntryOptions? cacheOptions = null) where TEntity : Entity + where TKey : notnull { services.TryAddTransient, EntityCacheWithoutCacheItem>(); services.TryAddTransient>(); @@ -36,6 +37,7 @@ public static class EntityCacheServiceCollectionExtensions DistributedCacheEntryOptions? cacheOptions = null) where TEntity : Entity where TEntityCacheItem : class + where TKey : notnull { services.TryAddTransient, EntityCacheWithObjectMapper>(); services.TryAddTransient>(); @@ -53,6 +55,7 @@ public static class EntityCacheServiceCollectionExtensions DistributedCacheEntryOptions? cacheOptions = null) where TEntity : Entity where TEntityCacheItem : class + where TKey : notnull { services.TryAddTransient, EntityCacheWithObjectMapperContext>(); services.TryAddTransient>(); @@ -65,6 +68,33 @@ public static class EntityCacheServiceCollectionExtensions return services; } + public static IServiceCollection ReplaceEntityCache( + this IServiceCollection services, + DistributedCacheEntryOptions? cacheOptions = null) + where TEntityCache : EntityCacheBase + where TEntity : Entity + where TEntityCacheItem : class + where TKey : notnull + { + services.Replace(ServiceDescriptor.Transient, TEntityCache>()); + services.TryAddTransient(); + + services.Configure(options => + { + options.ConfigureCache>(cacheOptions ?? GetDefaultCacheOptions()); + }); + + if (typeof(TEntity) == typeof(TEntityCacheItem)) + { + services.Configure(options => + { + options.Modifiers.Add(new AbpIncludeNonPublicPropertiesModifiers().CreateModifyAction(x => x.Id)); + }); + } + + return services; + } + private static DistributedCacheEntryOptions GetDefaultCacheOptions() { return new DistributedCacheEntryOptions { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs index 62b3162cdd..359b9ad716 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs @@ -10,6 +10,7 @@ public class EntityCacheWithObjectMapper : EntityCacheBase where TEntity : Entity where TEntityCacheItem : class + where TKey : notnull { protected IObjectMapper ObjectMapper { get; } @@ -30,11 +31,16 @@ public class EntityCacheWithObjectMapper : return null; } + return new EntityCacheItemWrapper(MapToValue(entity)); + } + + protected virtual TEntityCacheItem MapToValue(TEntity entity) + { if (typeof(TEntity) == typeof(TEntityCacheItem)) { - return new EntityCacheItemWrapper(entity.As()); + return entity.As(); } - return new EntityCacheItemWrapper(ObjectMapper.Map(entity)); + return ObjectMapper.Map(entity); } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapperContext.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapperContext.cs index 2471e1057c..4121c2aeae 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapperContext.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapperContext.cs @@ -9,6 +9,7 @@ public class EntityCacheWithObjectMapperContext where TEntity : Entity where TEntityCacheItem : class + where TKey : notnull { public EntityCacheWithObjectMapperContext( IReadOnlyRepository repository, diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithoutCacheItem.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithoutCacheItem.cs index 4738534831..ecd3973685 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithoutCacheItem.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithoutCacheItem.cs @@ -7,6 +7,7 @@ namespace Volo.Abp.Domain.Entities.Caching; public class EntityCacheWithoutCacheItem : EntityCacheBase where TEntity : Entity + where TKey : notnull { public EntityCacheWithoutCacheItem( IReadOnlyRepository repository, diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs index bd8ba80a63..202d7ca51b 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs @@ -1,21 +1,49 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using JetBrains.Annotations; namespace Volo.Abp.Domain.Entities.Caching; -public interface IEntityCache +public interface IEntityCache where TEntityCacheItem : class + where TKey : notnull { /// /// Gets the entity with given , /// or returns null if the entity was not found. /// Task FindAsync(TKey id); - + + /// + /// Gets multiple entities with the given . + /// Returns a list where each entry corresponds to the given id in the same order. + /// An entry will be null if the entity was not found for the corresponding id. + /// + Task> FindManyAsync(IEnumerable ids); + + /// + /// Gets multiple entities with the given as a dictionary keyed by id. + /// An entry will be null if the entity was not found for the corresponding id. + /// + Task> FindManyAsDictionaryAsync(IEnumerable ids); + /// /// Gets the entity with given , /// or throws if the entity was not found. /// - [ItemNotNull] + [ItemNotNull] Task GetAsync(TKey id); -} \ No newline at end of file + + /// + /// Gets multiple entities with the given . + /// Returns a list where each entry corresponds to the given id in the same order. + /// Throws if any entity was not found. + /// + Task> GetManyAsync(IEnumerable ids); + + /// + /// Gets multiple entities with the given as a dictionary keyed by id. + /// Throws if any entity was not found. + /// + Task> GetManyAsDictionaryAsync(IEnumerable ids); +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs index c84855e0ea..9fdab66aee 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Volo.Abp.EventBus.Distributed; @@ -14,15 +14,55 @@ public interface IDistributedEventBus : IEventBus IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class; + /// + /// Triggers an event. + /// + /// Event type + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// True, to use the outbox pattern for reliable event publishing + /// The task to handle async operation Task PublishAsync( TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class; + /// + /// Triggers an event. + /// + /// Event type + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// True, to use the outbox pattern for reliable event publishing + /// The task to handle async operation Task PublishAsync( Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true); + + /// + /// Registers to an event by its string-based event name. + /// Same (given) instance of the handler is used for all event occurrences. + /// Wraps the handler as . + /// + /// Name of the event + /// Object to handle the event + IDisposable Subscribe(string eventName, IDistributedEventHandler handler); + + /// + /// Triggers an event by its string-based event name. + /// Used for dynamic (type-less) event publishing over distributed event bus. + /// + /// Name of the event + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// True, to use the outbox pattern for reliable event publishing + /// The task to handle async operation + Task PublishAsync( + string eventName, + object eventData, + bool onUnitOfWorkComplete = true, + bool useOutbox = true); } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs new file mode 100644 index 0000000000..1af2f600a3 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs @@ -0,0 +1,17 @@ +namespace Volo.Abp.EventBus; + +/// +/// Wraps arbitrary event data with a string-based event name for dynamic (type-less) event handling. +/// +public class DynamicEventData +{ + public string EventName { get; } + + public object Data { get; } + + public DynamicEventData(string eventName, object data) + { + EventName = Check.NotNullOrWhiteSpace(eventName, nameof(eventName)); + Data = Check.NotNull(data, nameof(data)); + } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs index f1ceae8617..5430f20155 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs @@ -23,6 +23,16 @@ public interface IEventBus /// True, to publish the event at the end of the current unit of work, if available /// The task to handle async operation Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true); + + /// + /// Triggers an event by its string-based event name. + /// Used for dynamic (type-less) event publishing. + /// + /// Name of the event + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// The task to handle async operation + Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true); /// /// Registers to an event. @@ -50,6 +60,22 @@ public interface IEventBus /// Event type /// Object to handle the event IDisposable Subscribe(Type eventType, IEventHandler handler); + + /// + /// Registers to an event by its string-based event name. + /// Same (given) instance of the handler is used for all event occurrences. + /// + /// Name of the event + /// Object to handle the event + IDisposable Subscribe(string eventName, IEventHandler handler); + + /// + /// Registers to an event by its string-based event name. + /// Given factory is used to create/release handlers. + /// + /// Name of the event + /// A factory to create/release handlers + IDisposable Subscribe(string eventName, IEventHandlerFactory handler); /// /// Registers to an event. @@ -104,6 +130,20 @@ public interface IEventBus /// Event type /// Factory object that is registered before void Unsubscribe(Type eventType, IEventHandlerFactory factory); + + /// + /// Unregisters from an event by its string-based event name. + /// + /// Name of the event + /// Factory object that is registered before + void Unsubscribe(string eventName, IEventHandlerFactory factory); + + /// + /// Unregisters from an event by its string-based event name. + /// + /// Name of the event + /// Handler object that is registered before + void Unsubscribe(string eventName, IEventHandler handler); /// /// Unregisters all event handlers of given event type. @@ -117,4 +157,10 @@ public interface IEventBus /// /// Event type void UnsubscribeAll(Type eventType); + + /// + /// Unregisters all event handlers of given string-based event name. + /// + /// Name of the event + void UnsubscribeAll(string eventName); } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs index e691b6c58c..733b2dafca 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs @@ -23,4 +23,11 @@ public interface ILocalEventBus : IEventBus /// Event type /// List GetEventHandlerFactories(Type eventType); + + /// + /// Gets the list of event handler factories for the given string-based event name. + /// + /// Name of the event + /// List of event handler factories registered for the given event name + List GetDynamicEventHandlerFactories(string eventName); } diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 32b07f1b82..296f353681 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -29,6 +29,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected IAzureServiceBusSerializer Serializer { get; } protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected IAzureServiceBusMessageConsumer Consumer { get; private set; } = default!; public AzureDistributedEventBus( @@ -61,6 +62,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen PublisherPool = publisherPool; HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -81,14 +83,25 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return; } + var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); + } + else if (DynamicHandlerFactories.ContainsKey(eventName)) + { + var rawBytes = message.Body.ToArray(); + eventData = new DynamicEventData(eventName, Serializer.Deserialize(rawBytes)); + eventType = typeof(DynamicEventData); + } + else { return; } - var eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); - if (await AddToInboxAsync(message.MessageId, eventName, eventType, eventData, message.CorrelationId)) { return; @@ -100,6 +113,113 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen } } + public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) + { + var handlerFactories = GetOrCreateHandlerFactories(eventType); + + if (factory.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(factory); + + return new EventHandlerFactoryUnregistrar(this, eventType, factory); + } + + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); + } + + public override void Unsubscribe(Func action) + { + Check.NotNull(action, nameof(action)); + + GetOrCreateHandlerFactories(typeof(TEvent)) + .Locking(factories => + { + factories.RemoveAll( + factory => + { + var singleInstanceFactory = factory as SingleInstanceHandlerFactory; + if (singleInstanceFactory == null) + { + return false; + } + + var actionHandler = singleInstanceFactory.HandlerInstance as ActionEventHandler; + if (actionHandler == null) + { + return false; + } + + return actionHandler.Action == action; + }); + }); + } + + public override void Unsubscribe(Type eventType, IEventHandler handler) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory handlerFactory && + handlerFactory.HandlerInstance == handler + ); + }); + } + + public override void Unsubscribe(Type eventType, IEventHandlerFactory factory) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => factories.Remove(factory)); + } + + /// + public override void UnsubscribeAll(Type eventType) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => factories.Clear()); + } + + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); + } + + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); + } + + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) + { + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + await PublishAsync(eventName, resolvedData); + } + + protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) + { + unitOfWork.AddOrReplaceDistributedEvent(eventRecord); + } + public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { await PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData, outgoingEvent.GetCorrelationId(), outgoingEvent.Id); @@ -162,12 +282,21 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); + eventType = typeof(DynamicEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -184,82 +313,6 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return Serializer.Serialize(eventData); } - public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) - { - var handlerFactories = GetOrCreateHandlerFactories(eventType); - - if (factory.IsInFactories(handlerFactories)) - { - return NullDisposable.Instance; - } - - handlerFactories.Add(factory); - - return new EventHandlerFactoryUnregistrar(this, eventType, factory); - } - - public override void Unsubscribe(Func action) - { - Check.NotNull(action, nameof(action)); - - GetOrCreateHandlerFactories(typeof(TEvent)) - .Locking(factories => - { - factories.RemoveAll( - factory => - { - var singleInstanceFactory = factory as SingleInstanceHandlerFactory; - if (singleInstanceFactory == null) - { - return false; - } - - var actionHandler = singleInstanceFactory.HandlerInstance as ActionEventHandler; - if (actionHandler == null) - { - return false; - } - - return actionHandler.Action == action; - }); - }); - } - - public override void Unsubscribe(Type eventType, IEventHandler handler) - { - GetOrCreateHandlerFactories(eventType) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory handlerFactory && - handlerFactory.HandlerInstance == handler - ); - }); - } - - public override void Unsubscribe(Type eventType, IEventHandlerFactory factory) - { - GetOrCreateHandlerFactories(eventType) - .Locking(factories => factories.Remove(factory)); - } - - public override void UnsubscribeAll(Type eventType) - { - GetOrCreateHandlerFactories(eventType) - .Locking(factories => factories.Clear()); - } - - protected async override Task PublishToEventBusAsync(Type eventType, object eventData) - { - await PublishAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData); - } - - protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) - { - unitOfWork.AddOrReplaceDistributedEvent(eventRecord); - } - protected virtual Task PublishAsync(string eventName, object eventData) { var body = Serializer.Serialize(eventData); @@ -292,23 +345,12 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen await publisher.SendMessageAsync(message); } - protected override IEnumerable GetHandlerFactories(Type eventType) - { - return HandlerFactories - .Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) - .Select(handlerFactory => - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)) - .ToArray(); - } - - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) - { - return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); - } - protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(DynamicEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -324,4 +366,83 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen } ); } + + protected override IEnumerable GetHandlerFactories(Type eventType) + { + var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => factories.Clear()); + } + + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + var result = new List(); + + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateDynamicHandlerFactories(string eventName) + { + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) + { + return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); + } } diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 7c77340dda..5e89571554 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -79,6 +79,15 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + throw new AbpException( + "Dapr distributed event bus does not support dynamic event subscriptions. " + + "Dapr requires topic subscriptions to be declared at startup and cannot add subscriptions at runtime. " + + "Use a typed event handler (IDistributedEventHandler) instead."); + } + public override void Unsubscribe(Func action) { Check.NotNull(action, nameof(action)); @@ -129,37 +138,43 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } - protected async override Task PublishToEventBusAsync(Type eventType, object eventData) + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - await PublishToDaprAsync(eventType, eventData, null, CorrelationIdProvider.Get()); + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType != null) + { + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); + } + + throw new AbpException( + "Dapr distributed event bus does not support dynamic event publishing. " + + "Dapr requires topic subscriptions to be declared at startup. " + + "Use a typed event (PublishAsync) or ensure the event name matches a registered typed event."); } - protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - unitOfWork.AddOrReplaceDistributedEvent(eventRecord); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + await PublishToDaprAsync(eventName, resolvedData, null, CorrelationIdProvider.Get()); } - protected override IEnumerable GetHandlerFactories(Type eventType) + protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) { - var handlerFactoryList = new List(); - - foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) - { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); - } - - return handlerFactoryList.ToArray(); + unitOfWork.AddOrReplaceDistributedEvent(eventRecord); } public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { - var eventType = GetEventType(outgoingEvent.EventName); + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); if (eventType == null) { return; } - await PublishToDaprAsync(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, eventType), outgoingEvent.Id, outgoingEvent.GetCorrelationId()); + var eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); + await PublishToDaprAsync(outgoingEvent.EventName, eventData, outgoingEvent.Id, outgoingEvent.GetCorrelationId()); using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) { @@ -182,7 +197,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public virtual async Task TriggerHandlersAsync(Type eventType, object eventData, string? messageId = null, string? correlationId = null) { - if (await AddToInboxAsync(messageId, EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, correlationId)) + if (await AddToInboxAsync(messageId, GetEventName(eventType, eventData), eventType, eventData, correlationId)) { return; } @@ -195,13 +210,16 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = GetEventType(incomingEvent.EventName); + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + if (eventType == null) { return; } - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -218,6 +236,18 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return Serializer.Serialize(eventData); } + protected virtual async Task PublishToDaprAsync(Type eventType, object eventData, Guid? messageId = null, string? correlationId = null) + { + await PublishToDaprAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData, messageId, correlationId); + } + + protected virtual async Task PublishToDaprAsync(string eventName, object eventData, Guid? messageId = null, string? correlationId = null) + { + var client = await DaprClientFactory.CreateAsync(); + var data = new AbpDaprEventData(DaprEventBusOptions.PubSubName, eventName, (messageId ?? GuidGenerator.Create()).ToString("N"), Serializer.SerializeToString(eventData), correlationId); + await client.PublishEventAsync(pubsubName: DaprEventBusOptions.PubSubName, topicName: eventName, data: data); + } + protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { EventTypes.GetOrAdd(eventName, eventType); @@ -237,21 +267,55 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend ); } + protected override IEnumerable GetHandlerFactories(Type eventType) + { + var handlerFactoryList = new List(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + public Type? GetEventType(string eventName) { return EventTypes.GetOrDefault(eventName); } - protected virtual async Task PublishToDaprAsync(Type eventType, object eventData, Guid? messageId = null, string? correlationId = null) + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - await PublishToDaprAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData, messageId, correlationId); + throw new AbpException("Dapr distributed event bus does not support dynamic event subscriptions."); } - protected virtual async Task PublishToDaprAsync(string eventName, object eventData, Guid? messageId = null, string? correlationId = null) + /// + public override void Unsubscribe(string eventName, IEventHandler handler) { - var client = await DaprClientFactory.CreateAsync(); - var data = new AbpDaprEventData(DaprEventBusOptions.PubSubName, eventName, (messageId ?? GuidGenerator.Create()).ToString("N"), Serializer.SerializeToString(eventData), correlationId); - await client.PublishEventAsync(pubsubName: DaprEventBusOptions.PubSubName, topicName: eventName, data: data); + throw new AbpException("Dapr distributed event bus does not support dynamic event subscriptions."); + } + + /// + public override void UnsubscribeAll(string eventName) + { + throw new AbpException("Dapr distributed event bus does not support dynamic event subscriptions."); + } + + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + return Array.Empty(); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index a3376d9429..a5b2b2869a 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -29,6 +29,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected IProducerPool ProducerPool { get; } protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected IKafkaMessageConsumer Consumer { get; private set; } = default!; public KafkaDistributedEventBus( @@ -63,6 +64,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -80,14 +82,24 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { var eventName = message.Key; var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return; - } var messageId = message.GetMessageId(); - var eventData = Serializer.Deserialize(message.Value, eventType); var correlationId = message.GetCorrelationId(); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(message.Value, eventType); + } + else if (DynamicHandlerFactories.ContainsKey(eventName)) + { + eventData = new DynamicEventData(eventName, Serializer.Deserialize(message.Value)); + eventType = typeof(DynamicEventData); + } + else + { + return; + } if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) { @@ -114,6 +126,21 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); + } + /// public override void Unsubscribe(Func action) { @@ -168,6 +195,20 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); + } + + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); + } + protected override async Task PublishToEventBusAsync(Type eventType, object eventData) { var headers = new Headers @@ -278,12 +319,21 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); + eventType = typeof(DynamicEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -302,8 +352,8 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen private async Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers) { - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - var body = Serializer.Serialize(eventData); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + var body = Serializer.Serialize(resolvedData); var result = await PublishAsync(topicName, eventName, body, headers); if (result.Status != PersistenceStatus.Persisted) @@ -332,7 +382,10 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(DynamicEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -352,17 +405,75 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); - foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) - ) + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) { - handlerFactoryList.Add( - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + var result = new List(); + + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateDynamicHandlerFactories(string eventName) + { + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 3a647388b5..b18afd2ea8 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -33,6 +33,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected IRabbitMqMessageConsumerFactory MessageConsumerFactory { get; } protected IRabbitMqMessageConsumer Consumer { get; private set; } = default!; @@ -70,6 +71,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public virtual void Initialize() @@ -101,13 +103,23 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis { var eventName = ea.RoutingKey; var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); + } + else if (DynamicHandlerFactories.ContainsKey(eventName)) + { + var rawBytes = ea.Body.ToArray(); + eventType = typeof(DynamicEventData); + eventData = new DynamicEventData(eventName, Serializer.Deserialize(rawBytes)); + } + else { return; } - var eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); - var correlationId = ea.BasicProperties.CorrelationId; if (await AddToInboxAsync(ea.BasicProperties.MessageId, eventName, eventType, eventData, correlationId)) { @@ -139,6 +151,26 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + if (handlerFactories.Count == 1) //TODO: Multi-threading! + { + Consumer.BindAsync(eventName); + } + + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); + } + /// public override void Unsubscribe(Func action) { @@ -193,6 +225,20 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); + } + + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(eventType, eventData, correlationId: CorrelationIdProvider.Get()); @@ -256,12 +302,21 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); + eventType = typeof(DynamicEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -285,8 +340,8 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis Guid? eventId = null, string? correlationId = null) { - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - var body = Serializer.Serialize(eventData); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + var body = Serializer.Serialize(resolvedData); return PublishAsync(eventName, body, headersArguments, eventId, correlationId); } @@ -382,7 +437,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(DynamicEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -398,21 +456,79 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis } ); } - + protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } - foreach (var handlerFactory in - HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add( - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateDynamicHandlerFactories(string eventName) + { + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index 7a3d79e7d8..42ba15a10d 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -31,6 +31,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected AbpRebusEventBusOptions AbpRebusEventBusOptions { get; } public RebusDistributedEventBus( @@ -63,6 +64,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -70,6 +72,31 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen SubscribeHandlers(AbpDistributedEventBusOptions.Handlers); } + public async Task ProcessEventAsync(Type eventType, object eventData) + { + var messageId = MessageContext.Current.TransportMessage.GetMessageId(); + string eventName; + if (eventType == typeof(DynamicEventData) && eventData is DynamicEventData dynamicEventData) + { + eventName = dynamicEventData.EventName; + } + else + { + eventName = EventNameAttribute.GetNameOrDefault(eventType); + } + var correlationId = MessageContext.Current.Headers.GetOrDefault(EventBusConsts.CorrelationIdHeaderName); + + if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) + { + return; + } + + using (CorrelationIdProvider.Change(correlationId)) + { + await TriggerHandlersDirectAsync(eventType, eventData); + } + } + public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); @@ -89,6 +116,26 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + if (DynamicHandlerFactories.Count == 1) //TODO: Multi-threading! + { + Rebus.Subscribe(typeof(DynamicEventData)); + } + + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); + } + public override void Unsubscribe(Func action) { Check.NotNull(action, nameof(action)); @@ -143,21 +190,18 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen Rebus.Unsubscribe(eventType); } - public async Task ProcessEventAsync(Type eventType, object eventData) + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var messageId = MessageContext.Current.TransportMessage.GetMessageId(); - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - var correlationId = MessageContext.Current.Headers.GetOrDefault(EventBusConsts.CorrelationIdHeaderName); + var eventType = EventTypes.GetOrDefault(eventName); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); - if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) + if (eventType != null) { - return; + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - using (CorrelationIdProvider.Change(correlationId)) - { - await TriggerHandlersDirectAsync(eventType, eventData); - } + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -170,94 +214,32 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen await PublishAsync(eventType, eventData, headersArguments: headers); } - protected virtual async Task PublishAsync( - Type eventType, - object eventData, - Guid? eventId = null, - Dictionary? headersArguments = null) - { - if (AbpRebusEventBusOptions.Publish != null) - { - await AbpRebusEventBusOptions.Publish(Rebus, eventType, eventData); - return; - } - - headersArguments ??= new Dictionary(); - if (!headersArguments.ContainsKey(Headers.MessageId)) - { - headersArguments[Headers.MessageId] = (eventId ?? GuidGenerator.Create()).ToString("N"); - } - - await Rebus.Publish(eventData, headersArguments); - } - protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) { unitOfWork.AddOrReplaceDistributedEvent(eventRecord); } - protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) - { - EventTypes.GetOrAdd(eventName, eventType); - return base.OnAddToOutboxAsync(eventName, eventType, eventData); - } - - private List GetOrCreateHandlerFactories(Type eventType) - { - return HandlerFactories.GetOrAdd( - eventType, - type => - { - var eventName = EventNameAttribute.GetNameOrDefault(type); - EventTypes.GetOrAdd(eventName, eventType); - return new List(); - } - ); - } - - protected override IEnumerable GetHandlerFactories(Type eventType) + public async override Task PublishFromOutboxAsync( + OutgoingEventInfo outgoingEvent, + OutboxConfig outboxConfig) { - var handlerFactoryList = new List(); + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); + object eventData; - foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) - ) + if (eventType != null) { - handlerFactoryList.Add( - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); } - - return handlerFactoryList.ToArray(); - } - - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) - { - //Should trigger same type - if (handlerEventType == targetEventType) + else if (DynamicHandlerFactories.ContainsKey(outgoingEvent.EventName)) { - return true; + eventData = new DynamicEventData(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, typeof(object))); + eventType = typeof(DynamicEventData); } - - //Should trigger for inherited types - if (handlerEventType.IsAssignableFrom(targetEventType)) - { - return true; - } - - return false; - } - - public async override Task PublishFromOutboxAsync( - OutgoingEventInfo outgoingEvent, - OutboxConfig outboxConfig) - { - var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); - if (eventType == null) + else { return; } - var eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); - var headers = new Dictionary(); if (outgoingEvent.GetCorrelationId() != null) { @@ -306,12 +288,21 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(DynamicEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -327,4 +318,136 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return Serializer.Serialize(eventData); } + + protected virtual async Task PublishAsync( + Type eventType, + object eventData, + Guid? eventId = null, + Dictionary? headersArguments = null) + { + if (AbpRebusEventBusOptions.Publish != null) + { + await AbpRebusEventBusOptions.Publish(Rebus, eventType, eventData); + return; + } + + headersArguments ??= new Dictionary(); + if (!headersArguments.ContainsKey(Headers.MessageId)) + { + headersArguments[Headers.MessageId] = (eventId ?? GuidGenerator.Create()).ToString("N"); + } + + await Rebus.Publish(eventData, headersArguments); + } + + protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) + { + if (typeof(DynamicEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } + return base.OnAddToOutboxAsync(eventName, eventType, eventData); + } + + private List GetOrCreateHandlerFactories(Type eventType) + { + return HandlerFactories.GetOrAdd( + eventType, + type => + { + var eventName = EventNameAttribute.GetNameOrDefault(type); + EventTypes.GetOrAdd(eventName, eventType); + return new List(); + } + ); + } + + protected override IEnumerable GetHandlerFactories(Type eventType) + { + var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + var result = new List(); + + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateDynamicHandlerFactories(string eventName) + { + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) + { + //Should trigger same type + if (handlerEventType == targetEventType) + { + return true; + } + + //Should trigger for inherited types + if (handlerEventType.IsAssignableFrom(targetEventType)) + { + return true; + } + + return false; + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 3668193c9b..c4d186e2b3 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -43,16 +43,25 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB CorrelationIdProvider = correlationIdProvider; } + /// public virtual IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class { return Subscribe(typeof(TEvent), handler); } + /// + public virtual IDisposable Subscribe(string eventName, IDistributedEventHandler handler) + { + return Subscribe(eventName, (IEventHandler)handler); + } + + /// public override Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) { return PublishAsync(eventType, eventData, onUnitOfWorkComplete, useOutbox: true); } + /// public virtual Task PublishAsync( TEvent eventData, bool onUnitOfWorkComplete = true, @@ -62,6 +71,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB return PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete, useOutbox); } + /// public virtual async Task PublishAsync( Type eventType, object eventData, @@ -90,11 +100,29 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB await TriggerDistributedEventSentAsync(new DistributedEventSent() { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); } + /// + public virtual Task PublishAsync( + string eventName, + object eventData, + bool onUnitOfWorkComplete = true, + bool useOutbox = true) + { + var eventType = GetEventTypeByEventName(eventName); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete, useOutbox); + } + + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete, useOutbox); + } + public abstract Task PublishFromOutboxAsync( OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig @@ -124,7 +152,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB if (outboxConfig.Selector == null || outboxConfig.Selector(eventType)) { var eventOutbox = (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); - var eventName = EventNameAttribute.GetNameOrDefault(eventType); + (var eventName, eventData) = ResolveEventForPublishing(eventType, eventData); await OnAddToOutboxAsync(eventName, eventType, eventData); @@ -181,12 +209,12 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (await eventInbox.ExistsByMessageIdAsync(messageId!)) { - // Message already exists in the inbox, no need to add again. - // This can happen in case of retries from the sender side. addToInbox = true; continue; } } + + eventData = GetEventData(eventData); var incomingEventInfo = new IncomingEventInfo( GuidGenerator.Create(), @@ -212,8 +240,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB await TriggerDistributedEventReceivedAsync(new DistributedEventReceived { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await TriggerHandlersAsync(eventType, eventData); @@ -224,8 +252,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB await TriggerDistributedEventReceivedAsync(new DistributedEventReceived { Source = DistributedEventSource.Inbox, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await TriggerHandlersAsync(eventType, eventData, exceptions, inboxConfig); @@ -254,4 +282,29 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB // ignored } } + + protected virtual string GetEventName(Type eventType, object eventData) + { + if (eventData is DynamicEventData dynamicEventData) + { + return dynamicEventData.EventName; + } + + return EventNameAttribute.GetNameOrDefault(eventType); + } + + protected virtual object GetEventData(object eventData) + { + if (eventData is DynamicEventData dynamicEventData) + { + return dynamicEventData.Data; + } + + return eventData; + } + + protected virtual (string EventName, object EventData) ResolveEventForPublishing(Type eventType, object eventData) + { + return (GetEventName(eventType, eventData), GetEventData(eventData)); + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index 843fb4f8ea..1d8e1ed680 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -3,9 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; using System.Text.Json; -using System.Text.Unicode; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -26,6 +24,8 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary DynamicEventNames { get; } + public LocalDistributedEventBus( IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant, @@ -47,6 +47,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen correlationIdProvider) { EventTypes = new ConcurrentDictionary(); + DynamicEventNames = new ConcurrentDictionary(); Subscribe(abpDistributedEventBusOptions.Value.Handlers); } @@ -71,6 +72,14 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen } } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + DynamicEventNames.GetOrAdd(eventName, true); + return LocalEventBus.Subscribe(eventName, handler); + } + + /// public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var eventName = EventNameAttribute.GetNameOrDefault(eventType); @@ -93,11 +102,31 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen LocalEventBus.Unsubscribe(eventType, factory); } + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + LocalEventBus.Unsubscribe(eventName, factory); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + LocalEventBus.Unsubscribe(eventName, handler); + } + + /// public override void UnsubscribeAll(Type eventType) { LocalEventBus.UnsubscribeAll(eventType); } + /// + public override void UnsubscribeAll(string eventName) + { + LocalEventBus.UnsubscribeAll(eventName); + } + + /// public async override Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null) @@ -120,23 +149,43 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen await TriggerDistributedEventSentAsync(new DistributedEventSent() { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await TriggerDistributedEventReceivedAsync(new DistributedEventReceived { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await PublishToEventBusAsync(eventType, eventData); } + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + return PublishAsync(eventName, eventData, onUnitOfWorkComplete, useOutbox: true); + } + + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete, useOutbox); + } + + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete, useOutbox); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - if (await AddToInboxAsync(Guid.NewGuid().ToString(), EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, null)) + if (await AddToInboxAsync(Guid.NewGuid().ToString(), GetEventName(eventType, eventData), eventType, eventData, null)) { return; } @@ -168,10 +217,27 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); if (eventType == null) { - return; + var isDynamic = DynamicEventNames.ContainsKey(outgoingEvent.EventName); + if (!isDynamic) + { + return; + } + + eventType = typeof(DynamicEventData); + } + + object eventData; + if (eventType == typeof(DynamicEventData)) + { + eventData = new DynamicEventData( + outgoingEvent.EventName, + System.Text.Json.JsonSerializer.Deserialize(outgoingEvent.EventData)!); + } + else + { + eventData = System.Text.Json.JsonSerializer.Deserialize(outgoingEvent.EventData, eventType)!; } - var eventData = JsonSerializer.Deserialize(Encoding.UTF8.GetString(outgoingEvent.EventData), eventType)!; if (await AddToInboxAsync(Guid.NewGuid().ToString(), outgoingEvent.EventName, eventType, eventData, null)) { return; @@ -193,10 +259,27 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); if (eventType == null) { - return; + var isDynamic = DynamicEventNames.ContainsKey(incomingEvent.EventName); + if (!isDynamic) + { + return; + } + + eventType = typeof(DynamicEventData); + } + + object eventData; + if (eventType == typeof(DynamicEventData)) + { + eventData = new DynamicEventData( + incomingEvent.EventName, + System.Text.Json.JsonSerializer.Deserialize(incomingEvent.EventData)!); + } + else + { + eventData = System.Text.Json.JsonSerializer.Deserialize(incomingEvent.EventData, eventType)!; } - var eventData = JsonSerializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -210,12 +293,15 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override byte[] Serialize(object eventData) { - return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(eventData)); + return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(eventData); } protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (eventType != typeof(DynamicEventData)) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -223,4 +309,14 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return LocalEventBus.GetEventHandlerFactories(eventType); } + + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + return LocalEventBus.GetDynamicEventHandlerFactories(eventName); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs index bf64839863..73636f49a5 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Volo.Abp.EventBus.Distributed; @@ -12,6 +12,12 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } + /// + public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + return Task.CompletedTask; + } + public IDisposable Subscribe(Func action) where TEvent : class { return NullDisposable.Instance; @@ -32,6 +38,24 @@ public sealed class NullDistributedEventBus : IDistributedEventBus return NullDisposable.Instance; } + /// + public IDisposable Subscribe(string eventName, IEventHandler handler) + { + return NullDisposable.Instance; + } + + /// + public IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + return NullDisposable.Instance; + } + + /// + public IDisposable Subscribe(string eventName, IDistributedEventHandler handler) + { + return NullDisposable.Instance; + } + public IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { return NullDisposable.Instance; @@ -67,6 +91,16 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } + /// + public void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + } + + /// + public void Unsubscribe(string eventName, IEventHandler handler) + { + } + public void UnsubscribeAll() where TEvent : class { @@ -74,7 +108,11 @@ public sealed class NullDistributedEventBus : IDistributedEventBus public void UnsubscribeAll(Type eventType) { + } + /// + public void UnsubscribeAll(string eventName) + { } public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class @@ -96,4 +134,10 @@ public sealed class NullDistributedEventBus : IDistributedEventBus { return Task.CompletedTask; } + + /// + public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + { + return Task.CompletedTask; + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index c8ce733b22..e59cd95613 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Collections; using Volo.Abp.DynamicProxy; using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Json; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Uow; @@ -57,6 +58,15 @@ public abstract class EventBusBase : IEventBus return Subscribe(eventType, new SingleInstanceHandlerFactory(handler)); } + /// + public virtual IDisposable Subscribe(string eventName, IEventHandler handler) + { + return Subscribe(eventName, new SingleInstanceHandlerFactory(handler)); + } + + /// + public abstract IDisposable Subscribe(string eventName, IEventHandlerFactory handler); + /// public virtual IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { @@ -83,6 +93,12 @@ public abstract class EventBusBase : IEventBus public abstract void Unsubscribe(Type eventType, IEventHandlerFactory factory); + /// + public abstract void Unsubscribe(string eventName, IEventHandlerFactory factory); + + /// + public abstract void Unsubscribe(string eventName, IEventHandler handler); + /// public virtual void UnsubscribeAll() where TEvent : class { @@ -92,6 +108,9 @@ public abstract class EventBusBase : IEventBus /// public abstract void UnsubscribeAll(Type eventType); + /// + public abstract void UnsubscribeAll(string eventName); + /// public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class @@ -117,6 +136,9 @@ public abstract class EventBusBase : IEventBus await PublishToEventBusAsync(eventType, eventData); } + /// + public abstract Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true); + protected abstract Task PublishToEventBusAsync(Type eventType, object eventData); protected abstract void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord); @@ -137,31 +159,81 @@ public abstract class EventBusBase : IEventBus { await new SynchronizationContextRemover(); - foreach (var handlerFactories in GetHandlerFactories(eventType).ToList()) + var (handlerFactoriesList, actualEventType) = ResolveHandlerFactories(eventType, eventData); + + foreach (var handlerFactories in handlerFactoriesList) { foreach (var handlerFactory in handlerFactories.EventHandlerFactories.ToList()) { - await TriggerHandlerAsync(handlerFactory, handlerFactories.EventType, eventData, exceptions, inboxConfig); + var resolvedEventData = ResolveEventDataForHandler(eventData, eventType, handlerFactories.EventType); + await TriggerHandlerAsync(handlerFactory, handlerFactories.EventType, resolvedEventData, exceptions, inboxConfig); } } - //Implements generic argument inheritance. See IEventDataWithInheritableGenericArgument - if (eventType.GetTypeInfo().IsGenericType && - eventType.GetGenericArguments().Length == 1 && - typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(eventType)) + if (actualEventType != null && + actualEventType.GetTypeInfo().IsGenericType && + actualEventType.GetGenericArguments().Length == 1 && + typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(actualEventType)) { - var genericArg = eventType.GetGenericArguments()[0]; + var resolvedEventData = eventData is DynamicEventData aed + ? ConvertDynamicEventData(aed.Data, actualEventType) + : eventData; + + var genericArg = actualEventType.GetGenericArguments()[0]; var baseArg = genericArg.GetTypeInfo().BaseType; if (baseArg != null) { - var baseEventType = eventType.GetGenericTypeDefinition().MakeGenericType(baseArg); - var constructorArgs = ((IEventDataWithInheritableGenericArgument)eventData).GetConstructorArgs(); + var baseEventType = actualEventType.GetGenericTypeDefinition().MakeGenericType(baseArg); + var constructorArgs = ((IEventDataWithInheritableGenericArgument)resolvedEventData).GetConstructorArgs(); var baseEventData = Activator.CreateInstance(baseEventType, constructorArgs)!; await PublishToEventBusAsync(baseEventType, baseEventData); } } } + protected virtual (List Factories, Type? ActualEventType) ResolveHandlerFactories( + Type eventType, + object eventData) + { + if (eventData is DynamicEventData dynamicEventData) + { + return ( + GetDynamicHandlerFactories(dynamicEventData.EventName).ToList(), + GetEventTypeByEventName(dynamicEventData.EventName) + ); + } + + return (GetHandlerFactories(eventType).ToList(), eventType); + } + + protected virtual object ResolveEventDataForHandler(object eventData, Type sourceEventType, Type handlerEventType) + { + if (eventData is DynamicEventData dynamicEventData && handlerEventType != typeof(DynamicEventData)) + { + return ConvertDynamicEventData(dynamicEventData.Data, handlerEventType); + } + + if (handlerEventType == typeof(DynamicEventData) && eventData is not DynamicEventData) + { + return new DynamicEventData(EventNameAttribute.GetNameOrDefault(sourceEventType), eventData); + } + + return eventData; + } + + protected virtual object ConvertDynamicEventData(object data, Type targetType) + { + if (targetType.IsInstanceOfType(data)) + { + return data; + } + + using var scope = ServiceScopeFactory.CreateScope(); + var jsonSerializer = scope.ServiceProvider.GetRequiredService(); + var json = jsonSerializer.Serialize(data); + return jsonSerializer.Deserialize(targetType, json); + } + protected void ThrowOriginalExceptions(Type eventType, List exceptions) { if (exceptions.Count == 1) @@ -198,6 +270,10 @@ public abstract class EventBusBase : IEventBus protected abstract IEnumerable GetHandlerFactories(Type eventType); + protected abstract IEnumerable GetDynamicHandlerFactories(string eventName); + + protected abstract Type? GetEventTypeByEventName(string eventName); + protected virtual async Task TriggerHandlerAsync(IEventHandlerFactory asyncHandlerFactory, Type eventType, object eventData, List exceptions, InboxConfig? inboxConfig = null) { diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs index f94d7a4ed1..ffbf14d25f 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs @@ -23,3 +23,25 @@ public class EventHandlerFactoryUnregistrar : IDisposable _eventBus.Unsubscribe(_eventType, _factory); } } + +/// +/// Used to unregister an for a string-based event name on method. +/// +public class DynamicEventHandlerFactoryUnregistrar : IDisposable +{ + private readonly IEventBus _eventBus; + private readonly string _eventName; + private readonly IEventHandlerFactory _factory; + + public DynamicEventHandlerFactoryUnregistrar(IEventBus eventBus, string eventName, IEventHandlerFactory factory) + { + _eventBus = eventBus; + _eventName = eventName; + _factory = factory; + } + + public void Dispose() + { + _eventBus.Unsubscribe(_eventName, _factory); + } +} diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 7123ff340a..729afa2915 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -30,6 +30,10 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency protected ConcurrentDictionary> HandlerFactories { get; } + protected ConcurrentDictionary EventTypes { get; } + + protected ConcurrentDictionary> DynamicEventHandlerFactories { get; } + public LocalEventBus( IOptions options, IServiceScopeFactory serviceScopeFactory, @@ -42,6 +46,8 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency Logger = NullLogger.Instance; HandlerFactories = new ConcurrentDictionary>(); + EventTypes = new ConcurrentDictionary(); + DynamicEventHandlerFactories = new ConcurrentDictionary>(); SubscribeHandlers(Options.Handlers); } @@ -51,9 +57,25 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return Subscribe(typeof(TEvent), handler); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); + } + /// public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { + EventTypes.GetOrAdd(EventNameAttribute.GetNameOrDefault(eventType), eventType); + GetOrCreateHandlerFactories(eventType) .Locking(factories => { @@ -115,12 +137,53 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory)); } + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateDynamicHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + /// public override void UnsubscribeAll(Type eventType) { GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); + } + + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); + } + protected override async Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(new LocalEventMessage(Guid.NewGuid(), eventData, eventType)); @@ -141,9 +204,17 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return GetHandlerFactories(eventType).ToList(); } + /// + public virtual List GetDynamicEventHandlerFactories(string eventName) + { + return GetDynamicHandlerFactories(eventName).ToList(); + } + protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List>(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) { foreach (var factory in handlerFactory.Value) @@ -155,23 +226,71 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency } } + foreach (var handlerFactory in DynamicEventHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + foreach (var factory in handlerFactory.Value) + { + handlerFactoryList.Add(new Tuple( + factory, + typeof(DynamicEventData), + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); + } + } + return handlerFactoryList.OrderBy(x => x.Item3).Select(x => new EventTypeWithEventHandlerFactories(x.Item2, new List {x.Item1})).ToArray(); } + protected override IEnumerable GetDynamicHandlerFactories(string eventName) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + var handlerFactoryList = new List>(); + + foreach (var handlerFactory in DynamicEventHandlerFactories.Where(aehf => aehf.Key == eventName)) + { + foreach (var factory in handlerFactory.Value) + { + using var handler = factory.GetHandler(); + var handlerType = handler.EventHandler.GetType(); + handlerFactoryList.Add(new Tuple( + factory, + typeof(DynamicEventData), + ReflectionHelper + .GetAttributesOfMemberOrDeclaringType(handlerType) + .FirstOrDefault()?.Order ?? 0)); + } + } + + return handlerFactoryList.OrderBy(x => x.Item3).Select(x => + new EventTypeWithEventHandlerFactories(x.Item2, new List { x.Item1 })).ToArray(); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + private List GetOrCreateHandlerFactories(Type eventType) { return HandlerFactories.GetOrAdd(eventType, (type) => new List()); } + private List GetOrCreateDynamicHandlerFactories(string eventName) + { + return DynamicEventHandlerFactories.GetOrAdd(eventName, (name) => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { - //Should trigger same type if (handlerEventType == targetEventType) { return true; } - //Should trigger for inherited types if (handlerEventType.IsAssignableFrom(targetEventType)) { return true; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs index 3ffcd911ce..682b49d939 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -13,6 +13,12 @@ public sealed class NullLocalEventBus : ILocalEventBus } + /// + public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + return Task.CompletedTask; + } + public IDisposable Subscribe(Func action) where TEvent : class { return NullDisposable.Instance; @@ -28,6 +34,12 @@ public sealed class NullLocalEventBus : ILocalEventBus return new List(); } + /// + public List GetDynamicEventHandlerFactories(string eventName) + { + return new List(); + } + public IDisposable Subscribe() where TEvent : class where THandler : IEventHandler, new() { return NullDisposable.Instance; @@ -38,6 +50,18 @@ public sealed class NullLocalEventBus : ILocalEventBus return NullDisposable.Instance; } + /// + public IDisposable Subscribe(string eventName, IEventHandler handler) + { + return NullDisposable.Instance; + } + + /// + public IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + return NullDisposable.Instance; + } + public IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { return NullDisposable.Instance; @@ -73,6 +97,16 @@ public sealed class NullLocalEventBus : ILocalEventBus } + /// + public void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + } + + /// + public void Unsubscribe(string eventName, IEventHandler handler) + { + } + public void UnsubscribeAll() where TEvent : class { @@ -80,7 +114,11 @@ public sealed class NullLocalEventBus : ILocalEventBus public void UnsubscribeAll(Type eventType) { + } + /// + public void UnsubscribeAll(string eventName) + { } public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs index 83bacddd8c..7650e40f88 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs @@ -32,6 +32,14 @@ public class ActionApiDescriptionModel public string? ImplementFrom { get; set; } + public string? Summary { get; set; } + + public string? Remarks { get; set; } + + public string? Description { get; set; } + + public string? DisplayName { get; set; } + public ActionApiDescriptionModel() { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs index 7f178c47e4..b70355daf5 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs @@ -3,4 +3,6 @@ public class ApplicationApiDescriptionModelRequestDto { public bool IncludeTypes { get; set; } + + public bool IncludeDescriptions { get; set; } } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs index 40188c4b93..04daecc72f 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs @@ -19,6 +19,14 @@ public class ControllerApiDescriptionModel public string Type { get; set; } = default!; + public string? Summary { get; set; } + + public string? Remarks { get; set; } + + public string? Description { get; set; } + + public string? DisplayName { get; set; } + public List Interfaces { get; set; } = default!; public Dictionary Actions { get; set; } = default!; @@ -66,6 +74,14 @@ public class ControllerApiDescriptionModel Type = Type, Interfaces = Interfaces, ControllerName = ControllerName, + ControllerGroupName = ControllerGroupName, + IsRemoteService = IsRemoteService, + IsIntegrationService = IsIntegrationService, + ApiVersion = ApiVersion, + Summary = Summary, + Remarks = Remarks, + Description = Description, + DisplayName = DisplayName, Actions = new Dictionary() }; diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs index 145f3ad175..107e68008f 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs @@ -1,6 +1,8 @@ +using System.Threading.Tasks; + namespace Volo.Abp.Http.Modeling; public interface IApiDescriptionModelProvider { - ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input); + Task CreateApiModelAsync(ApplicationApiDescriptionModelRequestDto input); } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs index c3ff20b897..fb5c47245c 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs @@ -19,6 +19,12 @@ public class MethodParameterApiDescriptionModel public object? DefaultValue { get; set; } + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? DisplayName { get; set; } + public MethodParameterApiDescriptionModel() { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs index a863d1bfad..7bcac1510c 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs @@ -26,6 +26,12 @@ public class ParameterApiDescriptionModel public string? DescriptorName { get; set; } + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? DisplayName { get; set; } + public ParameterApiDescriptionModel() { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs index ed604793b0..d0bf430546 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs @@ -32,6 +32,12 @@ public class PropertyApiDescriptionModel public bool IsNullable { get; set; } + public string? Summary { get; set; } + + public string? Description { get; set; } + + public string? DisplayName { get; set; } + public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo) { var customAttributes = propertyInfo.GetCustomAttributes(true); diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs index e5a7e120a8..e77d2f7fea 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs @@ -11,6 +11,8 @@ public class ReturnValueApiDescriptionModel public string TypeSimple { get; set; } = default!; + public string? Summary { get; set; } + public ReturnValueApiDescriptionModel() { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs index d1733577e5..703c5a8583 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs @@ -20,6 +20,14 @@ public class TypeApiDescriptionModel public PropertyApiDescriptionModel[]? Properties { get; set; } + public string? Summary { get; set; } + + public string? Remarks { get; set; } + + public string? Description { get; set; } + + public string? DisplayName { get; set; } + public TypeApiDescriptionModel() { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManager.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManager.cs index a39f96e35a..193936d1a3 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManager.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManager.cs @@ -1,6 +1,8 @@ +using System.Threading.Tasks; + namespace Volo.Abp.Http.ProxyScripting; public interface IProxyScriptManager { - string GetScript(ProxyScriptingModel scriptingModel); + Task GetScriptAsync(ProxyScriptingModel scriptingModel); } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs index e08a608905..10d812e3ce 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs @@ -1,10 +1,9 @@ -using System; +using System; +using System.Threading.Tasks; namespace Volo.Abp.Http.ProxyScripting; public interface IProxyScriptManagerCache { - string GetOrAdd(string key, Func factory); - - void Set(string key, string value); + Task GetOrAddAsync(string key, Func> factory); } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs index c8fd93c220..178637b7ea 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; @@ -32,23 +33,21 @@ public class ProxyScriptManager : IProxyScriptManager, ITransientDependency _options = options.Value; } - public string GetScript(ProxyScriptingModel scriptingModel) + public async Task GetScriptAsync(ProxyScriptingModel scriptingModel) { var cacheKey = CreateCacheKey(scriptingModel); if (scriptingModel.UseCache) { - return _cache.GetOrAdd(cacheKey, () => CreateScript(scriptingModel)); + return await _cache.GetOrAddAsync(cacheKey, () => CreateScriptAsync(scriptingModel)); } - var script = CreateScript(scriptingModel); - _cache.Set(cacheKey, script); - return script; + return await CreateScriptAsync(scriptingModel); } - private string CreateScript(ProxyScriptingModel scriptingModel) + private async Task CreateScriptAsync(ProxyScriptingModel scriptingModel) { - var apiModel = _modelProvider.CreateApiModel(new ApplicationApiDescriptionModelRequestDto { IncludeTypes = false }); + var apiModel = await _modelProvider.CreateApiModelAsync(new ApplicationApiDescriptionModelRequestDto { IncludeTypes = false }); if (scriptingModel.IsPartialRequest()) { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs index 9bfe12e86f..95bb999c9d 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs @@ -1,26 +1,31 @@ -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Http.ProxyScripting; public class ProxyScriptManagerCache : IProxyScriptManagerCache, ISingletonDependency { - private readonly ConcurrentDictionary _cache; + private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary>> _asyncCache = new(); - public ProxyScriptManagerCache() + public async Task GetOrAddAsync(string key, Func> factory) { - _cache = new ConcurrentDictionary(); - } + if (_cache.TryGetValue(key, out var cached)) + { + return cached; + } - public string GetOrAdd(string key, Func factory) - { - return _cache.GetOrAdd(key, factory); - } + var result = await _asyncCache.GetOrAdd( + key, + _ => new Lazy>(factory, LazyThreadSafetyMode.ExecutionAndPublication) + ).Value; - public void Set(string key, string value) - { - _cache[key] = value; + _cache[key] = result; + _asyncCache.TryRemove(key, out _); + + return result; } } diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs new file mode 100644 index 0000000000..ea746500d6 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Volo.Abp.AutoMapper; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace AutoMapper; + +public static class AbpAutoMapperExtensibleObjectExtensions +{ + public static IMappingExpression MapExtraProperties( + this IMappingExpression mappingExpression, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[]? ignoredProperties = null, + bool mapToRegularProperties = false) + where TDestination : IHasExtraProperties + where TSource : IHasExtraProperties + { + return mappingExpression + .ForMember( + x => x.ExtraProperties, + y => y.MapFrom( + (source, destination, extraProps) => + { + var result = extraProps.IsNullOrEmpty() + ? new Dictionary() + : new Dictionary(extraProps); + + if (source.ExtraProperties == null || destination.ExtraProperties == null) + { + return result; + } + + ExtensibleObjectMapper + .MapExtraPropertiesTo( + source.ExtraProperties, + result, + definitionChecks, + ignoredProperties + ); + + return result; + }) + ) + .ForSourceMember(x => x.ExtraProperties, x => x.DoNotValidate()) + .AfterMap((source, destination, context) => + { + if (mapToRegularProperties) + { + destination.SetExtraPropertiesToRegularProperties(); + } + }); + } + + public static IMappingExpression IgnoreExtraProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasExtraProperties + where TSource : IHasExtraProperties + { + return mappingExpression.Ignore(x => x.ExtraProperties); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xml b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xsd b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/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.LuckyPenny.AutoMapper/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs new file mode 100644 index 0000000000..93cd0ef03b --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.AutoMapper; +using Volo.Abp.ObjectMapping; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AbpAutoMapperServiceCollectionExtensions +{ + public static IServiceCollection AddAutoMapperObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient() + ); + } + + public static IServiceCollection AddAutoMapperObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient, AutoMapperAutoObjectMappingProvider>() + ); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Properties/AssemblyInfo.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3fb111fede --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Volo.Abp.LuckyPenny.AutoMapper.Tests")] diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg new file mode 100644 index 0000000000..e0c33eaab1 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg.analyze.json b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg.analyze.json new file mode 100644 index 0000000000..f06064e723 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg.analyze.json @@ -0,0 +1,73 @@ +{ + "name": "Volo.Abp.LuckyPenny.AutoMapper", + "hash": "", + "contents": [ + { + "namespace": "Volo.Abp.AutoMapper", + "dependsOnModules": [ + { + "declaringAssemblyName": "Volo.Abp.ObjectMapping", + "namespace": "Volo.Abp.ObjectMapping", + "name": "AbpObjectMappingModule" + }, + { + "declaringAssemblyName": "Volo.Abp.ObjectExtending", + "namespace": "Volo.Abp.ObjectExtending", + "name": "AbpObjectExtendingModule" + }, + { + "declaringAssemblyName": "Volo.Abp.Auditing", + "namespace": "Volo.Abp.Auditing", + "name": "AbpAuditingModule" + } + ], + "implementingInterfaces": [ + { + "name": "IAbpModule", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IAbpModule" + }, + { + "name": "IOnPreApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" + }, + { + "name": "IOnApplicationInitialization", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationInitialization" + }, + { + "name": "IOnPostApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" + }, + { + "name": "IOnApplicationShutdown", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationShutdown" + }, + { + "name": "IPreConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPreConfigureServices" + }, + { + "name": "IPostConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPostConfigureServices" + } + ], + "contentType": "abpModule", + "name": "AbpLuckyPennyAutoMapperModule", + "summary": null + } + ] +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.csproj b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.csproj new file mode 100644 index 0000000000..661c9e7e5b --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.csproj @@ -0,0 +1,29 @@ + + + + + + + net8.0;net9.0;net10.0 + enable + Nullable + Volo.Abp.LuckyPenny.AutoMapper + Volo.Abp.LuckyPenny.AutoMapper + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConfigurationContext.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConfigurationContext.cs new file mode 100644 index 0000000000..c8754abd37 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConfigurationContext.cs @@ -0,0 +1,19 @@ +using System; +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperConfigurationContext : IAbpAutoMapperConfigurationContext +{ + public IMapperConfigurationExpression MapperConfiguration { get; } + + public IServiceProvider ServiceProvider { get; } + + public AbpAutoMapperConfigurationContext( + IMapperConfigurationExpression mapperConfigurationExpression, + IServiceProvider serviceProvider) + { + MapperConfiguration = mapperConfigurationExpression; + ServiceProvider = serviceProvider; + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs new file mode 100644 index 0000000000..39c21e2ca7 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperConventionalRegistrar : DefaultConventionalRegistrar +{ + protected readonly Type[] OpenTypes = { + typeof(IValueResolver<,,>), + typeof(IMemberValueResolver<,,,>), + typeof(ITypeConverter<,>), + typeof(IValueConverter<,>), + typeof(IMappingAction<,>) + }; + + protected override bool IsConventionalRegistrationDisabled(Type type) + { + return !type.GetInterfaces().Any(x => x.IsGenericType && OpenTypes.Contains(x.GetGenericTypeDefinition())) || + base.IsConventionalRegistrationDisabled(type); + } + + protected override ServiceLifetime? GetDefaultLifeTimeOrNull(Type type) + { + return ServiceLifetime.Transient; + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperOptions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperOptions.cs new file mode 100644 index 0000000000..f6ab5a5408 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperOptions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Volo.Abp.Collections; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperOptions +{ + public List> Configurators { get; } + + public ITypeList ValidatingProfiles { get; set; } + + /// + /// Default MaxDepth applied to all maps that don't have an explicit MaxDepth configured. + /// Set to null to disable the default MaxDepth behavior. + /// Default: 64. + /// + public int? DefaultMaxDepth { get; set; } = 64; + + public AbpAutoMapperOptions() + { + Configurators = new List>(); + ValidatingProfiles = new TypeList(); + } + + public void AddMaps(bool validate = false) + { + var assembly = typeof(TModule).Assembly; + + Configurators.Add(context => + { + context.MapperConfiguration.AddMaps(assembly); + }); + + if (validate) + { + var profileTypes = assembly + .DefinedTypes + .Where(type => typeof(Profile).IsAssignableFrom(type) && !type.IsAbstract && !type.IsGenericType); + + foreach (var profileType in profileTypes) + { + ValidatingProfiles.Add(profileType); + } + } + } + + public void AddProfile(bool validate = false) + where TProfile : Profile, new() + { + Configurators.Add(context => + { + context.MapperConfiguration.AddProfile(); + }); + + if (validate) + { + ValidateProfile(typeof(TProfile)); + } + } + + public void ValidateProfile(bool validate = true) + where TProfile : Profile + { + ValidateProfile(typeof(TProfile), validate); + } + + public void ValidateProfile(Type profileType, bool validate = true) + { + if (validate) + { + ValidatingProfiles.AddIfNotContains(profileType); + } + else + { + ValidatingProfiles.Remove(profileType); + } + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpLuckyPennyAutoMapperModule.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpLuckyPennyAutoMapperModule.cs new file mode 100644 index 0000000000..51833d1a84 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpLuckyPennyAutoMapperModule.cs @@ -0,0 +1,76 @@ +using System; +using AutoMapper; +using AutoMapper.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.Auditing; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.AutoMapper; + +[DependsOn( + typeof(AbpObjectMappingModule), + typeof(AbpObjectExtendingModule), + typeof(AbpAuditingModule) +)] +public class AbpLuckyPennyAutoMapperModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddConventionalRegistrar(new AbpAutoMapperConventionalRegistrar()); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAutoMapperObjectMapper(); + + context.Services.AddSingleton(sp => + { + using (var scope = sp.CreateScope()) + { + var options = scope.ServiceProvider.GetRequiredService>().Value; + + var mapperConfigurationExpression = sp.GetRequiredService>().Value; + var autoMapperConfigurationContext = new AbpAutoMapperConfigurationContext(mapperConfigurationExpression, scope.ServiceProvider); + + foreach (var configurator in options.Configurators) + { + configurator(autoMapperConfigurationContext); + } + + if (options.DefaultMaxDepth.HasValue) + { + mapperConfigurationExpression.Internal().ForAllMaps((typeMap, _) => + { + if (typeMap.MaxDepth == 0) + { + typeMap.MaxDepth = options.DefaultMaxDepth.Value; + } + }); + } + + var loggerFactory = sp.GetService() ?? NullLoggerFactory.Instance; + var mapperConfiguration = new MapperConfiguration(mapperConfigurationExpression, loggerFactory); + + foreach (var profileType in options.ValidatingProfiles) + { + mapperConfiguration.Internal().AssertConfigurationIsValid(((Profile)Activator.CreateInstance(profileType)!).ProfileName); + } + + return mapperConfiguration; + } + }); + + context.Services.AddTransient(sp => sp.GetRequiredService().CreateMapper(sp.GetService)); + + context.Services.AddTransient(sp => new MapperAccessor() + { + Mapper = sp.GetRequiredService() + }); + context.Services.AddTransient(provider => provider.GetRequiredService()); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperAutoObjectMappingProvider.cs new file mode 100644 index 0000000000..fcddf444ca --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperAutoObjectMappingProvider.cs @@ -0,0 +1,30 @@ +using Volo.Abp.ObjectMapping; +namespace Volo.Abp.AutoMapper; + +public class AutoMapperAutoObjectMappingProvider : AutoMapperAutoObjectMappingProvider, IAutoObjectMappingProvider +{ + public AutoMapperAutoObjectMappingProvider(IMapperAccessor mapperAccessor) + : base(mapperAccessor) + { + } +} + +public class AutoMapperAutoObjectMappingProvider : IAutoObjectMappingProvider +{ + public IMapperAccessor MapperAccessor { get; } + + public AutoMapperAutoObjectMappingProvider(IMapperAccessor mapperAccessor) + { + MapperAccessor = mapperAccessor; + } + + public virtual TDestination Map(object source) + { + return MapperAccessor.Mapper.Map(source); + } + + public virtual TDestination Map(TSource source, TDestination destination) + { + return MapperAccessor.Mapper.Map(source, destination); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions.cs new file mode 100644 index 0000000000..e955eef24c --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq.Expressions; +using AutoMapper; +using Volo.Abp.Auditing; + +namespace Volo.Abp.AutoMapper; + +public static class AutoMapperExpressionExtensions +{ + public static IMappingExpression Ignore(this IMappingExpression mappingExpression, Expression> destinationMember) + { + return mappingExpression.ForMember(destinationMember, opts => opts.Ignore()); + } + + public static IMappingExpression IgnoreHasCreationTimeProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasCreationTime + { + return mappingExpression.Ignore(x => x.CreationTime); + } + + public static IMappingExpression IgnoreMayHaveCreatorProperties( + this IMappingExpression mappingExpression) + where TDestination : IMayHaveCreator + { + return mappingExpression.Ignore(x => x.CreatorId); + } + + public static IMappingExpression IgnoreCreationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : ICreationAuditedObject + { + return mappingExpression + .IgnoreHasCreationTimeProperties() + .IgnoreMayHaveCreatorProperties(); + } + + public static IMappingExpression IgnoreHasModificationTimeProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasModificationTime + { + return mappingExpression.Ignore(x => x.LastModificationTime); + } + + public static IMappingExpression IgnoreModificationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IModificationAuditedObject + { + return mappingExpression + .IgnoreHasModificationTimeProperties() + .Ignore(x => x.LastModifierId); + } + + public static IMappingExpression IgnoreAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IAuditedObject + { + return mappingExpression + .IgnoreCreationAuditedObjectProperties() + .IgnoreModificationAuditedObjectProperties(); + } + + public static IMappingExpression IgnoreSoftDeleteProperties( + this IMappingExpression mappingExpression) + where TDestination : ISoftDelete + { + return mappingExpression.Ignore(x => x.IsDeleted); + } + + public static IMappingExpression IgnoreHasDeletionTimeProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasDeletionTime + { + return mappingExpression + .IgnoreSoftDeleteProperties() + .Ignore(x => x.DeletionTime); + } + + public static IMappingExpression IgnoreDeletionAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IDeletionAuditedObject + { + return mappingExpression + .IgnoreHasDeletionTimeProperties() + .Ignore(x => x.DeleterId); + } + + public static IMappingExpression IgnoreFullAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IFullAuditedObject + { + return mappingExpression + .IgnoreAuditedObjectProperties() + .IgnoreDeletionAuditedObjectProperties(); + } + + public static IMappingExpression IgnoreMayHaveCreatorProperties( + this IMappingExpression mappingExpression) + where TDestination : IMayHaveCreator + { + return mappingExpression + .Ignore(x => x.Creator); + } + + public static IMappingExpression IgnoreCreationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : ICreationAuditedObject + { + return mappingExpression + .IgnoreCreationAuditedObjectProperties() + .IgnoreMayHaveCreatorProperties(); + } + + public static IMappingExpression IgnoreModificationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IModificationAuditedObject + { + return mappingExpression + .IgnoreModificationAuditedObjectProperties() + .Ignore(x => x.LastModifier); + } + + public static IMappingExpression IgnoreAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IAuditedObject + { + return mappingExpression + .IgnoreCreationAuditedObjectProperties() + .IgnoreModificationAuditedObjectProperties(); + } + + public static IMappingExpression IgnoreDeletionAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IDeletionAuditedObject + { + return mappingExpression + .IgnoreDeletionAuditedObjectProperties() + .Ignore(x => x.Deleter); + } + + + public static IMappingExpression IgnoreFullAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IFullAuditedObject + { + return mappingExpression + .IgnoreAuditedObjectProperties() + .IgnoreDeletionAuditedObjectProperties(); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IAbpAutoMapperConfigurationContext.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IAbpAutoMapperConfigurationContext.cs new file mode 100644 index 0000000000..f7dece6633 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IAbpAutoMapperConfigurationContext.cs @@ -0,0 +1,11 @@ +using System; +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +public interface IAbpAutoMapperConfigurationContext +{ + IMapperConfigurationExpression MapperConfiguration { get; } + + IServiceProvider ServiceProvider { get; } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IMapperAccessor.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IMapperAccessor.cs new file mode 100644 index 0000000000..9997289a22 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IMapperAccessor.cs @@ -0,0 +1,8 @@ +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +public interface IMapperAccessor +{ + IMapper Mapper { get; } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/MapperAccessor.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/MapperAccessor.cs new file mode 100644 index 0000000000..5751180185 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/MapperAccessor.cs @@ -0,0 +1,8 @@ +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +internal class MapperAccessor : IMapperAccessor +{ + public IMapper Mapper { get; set; } = default!; +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/ObjectMapping/AbpAutoMapperObjectMapperExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/ObjectMapping/AbpAutoMapperObjectMapperExtensions.cs new file mode 100644 index 0000000000..33d73a77e6 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/ObjectMapping/AbpAutoMapperObjectMapperExtensions.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Volo.Abp.AutoMapper; + +namespace Volo.Abp.ObjectMapping; + +public static class AbpAutoMapperObjectMapperExtensions +{ + public static IMapper GetMapper(this IObjectMapper objectMapper) + { + return objectMapper.AutoObjectMappingProvider.GetMapper(); + } + + public static IMapper GetMapper(this IAutoObjectMappingProvider autoObjectMappingProvider) + { + if (autoObjectMappingProvider is AutoMapperAutoObjectMappingProvider autoMapperAutoObjectMappingProvider) + { + return autoMapperAutoObjectMappingProvider.MapperAccessor.Mapper; + } + + throw new AbpException($"Given object is not an instance of {typeof(AutoMapperAutoObjectMappingProvider).AssemblyQualifiedName}. The type of the given object it {autoObjectMappingProvider.GetType().AssemblyQualifiedName}"); + } +} diff --git a/framework/src/Volo.Abp.TickerQ/Volo/Abp/TickerQ/AbpTickerQFunctionProvider.cs b/framework/src/Volo.Abp.TickerQ/Volo/Abp/TickerQ/AbpTickerQFunctionProvider.cs index b92888e533..b6b05e7787 100644 --- a/framework/src/Volo.Abp.TickerQ/Volo/Abp/TickerQ/AbpTickerQFunctionProvider.cs +++ b/framework/src/Volo.Abp.TickerQ/Volo/Abp/TickerQ/AbpTickerQFunctionProvider.cs @@ -8,13 +8,33 @@ namespace Volo.Abp.TickerQ; public class AbpTickerQFunctionProvider : ISingletonDependency { - public Dictionary Functions { get;} + public Dictionary Functions { get; } - public Dictionary RequestTypes { get; } + public Dictionary RequestTypes { get; } public AbpTickerQFunctionProvider() { - Functions = new Dictionary(); + Functions = new Dictionary(); RequestTypes = new Dictionary(); } + + public void AddFunction( + string name, + TickerFunctionDelegate function, + TickerTaskPriority priority = TickerTaskPriority.Normal, + int maxConcurrency = 0) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); + Check.NotNull(function, nameof(function)); + + if (maxConcurrency < 0) + { + throw new ArgumentException("maxConcurrency must be greater than or equal to 0.", nameof(maxConcurrency)); + } + + if (!Functions.TryAdd(name, (string.Empty, priority, function, maxConcurrency))) + { + throw new AbpException($"A function with the name '{name}' is already registered."); + } + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs new file mode 100644 index 0000000000..9308026cd8 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs @@ -0,0 +1,536 @@ +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Http.Modeling; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBase +{ + [Fact] + public async Task Default_Should_Not_Include_Controller_Descriptions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition"); + + var controller = GetDocumentedController(model); + controller.Summary.ShouldBeNull(); + controller.Remarks.ShouldBeNull(); + controller.Description.ShouldBeNull(); + controller.DisplayName.ShouldBeNull(); + } + + [Fact] + public async Task Default_Should_Not_Include_Action_Descriptions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition"); + + var controller = GetDocumentedController(model); + var action = GetAction(controller, "GetGreeting"); + action.Summary.ShouldBeNull(); + action.Remarks.ShouldBeNull(); + action.Description.ShouldBeNull(); + action.DisplayName.ShouldBeNull(); + action.ReturnValue.Summary.ShouldBeNull(); + } + + [Fact] + public async Task Default_Should_Not_Include_Parameter_Descriptions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition"); + + var controller = GetDocumentedController(model); + var action = GetAction(controller, "GetGreeting"); + + var methodParam = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "name"); + methodParam.ShouldNotBeNull(); + methodParam.Summary.ShouldBeNull(); + methodParam.Description.ShouldBeNull(); + methodParam.DisplayName.ShouldBeNull(); + + var httpParam = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "name"); + httpParam.ShouldNotBeNull(); + httpParam.Summary.ShouldBeNull(); + } + + [Fact] + public async Task Default_Should_Not_Include_Type_Descriptions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + documentedDtoType.Value.Summary.ShouldBeNull(); + documentedDtoType.Value.Remarks.ShouldBeNull(); + documentedDtoType.Value.Description.ShouldBeNull(); + documentedDtoType.Value.DisplayName.ShouldBeNull(); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Controller_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetDocumentedController(model); + controller.Summary.ShouldNotBeNullOrEmpty(); + controller.Summary.ShouldContain("documented application service"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Controller_Remarks() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetDocumentedController(model); + controller.Remarks.ShouldNotBeNullOrEmpty(); + controller.Remarks.ShouldContain("integration tests"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Controller_Description_Attribute() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetDocumentedController(model); + controller.Description.ShouldBe("Documented service description from attribute"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Controller_DisplayName_Attribute() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetDocumentedController(model); + controller.DisplayName.ShouldBe("Documented Service"); + } + + [Fact] + public async Task Controller_Descriptions_Should_Be_Populated_Only_Once_For_Multiple_Actions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetDocumentedController(model); + + controller.Actions.Count.ShouldBeGreaterThan(1); + controller.Summary.ShouldNotBeNullOrEmpty(); + controller.Summary.ShouldContain("documented application service"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Action_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + action.Summary.ShouldNotBeNullOrEmpty(); + action.Summary.ShouldContain("greeting message"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Leave_Action_Remarks_Null_When_Not_Documented() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + action.Remarks.ShouldBeNull(); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Action_Description_Attribute() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + action.Description.ShouldBe("Get greeting description from attribute"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Action_DisplayName_Attribute() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + action.DisplayName.ShouldBe("Get Greeting"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_ReturnValue_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + action.ReturnValue.Summary.ShouldNotBeNullOrEmpty(); + action.ReturnValue.Summary.ShouldContain("personalized greeting"); + } + + [Fact] + public async Task Undocumented_Action_Should_Have_Null_Descriptions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "Delete"); + action.Summary.ShouldBeNull(); + action.Remarks.ShouldBeNull(); + action.Description.ShouldBeNull(); + action.DisplayName.ShouldBeNull(); + action.ReturnValue.Summary.ShouldBeNull(); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "name"); + param.ShouldNotBeNull(); + param.Summary.ShouldNotBeNullOrEmpty(); + param.Summary.ShouldContain("name of the person"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Description_And_DisplayName_From_Attribute() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "Search"); + var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "query"); + param.ShouldNotBeNull(); + param.Summary.ShouldNotBeNullOrEmpty(); + param.Summary.ShouldContain("search query"); + param.Description.ShouldBe("Query param description from attribute"); + param.DisplayName.ShouldBe("Search Query"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Leave_Parameter_Attributes_Null_When_Not_Annotated() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "Search"); + var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "maxResults"); + param.ShouldNotBeNull(); + param.Summary.ShouldNotBeNullOrEmpty(); + param.Description.ShouldBeNull(); + param.DisplayName.ShouldBeNull(); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Parameter_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "GetGreeting"); + var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "name"); + param.ShouldNotBeNull(); + param.Summary.ShouldNotBeNullOrEmpty(); + param.Summary.ShouldContain("name of the person"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Populate_Parameter_Description_And_DisplayName_From_Attribute() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "Search"); + var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "query"); + param.ShouldNotBeNull(); + param.Summary.ShouldNotBeNullOrEmpty(); + param.Description.ShouldBe("Query param description from attribute"); + param.DisplayName.ShouldBe("Search Query"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Leave_Parameter_Attributes_Null_When_Not_Annotated_Http() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var action = GetAction(GetDocumentedController(model), "Search"); + var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "maxResults"); + param.ShouldNotBeNull(); + param.Description.ShouldBeNull(); + param.DisplayName.ShouldBeNull(); + } + + [Fact] + public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Type_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + documentedDtoType.Value.Summary.ShouldNotBeNullOrEmpty(); + documentedDtoType.Value.Summary.ShouldContain("documented DTO"); + } + + [Fact] + public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Type_Description_And_DisplayName() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + documentedDtoType.Value.Description.ShouldBe("Documented DTO description from attribute"); + documentedDtoType.Value.DisplayName.ShouldBe("Documented DTO"); + } + + [Fact] + public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Property_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + documentedDtoType.Value.Properties.ShouldNotBeNull(); + + var nameProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Name"); + nameProp.ShouldNotBeNull(); + nameProp.Summary.ShouldNotBeNullOrEmpty(); + nameProp.Summary.ShouldContain("name of the documented item"); + } + + [Fact] + public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Property_Description_And_DisplayName() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + + var nameProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Name"); + nameProp.ShouldNotBeNull(); + nameProp.Description.ShouldBe("Name description from attribute"); + nameProp.DisplayName.ShouldBe("Item Name"); + } + + [Fact] + public async Task IncludeDescriptions_With_IncludeTypes_Should_Leave_Property_DisplayName_Null_When_Not_Set() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + + var valueProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Value"); + valueProp.ShouldNotBeNull(); + valueProp.Summary.ShouldNotBeNullOrEmpty(); + valueProp.Description.ShouldBe("Value description from attribute"); + valueProp.DisplayName.ShouldBeNull(); + } + + [Fact] + public async Task IncludeTypes_Without_IncludeDescriptions_Should_Not_Populate_Type_Descriptions() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeTypes=true"); + + var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); + documentedDtoType.Value.ShouldNotBeNull(); + documentedDtoType.Value.Summary.ShouldBeNull(); + documentedDtoType.Value.Remarks.ShouldBeNull(); + documentedDtoType.Value.Description.ShouldBeNull(); + documentedDtoType.Value.DisplayName.ShouldBeNull(); + + if (documentedDtoType.Value.Properties != null) + { + foreach (var prop in documentedDtoType.Value.Properties) + { + prop.Summary.ShouldBeNull(); + prop.Description.ShouldBeNull(); + prop.DisplayName.ShouldBeNull(); + } + } + } + + [Fact] + public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Controller_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetInterfaceOnlyController(model); + controller.Summary.ShouldNotBeNullOrEmpty(); + controller.Summary.ShouldContain("documented only on the interface"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Controller_Remarks() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetInterfaceOnlyController(model); + controller.Remarks.ShouldNotBeNullOrEmpty(); + controller.Remarks.ShouldContain("resolved from the interface"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Action_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetInterfaceOnlyController(model); + var action = GetAction(controller, "GetMessage"); + action.Summary.ShouldNotBeNullOrEmpty(); + action.Summary.ShouldContain("documented only on the interface"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Action_ReturnValue_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetInterfaceOnlyController(model); + var action = GetAction(controller, "GetMessage"); + action.ReturnValue.Summary.ShouldNotBeNullOrEmpty(); + action.ReturnValue.Summary.ShouldContain("resolved message"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Parameter_Summary() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetInterfaceOnlyController(model); + var action = GetAction(controller, "GetMessage"); + + var methodParam = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "key"); + methodParam.ShouldNotBeNull(); + methodParam.Summary.ShouldNotBeNullOrEmpty(); + methodParam.Summary.ShouldContain("message key"); + + var httpParam = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "key"); + httpParam.ShouldNotBeNull(); + httpParam.Summary.ShouldNotBeNullOrEmpty(); + httpParam.Summary.ShouldContain("message key"); + } + + [Fact] + public async Task IncludeDescriptions_Should_Not_Apply_Container_Param_Summary_To_Expanded_Properties() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition?includeDescriptions=true"); + + var controller = GetDocumentedController(model); + var action = GetAction(controller, "Create"); + + // Expanded properties from DocumentedDto should not have the container parameter's summary + var expandedParams = action.Parameters + .Where(p => !string.IsNullOrEmpty(p.DescriptorName) && p.Name != p.NameOnMethod) + .ToList(); + + foreach (var param in expandedParams) + { + param.Summary.ShouldBeNull(); + param.Description.ShouldBeNull(); + param.DisplayName.ShouldBeNull(); + } + } + + [Fact] + public async Task Action_ImplementFrom_Should_Point_To_Implemented_Interface() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition"); + + var controller = GetDocumentedController(model); + var action = GetAction(controller, "GetGreeting"); + + action.ImplementFrom.ShouldNotBeNullOrEmpty(); + action.ImplementFrom.ShouldContain("IDocumentedAppService"); + action.ImplementFrom.ShouldNotContain("DocumentedAppService."); + } + + [Fact] + public async Task Action_ImplementFrom_Should_Point_To_Interface_When_Only_Documented_On_Interface() + { + var model = await GetResponseAsObjectAsync( + "/api/abp/api-definition"); + + var controller = GetInterfaceOnlyController(model); + var action = GetAction(controller, "GetMessage"); + + action.ImplementFrom.ShouldNotBeNullOrEmpty(); + action.ImplementFrom.ShouldContain("IInterfaceOnlyDocumentedAppService"); + action.ImplementFrom.ShouldNotContain("InterfaceOnlyDocumentedAppService."); + } + + [Fact] + public void CreateSubModel_Should_Preserve_All_Controller_Properties() + { + var controller = ControllerApiDescriptionModel.Create( + "TestController", + "TestGroup", + isRemoteService: true, + isIntegrationService: false, + apiVersion: "1.0", + typeof(AbpApiDefinitionController_Description_Tests)); + + controller.Summary = "Test summary"; + controller.Remarks = "Test remarks"; + controller.Description = "Test description"; + controller.DisplayName = "Test display name"; + + var subModel = controller.CreateSubModel(null); + + subModel.ControllerName.ShouldBe("TestController"); + subModel.ControllerGroupName.ShouldBe("TestGroup"); + subModel.IsRemoteService.ShouldBeTrue(); + subModel.IsIntegrationService.ShouldBeFalse(); + subModel.ApiVersion.ShouldBe("1.0"); + subModel.Summary.ShouldBe("Test summary"); + subModel.Remarks.ShouldBe("Test remarks"); + subModel.Description.ShouldBe("Test description"); + subModel.DisplayName.ShouldBe("Test display name"); + subModel.Type.ShouldBe(controller.Type); + } + + private static ControllerApiDescriptionModel GetDocumentedController(ApplicationApiDescriptionModel model) + { + return model.Modules.Values + .SelectMany(m => m.Controllers.Values) + .First(c => c.ControllerName == "Documented"); + } + + private static ControllerApiDescriptionModel GetInterfaceOnlyController(ApplicationApiDescriptionModel model) + { + return model.Modules.Values + .SelectMany(m => m.Controllers.Values) + .First(c => c.ControllerName == "InterfaceOnlyDocumented"); + } + + private static ActionApiDescriptionModel GetAction(ControllerApiDescriptionModel controller, string actionName) + { + return controller.Actions.Values + .First(a => a.Name == actionName + "Async" || a.Name == actionName); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs new file mode 100644 index 0000000000..6f6c34ba53 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs @@ -0,0 +1,427 @@ +#nullable enable +using System; +using System.Reflection; +using System.Threading.Tasks; +using System.Xml.Linq; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public class XmlDocumentationProviderTests +{ + // A stub type so we can construct member-name keys that the provider can look up. + private class StubType { } + + private static XmlDocumentationProvider CreateProvider(string xmlDocBody) + { + var xml = $@" + + + {xmlDocBody} + +"; + return new FakeXmlDocumentationProvider(xml); + } + + private static string StubTypeMemberName(string elementName, string xmlContent) + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + return $@" + {xmlContent} + "; + } + + // Tests for CleanXmlText via GetSummaryAsync(Type) + + [Fact] + public async Task GetSummary_Returns_PlainText() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"A simple summary."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("A simple summary."); + } + + [Fact] + public async Task GetSummary_Expands_SeeCref_To_ShortTypeName() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Returns a value."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Returns a String value."); + } + + [Fact] + public async Task GetSummary_Expands_SeeCref_NestedType() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"See for details."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("See List`1 for details."); + } + + [Fact] + public async Task GetSummary_Expands_SeeLangword() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Returns when not found."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Returns null when not found."); + } + + [Fact] + public async Task GetSummary_Strips_CodeTag() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Use DoSomething() to start."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Use DoSomething() to start."); + } + + [Fact] + public async Task GetSummary_Strips_ParaTag() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"First paragraph."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("First paragraph."); + } + + [Fact] + public async Task GetSummary_Collapses_Whitespace() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@" + Multiple + spaces here. + "); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Multiple spaces here."); + } + + [Fact] + public async Task GetSummary_Returns_Null_When_Member_Not_Found() + { + var provider = CreateProvider(string.Empty); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetSummary_Returns_Null_When_Summary_Is_Empty() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@" "); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetSummary_Expands_Paramref() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Use the parameter."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Use the input parameter."); + } + + [Fact] + public async Task GetSummary_Expands_Typeparamref() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Returns instance."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Returns T instance."); + } + + [Fact] + public async Task GetSummary_Expands_Mixed_Tags() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Returns or if not found."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("Returns String or null if key not found."); + } + + [Fact] + public async Task GetRemarks_Returns_Null_When_No_Remarks_Element() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Only summary."); + + var result = await provider.GetRemarksAsync(typeof(StubType)); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetRemarks_Returns_Remarks_Content() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"Summary text.Some remarks here."); + + var result = await provider.GetRemarksAsync(typeof(StubType)); + + result.ShouldBe("Some remarks here."); + } + + [Fact] + public async Task GetSummary_Returns_Null_For_SelfClosing_Summary_Tag() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@""); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetSummary_Expands_SeeCref_Without_TypePrefix() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"See for details."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("See DoWork for details."); + } + + [Fact] + public async Task GetSummary_Expands_SeeCref_Property() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"See property."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("See Name property."); + } + + [Fact] + public async Task GetSummary_Strips_Multiple_Xml_Tags() + { + var typeName = typeof(StubType).FullName!.Replace('+', '.'); + var provider = CreateProvider( + $@"First. code bold end."); + + var result = await provider.GetSummaryAsync(typeof(StubType)); + + result.ShouldBe("First. code bold end."); + } + + // Tests for GetSummaryAsync(MethodInfo) and GetReturnsAsync(MethodInfo) + + private class StubService + { + public string GetValue(string key, int count) => key; + public string NoParams() => string.Empty; + public string Name { get; set; } = default!; + } + + [Fact] + public async Task GetSummary_For_Method_Returns_Summary() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"Gets a value by key."); + + var result = await provider.GetSummaryAsync(method); + + result.ShouldBe("Gets a value by key."); + } + + [Fact] + public async Task GetSummary_For_Method_Without_Parameters_Returns_Summary() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.NoParams))!; + var provider = CreateProvider( + $@"No params method."); + + var result = await provider.GetSummaryAsync(method); + + result.ShouldBe("No params method."); + } + + [Fact] + public async Task GetReturns_For_Method_Returns_Content() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"The resolved value."); + + var result = await provider.GetReturnsAsync(method); + + result.ShouldBe("The resolved value."); + } + + [Fact] + public async Task GetReturns_Returns_Null_When_No_Returns_Element() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"Gets a value."); + + var result = await provider.GetReturnsAsync(method); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetParameterSummary_Returns_Content() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"The lookup key.Max results."); + + var result = await provider.GetParameterSummaryAsync(method, "key"); + result.ShouldBe("The lookup key."); + + var result2 = await provider.GetParameterSummaryAsync(method, "count"); + result2.ShouldBe("Max results."); + } + + [Fact] + public async Task GetParameterSummary_Returns_Null_When_Param_Not_Found() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"The key."); + + var result = await provider.GetParameterSummaryAsync(method, "nonExistent"); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetParameterSummary_Returns_Null_When_Member_Not_Found() + { + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider(string.Empty); + + var result = await provider.GetParameterSummaryAsync(method, "key"); + + result.ShouldBeNull(); + } + + // Tests for GetSummaryAsync(PropertyInfo) + + [Fact] + public async Task GetSummary_For_Property_Returns_Summary() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var property = typeof(StubService).GetProperty(nameof(StubService.Name))!; + var provider = CreateProvider( + $@"The name property."); + + var result = await provider.GetSummaryAsync(property); + + result.ShouldBe("The name property."); + } + + [Fact] + public async Task GetSummary_For_Property_Returns_Null_When_Not_Found() + { + var property = typeof(StubService).GetProperty(nameof(StubService.Name))!; + var provider = CreateProvider(string.Empty); + + var result = await provider.GetSummaryAsync(property); + + result.ShouldBeNull(); + } + + // Tests for GetRemarksAsync(MethodInfo) + + [Fact] + public async Task GetRemarks_For_Method_Returns_Content() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"Implementation note."); + + var result = await provider.GetRemarksAsync(method); + + result.ShouldBe("Implementation note."); + } + + [Fact] + public async Task GetRemarks_For_Method_Returns_Null_When_Not_Found() + { + var typeName = typeof(StubService).FullName!.Replace('+', '.'); + var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; + var provider = CreateProvider( + $@"Summary only."); + + var result = await provider.GetRemarksAsync(method); + + result.ShouldBeNull(); + } + + /// + /// A fake provider that loads XML from an in-memory string instead of the file system. + /// + [DisableConventionalRegistration] + private sealed class FakeXmlDocumentationProvider : XmlDocumentationProvider + { + private readonly XDocument _document; + + public FakeXmlDocumentationProvider(string xml) + { + _document = XDocument.Parse(xml); + } + + protected override Task LoadXmlDocumentationFromDiskAsync(Assembly assembly) + { + return Task.FromResult(_document); + } + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobsTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobsTestModule.cs index f2464013fd..8f08d67c79 100644 --- a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobsTestModule.cs +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobsTestModule.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Autofac; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; using Volo.Abp.Modularity; namespace Volo.Abp.BackgroundJobs; @@ -10,5 +11,22 @@ namespace Volo.Abp.BackgroundJobs; )] public class AbpBackgroundJobsTestModule : AbpModule { + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + } + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + // Register handler via the singleton registry (through the transient manager). + // The handler persists because IDynamicBackgroundJobHandlerRegistry is a singleton. + var dynamicJobManager = context.ServiceProvider.GetRequiredService(); + var tracker = context.ServiceProvider.GetRequiredService(); + + dynamicJobManager.RegisterHandler("TestDynamicJob", (ctx, ct) => + { + tracker.ExecutedJsonData.Add(ctx.JsonData); + return System.Threading.Tasks.Task.CompletedTask; + }); + } } diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs index 30ee049746..9ef0755d0f 100644 --- a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Shouldly; using Xunit; @@ -8,12 +8,18 @@ namespace Volo.Abp.BackgroundJobs; public class BackgroundJobManager_Tests : BackgroundJobsTestBase { private readonly IBackgroundJobManager _backgroundJobManager; + private readonly IDynamicBackgroundJobManager _dynamicBackgroundJobManager; private readonly IBackgroundJobStore _backgroundJobStore; + private readonly IBackgroundJobExecuter _backgroundJobExecuter; + private readonly DynamicJobExecutionTracker _tracker; public BackgroundJobManager_Tests() { _backgroundJobManager = GetRequiredService(); + _dynamicBackgroundJobManager = GetRequiredService(); _backgroundJobStore = GetRequiredService(); + _backgroundJobExecuter = GetRequiredService(); + _tracker = GetRequiredService(); } [Fact] @@ -31,4 +37,141 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase jobIdAsString.ShouldNotBe(default); (await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString))).ShouldNotBeNull(); } + + [Fact] + public async Task Should_Enqueue_Typed_Job_By_Name() + { + var jobName = BackgroundJobNameAttribute.GetName(); + var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync(jobName, new + { + Value = "42" + }); + jobIdAsString.ShouldNotBe(default); + + var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString)); + jobInfo.ShouldNotBeNull(); + jobInfo.JobName.ShouldBe(jobName); + } + + [Fact] + public async Task Should_Enqueue_Async_Typed_Job_By_Name() + { + var jobName = BackgroundJobNameAttribute.GetName(); + var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync(jobName, new { Value = "42" }); + jobIdAsString.ShouldNotBe(default); + + var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString)); + jobInfo.ShouldNotBeNull(); + jobInfo.JobName.ShouldBe(jobName); + } + + [Fact] + public async Task Should_Enqueue_Dynamic_Handler_Job() + { + var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync("TestDynamicJob", new { OrderId = "ORD-001" }); + jobIdAsString.ShouldNotBe(default); + + var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString)); + jobInfo.ShouldNotBeNull(); + jobInfo.JobName.ShouldBe(DynamicBackgroundJobArgs.JobNameConstant); + jobInfo.JobArgs.ShouldContain("TestDynamicJob"); + jobInfo.JobArgs.ShouldContain("ORD-001"); + } + + [Fact] + public async Task Should_Execute_Dynamic_Handler_Job() + { + var countBefore = _tracker.ExecutedJsonData.Count; + + await _backgroundJobExecuter.ExecuteAsync( + new JobExecutionContext( + ServiceProvider, + typeof(DynamicBackgroundJobExecutorJob), + new DynamicBackgroundJobArgs("TestDynamicJob", "{\"OrderId\":\"ORD-001\"}") + ) + ); + + _tracker.ExecutedJsonData.Count.ShouldBeGreaterThan(countBefore); + _tracker.ExecutedJsonData.ShouldContain(d => d.Contains("ORD-001")); + } + + [Fact] + public async Task Should_Prefer_Typed_Job_Over_Dynamic_Handler() + { + var typedJobName = BackgroundJobNameAttribute.GetName(); + _dynamicBackgroundJobManager.RegisterHandler(typedJobName, (_, _) => Task.CompletedTask); + + try + { + var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync(typedJobName, new { Value = "42" }); + jobIdAsString.ShouldNotBe(default); + + var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString)); + jobInfo.ShouldNotBeNull(); + jobInfo.JobName.ShouldBe(typedJobName); + jobInfo.JobName.ShouldNotBe(DynamicBackgroundJobArgs.JobNameConstant); + } + finally + { + _dynamicBackgroundJobManager.UnregisterHandler(typedJobName); + } + } + + [Fact] + public async Task Should_Throw_For_Unknown_Job_Name() + { + await Assert.ThrowsAsync(() => + _dynamicBackgroundJobManager.EnqueueAsync("NonExistentJob", new { Value = "42" }) + ); + } + + [Fact] + public void Should_Register_And_Unregister_Handler() + { + _dynamicBackgroundJobManager.IsHandlerRegistered("TestRegister").ShouldBeFalse(); + + _dynamicBackgroundJobManager.RegisterHandler("TestRegister", (_, _) => Task.CompletedTask); + _dynamicBackgroundJobManager.IsHandlerRegistered("TestRegister").ShouldBeTrue(); + + _dynamicBackgroundJobManager.UnregisterHandler("TestRegister").ShouldBeTrue(); + _dynamicBackgroundJobManager.IsHandlerRegistered("TestRegister").ShouldBeFalse(); + } + + [Fact] + public void Should_GetAllNames_From_Handler_Registry() + { + var registry = GetRequiredService(); + + registry.Register("RegistryJob1", (_, _) => Task.CompletedTask); + registry.Register("RegistryJob2", (_, _) => Task.CompletedTask); + + try + { + var names = registry.GetAllNames(); + names.ShouldContain("RegistryJob1"); + names.ShouldContain("RegistryJob2"); + } + finally + { + registry.Unregister("RegistryJob1"); + registry.Unregister("RegistryJob2"); + } + } + + [Fact] + public void Should_Clear_Handler_Registry() + { + var registry = GetRequiredService(); + + registry.Register("ClearJob1", (_, _) => Task.CompletedTask); + registry.Register("ClearJob2", (_, _) => Task.CompletedTask); + + registry.GetAllNames().ShouldContain("ClearJob1"); + registry.GetAllNames().ShouldContain("ClearJob2"); + + registry.Clear(); + + registry.IsRegistered("ClearJob1").ShouldBeFalse(); + registry.IsRegistered("ClearJob2").ShouldBeFalse(); + } } diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicJobExecutionTracker.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicJobExecutionTracker.cs new file mode 100644 index 0000000000..3e496cdddd --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/DynamicJobExecutionTracker.cs @@ -0,0 +1,8 @@ +using System.Collections.Concurrent; + +namespace Volo.Abp.BackgroundJobs; + +public class DynamicJobExecutionTracker +{ + public ConcurrentBag ExecutedJsonData { get; } = new(); +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_StopAll_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_StopAll_Tests.cs new file mode 100644 index 0000000000..e7f8ae6bc6 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_StopAll_Tests.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.BackgroundWorkers; +using Xunit; + +namespace Volo.Abp.BackgroundWorkers; + +/// +/// Isolated tests for . +/// Kept in a separate class so the singleton manager is fresh per test (xUnit creates +/// a new test class instance — and therefore a new ABP application — for each test method). +/// +public class DynamicBackgroundWorkerManager_StopAll_Tests : BackgroundJobsTestBase +{ + private readonly IDynamicBackgroundWorkerManager _dynamicWorkerManager; + + public DynamicBackgroundWorkerManager_StopAll_Tests() + { + _dynamicWorkerManager = GetRequiredService(); + } + + [Fact] + public async Task Should_Stop_All_Workers_And_Clear_Registry() + { + var workerName1 = "stop-all-worker-1-" + Guid.NewGuid(); + var workerName2 = "stop-all-worker-2-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName1, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + await _dynamicWorkerManager.AddAsync( + workerName2, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName1).ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName2).ShouldBeTrue(); + + await _dynamicWorkerManager.StopAllAsync(); + + _dynamicWorkerManager.IsRegistered(workerName1).ShouldBeFalse(); + _dynamicWorkerManager.IsRegistered(workerName2).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Throw_ObjectDisposedException_When_AddAsync_Called_After_StopAllAsync() + { + await _dynamicWorkerManager.StopAllAsync(); + + await Assert.ThrowsAsync(() => + _dynamicWorkerManager.AddAsync( + "post-stop-worker-" + Guid.NewGuid(), + new DynamicBackgroundWorkerSchedule { Period = 1000 }, + (_, _) => Task.CompletedTask + ) + ); + } + + [Fact] + public async Task Should_Throw_ObjectDisposedException_When_UpdateScheduleAsync_Called_After_StopAllAsync() + { + var workerName = "update-after-stop-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + + await _dynamicWorkerManager.StopAllAsync(); + + await Assert.ThrowsAsync(() => + _dynamicWorkerManager.UpdateScheduleAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 1000 } + ) + ); + } + + [Fact] + public async Task Should_Be_Idempotent_When_StopAllAsync_Called_Multiple_Times() + { + await _dynamicWorkerManager.AddAsync( + "idempotent-stop-" + Guid.NewGuid(), + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + + // Should not throw when called multiple times + await _dynamicWorkerManager.StopAllAsync(); + await _dynamicWorkerManager.StopAllAsync(); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs new file mode 100644 index 0000000000..a61c99fdc1 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.BackgroundWorkers; +using Xunit; + +namespace Volo.Abp.BackgroundWorkers; + +public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase +{ + private readonly IDynamicBackgroundWorkerManager _dynamicWorkerManager; + + public DynamicBackgroundWorkerManager_Tests() + { + _dynamicWorkerManager = GetRequiredService(); + } + + [Fact] + public async Task Should_Register_Dynamic_Worker() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 1000 + }, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + } + + [Fact] + public async Task Should_Execute_Dynamic_Handler() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 50 + }, + (context, _) => + { + if (context.WorkerName == workerName) + { + tcs.TrySetResult(true); + } + + return Task.CompletedTask; + } + ); + + var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(5000)); + completedTask.ShouldBe(tcs.Task); + (await tcs.Task).ShouldBeTrue(); + } + + [Fact] + public async Task Should_Add_Dynamic_Worker_With_Default_Schedule() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + } + + [Fact] + public async Task Should_Remove_Dynamic_Worker() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 1000 + }, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + var result = await _dynamicWorkerManager.RemoveAsync(workerName); + result.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Return_False_When_Removing_NonExistent_Worker() + { + var result = await _dynamicWorkerManager.RemoveAsync("non-existent-worker-" + Guid.NewGuid()); + result.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Update_Dynamic_Worker_Schedule() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var executionCount = 0; + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 60000 + }, + (_, _) => + { + Interlocked.Increment(ref executionCount); + return Task.CompletedTask; + } + ); + + var result = await _dynamicWorkerManager.UpdateScheduleAsync( + workerName, + new DynamicBackgroundWorkerSchedule + { + Period = 50 + } + ); + + result.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + var timeout = TimeSpan.FromSeconds(5); + var startTime = DateTime.UtcNow; + while (executionCount == 0 && DateTime.UtcNow - startTime < timeout) + { + await Task.Delay(50); + } + + executionCount.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task Should_Return_False_When_Updating_NonExistent_Worker() + { + var result = await _dynamicWorkerManager.UpdateScheduleAsync( + "non-existent-worker-" + Guid.NewGuid(), + new DynamicBackgroundWorkerSchedule { Period = 1000 } + ); + + result.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Replace_Existing_Worker_When_Same_Name_Added() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var secondHandlerTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 50 }, + (_, _) => + { + secondHandlerTcs.TrySetResult(true); + return Task.CompletedTask; + } + ); + + var completedTask = await Task.WhenAny(secondHandlerTcs.Task, Task.Delay(5000)); + completedTask.ShouldBe(secondHandlerTcs.Task); + (await secondHandlerTcs.Task).ShouldBeTrue(); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + var removed = await _dynamicWorkerManager.RemoveAsync(workerName); + removed.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Throw_When_Period_Is_Zero() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await Assert.ThrowsAsync(async () => + { + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 0 }, + (_, _) => Task.CompletedTask + ); + }); + } + + [Fact] + public async Task Should_Throw_When_Period_Is_Negative() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await Assert.ThrowsAsync(async () => + { + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = -1000 }, + (_, _) => Task.CompletedTask + ); + }); + } + + [Fact] + public async Task Should_Throw_When_No_Period_And_No_CronExpression() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + + await Assert.ThrowsAsync(async () => + { + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule(), + (_, _) => Task.CompletedTask + ); + }); + } + + [Fact] + public async Task Should_Continue_Running_After_Handler_Throws_Exception() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + var callCount = 0; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 50 }, + (_, _) => + { + var count = Interlocked.Increment(ref callCount); + if (count == 1) + { + throw new InvalidOperationException("Simulated failure"); + } + + tcs.TrySetResult(true); + return Task.CompletedTask; + } + ); + + var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(5000)); + completedTask.ShouldBe(tcs.Task); + callCount.ShouldBeGreaterThan(1); + } + + [Fact] + public async Task Should_Not_Be_Registered_After_Remove() + { + var workerName = "dynamic-worker-" + Guid.NewGuid(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 1000 }, + (_, _) => Task.CompletedTask + ); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + await _dynamicWorkerManager.RemoveAsync(workerName); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Handle_Concurrent_Add_With_Same_Name() + { + var workerName = "concurrent-worker-" + Guid.NewGuid(); + var executedHandlerIds = new ConcurrentBag(); + + var tasks = Enumerable.Range(0, 10).Select(i => + _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => + { + executedHandlerIds.Add(i); + return Task.CompletedTask; + } + ) + ).ToList(); + + await Task.WhenAll(tasks); + + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeTrue(); + + var removed = await _dynamicWorkerManager.RemoveAsync(workerName); + removed.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Handle_Concurrent_Add_And_Remove() + { + var workerNames = Enumerable.Range(0, 10) + .Select(i => $"concurrent-worker-{i}-" + Guid.NewGuid()) + .ToList(); + + var addTasks = workerNames.Select(name => + _dynamicWorkerManager.AddAsync( + name, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ) + ).ToList(); + + await Task.WhenAll(addTasks); + + foreach (var name in workerNames) + { + _dynamicWorkerManager.IsRegistered(name).ShouldBeTrue(); + } + + var removeTasks = workerNames.Select(name => + _dynamicWorkerManager.RemoveAsync(name) + ).ToList(); + + var results = await Task.WhenAll(removeTasks); + + results.ShouldAllBe(r => r); + + foreach (var name in workerNames) + { + _dynamicWorkerManager.IsRegistered(name).ShouldBeFalse(); + } + } + + [Fact] + public async Task Should_Handle_Concurrent_Add_Remove_Update() + { + var workerName = "concurrent-mixed-" + Guid.NewGuid(); + + await _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 60000 }, + (_, _) => Task.CompletedTask + ); + + var tasks = new List + { + _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 30000 }, + (_, _) => Task.CompletedTask + ), + _dynamicWorkerManager.UpdateScheduleAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 20000 } + ), + _dynamicWorkerManager.AddAsync( + workerName, + new DynamicBackgroundWorkerSchedule { Period = 10000 }, + (_, _) => Task.CompletedTask + ) + }; + + await Task.WhenAll(tasks); + + // After all concurrent operations, worker should still be in a consistent state + var isRegistered = _dynamicWorkerManager.IsRegistered(workerName); + isRegistered.ShouldBeTrue(); + + var removed = await _dynamicWorkerManager.RemoveAsync(workerName); + removed.ShouldBeTrue(); + _dynamicWorkerManager.IsRegistered(workerName).ShouldBeFalse(); + } +} diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 88c515f8dc..b77f282a85 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -10,10 +11,17 @@ namespace Volo.Abp.EventBus.Distributed; public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { + public LocalDistributedEventBus_Test() + { + MySimpleDistributedTransientEventHandler.HandleCount = 0; + MySimpleDistributedTransientEventHandler.DisposeCount = 0; + MySimpleDistributedSingleInstanceEventHandler.TenantId = null; + } + [Fact] public async Task Should_Call_Handler_AndDispose() { - DistributedEventBus.Subscribe(); + using var subscription = DistributedEventBus.Subscribe(); await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); await DistributedEventBus.PublishAsync(new MySimpleEventData(2)); @@ -23,12 +31,255 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); } + [Fact] + public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() + { + using var subscription = DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(eventName, new Dictionary() + { + {"Value", 2} + }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 3 }); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); + } + + [Fact] + public async Task Should_Handle_Dynamic_Handler_When_Published_With_EventName() + { + var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = DistributedEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(eventName, new Dictionary() + { + {"Value", 2} + }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 3 }); + await DistributedEventBus.PublishAsync(eventName, new[] { 1, 2, 3 }); + + Assert.Equal(4, handleCount); + } + + [Fact] + public async Task Should_Handle_Dynamic_Handler_When_Published_With_DynamicEventData() + { + var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = DistributedEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + d.Data.ShouldNotBeNull(); + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new Dictionary() + { + {"Value", 2} + })); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new { Value = 3 })); + + Assert.Equal(3, handleCount); + } + + [Fact] + public async Task Should_Handle_Typed_Handler_When_Published_With_DynamicEventData() + { + using var subscription = DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new Dictionary() + { + {"Value", 2} + })); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new { Value = 3 })); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + } + + [Fact] + public async Task Should_Trigger_Both_Typed_And_Dynamic_Handlers_For_Typed_Event() + { + using var typedSubscription = DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + var dynamicHandleCount = 0; + + using var dynamicSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + dynamicHandleCount++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(new MySimpleEventData(2)); + await DistributedEventBus.PublishAsync(new MySimpleEventData(3)); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, dynamicHandleCount); + } + + [Fact] + public async Task Should_Trigger_Both_Handlers_For_Mixed_Typed_And_Dynamic_Publish() + { + using var typedSubscription = DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + var dynamicHandleCount = 0; + + using var dynamicSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + dynamicHandleCount++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new Dictionary() + { + {"Value", 2} + })); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new { Value = 3 })); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, dynamicHandleCount); + } + + [Fact] + public async Task Should_Unsubscribe_Dynamic_Handler() + { + var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + var handler = new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }); + var factory = new SingleInstanceHandlerFactory(handler); + + var disposable = DistributedEventBus.Subscribe(eventName, factory); + + await DistributedEventBus.PublishAsync(eventName, new { Value = 1 }); + Assert.Equal(1, handleCount); + + disposable.Dispose(); + + await DistributedEventBus.PublishAsync(eventName, new { Value = 2 }); + Assert.Equal(1, handleCount); + } + + [Fact] + public async Task Should_Not_Throw_For_Unknown_Event_Name() + { + // Publishing to an unknown event name should not throw (consistent with typed PublishAsync behavior) + await DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); + } + + [Fact] + public async Task Should_Convert_DynamicEventData_To_Typed_Object() + { + MySimpleEventData? receivedData = null; + + using var subscription = DistributedEventBus.Subscribe(async (data) => + { + receivedData = data; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await DistributedEventBus.PublishAsync(eventName, new { Value = 42 }); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(42); + } + + [Fact] + public async Task Should_Subscribe_With_IDistributedEventHandler() + { + var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = DistributedEventBus.Subscribe(eventName, + new TestDynamicDistributedEventHandler(() => handleCount++)); + + await DistributedEventBus.PublishAsync(eventName, new { Value = 1 }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 2 }); + + Assert.Equal(2, handleCount); + } + + [Fact] + public async Task Should_Handle_Multiple_Dynamic_Events_Independently() + { + var countA = 0; + var countB = 0; + var eventNameA = "EventA-" + Guid.NewGuid().ToString("N"); + var eventNameB = "EventB-" + Guid.NewGuid().ToString("N"); + + using var subA = DistributedEventBus.Subscribe(eventNameA, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countA++; + await Task.CompletedTask; + }))); + + using var subB = DistributedEventBus.Subscribe(eventNameB, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countB++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(eventNameA, new { Value = 1 }); + await DistributedEventBus.PublishAsync(eventNameB, new { Value = 2 }); + await DistributedEventBus.PublishAsync(eventNameA, new { Value = 3 }); + + Assert.Equal(2, countA); + Assert.Equal(1, countB); + } + + [Fact] + public async Task Should_Receive_EventName_In_DynamicEventData() + { + string? receivedEventName = null; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = DistributedEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + receivedEventName = d.EventName; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(eventName, new { Value = 1 }); + + receivedEventName.ShouldBe(eventName); + } + [Fact] public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() { var tenantId = Guid.NewGuid(); - DistributedEventBus.Subscribe(GetRequiredService()); + using var subscription = DistributedEventBus.Subscribe(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEventData(3, tenantId)); @@ -40,7 +291,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var tenantId = Guid.NewGuid(); - DistributedEventBus.Subscribe>(GetRequiredService()); + using var subscription = DistributedEventBus.Subscribe>(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEventData(3, tenantId)); @@ -52,7 +303,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var tenantId = Guid.NewGuid(); - DistributedEventBus.Subscribe(GetRequiredService()); + using var subscription = DistributedEventBus.Subscribe(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEto { @@ -70,10 +321,10 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var localEventBus = GetRequiredService(); - localEventBus.Subscribe(); - localEventBus.Subscribe(); + using var distributedEventSentSubscription = localEventBus.Subscribe(); + using var distributedEventReceivedSubscription = localEventBus.Subscribe(); - DistributedEventBus.Subscribe(); + using var subscription = DistributedEventBus.Subscribe(); using (var uow = GetRequiredService().Begin()) { @@ -121,4 +372,19 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } } + class TestDynamicDistributedEventHandler : IDistributedEventHandler + { + private readonly Action _onHandle; + + public TestDynamicDistributedEventHandler(Action onHandle) + { + _onHandle = onHandle; + } + + public Task HandleEventAsync(DynamicEventData eventData) + { + _onHandle(); + return Task.CompletedTask; + } + } } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs new file mode 100644 index 0000000000..c9813b8717 --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.EventBus.Local; + +public class LocalEventBus_Dynamic_Test : EventBusTestBase +{ + [Fact] + public async Task Should_Handle_Dynamic_Handler_With_EventName() + { + var handleCount = 0; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + d.EventName.ShouldBe(eventName); + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + await LocalEventBus.PublishAsync(eventName, new { Value = 2 }); + + handleCount.ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() + { + var handleCount = 0; + + using var subscription = LocalEventBus.Subscribe(async (data) => + { + handleCount++; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await LocalEventBus.PublishAsync(eventName, new MySimpleEventData(42)); + + handleCount.ShouldBe(1); + } + + [Fact] + public async Task Should_Convert_Dictionary_To_Typed_Handler() + { + MySimpleEventData? receivedData = null; + + using var subscription = LocalEventBus.Subscribe(async (data) => + { + receivedData = data; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await LocalEventBus.PublishAsync(eventName, new Dictionary + { + { "Value", 42 } + }); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(42); + } + + [Fact] + public async Task Should_Trigger_Both_Typed_And_Dynamic_Handlers() + { + var typedHandleCount = 0; + var dynamicHandleCount = 0; + + using var typedSubscription = LocalEventBus.Subscribe(async (data) => + { + typedHandleCount++; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + using var dynamicSubscription = LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + dynamicHandleCount++; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(new MySimpleEventData(1)); + + typedHandleCount.ShouldBe(1); + dynamicHandleCount.ShouldBe(1); + } + + [Fact] + public async Task Should_Unsubscribe_Dynamic_Handler() + { + var handleCount = 0; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + var handler = new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }); + var factory = new SingleInstanceHandlerFactory(handler); + + var disposable = LocalEventBus.Subscribe(eventName, factory); + + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + handleCount.ShouldBe(1); + + disposable.Dispose(); + + await LocalEventBus.PublishAsync(eventName, new { Value = 2 }); + handleCount.ShouldBe(1); + } + + [Fact] + public async Task Should_Not_Throw_For_Unknown_Event_Name() + { + // Publishing to an unknown event name should not throw (consistent with typed PublishAsync behavior) + await LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); + } + + [Fact] + public async Task Should_Access_Data_In_Dynamic_Handler() + { + object? receivedData = null; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + receivedData = d.Data; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventName, new { Name = "Hello", Count = 42 }); + + receivedData.ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Receive_Typed_Data_Via_Typed_Handler_From_Dynamic_Publish() + { + MySimpleEventData? receivedData = null; + var eventName = EventNameAttribute.GetNameOrDefault(); + + using var subscription = LocalEventBus.Subscribe(async (d) => + { + receivedData = d; + await Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(eventName, new MySimpleEventData(99)); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(99); + } + + [Fact] + public async Task Should_Unsubscribe_All_Dynamic_Handlers() + { + var handleCount = 0; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }))); + + LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + handleCount.ShouldBe(2); + + LocalEventBus.UnsubscribeAll(eventName); + + // After UnsubscribeAll, publishing still works (key exists) but no handlers are invoked + await LocalEventBus.PublishAsync(eventName, new { Value = 2 }); + handleCount.ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Multiple_Dynamic_Events_Independently() + { + var countA = 0; + var countB = 0; + var eventNameA = "EventA-" + Guid.NewGuid().ToString("N"); + var eventNameB = "EventB-" + Guid.NewGuid().ToString("N"); + + using var subA = LocalEventBus.Subscribe(eventNameA, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countA++; + await Task.CompletedTask; + }))); + + using var subB = LocalEventBus.Subscribe(eventNameB, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countB++; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventNameA, new { Value = 1 }); + await LocalEventBus.PublishAsync(eventNameB, new { Value = 2 }); + await LocalEventBus.PublishAsync(eventNameA, new { Value = 3 }); + + countA.ShouldBe(2); + countB.ShouldBe(1); + } + + [Fact] + public async Task Should_Receive_EventName_In_DynamicEventData() + { + string? receivedEventName = null; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + receivedEventName = d.EventName; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + + receivedEventName.ShouldBe(eventName); + } + + [Fact] + public async Task Should_Convert_Anonymous_Object_To_Typed_Handler() + { + MySimpleEventData? receivedData = null; + var eventName = EventNameAttribute.GetNameOrDefault(); + + using var subscription = LocalEventBus.Subscribe(async (d) => + { + receivedData = d; + await Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(eventName, new { Value = 77 }); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(77); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs new file mode 100644 index 0000000000..e9bc5464ea --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending.TestObjects; +using Volo.Abp.Testing; +using Xunit; + +namespace AutoMapper; + +public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest +{ + private readonly Volo.Abp.ObjectMapping.IObjectMapper _objectMapper; + + public AbpAutoMapperExtensibleDtoExtensions_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default() + { + var person = new ExtensibleTestPerson() + .SetProperty("Name", "John") + .SetProperty("Age", 42) + .SetProperty("ChildCount", 2) + .SetProperty("Sex", "male") + .SetProperty("CityName", "Adana"); + + var personDto = new ExtensibleTestPersonDto() + .SetProperty("ExistingDtoProperty", "existing-value"); + + _objectMapper.Map(person, personDto); + + personDto.GetProperty("Name").ShouldBe("John"); //Defined in both classes + personDto.GetProperty("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values + personDto.GetProperty("ChildCount").ShouldBe(0); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.GetProperty("CityName").ShouldBeNull(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination + personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes + } + + [Fact] + public void MapExtraProperties_Also_Should_Map_To_RegularProperties() + { + var person = new ExtensibleTestPerson() + .SetProperty("Name", "John") + .SetProperty("Age", 42); + + var personDto = new ExtensibleTestPersonWithRegularPropertiesDto() + .SetProperty("IsActive", true); + + _objectMapper.Map(person, personDto); + + //Defined in both classes + personDto.HasProperty("Name").ShouldBe(false); + personDto.Name.ShouldBe("John"); + + //Defined in both classes + personDto.HasProperty("Age").ShouldBe(false); + personDto.Age.ShouldBe(42); + + //Should not clear existing values + personDto.HasProperty("IsActive").ShouldBe(false); + personDto.IsActive.ShouldBe(true); + } + + [Fact] + public void MapExtraPropertiesTo_Should_Ignored_If_ExtraProperties_Is_Null() + { + var person = new ExtensibleTestPerson(); + person.SetExtraPropertiesAsNull(); + + var personDto = new ExtensibleTestPersonDto(); + personDto.SetExtraPropertiesAsNull(); + + Should.NotThrow(() => _objectMapper.Map(person, personDto)); + + person.ExtraProperties.ShouldBe(null); + personDto.ExtraProperties.ShouldBeEmpty(); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.abppkg b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.abppkg new file mode 100644 index 0000000000..037b12d73d --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "test.framework" +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.csproj b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.csproj new file mode 100644 index 0000000000..384447a180 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.csproj @@ -0,0 +1,21 @@ + + + + + + net10.0 + Volo.Abp.LuckyPenny.AutoMapper.Tests + Volo.Abp.LuckyPenny.AutoMapper.Tests + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs new file mode 100644 index 0000000000..82a0369aa0 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperModule_Basic_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpAutoMapperModule_Basic_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Replace_IAutoObjectMappingProvider() + { + Assert.True(ServiceProvider.GetRequiredService() is AutoMapperAutoObjectMappingProvider); + } + + [Fact] + public void Should_Get_Internal_Mapper() + { + _objectMapper.GetMapper().ShouldNotBeNull(); + _objectMapper.AutoObjectMappingProvider.GetMapper().ShouldNotBeNull(); + } + + [Fact] + public void Should_Map_Objects_With_AutoMap_Attributes() + { + var dto = _objectMapper.Map(new MyEntity { Number = 42 }); + dto.Number.ShouldBe(42); + } + + [Fact] + public void Should_Map_Enum() + { + var dto = _objectMapper.Map(MyEnum.Value3); + dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3 + } + + //[Fact] TODO: Disabled because of https://github.com/AutoMapper/AutoMapper/pull/2379#issuecomment-355899664 + /*public void Should_Not_Map_Objects_With_AutoMap_Attributes() + { + Assert.ThrowsAny(() => + { + _objectMapper.Map(new MyEntity {Number = 42}); + }); + }*/ +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_MaxDepth_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_MaxDepth_Tests.cs new file mode 100644 index 0000000000..03b90e63ea --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_MaxDepth_Tests.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using AutoMapper.Internal; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperModule_MaxDepth_Tests : AbpIntegratedTest +{ + private readonly IConfigurationProvider _configurationProvider; + + public AbpAutoMapperModule_MaxDepth_Tests() + { + _configurationProvider = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Set_Default_MaxDepth_For_All_Maps() + { + var typeMap = _configurationProvider.Internal().FindTypeMapFor(); + typeMap.ShouldNotBeNull(); + typeMap.MaxDepth.ShouldBe(64); + } +} + +public class AbpAutoMapperModule_CustomMaxDepth_Tests : AbpIntegratedTest +{ + private readonly IConfigurationProvider _configurationProvider; + + public AbpAutoMapperModule_CustomMaxDepth_Tests() + { + _configurationProvider = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Not_Override_Custom_MaxDepth() + { + var typeMap = _configurationProvider.Internal().FindTypeMapFor(); + typeMap.ShouldNotBeNull(); + typeMap.MaxDepth.ShouldBe(10); + } + + [DependsOn( + typeof(AbpLuckyPennyAutoMapperModule), + typeof(AbpObjectExtendingTestModule) + )] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.CreateMap().MaxDepth(10); + }); + }); + } + } +} + +public class AbpAutoMapperModule_DisabledMaxDepth_Tests : AbpIntegratedTest +{ + private readonly IConfigurationProvider _configurationProvider; + + public AbpAutoMapperModule_DisabledMaxDepth_Tests() + { + _configurationProvider = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Not_Set_MaxDepth_When_Disabled() + { + var typeMap = _configurationProvider.Internal().FindTypeMapFor(); + typeMap.ShouldNotBeNull(); + typeMap.MaxDepth.ShouldBe(0); + } + + [DependsOn( + typeof(AbpLuckyPennyAutoMapperModule), + typeof(AbpObjectExtendingTestModule) + )] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.DefaultMaxDepth = null; + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.CreateMap(); + }); + }); + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs new file mode 100644 index 0000000000..47b684538c --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpAutoMapperModule_Specific_ObjectMapper_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Use_Specific_Object_Mapper_If_Registered() + { + var dto = _objectMapper.Map(new MyEntity { Number = 42 }); + dto.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + } + + [Fact] + public void Specific_Object_Mapper_Should_Be_Used_For_Collections_If_Registered() + { + // IEnumerable + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination = new List() + { + new MyEntityDto2 { Number = 44 } + }; + var returnIEnumerable = _objectMapper.Map, IEnumerable>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnIEnumerable.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnIEnumerable).ShouldBeTrue(); + + // ICollection + _objectMapper.Map, ICollection>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnICollection = _objectMapper.Map, ICollection>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnICollection.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnICollection).ShouldBeTrue(); + + // Collection + _objectMapper.Map, Collection>(new Collection() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination2 = new Collection() + { + new MyEntityDto2 { Number = 44 } + }; + var returnCollection = _objectMapper.Map, Collection>( + new Collection() + { + new MyEntity { Number = 42 } + }, destination2); + returnCollection.First().Number.ShouldBe(43); + ReferenceEquals(destination2, returnCollection).ShouldBeTrue(); + + // IList + _objectMapper.Map, IList>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnIList = _objectMapper.Map, IList>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnIList.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnIList).ShouldBeTrue(); + + // List + _objectMapper.Map, List>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnList = _objectMapper.Map, List>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnList.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnList).ShouldBeTrue(); + + // Array + _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destinationArray = new MyEntityDto2[] + { + new MyEntityDto2 { Number = 40 } + }; + var returnArray = _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }, destinationArray); + + returnArray.First().Number.ShouldBe(43); + + // array should not be changed. Same as AutoMapper. + destinationArray.First().Number.ShouldBe(40); + ReferenceEquals(returnArray, destinationArray).ShouldBeFalse(); + } + + [Fact] + public void Specific_Object_Mapper_Should_Support_Multiple_IObjectMapper_Interfaces() + { + var myEntityDto2 = _objectMapper.Map(new MyEntity { Number = 42 }); + myEntityDto2.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var myEntity = _objectMapper.Map(new MyEntityDto2 { Number = 42 }); + myEntity.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + // IEnumerable + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntityDto2 { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + } + + [Fact] + public void Should_Use_Destination_Object_Constructor_If_Available() + { + var id = Guid.NewGuid(); + var dto = _objectMapper.Map(new MyEntity { Number = 42, Id = id }); + dto.Key.ShouldBe(id); + dto.No.ShouldBe(42); + } + + [Fact] + public void Should_Use_Destination_Object_MapFrom_Method_If_Available() + { + var id = Guid.NewGuid(); + var dto = new MyEntityDtoWithMappingMethods(); + _objectMapper.Map(new MyEntity { Number = 42, Id = id }, dto); + dto.Key.ShouldBe(id); + dto.No.ShouldBe(42); + } + + [Fact] + public void Should_Use_Source_Object_Method_If_Available_To_Create_New_Object() + { + var id = Guid.NewGuid(); + var entity = _objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }); + entity.Id.ShouldBe(id); + entity.Number.ShouldBe(42); + } + + [Fact] + public void Should_Use_Source_Object_Method_If_Available_To_Map_Existing_Object() + { + var id = Guid.NewGuid(); + var entity = new MyEntity(); + _objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }, entity); + entity.Id.ShouldBe(id); + entity.Number.ShouldBe(42); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions_Tests.cs new file mode 100644 index 0000000000..c57f141837 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions_Tests.cs @@ -0,0 +1,177 @@ +using System; +using AutoMapper; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Volo.Abp.Auditing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapperExpressionExtensions_Tests +{ + [Fact] + public void Should_Ignore_Configured_Property() + { + var mapper = CreateMapper( + cfg => cfg + .CreateMap() + .Ignore(x => x.Value2) + .Ignore(x => x.Value3) + ); + + var obj2 = mapper.Map( + new SimpleClass1 + { + Value1 = "v1", + Value2 = "v2" + } + ); + + obj2.Value1.ShouldBe("v1"); + obj2.Value2.ShouldBeNull(); + obj2.Value3.ShouldBeNull(); + } + + [Fact] + public void Should_Ignore_Audit_Properties() + { + var mapper = CreateMapper( + cfg => cfg + .CreateMap() + .IgnoreFullAuditedObjectProperties() + ); + + var obj2 = mapper.Map( + new SimpleClassAudited1 + { + CreationTime = DateTime.Now, + CreatorId = Guid.NewGuid(), + LastModificationTime = DateTime.Now, + LastModifierId = Guid.NewGuid(), + DeleterId = Guid.NewGuid(), + DeletionTime = DateTime.Now, + IsDeleted = true + } + ); + + obj2.CreationTime.ShouldBe(default); + obj2.CreatorId.ShouldBeNull(); + obj2.LastModificationTime.ShouldBe(default); + obj2.LastModifierId.ShouldBeNull(); + obj2.DeleterId.ShouldBeNull(); + obj2.DeletionTime.ShouldBeNull(); + obj2.IsDeleted.ShouldBeFalse(); + } + + [Fact] + public void Should_Ignore_Audit_Properties_With_User() + { + var mapper = CreateMapper( + cfg => cfg + .CreateMap() + .IgnoreFullAuditedObjectProperties() + ); + + var obj2 = mapper.Map( + new SimpleClassAuditedWithUser1 + { + CreationTime = DateTime.Now, + CreatorId = Guid.NewGuid(), + LastModificationTime = DateTime.Now, + LastModifierId = Guid.NewGuid(), + DeleterId = Guid.NewGuid(), + DeletionTime = DateTime.Now, + IsDeleted = true, + Creator = new SimpleUser(), + Deleter = new SimpleUser(), + LastModifier = new SimpleUser() + } + ); + + obj2.CreationTime.ShouldBe(default); + obj2.CreatorId.ShouldBeNull(); + obj2.LastModificationTime.ShouldBe(default); + obj2.LastModifierId.ShouldBeNull(); + obj2.DeleterId.ShouldBeNull(); + obj2.DeletionTime.ShouldBeNull(); + obj2.IsDeleted.ShouldBeFalse(); + obj2.Creator.ShouldBeNull(); + obj2.Deleter.ShouldBeNull(); + obj2.LastModifier.ShouldBeNull(); + } + + private static IMapper CreateMapper(Action configure) + { + var configuration = new MapperConfiguration(configure, NullLoggerFactory.Instance); + configuration.AssertConfigurationIsValid(); + return configuration.CreateMapper(); + } + + public class SimpleClass1 + { + public string Value1 { get; set; } + public string Value2 { get; set; } + } + + public class SimpleClass2 + { + public string Value1 { get; set; } + public string Value2 { get; set; } + public string Value3 { get; set; } + } + + public class SimpleClassAudited1 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + } + + public class SimpleClassAudited2 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + } + + public class SimpleClassAuditedWithUser1 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + public SimpleUser Creator { get; set; } + public SimpleUser LastModifier { get; set; } + public SimpleUser Deleter { get; set; } + } + + public class SimpleClassAuditedWithUser2 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + public SimpleUser Creator { get; set; } + public SimpleUser LastModifier { get; set; } + public SimpleUser Deleter { get; set; } + } + + public class SimpleUser + { + + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperTestModule.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperTestModule.cs new file mode 100644 index 0000000000..d484fc03ff --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperTestModule.cs @@ -0,0 +1,19 @@ +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.AutoMapper; + +[DependsOn( + typeof(AbpLuckyPennyAutoMapperModule), + typeof(AbpObjectExtendingTestModule) +)] +public class AutoMapperTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.AddMaps(); + }); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_ConfigurationValidation_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_ConfigurationValidation_Tests.cs new file mode 100644 index 0000000000..c71dac7c53 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_ConfigurationValidation_Tests.cs @@ -0,0 +1,70 @@ +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; +using Xunit; +using IObjectMapper = Volo.Abp.ObjectMapping.IObjectMapper; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapper_ConfigurationValidation_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AutoMapper_ConfigurationValidation_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Validate_Configuration() + { + _objectMapper.Map(new MySourceClass { Value = "42" }).Value.ShouldBe("42"); + _objectMapper.Map(new MySourceClass { Value = "42" }).ValueNotMatched.ShouldBe(null); + } + + [DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.AddMaps(validate: true); //Adds all profiles in the TestModule assembly by validating configurations + options.ValidateProfile(validate: false); //Exclude a profile from the configuration validation + }); + } + } + + public class ValidatedProfile : Profile + { + public ValidatedProfile() + { + CreateMap(); + } + } + + public class NonValidatedProfile : Profile + { + public NonValidatedProfile() + { + CreateMap(); + } + } + + public class MySourceClass + { + public string Value { get; set; } + } + + public class MyClassValidated + { + public string Value { get; set; } + } + + public class MyClassNonValidated + { + public string ValueNotMatched { get; set; } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_CustomServiceConstruction_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_CustomServiceConstruction_Tests.cs new file mode 100644 index 0000000000..02f4b7bfd0 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_CustomServiceConstruction_Tests.cs @@ -0,0 +1,87 @@ +using System; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; +using Xunit; +using IObjectMapper = Volo.Abp.ObjectMapping.IObjectMapper; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapper_CustomServiceConstruction_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AutoMapper_CustomServiceConstruction_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Custom_Service_Construction() + { + var source = new SourceModel + { + Name = nameof(SourceModel) + }; + + _objectMapper.Map(source).Name.ShouldBe(nameof(CustomMappingAction)); + } + + [DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Replace the build-in IMapper with a custom one to use ConstructServicesUsing. + context.Services.Replace(ServiceDescriptor.Transient(sp => sp.GetRequiredService().CreateMapper())); + + Configure(options => + { + options.AddMaps(); + options.Configurators.Add(configurationContext => + { + configurationContext.MapperConfiguration.ConstructServicesUsing(type => + type.Name.Contains(nameof(CustomMappingAction)) + ? new CustomMappingAction(nameof(CustomMappingAction)) + : Activator.CreateInstance(type)); + }); + }); + } + } + + public class SourceModel + { + public string Name { get; set; } + } + + public class DestModel + { + public string Name { get; set; } + } + + public class MapperActionProfile : Profile + { + public MapperActionProfile() + { + CreateMap().AfterMap(); + } + } + + public class CustomMappingAction : IMappingAction + { + private readonly string _name; + + public CustomMappingAction(string name) + { + _name = name; + } + + public void Process(SourceModel source, DestModel destination, ResolutionContext context) + { + destination.Name = _name; + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_Dependency_Injection_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_Dependency_Injection_Tests.cs new file mode 100644 index 0000000000..fcc77276e5 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_Dependency_Injection_Tests.cs @@ -0,0 +1,83 @@ +using System; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Testing; +using Xunit; +using IObjectMapper = Volo.Abp.ObjectMapping.IObjectMapper; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapper_Dependency_Injection_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Registered_AutoMapper_Service() + { + GetService().ShouldNotBeNull(); + } + + [Fact] + public void Custom_MappingAction_Test() + { + var sourceModel = new SourceModel + { + Name = "Source" + }; + + using (var scope = ServiceProvider.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Map(sourceModel).Name.ShouldBe(nameof(CustomMappingActionService)); + } + + CustomMappingAction.IsDisposed.ShouldBeTrue(); + } + + public class SourceModel + { + public string Name { get; set; } + } + + public class DestModel + { + public string Name { get; set; } + } + + public class MapperActionProfile : Profile + { + public MapperActionProfile() + { + CreateMap().AfterMap(); + } + } + + public class CustomMappingAction : IMappingAction, IDisposable + { + public static bool IsDisposed = false; + + private readonly CustomMappingActionService _customMappingActionService; + + public CustomMappingAction(CustomMappingActionService customMappingActionService) + { + _customMappingActionService = customMappingActionService; + } + + public void Process(SourceModel source, DestModel destination, ResolutionContext context) + { + destination.Name = _customMappingActionService.GetName(); + } + + public void Dispose() + { + IsDisposed = true; + } + } + + public class CustomMappingActionService : ITransientDependency + { + public string GetName() + { + return nameof(CustomMappingActionService); + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs new file mode 100644 index 0000000000..7d4bfbe707 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using System; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class ObjectMapperExtensions_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public ObjectMapperExtensions_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Map_Objects_With_AutoMap_Attributes() + { + var dto = _objectMapper.Map( + new MyEntity + { + Number = 42 + } + ); + + dto.As().Number.ShouldBe(42); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntity.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntity.cs new file mode 100644 index 0000000000..95cd55215f --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntity.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntity +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto.cs new file mode 100644 index 0000000000..0c498ca0c6 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntityDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto2.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto2.cs new file mode 100644 index 0000000000..a4a6942e55 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto2.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntityDto2 +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDtoWithMappingMethods.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDtoWithMappingMethods.cs new file mode 100644 index 0000000000..89a9048069 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDtoWithMappingMethods.cs @@ -0,0 +1,43 @@ +using System; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +//TODO: Move tests to Volo.Abp.ObjectMapping test project +public class MyEntityDtoWithMappingMethods : IMapFrom, IMapTo +{ + public Guid Key { get; set; } + + public int No { get; set; } + + public MyEntityDtoWithMappingMethods() + { + + } + + public MyEntityDtoWithMappingMethods(MyEntity entity) + { + MapFrom(entity); + } + + public void MapFrom(MyEntity source) + { + Key = source.Id; + No = source.Number; + } + + MyEntity IMapTo.MapTo() + { + return new MyEntity + { + Id = Key, + Number = No + }; + } + + void IMapTo.MapTo(MyEntity destination) + { + destination.Id = Key; + destination.Number = No; + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityToMyEntityDto2Mapper.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityToMyEntityDto2Mapper.cs new file mode 100644 index 0000000000..6bf16fce8e --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityToMyEntityDto2Mapper.cs @@ -0,0 +1,39 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntityToMyEntityDto2Mapper : IObjectMapper, IObjectMapper, ITransientDependency +{ + public MyEntityDto2 Map(MyEntity source) + { + return new MyEntityDto2 + { + Id = source.Id, + Number = source.Number + 1 + }; + } + + public MyEntityDto2 Map(MyEntity source, MyEntityDto2 destination) + { + destination.Id = source.Id; + destination.Number = source.Number + 1; + return destination; + } + + public MyEntity Map(MyEntityDto2 source) + { + return new MyEntity + { + Id = source.Id, + Number = source.Number + 1 + }; + } + + public MyEntity Map(MyEntityDto2 source, MyEntity destination) + { + destination.Id = source.Id; + destination.Number = source.Number + 1; + return destination; + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs new file mode 100644 index 0000000000..61b23ea6f4 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnum +{ + Value1 = 1, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs new file mode 100644 index 0000000000..6e553adfb9 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnumDto +{ + Value1 = 2, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs new file mode 100644 index 0000000000..295ff6b08e --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using Volo.Abp.ObjectExtending.TestObjects; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyMapProfile : Profile +{ + public MyMapProfile() + { + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap() + .MapExtraProperties(ignoredProperties: new[] { "CityName" }); + + CreateMap() + .ForMember(x => x.Name, y => y.Ignore()) + .ForMember(x => x.Age, y => y.Ignore()) + .ForMember(x => x.IsActive, y => y.Ignore()) + .MapExtraProperties(mapToRegularProperties: true); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyNotMappedDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyNotMappedDto.cs new file mode 100644 index 0000000000..304fe6e7b4 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyNotMappedDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyNotMappedDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/AbpLuckyPennyMultiLingualObjectsTestModule.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/AbpLuckyPennyMultiLingualObjectsTestModule.cs new file mode 100644 index 0000000000..b709fed153 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/AbpLuckyPennyMultiLingualObjectsTestModule.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; +using Volo.Abp.AutoMapper; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Settings; + +namespace Volo.Abp.MultiLingualObjects; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpLocalizationModule), + typeof(AbpSettingsModule), + typeof(AbpObjectMappingModule), + typeof(AbpMultiLingualObjectsModule), + typeof(AbpTestBaseModule), + typeof(AbpLuckyPennyAutoMapperModule) +)] +public class AbpLuckyPennyMultiLingualObjectsTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.DefinitionProviders.Add(); + }); + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddProfile(validate: true); + }); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs new file mode 100644 index 0000000000..9053472c48 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper; +using Volo.Abp.Localization; +using Volo.Abp.MultiLingualObjects.TestObjects; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.MultiLingualObjects; + +public class MultiLingualObjectManager_Tests : AbpIntegratedTest +{ + private readonly IMultiLingualObjectManager _multiLingualObjectManager; + private readonly MultiLingualBook _book; + private readonly List _books; + private readonly IMapperAccessor _mapperAccessor; + private readonly FrozenDictionary _testTranslations = new Dictionary + { + ["ar"] = "C# التعمق في", + ["zh-Hans"] = "深入理解C#", + ["en"] = "C# in Depth" + }.ToFrozenDictionary(); + + public MultiLingualObjectManager_Tests() + { + _multiLingualObjectManager = ServiceProvider.GetRequiredService(); + + //Single Lookup + _book = GetTestBook("en", "zh-Hans"); + //Bulk lookup + _books = new List + { + //has no translations + GetTestBook(), + //english only + GetTestBook("en"), + //arabic only + GetTestBook("ar"), + //arabic + english + GetTestBook("en","ar"), + //arabic + english + chineese + GetTestBook("en", "ar", "zh-Hans") + }; + _mapperAccessor = ServiceProvider.GetRequiredService(); + } + + MultiLingualBook GetTestBook(params string[] included) + { + var id = Guid.NewGuid(); + var res = new MultiLingualBook(id, 100); + + foreach (var language in included) + { + res.Translations.Add(new MultiLingualBookTranslation + { + Language = language, + Name = _testTranslations[language], + }); + } + + return res; + } + + [Fact] + public async Task GetTranslationAsync() + { + using (CultureHelper.Use("en-us")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task GetTranslationFromListAsync() + { + using (CultureHelper.Use("en-us")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book.Translations); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task Should_Get_Specified_Language() + { + using (CultureHelper.Use("zh-Hans")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book, culture: "en"); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task GetBulkTranslationsAsync() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); + foreach (var (entity, translation) in translations) + { + if (entity.Translations.Any(x => x.Language == "en")) + { + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + else + { + translation.ShouldBeNull(); + } + } + } + } + + [Fact] + public async Task GetBulkTranslationsFromListAsync() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books.Select(x => x.Translations)); + foreach (var translation in translations) + { + translation?.Name.ShouldBe(_testTranslations["en"]); + } + } + } + + [Fact] + public async Task TestBulkMapping() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); + var translationsDict = translations.ToDictionary(x => x.entity.Id, x => x.translation); + var mapped = _mapperAccessor.Mapper.Map, List>(_books, options => + { + options.Items.Add(nameof(MultiLingualBookTranslation), translationsDict); + }); + Assert.Equal(mapped.Count, _books.Count); + for (int i = 0; i < mapped.Count; i++) + { + var og = _books[i]; + var m = mapped[i]; + Assert.Equal(og.Translations.FirstOrDefault(x => x.Language == "en")?.Name, m.Name); + } + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs new file mode 100644 index 0000000000..ded671c8c2 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs @@ -0,0 +1,23 @@ +namespace Volo.Abp.MultiLingualObjects; + +using System; +using System.Collections.Generic; +using global::AutoMapper; +using Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualObjectTestProfile : Profile +{ + public MultiLingualObjectTestProfile() + { + CreateMap() + .ForMember(x => x.Name, + x => x.MapFrom((src, target, member, context) => + { + if (context.Items.TryGetValue(nameof(MultiLingualBookTranslation), out var translationsRaw) && translationsRaw is IReadOnlyDictionary translations) + { + return translations.GetValueOrDefault(src.Id)?.Name; + } + return null; + })); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs new file mode 100644 index 0000000000..3eb61e6cf3 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBook : IMultiLingualObject +{ + public MultiLingualBook(Guid id, decimal price) + { + Id = id; + Price = price; + } + + public Guid Id { get; } + + public decimal Price { get; set; } + + public ICollection Translations { get; set; } = new List(); +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs new file mode 100644 index 0000000000..5131d306b2 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs @@ -0,0 +1,12 @@ +using System; + +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBookDto +{ + public Guid Id { get; set; } + + public string? Name { get; set; } + + public decimal Price { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs new file mode 100644 index 0000000000..31ce5d74db --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBookTranslation : IObjectTranslation +{ + public string? Name { get; set; } + + public required string Language { get; set; } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj b/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj index ae0b3923a3..d5c3970ddc 100644 --- a/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj +++ b/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj @@ -6,6 +6,7 @@ net10.0 true + true diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DocumentedAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DocumentedAppService.cs new file mode 100644 index 0000000000..32dd58feb1 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DocumentedAppService.cs @@ -0,0 +1,58 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.TestApp.Application.Dto; + +namespace Volo.Abp.TestApp.Application; + +/// +/// A documented application service for testing API descriptions. +/// +/// +/// This service is used in integration tests to verify XML doc extraction. +/// +[Description("Documented service description from attribute")] +[Display(Name = "Documented Service")] +public class DocumentedAppService : ApplicationService, IDocumentedAppService +{ + /// + /// Gets a greeting message for the specified name. + /// + /// The name of the person to greet. + /// A personalized greeting message. + [Description("Get greeting description from attribute")] + [Display(Name = "Get Greeting")] + public async Task GetGreetingAsync(string name) + { + return await Task.FromResult($"Hello, {name}!"); + } + + /// + /// Creates a documented item. + /// + /// The input for creating a documented item. + /// The created documented item. + public async Task CreateAsync(DocumentedDto input) + { + return await Task.FromResult(input); + } + + /// + /// Searches for items matching the query. + /// + /// The search query string. + /// The maximum number of results to return. + /// A list of matching item names. + public async Task SearchAsync( + [Description("Query param description from attribute")] [Display(Name = "Search Query")] string query, + int maxResults) + { + return await Task.FromResult($"Results for {query}"); + } + + public async Task DeleteAsync(int id) + { + await Task.CompletedTask; + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DocumentedDto.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DocumentedDto.cs new file mode 100644 index 0000000000..56119f7939 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DocumentedDto.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.TestApp.Application.Dto; + +/// +/// A documented DTO for testing type and property descriptions. +/// +[Description("Documented DTO description from attribute")] +[Display(Name = "Documented DTO")] +public class DocumentedDto +{ + /// + /// The name of the documented item. + /// + [Description("Name description from attribute")] + [Display(Name = "Item Name")] + public string Name { get; set; } = default!; + + /// + /// The value of the documented item. + /// + [Description("Value description from attribute")] + public int Value { get; set; } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IDocumentedAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IDocumentedAppService.cs new file mode 100644 index 0000000000..e99c0e6b21 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IDocumentedAppService.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.TestApp.Application.Dto; + +namespace Volo.Abp.TestApp.Application; + +/// +/// A documented application service for testing API descriptions. +/// +/// +/// This service is used in integration tests to verify XML doc extraction. +/// +public interface IDocumentedAppService : IApplicationService +{ + /// + /// Gets a greeting message for the specified name. + /// + /// The name of the person to greet. + /// A personalized greeting message. + Task GetGreetingAsync(string name); + + /// + /// Creates a documented item. + /// + /// The input for creating a documented item. + /// The created documented item. + Task CreateAsync(DocumentedDto input); + + /// + /// Searches for items matching the query. + /// + /// The search query string. + /// The maximum number of results to return. + /// A list of matching item names. + Task SearchAsync(string query, int maxResults); + + Task DeleteAsync(int id); +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IInterfaceOnlyDocumentedAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IInterfaceOnlyDocumentedAppService.cs new file mode 100644 index 0000000000..d752fd0df9 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IInterfaceOnlyDocumentedAppService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Volo.Abp.TestApp.Application; + +/// +/// A service documented only on the interface to test XML doc fallback. +/// +/// +/// Used to verify that documentation is resolved from the interface when the implementation has none. +/// +public interface IInterfaceOnlyDocumentedAppService : IApplicationService +{ + /// + /// Gets a message documented only on the interface. + /// + /// The message key. + /// The resolved message. + Task GetMessageAsync(string key); +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/InterfaceOnlyDocumentedAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/InterfaceOnlyDocumentedAppService.cs new file mode 100644 index 0000000000..d9bc53981c --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/InterfaceOnlyDocumentedAppService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Volo.Abp.TestApp.Application; + +public class InterfaceOnlyDocumentedAppService : ApplicationService, IInterfaceOnlyDocumentedAppService +{ + public async Task GetMessageAsync(string key) + { + return await Task.FromResult(key); + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs index 438bf4b3b9..466d3c9dd7 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs @@ -39,6 +39,13 @@ public class TestAppModule : AbpModule AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(9) }); context.Services.AddEntityCache(); + + // Test ReplaceEntityCache: first add default, then replace with custom implementation + context.Services.AddEntityCache(); + context.Services.ReplaceEntityCache(); + + // Test ReplaceEntityCache without prior registration + context.Services.ReplaceEntityCache(); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs index 6ef8da9b7c..de5340b8d8 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs @@ -9,6 +9,8 @@ using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Caching; using Volo.Abp.Domain.Repositories; using Volo.Abp.Modularity; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.TestApp.Testing; @@ -19,12 +21,16 @@ public abstract class EntityCache_Tests : TestAppTestBase ProductRepository; protected readonly IEntityCache ProductEntityCache; protected readonly IEntityCache ProductCacheItem; + protected readonly IEntityCache CustomProductCacheItem; + protected readonly IEntityCache CustomProductCacheItemWithoutPriorRegistration; protected EntityCache_Tests() { ProductRepository = GetRequiredService>(); ProductEntityCache = GetRequiredService>(); ProductCacheItem = GetRequiredService>(); + CustomProductCacheItem = GetRequiredService>(); + CustomProductCacheItemWithoutPriorRegistration = GetRequiredService>(); } [Fact] @@ -35,6 +41,19 @@ public abstract class EntityCache_Tests : TestAppTestBase : TestAppTestBase>(() => ProductCacheItem.GetAsync(notExistId)); } + [Fact] + public async Task GetMany_Should_Throw_EntityNotFoundException_For_Not_Existing_Entities() + { + var notExistId = Guid.NewGuid(); + await Assert.ThrowsAsync>(() => ProductEntityCache.GetManyAsync(new[] { notExistId })); + await Assert.ThrowsAsync>(() => ProductCacheItem.GetManyAsync(new[] { notExistId })); + } + [Fact] public async Task Should_Return_EntityCache() { @@ -63,6 +90,56 @@ public abstract class EntityCache_Tests : TestAppTestBase : TestAppTestBase>(() => ProductEntityCache.GetManyAsDictionaryAsync(new[] { notExistId })); + await Assert.ThrowsAsync>(() => ProductCacheItem.GetManyAsDictionaryAsync(new[] { notExistId })); + } + + [Fact] + public async Task FindManyAsDictionary_Should_Return_EntityCache() + { + var notExistId = Guid.NewGuid(); + var ids = new[] { TestDataBuilder.ProductId, notExistId }; + + var products = await ProductEntityCache.FindManyAsDictionaryAsync(ids); + products.Count.ShouldBe(2); + products[TestDataBuilder.ProductId].ShouldNotBeNull(); + products[TestDataBuilder.ProductId]!.Id.ShouldBe(TestDataBuilder.ProductId); + products[TestDataBuilder.ProductId]!.Name.ShouldBe("Product1"); + products[TestDataBuilder.ProductId]!.Price.ShouldBe(decimal.One); + products[notExistId].ShouldBeNull(); + + // Call again to test caching + products = await ProductEntityCache.FindManyAsDictionaryAsync(ids); + products.Count.ShouldBe(2); + products[TestDataBuilder.ProductId].ShouldNotBeNull(); + + var productCacheItems = await ProductCacheItem.FindManyAsDictionaryAsync(ids); + productCacheItems.Count.ShouldBe(2); + productCacheItems[TestDataBuilder.ProductId].ShouldNotBeNull(); + productCacheItems[TestDataBuilder.ProductId]!.Id.ShouldBe(TestDataBuilder.ProductId); + productCacheItems[TestDataBuilder.ProductId]!.Name.ShouldBe("Product1"); + productCacheItems[TestDataBuilder.ProductId]!.Price.ShouldBe(decimal.One); + productCacheItems[notExistId].ShouldBeNull(); + } + + [Fact] + public async Task GetManyAsDictionary_Should_Return_EntityCache() + { + var products = await ProductEntityCache.GetManyAsDictionaryAsync(new[] { TestDataBuilder.ProductId }); + products.Count.ShouldBe(1); + products[TestDataBuilder.ProductId].Id.ShouldBe(TestDataBuilder.ProductId); + products[TestDataBuilder.ProductId].Name.ShouldBe("Product1"); + products[TestDataBuilder.ProductId].Price.ShouldBe(decimal.One); + + // Call again to test caching + products = await ProductEntityCache.GetManyAsDictionaryAsync(new[] { TestDataBuilder.ProductId }); + products.Count.ShouldBe(1); + products[TestDataBuilder.ProductId].Id.ShouldBe(TestDataBuilder.ProductId); + + var productCacheItems = await ProductCacheItem.GetManyAsDictionaryAsync(new[] { TestDataBuilder.ProductId }); + productCacheItems.Count.ShouldBe(1); + productCacheItems[TestDataBuilder.ProductId].Id.ShouldBe(TestDataBuilder.ProductId); + productCacheItems[TestDataBuilder.ProductId].Name.ShouldBe("Product1"); + productCacheItems[TestDataBuilder.ProductId].Price.ShouldBe(decimal.One); + } + + [Fact] + public async Task FindManyAsDictionary_Should_Handle_Duplicate_Ids() + { + var ids = new[] { TestDataBuilder.ProductId, TestDataBuilder.ProductId }; + + var products = await ProductEntityCache.FindManyAsDictionaryAsync(ids); + products.Count.ShouldBe(1); + products[TestDataBuilder.ProductId].ShouldNotBeNull(); + products[TestDataBuilder.ProductId]!.Id.ShouldBe(TestDataBuilder.ProductId); + } + + [Fact] + public async Task GetManyAsDictionary_Should_Handle_Duplicate_Ids() + { + var ids = new[] { TestDataBuilder.ProductId, TestDataBuilder.ProductId }; + + var products = await ProductEntityCache.GetManyAsDictionaryAsync(ids); + products.Count.ShouldBe(1); + products[TestDataBuilder.ProductId].Id.ShouldBe(TestDataBuilder.ProductId); + } + [Fact] public void EntityCache_Default_Options_Should_Be_2_Minutes() { @@ -204,3 +415,69 @@ public class ProductCacheItem2 public decimal Price { get; set; } } + +[Serializable] +[CacheName("CustomProductCacheItem")] +public class CustomProductCacheItem +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public decimal Price { get; set; } +} + +public class CustomProductEntityCache : EntityCacheWithObjectMapper +{ + public CustomProductEntityCache( + IReadOnlyRepository repository, + IDistributedCache, Guid> cache, + IUnitOfWorkManager unitOfWorkManager, + IObjectMapper objectMapper) + : base(repository, cache, unitOfWorkManager, objectMapper) + { + } + + protected override CustomProductCacheItem MapToValue(Product entity) + { + return new CustomProductCacheItem + { + Id = entity.Id, + Name = entity.Name.ToUpperInvariant(), + Price = entity.Price + }; + } +} + +[Serializable] +[CacheName("CustomProductCacheItemWithoutPriorRegistration")] +public class CustomProductCacheItemWithoutPriorRegistration +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public decimal Price { get; set; } +} + +public class CustomProductEntityCacheWithoutPriorRegistration : EntityCacheWithObjectMapper +{ + public CustomProductEntityCacheWithoutPriorRegistration( + IReadOnlyRepository repository, + IDistributedCache, Guid> cache, + IUnitOfWorkManager unitOfWorkManager, + IObjectMapper objectMapper) + : base(repository, cache, unitOfWorkManager, objectMapper) + { + } + + protected override CustomProductCacheItemWithoutPriorRegistration MapToValue(Product entity) + { + return new CustomProductCacheItemWithoutPriorRegistration + { + Id = entity.Id, + Name = entity.Name.ToUpperInvariant(), + Price = entity.Price + }; + } +} diff --git a/latest-versions.json b/latest-versions.json index 448a2fc3d8..0646ca8ece 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,4 +1,13 @@ [ + { + "version": "10.1.1", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "5.1.1" + } + }, { "version": "10.1.0", "releaseDate": "", diff --git a/lowcode/schema/definitions/command-interceptor-descriptor.schema.json b/lowcode/schema/definitions/command-interceptor-descriptor.schema.json deleted file mode 100644 index ad0d0b02bd..0000000000 --- a/lowcode/schema/definitions/command-interceptor-descriptor.schema.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "command-interceptor-descriptor.schema.json", - "title": "CommandInterceptorDescriptor", - "description": "Describes a command interceptor", - "type": "object", - "properties": { - "commandName": { - "type": "string", - "description": "Name of the command to intercept", - "enum": ["Create", "Update", "Delete"] - }, - "type": { - "$ref": "interceptor-type.schema.json" - }, - "javascript": { - "type": "string", - "description": "JavaScript code to execute" - } - }, - "required": ["commandName", "type", "javascript"], - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/endpoint-descriptor.schema.json b/lowcode/schema/definitions/endpoint-descriptor.schema.json deleted file mode 100644 index 7837e47f69..0000000000 --- a/lowcode/schema/definitions/endpoint-descriptor.schema.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:abp:lowcode:endpoint-descriptor", - "title": "Custom Endpoint Descriptor", - "description": "Defines a custom HTTP endpoint that executes JavaScript code", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique identifier for the endpoint" - }, - "route": { - "type": "string", - "description": "URL route pattern (e.g., '/api/custom/products/{id}')" - }, - "method": { - "type": "string", - "description": "HTTP method", - "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"], - "default": "GET" - }, - "javascript": { - "type": "string", - "description": "JavaScript code to execute. Has access to context object with request, db, currentUser, emailSender." - }, - "requireAuthentication": { - "type": "boolean", - "description": "Whether authentication is required", - "default": true - }, - "requiredPermissions": { - "type": "array", - "description": "Permission names required to access the endpoint", - "items": { - "type": "string" - } - }, - "description": { - "type": "string", - "description": "Optional description for documentation" - } - }, - "required": ["name", "route", "javascript"], - "additionalProperties": false -} diff --git a/lowcode/schema/definitions/entity-descriptor.schema.json b/lowcode/schema/definitions/entity-descriptor.schema.json deleted file mode 100644 index 2023a043bc..0000000000 --- a/lowcode/schema/definitions/entity-descriptor.schema.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "entity-descriptor.schema.json", - "title": "EntityDescriptor", - "description": "Describes an entity configuration", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Full name of the entity (e.g., 'Namespace.EntityName')", - "minLength": 1 - }, - "displayProperty": { - "type": "string", - "description": "The property to be used as the display property for the entity" - }, - "parent": { - "type": "string", - "description": "Full name of the parent entity (e.g., 'Namespace.EntityName')", - "minLength": 1 - }, - "ui": { - "$ref": "entity-ui-descriptor.schema.json" - }, - "properties": { - "type": "array", - "description": "List of property descriptors", - "items": { - "$ref": "entity-property-descriptor.schema.json" - } - }, - "interceptors": { - "type": "array", - "description": "List of command interceptors", - "items": { - "$ref": "command-interceptor-descriptor.schema.json" - } - } - }, - "required": ["name"], - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/entity-property-descriptor.schema.json b/lowcode/schema/definitions/entity-property-descriptor.schema.json deleted file mode 100644 index ceeeb9eb60..0000000000 --- a/lowcode/schema/definitions/entity-property-descriptor.schema.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "entity-property-descriptor.schema.json", - "title": "EntityPropertyDescriptor", - "description": "Describes a property configuration", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the property", - "minLength": 1 - }, - "type": { - "$ref": "entity-property-type.schema.json" - }, - "enumType": { - "type": "string", - "description": "Name of a JSON-defined enum (or full type name for code enums)" - }, - "allowSetByClients": { - "type": "boolean", - "description": "Indicates whether clients are allowed to set this property" - }, - "serverOnly": { - "type": "boolean", - "description": "When true, this property is completely hidden from clients (API responses and UI definitions). Use for sensitive data like passwords." - }, - "isMappedToDbField": { - "type": "boolean", - "description": "Indicates whether the property is mapped to a database field" - }, - "isUnique": { - "type": "boolean", - "description": "Indicates whether the property value must be unique across all entities" - }, - "isRequired": { - "type": "boolean", - "description": "When true, the property is required (not nullable). Affects DB column (NOT NULL), UI validation, and backend validation." - }, - "ui": { - "$ref": "entity-property-ui-descriptor.schema.json" - }, - "foreignKey": { - "$ref": "foreign-key-descriptor.schema.json" - }, - "validators": { - "type": "array", - "description": "Array of validators to apply to this property", - "items": { - "$ref": "validator-descriptor.schema.json" - } - } - }, - "required": [ - "name" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/entity-property-type.schema.json b/lowcode/schema/definitions/entity-property-type.schema.json deleted file mode 100644 index 5eb7defa6a..0000000000 --- a/lowcode/schema/definitions/entity-property-type.schema.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "entity-property-type.schema.json", - "title": "EntityPropertyType", - "description": "Data type of the property", - "type": "string", - "enum": [ - "string", - "String", - "int", - "Int", - "long", - "Long", - "decimal", - "Decimal", - "dateTime", - "DateTime", - "boolean", - "Boolean", - "guid", - "Guid", - "enum", - "Enum" - ] -} \ No newline at end of file diff --git a/lowcode/schema/definitions/entity-property-ui-descriptor.schema.json b/lowcode/schema/definitions/entity-property-ui-descriptor.schema.json deleted file mode 100644 index 1a0ded7b76..0000000000 --- a/lowcode/schema/definitions/entity-property-ui-descriptor.schema.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "entity-property-ui-descriptor.schema.json", - "title": "EntityPropertyUIDescriptor", - "description": "UI configuration for a property", - "type": "object", - "properties": { - "displayName": { - "type": "string", - "description": "Display name for the property in UI. Falls back to property name if not set." - }, - "isAvailableOnDataTable": { - "type": "boolean", - "description": "Whether the property is shown in the data table listing" - }, - "isAvailableOnDataTableFiltering": { - "type": "boolean", - "description": "Whether the property is available for filtering in the data table" - }, - "creationFormAvailability": { - "$ref": "entity-property-ui-form-availability.schema.json" - }, - "editingFormAvailability": { - "$ref": "entity-property-ui-form-availability.schema.json" - }, - "quickLookOrder": { - "type": "integer", - "description": "Order of the property in quick look views. Higher numbers appear first. Set to -1 to exclude from quick look. If no property has a value, first 5 properties by name are shown." - } - }, - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/entity-property-ui-form-availability.schema.json b/lowcode/schema/definitions/entity-property-ui-form-availability.schema.json deleted file mode 100644 index dff4a5665c..0000000000 --- a/lowcode/schema/definitions/entity-property-ui-form-availability.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "entity-property-ui-form-availability.schema.json", - "title": "EntityPropertyUIFormAvailability", - "description": "Availability of the property on forms", - "type": "string", - "enum": [ - "Available", - "available", - "Hidden", - "hidden", - "NotAvailable", - "notAvailable" - ] -} \ No newline at end of file diff --git a/lowcode/schema/definitions/entity-ui-descriptor.schema.json b/lowcode/schema/definitions/entity-ui-descriptor.schema.json deleted file mode 100644 index 3525d6c38e..0000000000 --- a/lowcode/schema/definitions/entity-ui-descriptor.schema.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "entity-ui-descriptor.schema.json", - "title": "EntityUIDescriptor", - "description": "UI configuration for the entity", - "type": "object", - "properties": { - "pageTitle": { - "type": "string", - "description": "Title to display on the entity's page" - } - }, - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/enum-descriptor.schema.json b/lowcode/schema/definitions/enum-descriptor.schema.json deleted file mode 100644 index 347f7e66ff..0000000000 --- a/lowcode/schema/definitions/enum-descriptor.schema.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "enum-descriptor.schema.json", - "title": "EnumDescriptor", - "description": "Describes an enum definition for use in entity properties", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Unique name for the enum", - "minLength": 1 - }, - "values": { - "type": "array", - "description": "List of enum values", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Display name of the enum value" - }, - "value": { - "type": "integer", - "description": "Integer value (auto-assigned if omitted)" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - }, - "minItems": 1 - } - }, - "required": [ - "name", - "values" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/foreign-key-descriptor.schema.json b/lowcode/schema/definitions/foreign-key-descriptor.schema.json deleted file mode 100644 index 27d0e81f03..0000000000 --- a/lowcode/schema/definitions/foreign-key-descriptor.schema.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "foreign-key-descriptor.schema.json", - "title": "ForeignKeyDescriptor", - "description": "Describes a foreign key relationship", - "type": "object", - "properties": { - "entityName": { - "type": "string", - "description": "Full name of the related entity", - "minLength": 1 - }, - "displayPropertyName": { - "type": "string", - "description": "Property name to display from the related entity", - "minLength": 1 - }, - "access": { - "type": "string", - "description": "Access level for managing this relation from the referenced entity side. When set to 'view' or 'edit', the referenced entity can see/manage items that reference it.", - "enum": ["none", "view", "edit"], - "default": "none" - } - }, - "required": ["entityName"], - "additionalProperties": false -} \ No newline at end of file diff --git a/lowcode/schema/definitions/interceptor-type.schema.json b/lowcode/schema/definitions/interceptor-type.schema.json deleted file mode 100644 index 435c26b434..0000000000 --- a/lowcode/schema/definitions/interceptor-type.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "interceptor-type.schema.json", - "title": "InterceptorType", - "description": "When the interceptor runs", - "type": "string", - "enum": [ - "Pre", - "pre", - "Post", - "post", - "Replace", - "replace" - ] -} \ No newline at end of file diff --git a/lowcode/schema/definitions/validator-descriptor.schema.json b/lowcode/schema/definitions/validator-descriptor.schema.json deleted file mode 100644 index 7eeac0eba9..0000000000 --- a/lowcode/schema/definitions/validator-descriptor.schema.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "validator-descriptor.schema.json", - "title": "ValidatorDescriptor", - "description": "A single validator in the validators array", - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "description": "Type of validator", - "enum": [ - "required", - "minLength", - "maxLength", - "stringLength", - "min", - "minimum", - "max", - "maximum", - "range", - "pattern", - "regularExpression", - "email", - "emailAddress", - "phone", - "url", - "creditCard" - ] - }, - "message": { - "type": "string", - "description": "Custom error message for this validator" - }, - "length": { - "type": "integer", - "description": "Length value for minLength, maxLength validators", - "minimum": 0 - }, - "minimumLength": { - "type": "integer", - "description": "Minimum length for stringLength validator", - "minimum": 0 - }, - "maximumLength": { - "type": "integer", - "description": "Maximum length for stringLength validator", - "minimum": 0 - }, - "value": { - "type": "number", - "description": "Value for min/minimum, max/maximum validators" - }, - "minimum": { - "type": "number", - "description": "Minimum value for range validator" - }, - "maximum": { - "type": "number", - "description": "Maximum value for range validator" - }, - "pattern": { - "type": "string", - "description": "Regular expression pattern for pattern/regularExpression validators" - } - }, - "additionalProperties": true -} - diff --git a/lowcode/schema/model.schema.json b/lowcode/schema/model.schema.json deleted file mode 100644 index c958f126fe..0000000000 --- a/lowcode/schema/model.schema.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "model.schema.json", - "title": "ABP Low Code Model", - "description": "Schema for ABP Low Code model.json configuration file", - "type": "object", - "properties": { - "$schema": { - "type": "string", - "description": "Reference to the JSON schema" - }, - "enums": { - "type": "array", - "description": "List of enum definitions", - "items": { - "$ref": "definitions/enum-descriptor.schema.json" - } - }, - "entities": { - "type": "array", - "description": "List of entity descriptors", - "items": { - "$ref": "definitions/entity-descriptor.schema.json" - } - }, - "endpoints": { - "type": "array", - "description": "List of custom HTTP endpoints that execute JavaScript code", - "items": { - "$ref": "definitions/endpoint-descriptor.schema.json" - } - } - }, - "additionalProperties": false -} \ No newline at end of file diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs index 0d521baf3d..b8aaf6e519 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/DemoAppQuartzModule.cs @@ -1,6 +1,7 @@ using Volo.Abp.Autofac; using Volo.Abp.BackgroundJobs.DemoApp.Shared; using Volo.Abp.BackgroundJobs.Quartz; +using Volo.Abp.BackgroundWorkers.Quartz; using Volo.Abp.Modularity; namespace Volo.Abp.BackgroundJobs.DemoApp.Quartz; @@ -8,7 +9,8 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Quartz; [DependsOn( typeof(DemoAppSharedModule), typeof(AbpAutofacModule), - typeof(AbpBackgroundJobsQuartzModule) + typeof(AbpBackgroundJobsQuartzModule), + typeof(AbpBackgroundWorkersQuartzModule) )] public class DemoAppQuartzModule : AbpModule { diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj index 8faef0c291..5f2be2b49b 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Quartz/Volo.Abp.BackgroundJobs.DemoApp.Quartz.csproj @@ -12,6 +12,7 @@ + diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs index 713355636f..f7fde67b44 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs @@ -1,5 +1,9 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs; +using Volo.Abp.BackgroundWorkers; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; @@ -8,11 +12,86 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared [DependsOn(typeof(AbpMultiTenancyModule))] public class DemoAppSharedModule : AbpModule { - public override void OnPostApplicationInitialization(ApplicationInitializationContext context) + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { + var dynamicJobManager = context.ServiceProvider.GetRequiredService(); + + dynamicJobManager.RegisterHandler("CompileTimeDynamicJob", (ctx, ct) => + { + using (var doc = JsonDocument.Parse(ctx.JsonData)) + { + var value = doc.RootElement.TryGetProperty("value", out var prop) + ? prop.GetString() + : doc.RootElement.TryGetProperty("Value", out prop) + ? prop.GetString() + : null; + Console.WriteLine($"[DYNAMIC-COMPILE] {value}"); + return Task.CompletedTask; + } + }); + context.ServiceProvider .GetRequiredService() .CreateJobs(); + + await DynamicBackgroundWorkerDemoAsync(context); + } + + private async Task DynamicBackgroundWorkerDemoAsync(ApplicationInitializationContext context) + { + var dynamicWorkerManager = context.ServiceProvider + .GetService(); + + if (dynamicWorkerManager == null) + { + return; + } + + // AddAsync: Register a dynamic worker with a schedule and handler + await dynamicWorkerManager.AddAsync( + "DemoHeartbeatWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 5000 //5 seconds + }, + async (workerContext, cancellationToken) => + { + Console.WriteLine($"[{DateTime.Now}] DemoHeartbeatWorker executed."); + await Task.CompletedTask; + } + ); + + // IsRegistered: Check if a dynamic worker is registered + var isRegistered = dynamicWorkerManager.IsRegistered("DemoHeartbeatWorker"); + Console.WriteLine($"DemoHeartbeatWorker is registered: {isRegistered}"); + + // UpdateScheduleAsync: Update the schedule of an existing dynamic worker + var updated = await dynamicWorkerManager.UpdateScheduleAsync( + "DemoHeartbeatWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 10000 //Change to 10 seconds + } + ); + Console.WriteLine($"DemoHeartbeatWorker schedule updated: {updated}"); + + // RemoveAsync: Remove a dynamic worker + var removed = await dynamicWorkerManager.RemoveAsync("DemoHeartbeatWorker"); + Console.WriteLine($"DemoHeartbeatWorker removed: {removed}"); + + // Re-add the worker to keep it running for demo purposes + await dynamicWorkerManager.AddAsync( + "DemoHeartbeatWorker", + new DynamicBackgroundWorkerSchedule + { + Period = 10000 //10 seconds + }, + async (workerContext, cancellationToken) => + { + Console.WriteLine($"[{DateTime.Now}] DemoHeartbeatWorker executed."); + await Task.CompletedTask; + } + ); } } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Jobs/SampleJobCreator.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Jobs/SampleJobCreator.cs index b131c5943d..61a715d84d 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Jobs/SampleJobCreator.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Jobs/SampleJobCreator.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System; +using System.Text.Json; +using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Threading; @@ -7,10 +9,14 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs public class SampleJobCreator : ITransientDependency { private readonly IBackgroundJobManager _backgroundJobManager; + private readonly IDynamicBackgroundJobManager _dynamicBackgroundJobManager; - public SampleJobCreator(IBackgroundJobManager backgroundJobManager) + public SampleJobCreator( + IBackgroundJobManager backgroundJobManager, + IDynamicBackgroundJobManager dynamicBackgroundJobManager) { _backgroundJobManager = backgroundJobManager; + _dynamicBackgroundJobManager = dynamicBackgroundJobManager; } public void CreateJobs() @@ -20,10 +26,54 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs public async Task CreateJobsAsync() { - await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 1 (green)" }); - await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 2 (green)" }); - await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 1 (yellow)" }); - await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 2 (yellow)" }); + // Type-safe enqueue (existing) + await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 1 (green) - typed" }); + await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 1 (yellow) - typed" }); + + // Register runtime dynamic handler + _dynamicBackgroundJobManager.RegisterHandler("RuntimeDynamicJob", (context, ct) => + { + using (var doc = JsonDocument.Parse(context.JsonData)) + { + var value = doc.RootElement.TryGetProperty("value", out var prop) + ? prop.GetString() + : doc.RootElement.TryGetProperty("Value", out prop) + ? prop.GetString() + : null; + Console.WriteLine($"[DYNAMIC-RUNTIME] {value}"); + return Task.CompletedTask; + } + }); + + // String-based enqueue with typed job (by name) + await _dynamicBackgroundJobManager.EnqueueAsync( + "GreenJob", + new WriteToConsoleGreenJobArgs { Value = "test 2 (green) - by name, typed args" } + ); + await _dynamicBackgroundJobManager.EnqueueAsync( + "YellowJob", + new WriteToConsoleYellowJobArgs { Value = "test 2 (yellow) - by name, typed args" } + ); + + // String-based enqueue with anonymous object (typed job path) + await _dynamicBackgroundJobManager.EnqueueAsync( + "GreenJob", + new { Value = "test 3 (green) - by name, dynamic", Time = DateTime.Now } + ); + await _dynamicBackgroundJobManager.EnqueueAsync( + "YellowJob", + new { Value = "test 3 (yellow) - by name, dynamic", Time = DateTime.Now } + ); + + // Dynamic job handlers + await _dynamicBackgroundJobManager.EnqueueAsync( + "CompileTimeDynamicJob", + new { Value = "test 4 (dynamic) - compile-time" } + ); + await _dynamicBackgroundJobManager.EnqueueAsync( + "RuntimeDynamicJob", + new { Value = "test 5 (dynamic) - runtime" } + ); } } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/DemoAppTickerQModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/DemoAppTickerQModule.cs index de3ef3c05d..29027a2a75 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/DemoAppTickerQModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/DemoAppTickerQModule.cs @@ -77,13 +77,13 @@ public class DemoAppTickerQModule : AbpModule public override Task OnPreApplicationInitializationAsync(ApplicationInitializationContext context) { var abpTickerQFunctionProvider = context.ServiceProvider.GetRequiredService(); - abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) => + abpTickerQFunctionProvider.AddFunction(nameof(CleanupJobs), async (cancellationToken, serviceProvider, tickerFunctionContext) => { var service = new CleanupJobs(); var request = await TickerRequestProvider.GetRequestAsync(tickerFunctionContext, cancellationToken); var genericContext = new TickerFunctionContext(tickerFunctionContext, request); await service.CleanupLogsAsync(genericContext, cancellationToken); - }))); + }, TickerTaskPriority.Normal); abpTickerQFunctionProvider.RequestTypes.TryAdd(nameof(CleanupJobs), (typeof(string).FullName, typeof(string))); return Task.CompletedTask; } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs similarity index 98% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs rename to modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs index 3225815926..fd21dc852f 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations { [DbContext(typeof(DemoAppDbContext))] - [Migration("20260119064307_Initial")] + [Migration("20260320082618_Initial")] partial class Initial { /// diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.cs similarity index 100% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs rename to modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260320082618_Initial.cs diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.Designer.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.Designer.cs new file mode 100644 index 0000000000..b95e114e8c --- /dev/null +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.Designer.cs @@ -0,0 +1,1538 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; +using VoloDocs.EntityFrameworkCore; + +#nullable disable + +namespace Migrations +{ + [DbContext(typeof(VoloDocsDbContext))] + [Migration("20260227074745_ABP10_2")] + partial class ABP10_2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Volo.Abp.BlobStoring.Database.DatabaseBlob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ContainerId") + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .HasMaxLength(2147483647) + .HasColumnType("varbinary(max)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("ContainerId"); + + b.HasIndex("TenantId", "ContainerId", "Name"); + + b.ToTable("AbpBlobs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.BlobStoring.Database.DatabaseBlobContainer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("AbpBlobContainers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityClaimType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsStatic") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Regex") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("RegexDescription") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Required") + .HasColumnType("bit"); + + b.Property("ValueType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AbpClaimTypes", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityLinkUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("SourceTenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetTenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("TargetUserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SourceUserId", "SourceTenantId", "TargetUserId", "TargetTenantId") + .IsUnique() + .HasFilter("[SourceTenantId] IS NOT NULL AND [TargetTenantId] IS NOT NULL"); + + b.ToTable("AbpLinkUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDefault") + .HasColumnType("bit") + .HasColumnName("IsDefault"); + + b.Property("IsPublic") + .HasColumnType("bit") + .HasColumnName("IsPublic"); + + b.Property("IsStatic") + .HasColumnType("bit") + .HasColumnName("IsStatic"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("AbpRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AbpRoleClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySecurityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("BrowserInfo") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ClientIpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CorrelationId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("CreationTime") + .HasColumnType("datetime2"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Identity") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("TenantName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Action"); + + b.HasIndex("TenantId", "ApplicationName"); + + b.HasIndex("TenantId", "Identity"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpSecurityLogs", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentitySession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientId") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("DeviceInfo") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IpAddresses") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("LastAccessed") + .HasColumnType("datetime2"); + + b.Property("SessionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("SignedIn") + .HasColumnType("datetime2"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Device"); + + b.HasIndex("SessionId"); + + b.HasIndex("TenantId", "UserId"); + + b.ToTable("AbpSessions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0) + .HasColumnName("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("Email"); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("EmailConfirmed"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsActive") + .HasColumnType("bit") + .HasColumnName("IsActive"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsExternal"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LastPasswordChangeTime") + .HasColumnType("datetimeoffset"); + + b.Property("LastSignInTime") + .HasColumnType("datetimeoffset"); + + b.Property("Leaved") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("Leaved"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("LockoutEnabled"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NormalizedEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("NormalizedEmail"); + + b.Property("NormalizedUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("NormalizedUserName"); + + b.Property("PasswordHash") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("PasswordHash"); + + b.Property("PhoneNumber") + .HasMaxLength(16) + .HasColumnType("nvarchar(16)") + .HasColumnName("PhoneNumber"); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("PhoneNumberConfirmed"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("SecurityStamp"); + + b.Property("ShouldChangePasswordOnNextLogin") + .HasColumnType("bit"); + + b.Property("Surname") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("Surname"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("TwoFactorEnabled"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)") + .HasColumnName("UserName"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("AbpUsers", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ClaimValue") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserClaims", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserDelegation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("SourceUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("TargetUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.ToTable("AbpUserDelegations", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderDisplayName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196) + .HasColumnType("nvarchar(196)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("AbpUserLogins", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "UserId"); + + b.HasIndex("UserId", "OrganizationUnitId"); + + b.ToTable("AbpUserOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("varbinary(1024)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserPasskeys", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Password") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "Password"); + + b.ToTable("AbpUserPasswordHistories", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("AbpUserRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AbpUserTokens", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(95) + .HasColumnType("nvarchar(95)") + .HasColumnName("Code"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("DisplayName"); + + b.Property("EntityVersion") + .HasColumnType("int"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("ParentId"); + + b.ToTable("AbpOrganizationUnits", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.Property("OrganizationUnitId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("OrganizationUnitId", "RoleId"); + + b.HasIndex("RoleId", "OrganizationUnitId"); + + b.ToTable("AbpOrganizationUnitRoles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("GroupName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("ManagementPermissionName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MultiTenancySide") + .HasColumnType("tinyint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ParentName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Providers") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ResourceName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("StateCheckers") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("GroupName"); + + b.HasIndex("ResourceName", "Name") + .IsUnique() + .HasFilter("[ResourceName] IS NOT NULL"); + + b.ToTable("AbpPermissions", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ProviderName", "ProviderKey") + .IsUnique() + .HasFilter("[TenantId] IS NOT NULL"); + + b.ToTable("AbpPermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGroupDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpPermissionGroups", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.ResourcePermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ResourceKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ResourceName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey") + .IsUnique() + .HasFilter("[TenantId] IS NOT NULL"); + + b.ToTable("AbpResourcePermissionGrants", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey") + .IsUnique() + .HasFilter("[ProviderName] IS NOT NULL AND [ProviderKey] IS NOT NULL"); + + b.ToTable("AbpSettings", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.SettingDefinitionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultValue") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsEncrypted") + .HasColumnType("bit"); + + b.Property("IsInherited") + .HasColumnType("bit"); + + b.Property("IsVisibleToClients") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Providers") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("AbpSettingDefinitions", (string)null); + }); + + modelBuilder.Entity("Volo.Docs.Documents.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .HasColumnType("datetime2"); + + b.Property("EditLink") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Format") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("LanguageCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("LastCachedTime") + .HasColumnType("datetime2"); + + b.Property("LastSignificantUpdateTime") + .HasColumnType("datetime2"); + + b.Property("LastUpdatedTime") + .HasColumnType("datetime2"); + + b.Property("LocalDirectory") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RawRootUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("RootUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.ToTable("DocsDocuments", (string)null); + }); + + modelBuilder.Entity("Volo.Docs.Documents.DocumentContributor", b => + { + b.Property("DocumentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Username") + .HasColumnType("nvarchar(450)"); + + b.Property("AvatarUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("CommitCount") + .HasColumnType("int"); + + b.Property("UserProfileUrl") + .HasColumnType("nvarchar(max)"); + + b.HasKey("DocumentId", "Username"); + + b.ToTable("DocsDocumentContributors", (string)null); + }); + + modelBuilder.Entity("Volo.Docs.Projects.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("DefaultDocumentName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("DocumentStoreType") + .HasColumnType("nvarchar(max)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Format") + .HasColumnType("nvarchar(max)"); + + b.Property("LatestVersionBranchName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("MainWebsiteUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("MinimumVersion") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("NavigationDocumentName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ParametersDocumentName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ShortName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.HasKey("Id"); + + b.ToTable("DocsProjects", (string)null); + }); + + modelBuilder.Entity("Volo.Docs.Projects.ProjectPdfFile", b => + { + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .HasColumnType("nvarchar(450)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("LanguageCode") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("ProjectId", "FileName"); + + b.ToTable("DocsProjectPdfFiles", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.BlobStoring.Database.DatabaseBlob", b => + { + b.HasOne("Volo.Abp.BlobStoring.Database.DatabaseBlobContainer", null) + .WithMany() + .HasForeignKey("ContainerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserOrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("OrganizationUnits") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Passkeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Volo.Abp.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject"); + + b1.Property("ClientDataJson"); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey"); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AbpUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("nvarchar(max)"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("PasswordHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnitRole", b => + { + b.HasOne("Volo.Abp.Identity.OrganizationUnit", null) + .WithMany("Roles") + .HasForeignKey("OrganizationUnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Volo.Abp.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Docs.Documents.DocumentContributor", b => + { + b.HasOne("Volo.Docs.Documents.Document", null) + .WithMany("Contributors") + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Docs.Projects.ProjectPdfFile", b => + { + b.HasOne("Volo.Docs.Projects.Project", null) + .WithMany("PdfFiles") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Navigation("Claims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("OrganizationUnits"); + + b.Navigation("Passkeys"); + + b.Navigation("PasswordHistories"); + + b.Navigation("Roles"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.OrganizationUnit", b => + { + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Volo.Docs.Documents.Document", b => + { + b.Navigation("Contributors"); + }); + + modelBuilder.Entity("Volo.Docs.Projects.Project", b => + { + b.Navigation("PdfFiles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.cs new file mode 100644 index 0000000000..b13305aa72 --- /dev/null +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.cs @@ -0,0 +1,160 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Migrations +{ + /// + public partial class ABP10_2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AbpPermissions_Name", + table: "AbpPermissions"); + + migrationBuilder.AddColumn( + name: "LastSignInTime", + table: "AbpUsers", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "Leaved", + table: "AbpUsers", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "GroupName", + table: "AbpPermissions", + type: "nvarchar(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(128)", + oldMaxLength: 128); + + migrationBuilder.AddColumn( + name: "ManagementPermissionName", + table: "AbpPermissions", + type: "nvarchar(128)", + maxLength: 128, + nullable: true); + + migrationBuilder.AddColumn( + name: "ResourceName", + table: "AbpPermissions", + type: "nvarchar(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.CreateTable( + name: "AbpResourcePermissionGrants", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + ProviderName = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + ProviderKey = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + ResourceName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + ResourceKey = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpResourcePermissionGrants", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AbpUserPasskeys", + columns: table => new + { + CredentialId = table.Column(type: "varbinary(1024)", maxLength: 1024, nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + UserId = table.Column(type: "uniqueidentifier", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpUserPasskeys", x => x.CredentialId); + table.ForeignKey( + name: "FK_AbpUserPasskeys_AbpUsers_UserId", + column: x => x.UserId, + principalTable: "AbpUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AbpPermissions_ResourceName_Name", + table: "AbpPermissions", + columns: new[] { "ResourceName", "Name" }, + unique: true, + filter: "[ResourceName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AbpResourcePermissionGrants_TenantId_Name_ResourceName_ResourceKey_ProviderName_ProviderKey", + table: "AbpResourcePermissionGrants", + columns: new[] { "TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey" }, + unique: true, + filter: "[TenantId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AbpUserPasskeys_UserId", + table: "AbpUserPasskeys", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpResourcePermissionGrants"); + + migrationBuilder.DropTable( + name: "AbpUserPasskeys"); + + migrationBuilder.DropIndex( + name: "IX_AbpPermissions_ResourceName_Name", + table: "AbpPermissions"); + + migrationBuilder.DropColumn( + name: "LastSignInTime", + table: "AbpUsers"); + + migrationBuilder.DropColumn( + name: "Leaved", + table: "AbpUsers"); + + migrationBuilder.DropColumn( + name: "ManagementPermissionName", + table: "AbpPermissions"); + + migrationBuilder.DropColumn( + name: "ResourceName", + table: "AbpPermissions"); + + migrationBuilder.AlterColumn( + name: "GroupName", + table: "AbpPermissions", + type: "nvarchar(128)", + maxLength: 128, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_AbpPermissions_Name", + table: "AbpPermissions", + column: "Name", + unique: true); + } + } +} diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs index b10ce9d10e..05bd51c9c1 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ namespace Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "10.0.0-rc.2.25502.107") + .HasAnnotation("ProductVersion", "10.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -482,6 +482,15 @@ namespace Migrations b.Property("LastPasswordChangeTime") .HasColumnType("datetimeoffset"); + b.Property("LastSignInTime") + .HasColumnType("datetimeoffset"); + + b.Property("Leaved") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("Leaved"); + b.Property("LockoutEnabled") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -678,6 +687,26 @@ namespace Migrations b.ToTable("AbpUserOrganizationUnits", (string)null); }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("varbinary(1024)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserPasskeys", (string)null); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => { b.Property("UserId") @@ -865,13 +894,16 @@ namespace Migrations .HasColumnName("ExtraProperties"); b.Property("GroupName") - .IsRequired() .HasMaxLength(128) .HasColumnType("nvarchar(128)"); b.Property("IsEnabled") .HasColumnType("bit"); + b.Property("ManagementPermissionName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + b.Property("MultiTenancySide") .HasColumnType("tinyint"); @@ -888,6 +920,10 @@ namespace Migrations .HasMaxLength(128) .HasColumnType("nvarchar(128)"); + b.Property("ResourceName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + b.Property("StateCheckers") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -896,8 +932,9 @@ namespace Migrations b.HasIndex("GroupName"); - b.HasIndex("Name") - .IsUnique(); + b.HasIndex("ResourceName", "Name") + .IsUnique() + .HasFilter("[ResourceName] IS NOT NULL"); b.ToTable("AbpPermissions", (string)null); }); @@ -964,6 +1001,50 @@ namespace Migrations b.ToTable("AbpPermissionGroups", (string)null); }); + modelBuilder.Entity("Volo.Abp.PermissionManagement.ResourcePermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ResourceKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ResourceName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey") + .IsUnique() + .HasFilter("[TenantId] IS NOT NULL"); + + b.ToTable("AbpResourcePermissionGrants", (string)null); + }); + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => { b.Property("Id") @@ -1292,6 +1373,53 @@ namespace Migrations .IsRequired(); }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Passkeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Volo.Abp.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject"); + + b1.Property("ClientDataJson"); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey"); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AbpUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("nvarchar(max)"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data"); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => { b.HasOne("Volo.Abp.Identity.IdentityUser", null) @@ -1378,6 +1506,8 @@ namespace Migrations b.Navigation("OrganizationUnits"); + b.Navigation("Passkeys"); + b.Navigation("PasswordHistories"); b.Navigation("Roles"); diff --git a/modules/docs/app/VoloDocs.Migrator/appsettings.json b/modules/docs/app/VoloDocs.Migrator/appsettings.json index 9668adc1e2..aa1a142134 100644 --- a/modules/docs/app/VoloDocs.Migrator/appsettings.json +++ b/modules/docs/app/VoloDocs.Migrator/appsettings.json @@ -1,3 +1,3 @@ { - "ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True", + "ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True" } \ No newline at end of file diff --git a/modules/docs/app/VoloDocs.Web/VoloDocs.Web.abppkg b/modules/docs/app/VoloDocs.Web/VoloDocs.Web.abppkg index 48875c29cf..9f088dc93e 100644 --- a/modules/docs/app/VoloDocs.Web/VoloDocs.Web.abppkg +++ b/modules/docs/app/VoloDocs.Web/VoloDocs.Web.abppkg @@ -1,3 +1,4 @@ { - "role": "host.mvc" + "role": "host.mvc", + "projectId": "47142bf8-4bb8-41c5-9900-990a97a67e8a" } \ No newline at end of file diff --git a/modules/docs/app/VoloDocs.Web/appsettings.json b/modules/docs/app/VoloDocs.Web/appsettings.json index 75d1b9890e..586d1d1a8a 100644 --- a/modules/docs/app/VoloDocs.Web/appsettings.json +++ b/modules/docs/app/VoloDocs.Web/appsettings.json @@ -12,7 +12,7 @@ "Redis": { "Configuration": "127.0.0.1" }, - "ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True", + "ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True", "ElasticSearch": { "Url": "http://localhost:9200" }, diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj b/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj index 67c3354faf..cd8ab94121 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj @@ -9,7 +9,20 @@ + + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs b/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs index a17e4ce5df..bb1142b76c 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs @@ -1,6 +1,11 @@ using System.Net.Http.Headers; +using System.Security.Claims; +using System.Security.Cryptography; using System.Text.Json; +using Duende.IdentityModel; using Duende.IdentityModel.Client; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; const string email = "admin@abp.io"; @@ -195,3 +200,89 @@ Console.WriteLine("ClientCredentials API response: {0}", JsonSerializer.Serializ })); Console.WriteLine(); + +// private_key_jwt client credentials flow +// The private key file is generated by `abp generate-jwks` and stored in the parent app/ directory, +// then copied to the output directory during build. +// The corresponding public key (jwks.json) is registered on the authorization server side. +var privateKeyPath = Path.Combine(AppContext.BaseDirectory, "jwks-private.pem"); +if (!File.Exists(privateKeyPath)) +{ + Console.WriteLine("private_key_jwt demo skipped: private key file not found at {0}.", privateKeyPath); + return; +} + +using var rsaKey = RSA.Create(); +rsaKey.ImportFromPem(await File.ReadAllTextAsync(privateKeyPath)); + +// Read the kid dynamically from the JWKS file so it stays in sync with the server-registered JWKS. +string? signingKid = null; +var jwksForKidPath = Path.Combine(AppContext.BaseDirectory, "jwks.json"); +if (File.Exists(jwksForKidPath)) +{ + using var jwksDoc = JsonDocument.Parse(await File.ReadAllTextAsync(jwksForKidPath)); + if (jwksDoc.RootElement.TryGetProperty("keys", out var keysElem) && + keysElem.GetArrayLength() > 0 && + keysElem[0].TryGetProperty("kid", out var kidElem)) + { + signingKid = kidElem.GetString(); + } +} +var signingKey = new RsaSecurityKey(rsaKey) { KeyId = signingKid }; +var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256); + +var now = DateTime.UtcNow; +var jwtHandler = new JsonWebTokenHandler(); +var clientAssertionToken = jwtHandler.CreateToken(new SecurityTokenDescriptor +{ + // OpenIddict requires typ = "client-authentication+jwt" for client assertion JWTs. + // The aud claim must equal the authorization server's issuer URI (Options.Issuer), not the token endpoint. + TokenType = "client-authentication+jwt", + Issuer = "AbpConsoleAppWithJwks", + Audience = configuration.Issuer, + Subject = new ClaimsIdentity(new[] + { + new Claim(JwtRegisteredClaimNames.Sub, "AbpConsoleAppWithJwks"), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }), + IssuedAt = now, + NotBefore = now, + Expires = now.AddMinutes(5), + SigningCredentials = signingCredentials, +}); + +client = new HttpClient(); + +var jwksTokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest +{ + Address = configuration.TokenEndpoint, + ClientId = "AbpConsoleAppWithJwks", + ClientCredentialStyle = ClientCredentialStyle.PostBody, + ClientAssertion = new ClientAssertion + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = clientAssertionToken, + }, + Scope = "AbpAPI", +}); + +if (jwksTokenResponse.IsError) +{ + Console.WriteLine("private_key_jwt error: {0}", jwksTokenResponse.Error); + return; +} + +Console.WriteLine("private_key_jwt Access token: {0}", jwksTokenResponse.AccessToken); +Console.WriteLine(); + +serverRequest = new HttpRequestMessage(HttpMethod.Get, api); +serverRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwksTokenResponse.AccessToken); +serverResponse = await client.SendAsync(serverRequest); +serverResponse.EnsureSuccessStatusCode(); + +Console.WriteLine("private_key_jwt API response: {0}", JsonSerializer.Serialize(JsonDocument.Parse(await serverResponse.Content.ReadAsStringAsync()), new JsonSerializerOptions +{ + WriteIndented = true +})); + +Console.WriteLine(); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs b/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs index e1fd97136b..71018b7744 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs @@ -1,4 +1,7 @@ using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Demo.Server.ExtensionGrants; using Volo.Abp.Data; @@ -13,6 +16,8 @@ public class ServerDataSeedContributor : IDataSeedContributor, ITransientDepende private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictScopeManager _scopeManager; + public ILogger Logger { get; set; } + public ServerDataSeedContributor( ICurrentTenant currentTenant, IOpenIddictApplicationManager applicationManager, @@ -21,6 +26,7 @@ public class ServerDataSeedContributor : IDataSeedContributor, ITransientDepende _currentTenant = currentTenant; _applicationManager = applicationManager; _scopeManager = scopeManager; + Logger = NullLogger.Instance; } public async Task SeedAsync(DataSeedContext context) @@ -159,6 +165,43 @@ public class ServerDataSeedContributor : IDataSeedContributor, ITransientDepende }); } + if (await _applicationManager.FindByClientIdAsync("AbpConsoleAppWithJwks") == null) + { + // Load the pre-generated JWKS (public key) from the jwks.json file. + // The corresponding private key (jwks-private.pem) is stored in the parent app/ directory + // and used by OpenIddict.Demo.Client.Console to sign JWT client assertions. + // Both files are generated with: abp generate-jwks + var jwksPath = Path.Combine(AppContext.BaseDirectory, "jwks.json"); + if (!File.Exists(jwksPath)) + { + Logger.LogWarning( + "JWKS file not found at '{JwksPath}'. " + + "Skipping creation of the 'AbpConsoleAppWithJwks' client. " + + "Run 'abp generate-jwks' in the app/ directory to generate the key pair.", + jwksPath); + } + else + { + var jwks = new JsonWebKeySet(await File.ReadAllTextAsync(jwksPath)); + + await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor + { + ApplicationType = OpenIddictConstants.ApplicationTypes.Web, + ClientId = "AbpConsoleAppWithJwks", + ClientType = OpenIddictConstants.ClientTypes.Confidential, + DisplayName = "Abp Console App (private_key_jwt)", + JsonWebKeySet = jwks, + Permissions = + { + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.Endpoints.Introspection, + OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, + OpenIddictConstants.Permissions.Prefixes.Scope + "AbpAPI" + } + }); + } + } + if (await _applicationManager.FindByClientIdAsync("Swagger") == null) { await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.Designer.cs b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20260311061448_Initial.Designer.cs similarity index 90% rename from modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.Designer.cs rename to modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20260311061448_Initial.Designer.cs index 1ba6189674..a67fcb4cc5 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.Designer.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20260311061448_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace OpenIddict.Demo.Server.Migrations { [DbContext(typeof(ServerDbContext))] - [Migration("20250710090114_Initial")] + [Migration("20260311061448_Initial")] partial class Initial { /// @@ -22,7 +22,7 @@ namespace OpenIddict.Demo.Server.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("ProductVersion", "10.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -408,8 +408,8 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnType("nvarchar(64)"); b.Property("DeviceInfo") - .HasMaxLength(64) - .HasColumnType("nvarchar(64)"); + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); b.Property("ExtraProperties") .HasColumnType("nvarchar(max)") @@ -530,6 +530,15 @@ namespace OpenIddict.Demo.Server.Migrations b.Property("LastPasswordChangeTime") .HasColumnType("datetimeoffset"); + b.Property("LastSignInTime") + .HasColumnType("datetimeoffset"); + + b.Property("Leaved") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("Leaved"); + b.Property("LockoutEnabled") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -726,6 +735,47 @@ namespace OpenIddict.Demo.Server.Migrations b.ToTable("AbpUserOrganizationUnits", (string)null); }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("varbinary(1024)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserPasskeys", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Password") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "Password"); + + b.ToTable("AbpUserPasswordHistories", (string)null); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => { b.Property("UserId") @@ -1194,13 +1244,16 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnName("ExtraProperties"); b.Property("GroupName") - .IsRequired() .HasMaxLength(128) .HasColumnType("nvarchar(128)"); b.Property("IsEnabled") .HasColumnType("bit"); + b.Property("ManagementPermissionName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + b.Property("MultiTenancySide") .HasColumnType("tinyint"); @@ -1217,6 +1270,10 @@ namespace OpenIddict.Demo.Server.Migrations .HasMaxLength(128) .HasColumnType("nvarchar(128)"); + b.Property("ResourceName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + b.Property("StateCheckers") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -1225,8 +1282,9 @@ namespace OpenIddict.Demo.Server.Migrations b.HasIndex("GroupName"); - b.HasIndex("Name") - .IsUnique(); + b.HasIndex("ResourceName", "Name") + .IsUnique() + .HasFilter("[ResourceName] IS NOT NULL"); b.ToTable("AbpPermissions", (string)null); }); @@ -1293,6 +1351,50 @@ namespace OpenIddict.Demo.Server.Migrations b.ToTable("AbpPermissionGroups", (string)null); }); + modelBuilder.Entity("Volo.Abp.PermissionManagement.ResourcePermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ResourceKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ResourceName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey") + .IsUnique() + .HasFilter("[TenantId] IS NOT NULL"); + + b.ToTable("AbpResourcePermissionGrants", (string)null); + }); + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => { b.Property("Id") @@ -1506,6 +1608,62 @@ namespace OpenIddict.Demo.Server.Migrations .IsRequired(); }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Passkeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Volo.Abp.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject"); + + b1.Property("ClientDataJson"); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey"); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AbpUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("nvarchar(max)"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("PasswordHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => { b.HasOne("Volo.Abp.Identity.IdentityRole", null) @@ -1592,6 +1750,10 @@ namespace OpenIddict.Demo.Server.Migrations b.Navigation("OrganizationUnits"); + b.Navigation("Passkeys"); + + b.Navigation("PasswordHistories"); + b.Navigation("Roles"); b.Navigation("Tokens"); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.cs b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20260311061448_Initial.cs similarity index 91% rename from modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.cs rename to modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20260311061448_Initial.cs index a992bf78c2..2524447b0d 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20260311061448_Initial.cs @@ -162,8 +162,10 @@ namespace OpenIddict.Demo.Server.Migrations columns: table => new { Id = table.Column(type: "uniqueidentifier", nullable: false), - GroupName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + GroupName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + ResourceName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ManagementPermissionName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), ParentName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), DisplayName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), IsEnabled = table.Column(type: "bit", nullable: false), @@ -177,6 +179,23 @@ namespace OpenIddict.Demo.Server.Migrations table.PrimaryKey("PK_AbpPermissions", x => x.Id); }); + migrationBuilder.CreateTable( + name: "AbpResourcePermissionGrants", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + ProviderName = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + ProviderKey = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + ResourceName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + ResourceKey = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpResourcePermissionGrants", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AbpRoles", columns: table => new @@ -230,7 +249,7 @@ namespace OpenIddict.Demo.Server.Migrations Id = table.Column(type: "uniqueidentifier", nullable: false), SessionId = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), Device = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), - DeviceInfo = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), + DeviceInfo = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), TenantId = table.Column(type: "uniqueidentifier", nullable: true), UserId = table.Column(type: "uniqueidentifier", nullable: false), ClientId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), @@ -344,6 +363,8 @@ namespace OpenIddict.Demo.Server.Migrations ShouldChangePasswordOnNextLogin = table.Column(type: "bit", nullable: false), EntityVersion = table.Column(type: "int", nullable: false), LastPasswordChangeTime = table.Column(type: "datetimeoffset", nullable: true), + LastSignInTime = table.Column(type: "datetimeoffset", nullable: true), + Leaved = table.Column(type: "bit", nullable: false, defaultValue: false), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false), CreationTime = table.Column(type: "datetime2", nullable: false), @@ -559,6 +580,46 @@ namespace OpenIddict.Demo.Server.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "AbpUserPasskeys", + columns: table => new + { + CredentialId = table.Column(type: "varbinary(1024)", maxLength: 1024, nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + UserId = table.Column(type: "uniqueidentifier", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpUserPasskeys", x => x.CredentialId); + table.ForeignKey( + name: "FK_AbpUserPasskeys_AbpUsers_UserId", + column: x => x.UserId, + principalTable: "AbpUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AbpUserPasswordHistories", + columns: table => new + { + UserId = table.Column(type: "uniqueidentifier", nullable: false), + Password = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpUserPasswordHistories", x => new { x.UserId, x.Password }); + table.ForeignKey( + name: "FK_AbpUserPasswordHistories_AbpUsers_UserId", + column: x => x.UserId, + principalTable: "AbpUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "AbpUserRoles", columns: table => new @@ -729,10 +790,18 @@ namespace OpenIddict.Demo.Server.Migrations column: "GroupName"); migrationBuilder.CreateIndex( - name: "IX_AbpPermissions_Name", + name: "IX_AbpPermissions_ResourceName_Name", table: "AbpPermissions", - column: "Name", - unique: true); + columns: new[] { "ResourceName", "Name" }, + unique: true, + filter: "[ResourceName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AbpResourcePermissionGrants_TenantId_Name_ResourceName_ResourceKey_ProviderName_ProviderKey", + table: "AbpResourcePermissionGrants", + columns: new[] { "TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey" }, + unique: true, + filter: "[TenantId] IS NOT NULL"); migrationBuilder.CreateIndex( name: "IX_AbpRoleClaims_RoleId", @@ -817,6 +886,11 @@ namespace OpenIddict.Demo.Server.Migrations table: "AbpUserOrganizationUnits", columns: new[] { "UserId", "OrganizationUnitId" }); + migrationBuilder.CreateIndex( + name: "IX_AbpUserPasskeys_UserId", + table: "AbpUserPasskeys", + column: "UserId"); + migrationBuilder.CreateIndex( name: "IX_AbpUserRoles_RoleId_UserId", table: "AbpUserRoles", @@ -903,6 +977,9 @@ namespace OpenIddict.Demo.Server.Migrations migrationBuilder.DropTable( name: "AbpPermissions"); + migrationBuilder.DropTable( + name: "AbpResourcePermissionGrants"); + migrationBuilder.DropTable( name: "AbpRoleClaims"); @@ -933,6 +1010,12 @@ namespace OpenIddict.Demo.Server.Migrations migrationBuilder.DropTable( name: "AbpUserOrganizationUnits"); + migrationBuilder.DropTable( + name: "AbpUserPasskeys"); + + migrationBuilder.DropTable( + name: "AbpUserPasswordHistories"); + migrationBuilder.DropTable( name: "AbpUserRoles"); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs index b1caafb242..b746bafee4 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ namespace OpenIddict.Demo.Server.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("ProductVersion", "10.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -405,8 +405,8 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnType("nvarchar(64)"); b.Property("DeviceInfo") - .HasMaxLength(64) - .HasColumnType("nvarchar(64)"); + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); b.Property("ExtraProperties") .HasColumnType("nvarchar(max)") @@ -527,6 +527,15 @@ namespace OpenIddict.Demo.Server.Migrations b.Property("LastPasswordChangeTime") .HasColumnType("datetimeoffset"); + b.Property("LastSignInTime") + .HasColumnType("datetimeoffset"); + + b.Property("Leaved") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("Leaved"); + b.Property("LockoutEnabled") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -723,6 +732,47 @@ namespace OpenIddict.Demo.Server.Migrations b.ToTable("AbpUserOrganizationUnits", (string)null); }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("varbinary(1024)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserPasskeys", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Password") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("UserId", "Password"); + + b.ToTable("AbpUserPasswordHistories", (string)null); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => { b.Property("UserId") @@ -1191,13 +1241,16 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnName("ExtraProperties"); b.Property("GroupName") - .IsRequired() .HasMaxLength(128) .HasColumnType("nvarchar(128)"); b.Property("IsEnabled") .HasColumnType("bit"); + b.Property("ManagementPermissionName") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + b.Property("MultiTenancySide") .HasColumnType("tinyint"); @@ -1214,6 +1267,10 @@ namespace OpenIddict.Demo.Server.Migrations .HasMaxLength(128) .HasColumnType("nvarchar(128)"); + b.Property("ResourceName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + b.Property("StateCheckers") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -1222,8 +1279,9 @@ namespace OpenIddict.Demo.Server.Migrations b.HasIndex("GroupName"); - b.HasIndex("Name") - .IsUnique(); + b.HasIndex("ResourceName", "Name") + .IsUnique() + .HasFilter("[ResourceName] IS NOT NULL"); b.ToTable("AbpPermissions", (string)null); }); @@ -1290,6 +1348,50 @@ namespace OpenIddict.Demo.Server.Migrations b.ToTable("AbpPermissionGroups", (string)null); }); + modelBuilder.Entity("Volo.Abp.PermissionManagement.ResourcePermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ResourceKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ResourceName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey") + .IsUnique() + .HasFilter("[TenantId] IS NOT NULL"); + + b.ToTable("AbpResourcePermissionGrants", (string)null); + }); + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => { b.Property("Id") @@ -1503,6 +1605,62 @@ namespace OpenIddict.Demo.Server.Migrations .IsRequired(); }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("Passkeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Volo.Abp.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject"); + + b1.Property("ClientDataJson"); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey"); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AbpUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("nvarchar(max)"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser", null) + .WithMany("PasswordHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => { b.HasOne("Volo.Abp.Identity.IdentityRole", null) @@ -1589,6 +1747,10 @@ namespace OpenIddict.Demo.Server.Migrations b.Navigation("OrganizationUnits"); + b.Navigation("Passkeys"); + + b.Navigation("PasswordHistories"); + b.Navigation("Roles"); b.Navigation("Tokens"); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj b/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj index c6c8742e4b..09a185919a 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj +++ b/modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj @@ -79,4 +79,13 @@ + + + + PreserveNewest + + + diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/openiddict.pfx b/modules/openiddict/app/OpenIddict.Demo.Server/openiddict.pfx index 8dc3bf1771..586e269982 100644 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/openiddict.pfx and b/modules/openiddict/app/OpenIddict.Demo.Server/openiddict.pfx differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/css/all.css b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/css/all.css deleted file mode 100644 index d9ade752d8..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/css/all.css +++ /dev/null @@ -1,4616 +0,0 @@ -/*! - * Font Awesome Free 5.15.4 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-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-bacteria:before { - content: "\e059"; } - -.fa-bacterium:before { - content: "\e05a"; } - -.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-box-tissue:before { - content: "\e05b"; } - -.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-cloudflare:before { - content: "\e07d"; } - -.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: "\e052"; } - -.fa-dashcube:before { - content: "\f210"; } - -.fa-database:before { - content: "\f1c0"; } - -.fa-deaf:before { - content: "\f2a4"; } - -.fa-deezer:before { - content: "\e077"; } - -.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-disease:before { - content: "\f7fa"; } - -.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-edge-legacy:before { - content: "\e078"; } - -.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-faucet:before { - content: "\e005"; } - -.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: "\e007"; } - -.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-pay:before { - content: "\e079"; } - -.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-guilded:before { - content: "\e07e"; } - -.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-medical:before { - content: "\e05c"; } - -.fa-hand-holding-usd:before { - content: "\f4c0"; } - -.fa-hand-holding-water:before { - content: "\f4c1"; } - -.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-sparkles:before { - content: "\e05d"; } - -.fa-hand-spock:before { - content: "\f259"; } - -.fa-hands:before { - content: "\f4c2"; } - -.fa-hands-helping:before { - content: "\f4c4"; } - -.fa-hands-wash:before { - content: "\e05e"; } - -.fa-handshake:before { - content: "\f2b5"; } - -.fa-handshake-alt-slash:before { - content: "\e05f"; } - -.fa-handshake-slash:before { - content: "\e060"; } - -.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-head-side-cough:before { - content: "\e061"; } - -.fa-head-side-cough-slash:before { - content: "\e062"; } - -.fa-head-side-mask:before { - content: "\e063"; } - -.fa-head-side-virus:before { - content: "\e064"; } - -.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-hive:before { - content: "\e07f"; } - -.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-hospital-user:before { - content: "\f80d"; } - -.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-house-user:before { - content: "\e065"; } - -.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: "\e013"; } - -.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-innosoft:before { - content: "\e080"; } - -.fa-instagram:before { - content: "\f16d"; } - -.fa-instagram-square:before { - content: "\e055"; } - -.fa-instalod:before { - content: "\e081"; } - -.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-house:before { - content: "\e066"; } - -.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-lungs:before { - content: "\f604"; } - -.fa-lungs-virus:before { - content: "\e067"; } - -.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: "\e01a"; } - -.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: "\e056"; } - -.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-octopus-deploy:before { - content: "\e082"; } - -.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-arrows:before { - content: "\e068"; } - -.fa-people-carry:before { - content: "\f4ce"; } - -.fa-pepper-hot:before { - content: "\f816"; } - -.fa-perbyte:before { - content: "\e083"; } - -.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: "\e01e"; } - -.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-plane-slash:before { - content: "\e069"; } - -.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-pump-medical:before { - content: "\e06a"; } - -.fa-pump-soap:before { - content: "\e06b"; } - -.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-rust:before { - content: "\e07a"; } - -.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-shield-virus:before { - content: "\e06c"; } - -.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: "\e057"; } - -.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-sink:before { - content: "\e06d"; } - -.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-soap:before { - content: "\e06e"; } - -.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-stopwatch-20:before { - content: "\e06f"; } - -.fa-store:before { - content: "\f54e"; } - -.fa-store-alt:before { - content: "\f54f"; } - -.fa-store-alt-slash:before { - content: "\e070"; } - -.fa-store-slash:before { - content: "\e071"; } - -.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-tiktok:before { - content: "\e07b"; } - -.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-toilet-paper-slash:before { - content: "\e072"; } - -.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: "\e041"; } - -.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-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-uncharted:before { - content: "\e084"; } - -.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: "\e049"; } - -.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-unsplash:before { - content: "\e07c"; } - -.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-users-slash:before { - content: "\e073"; } - -.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-vest:before { - content: "\e085"; } - -.fa-vest-patches:before { - content: "\e086"; } - -.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-virus:before { - content: "\e074"; } - -.fa-virus-slash:before { - content: "\e075"; } - -.fa-viruses:before { - content: "\e076"; } - -.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-watchman-monitoring:before { - content: "\e087"; } - -.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-wodu:before { - content: "\e088"; } - -.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: block; - 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: block; - 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: block; - 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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/css/v4-shims.css b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/css/v4-shims.css deleted file mode 100644 index 1ef4893b4a..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/css/v4-shims.css +++ /dev/null @@ -1,2172 +0,0 @@ -/*! - * Font Awesome Free 5.15.4 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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot deleted file mode 100644 index cba6c6cce8..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg deleted file mode 100644 index b9881a43b7..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg +++ /dev/null @@ -1,3717 +0,0 @@ - - - - -Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf deleted file mode 100644 index 8d75deddae..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff deleted file mode 100644 index 3375bef091..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 deleted file mode 100644 index 402f81c0bc..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2 and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot deleted file mode 100644 index a4e598936b..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg deleted file mode 100644 index 463af27c02..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg +++ /dev/null @@ -1,801 +0,0 @@ - - - - -Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf deleted file mode 100644 index 7157aafbac..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff deleted file mode 100644 index ad077c6bec..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 deleted file mode 100644 index 56328948b3..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2 and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot deleted file mode 100644 index e99417197e..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg deleted file mode 100644 index 00296e9598..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg +++ /dev/null @@ -1,5034 +0,0 @@ - - - - -Created by FontForge 20201107 at Wed Aug 4 12:25:29 2021 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf deleted file mode 100644 index 25abf389e2..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff deleted file mode 100644 index 23ee663443..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 deleted file mode 100644 index 2217164f0c..0000000000 Binary files a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2 and /dev/null differ diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/core/abp.css b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/core/abp.css deleted file mode 100644 index ee3c5080a5..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/core/abp.css +++ /dev/null @@ -1,56 +0,0 @@ -@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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/core/abp.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/core/abp.js deleted file mode 100644 index ad1d238448..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/core/abp.js +++ /dev/null @@ -1,823 +0,0 @@ -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.internal = abp.localization.internal || {}; - abp.localization.values = abp.localization.values || {}; - abp.localization.resources = abp.localization.resources || {}; - - abp.localization.internal.getResource = function (resourceName) { - var resource = abp.localization.resources[resourceName]; - if (resource) { - return resource; - } - - var legacySource = abp.localization.values[resourceName]; - if (legacySource) { - return { - texts: abp.localization.values[resourceName], - baseResources: [] - }; - } - - abp.log.warn('Could not find localization source: ' + resourceName); - return null; - }; - - abp.localization.internal.localize = function (key, sourceName) { - var resource = abp.localization.internal.getResource(sourceName); - if (!resource){ - return { - value: key, - found: false - }; - } - - var value = resource.texts[key]; - if (value === undefined) { - for (var i = 0; i < resource.baseResources.length; i++){ - var basedArguments = Array.prototype.slice.call(arguments, 0); - basedArguments[1] = resource.baseResources[i]; - - var result = abp.localization.internal.localize.apply(this, basedArguments); - if (result.found){ - return result; - } - } - - return { - value: key, - found: false - }; - } - - var copiedArguments = Array.prototype.slice.call(arguments, 0); - copiedArguments.splice(1, 1); - copiedArguments[0] = value; - - return { - value: abp.utils.formatString.apply(this, copiedArguments), - found: true - }; - }; - - abp.localization.localize = function (key, sourceName) { - if (sourceName === '_') { //A convention to suppress the localization - return key; - } - - if (sourceName) { - return abp.localization.internal.localize.apply(this, arguments).value; - } - - if (!abp.localization.defaultResourceName) { - abp.log.warn('Localization source name is not specified and the defaultResourceName was not defined!'); - return key; - } - - var copiedArguments = Array.prototype.slice.call(arguments, 0); - copiedArguments.splice(1, 1, abp.localization.defaultResourceName); - - return abp.localization.internal.localize.apply(this, copiedArguments).value; - }; - - abp.localization.isLocalized = function (key, sourceName) { - if (sourceName === '_') { //A convention to suppress the localization - return true; - } - - sourceName = sourceName || abp.localization.defaultResourceName; - if (!sourceName) { - return false; - } - - return abp.localization.internal.localize(key, sourceName).found; - }; - - 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; - abp.localization.currentCulture = { - cultureName: undefined - }; - - var getMapValue = function (packageMaps, packageName, language) { - language = language || abp.localization.currentCulture.name; - if (!packageMaps || !packageName || !language) { - return language; - } - - var packageMap = packageMaps[packageName]; - if (!packageMap) { - return language; - } - - for (var i = 0; i < packageMap.length; i++) { - var map = packageMap[i]; - if (map.name === language){ - return map.value; - } - } - - return language; - }; - - abp.localization.getLanguagesMap = function (packageName, language) { - return getMapValue(abp.localization.languagesMap, packageName, language); - }; - - abp.localization.getLanguageFilesMap = function (packageName, language) { - return getMapValue(abp.localization.languageFilesMap, packageName, language); - }; - - /* AUTHORIZATION **********************************************/ - - abp.auth = abp.auth || {}; - - abp.auth.grantedPolicies = abp.auth.grantedPolicies || {}; - - abp.auth.isGranted = function (policyName) { - return 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'); - if (element.parentElement) { - 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; - } - - /** - * Escape HTML to help prevent XSS attacks. - */ - abp.utils.htmlEscape = function (html) { - return typeof html === 'string' ? html.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') : html; - } - - /* SECURITY ***************************************/ - abp.security = abp.security || {}; - abp.security.antiForgery = abp.security.antiForgery || {}; - - abp.security.antiForgery.tokenCookieName = 'XSRF-TOKEN'; - abp.security.antiForgery.tokenHeaderName = 'RequestVerificationToken'; - - abp.security.antiForgery.getToken = function () { - return abp.utils.getCookieValue(abp.security.antiForgery.tokenCookieName); - }; - - /* CLOCK *****************************************/ - abp.clock = abp.clock || {}; - - abp.clock.kind = 'Unspecified'; - - abp.clock.supportsMultipleTimezone = function () { - return abp.clock.kind === 'Utc'; - }; - - var toLocal = function (date) { - return new Date( - date.getFullYear(), - date.getMonth(), - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds(), - date.getMilliseconds() - ); - }; - - var toUtc = function (date) { - return Date.UTC( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds(), - date.getUTCMilliseconds() - ); - }; - - abp.clock.now = function () { - if (abp.clock.kind === 'Utc') { - return toUtc(new Date()); - } - return new Date(); - }; - - abp.clock.normalize = function (date) { - var kind = abp.clock.kind; - - if (kind === 'Unspecified') { - return date; - } - - if (kind === 'Local') { - return toLocal(date); - } - - if (kind === 'Utc') { - return toUtc(date); - } - }; - - /* FEATURES *************************************************/ - - abp.features = abp.features || {}; - - abp.features.values = abp.features.values || {}; - - abp.features.isEnabled = function(name){ - var value = abp.features.get(name); - return value == 'true' || value == 'True'; - } - - abp.features.get = function (name) { - return abp.features.values[name]; - }; - - /* GLOBAL FEATURES *************************************************/ - - abp.globalFeatures = abp.globalFeatures || {}; - - abp.globalFeatures.enabledFeatures = abp.globalFeatures.enabledFeatures || []; - - abp.globalFeatures.isEnabled = function(name){ - return abp.globalFeatures.enabledFeatures.indexOf(name) != -1; - } - -})(); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/jquery/abp.jquery.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/jquery/abp.jquery.js deleted file mode 100644 index 9137fcc989..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/jquery/abp.jquery.js +++ /dev/null @@ -1,411 +0,0 @@ -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]) - } - ); - } - } - } - } - }; - - $(function(){ - 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; - - var xhr = null; - var promise = $.Deferred(function ($dfd) { - xhr = $.ajax(options) - .done(function (data, textStatus, jqXHR) { - $dfd.resolve(data); - userOptions.success && userOptions.success(data); - }).fail(function (jqXHR) { - if(jqXHR.statusText === 'abort') { - //ajax request is abort, ignore error handle. - return; - } - if (jqXHR.getResponseHeader('_AbpErrorFormat') === 'true') { - abp.ajax.handleAbpErrorResponse(jqXHR, userOptions, $dfd); - } else { - abp.ajax.handleNonAbpErrorResponse(jqXHR, userOptions, $dfd); - } - }); - }).promise(); - - promise['jqXHR'] = xhr; - - return promise; - }; - - $.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; - - var responseJSON = jqXHR.responseJSON ? jqXHR.responseJSON : JSON.parse(jqXHR.responseText); - - if (userOptions.abpHandleError !== false) { - messagePromise = abp.ajax.showError(responseJSON.error); - } - - abp.ajax.logError(responseJSON.error); - - $dfd && $dfd.reject(responseJSON.error, jqXHR); - userOptions.error && userOptions.error(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) { - var nonce = document.body.nonce || document.body.getAttribute('nonce'); - _loadFromUrl(url, loadCallback, failCallback, function (urlInfo) { - $.get({ - url: url, - dataType: 'text' - }) - .done(function (script) { - if(nonce){ - $.globalEval(script, { nonce: nonce}); - }else{ - $.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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/luxon/abp.luxon.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/luxon/abp.luxon.js deleted file mode 100644 index fcd9b44246..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/luxon/abp.luxon.js +++ /dev/null @@ -1,46 +0,0 @@ -var abp = abp || {}; -(function () { - - if (!luxon) { - throw "abp/luxon library requires the luxon library included to the page!"; - } - - /* TIMING *************************************************/ - - abp.timing = abp.timing || {}; - - var setObjectValue = function (obj, property, value) { - if (typeof property === "string") { - property = property.split('.'); - } - - if (property.length > 1) { - var p = property.shift(); - setObjectValue(obj[p], property, value); - } else { - obj[property[0]] = value; - } - } - - var getObjectValue = function (obj, property) { - return property.split('.').reduce((a, v) => a[v], obj) - } - - abp.timing.convertFieldsToIsoDate = function (form, fields) { - for (var field of fields) { - var dateTime = luxon.DateTime - .fromFormat( - getObjectValue(form, field), - abp.localization.currentCulture.dateTimeFormat.shortDatePattern, - {locale: abp.localization.currentCulture.cultureName} - ); - - if (!dateTime.invalid) { - setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) - } - } - - return form; - } - -})(jQuery); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.js deleted file mode 100644 index 359d2f7247..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.js +++ /dev/null @@ -1,694 +0,0 @@ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('just-compare')) : - typeof define === 'function' && define.amd ? define('@abp/utils', ['exports', 'just-compare'], factory) : - (global = global || self, factory((global.abp = global.abp || {}, global.abp.utils = global.abp.utils || {}, global.abp.utils.common = {}), global.compare)); -}(this, (function (exports, compare) { 'use strict'; - - compare = compare && Object.prototype.hasOwnProperty.call(compare, 'default') ? compare['default'] : compare; - - /*! ***************************************************************************** - Copyright (c) Microsoft Corporation. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */ - /* global Reflect, Promise */ - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) - if (b.hasOwnProperty(p)) - d[p] = b[p]; }; - return extendStatics(d, b); - }; - function __extends(d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - } - var __assign = function () { - __assign = Object.assign || function __assign(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) - if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); - }; - function __rest(s, e) { - var t = {}; - for (var p in s) - if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; - } - function __decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") - r = Reflect.decorate(decorators, target, key, desc); - else - for (var i = decorators.length - 1; i >= 0; i--) - if (d = decorators[i]) - r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; - } - function __param(paramIndex, decorator) { - return function (target, key) { decorator(target, key, paramIndex); }; - } - function __metadata(metadataKey, metadataValue) { - if (typeof Reflect === "object" && typeof Reflect.metadata === "function") - return Reflect.metadata(metadataKey, metadataValue); - } - function __awaiter(thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { - step(generator.next(value)); - } - catch (e) { - reject(e); - } } - function rejected(value) { try { - step(generator["throw"](value)); - } - catch (e) { - reject(e); - } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); - } - function __generator(thisArg, body) { - var _ = { label: 0, sent: function () { if (t[0] & 1) - throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function () { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) - throw new TypeError("Generator is already executing."); - while (_) - try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) - return t; - if (y = 0, t) - op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: - case 1: - t = op; - break; - case 4: - _.label++; - return { value: op[1], done: false }; - case 5: - _.label++; - y = op[1]; - op = [0]; - continue; - case 7: - op = _.ops.pop(); - _.trys.pop(); - continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { - _ = 0; - continue; - } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { - _.label = op[1]; - break; - } - if (op[0] === 6 && _.label < t[1]) { - _.label = t[1]; - t = op; - break; - } - if (t && _.label < t[2]) { - _.label = t[2]; - _.ops.push(op); - break; - } - if (t[2]) - _.ops.pop(); - _.trys.pop(); - continue; - } - op = body.call(thisArg, _); - } - catch (e) { - op = [6, e]; - y = 0; - } - finally { - f = t = 0; - } - if (op[0] & 5) - throw op[1]; - return { value: op[0] ? op[1] : void 0, done: true }; - } - } - var __createBinding = Object.create ? (function (o, m, k, k2) { - if (k2 === undefined) - k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function () { return m[k]; } }); - }) : (function (o, m, k, k2) { - if (k2 === undefined) - k2 = k; - o[k2] = m[k]; - }); - function __exportStar(m, exports) { - for (var p in m) - if (p !== "default" && !exports.hasOwnProperty(p)) - __createBinding(exports, m, p); - } - function __values(o) { - var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; - if (m) - return m.call(o); - if (o && typeof o.length === "number") - return { - next: function () { - if (o && i >= o.length) - o = void 0; - return { value: o && o[i++], done: !o }; - } - }; - throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); - } - function __read(o, n) { - var m = typeof Symbol === "function" && o[Symbol.iterator]; - if (!m) - return o; - var i = m.call(o), r, ar = [], e; - try { - while ((n === void 0 || n-- > 0) && !(r = i.next()).done) - ar.push(r.value); - } - catch (error) { - e = { error: error }; - } - finally { - try { - if (r && !r.done && (m = i["return"])) - m.call(i); - } - finally { - if (e) - throw e.error; - } - } - return ar; - } - function __spread() { - for (var ar = [], i = 0; i < arguments.length; i++) - ar = ar.concat(__read(arguments[i])); - return ar; - } - function __spreadArrays() { - for (var s = 0, i = 0, il = arguments.length; i < il; i++) - s += arguments[i].length; - for (var r = Array(s), k = 0, i = 0; i < il; i++) - for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) - r[k] = a[j]; - return r; - } - ; - function __await(v) { - return this instanceof __await ? (this.v = v, this) : new __await(v); - } - function __asyncGenerator(thisArg, _arguments, generator) { - if (!Symbol.asyncIterator) - throw new TypeError("Symbol.asyncIterator is not defined."); - var g = generator.apply(thisArg, _arguments || []), i, q = []; - return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; - function verb(n) { if (g[n]) - i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } - function resume(n, v) { try { - step(g[n](v)); - } - catch (e) { - settle(q[0][3], e); - } } - function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } - function fulfill(value) { resume("next", value); } - function reject(value) { resume("throw", value); } - function settle(f, v) { if (f(v), q.shift(), q.length) - resume(q[0][0], q[0][1]); } - } - function __asyncDelegator(o) { - var i, p; - return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; - function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; } - } - function __asyncValues(o) { - if (!Symbol.asyncIterator) - throw new TypeError("Symbol.asyncIterator is not defined."); - var m = o[Symbol.asyncIterator], i; - return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); - function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } - function settle(resolve, reject, d, v) { Promise.resolve(v).then(function (v) { resolve({ value: v, done: d }); }, reject); } - } - function __makeTemplateObject(cooked, raw) { - if (Object.defineProperty) { - Object.defineProperty(cooked, "raw", { value: raw }); - } - else { - cooked.raw = raw; - } - return cooked; - } - ; - var __setModuleDefault = Object.create ? (function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - }) : function (o, v) { - o["default"] = v; - }; - function __importStar(mod) { - if (mod && mod.__esModule) - return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (Object.hasOwnProperty.call(mod, k)) - __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - } - function __importDefault(mod) { - return (mod && mod.__esModule) ? mod : { default: mod }; - } - function __classPrivateFieldGet(receiver, privateMap) { - if (!privateMap.has(receiver)) { - throw new TypeError("attempted to get private field on non-instance"); - } - return privateMap.get(receiver); - } - function __classPrivateFieldSet(receiver, privateMap, value) { - if (!privateMap.has(receiver)) { - throw new TypeError("attempted to set private field on non-instance"); - } - privateMap.set(receiver, value); - return value; - } - - var ListNode = /** @class */ (function () { - function ListNode(value) { - this.value = value; - } - return ListNode; - }()); - var LinkedList = /** @class */ (function () { - function LinkedList() { - this.size = 0; - } - Object.defineProperty(LinkedList.prototype, "head", { - get: function () { - return this.first; - }, - enumerable: false, - configurable: true - }); - Object.defineProperty(LinkedList.prototype, "tail", { - get: function () { - return this.last; - }, - enumerable: false, - configurable: true - }); - Object.defineProperty(LinkedList.prototype, "length", { - get: function () { - return this.size; - }, - enumerable: false, - configurable: true - }); - LinkedList.prototype.attach = function (value, previousNode, nextNode) { - if (!previousNode) - return this.addHead(value); - if (!nextNode) - return this.addTail(value); - var node = new ListNode(value); - node.previous = previousNode; - previousNode.next = node; - node.next = nextNode; - nextNode.previous = node; - this.size++; - return node; - }; - LinkedList.prototype.attachMany = function (values, previousNode, nextNode) { - if (!values.length) - return []; - if (!previousNode) - return this.addManyHead(values); - if (!nextNode) - return this.addManyTail(values); - var list = new LinkedList(); - list.addManyTail(values); - list.first.previous = previousNode; - previousNode.next = list.first; - list.last.next = nextNode; - nextNode.previous = list.last; - this.size += values.length; - return list.toNodeArray(); - }; - LinkedList.prototype.detach = function (node) { - if (!node.previous) - return this.dropHead(); - if (!node.next) - return this.dropTail(); - node.previous.next = node.next; - node.next.previous = node.previous; - this.size--; - return node; - }; - LinkedList.prototype.add = function (value) { - var _this = this; - return { - after: function () { - var _a; - var params = []; - for (var _i = 0; _i < arguments.length; _i++) { - params[_i] = arguments[_i]; - } - return (_a = _this.addAfter).call.apply(_a, __spread([_this, value], params)); - }, - before: function () { - var _a; - var params = []; - for (var _i = 0; _i < arguments.length; _i++) { - params[_i] = arguments[_i]; - } - return (_a = _this.addBefore).call.apply(_a, __spread([_this, value], params)); - }, - byIndex: function (position) { return _this.addByIndex(value, position); }, - head: function () { return _this.addHead(value); }, - tail: function () { return _this.addTail(value); }, - }; - }; - LinkedList.prototype.addMany = function (values) { - var _this = this; - return { - after: function () { - var _a; - var params = []; - for (var _i = 0; _i < arguments.length; _i++) { - params[_i] = arguments[_i]; - } - return (_a = _this.addManyAfter).call.apply(_a, __spread([_this, values], params)); - }, - before: function () { - var _a; - var params = []; - for (var _i = 0; _i < arguments.length; _i++) { - params[_i] = arguments[_i]; - } - return (_a = _this.addManyBefore).call.apply(_a, __spread([_this, values], params)); - }, - byIndex: function (position) { return _this.addManyByIndex(values, position); }, - head: function () { return _this.addManyHead(values); }, - tail: function () { return _this.addManyTail(values); }, - }; - }; - LinkedList.prototype.addAfter = function (value, previousValue, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - var previous = this.find(function (node) { return compareFn(node.value, previousValue); }); - return previous ? this.attach(value, previous, previous.next) : this.addTail(value); - }; - LinkedList.prototype.addBefore = function (value, nextValue, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - var next = this.find(function (node) { return compareFn(node.value, nextValue); }); - return next ? this.attach(value, next.previous, next) : this.addHead(value); - }; - LinkedList.prototype.addByIndex = function (value, position) { - if (position < 0) - position += this.size; - else if (position >= this.size) - return this.addTail(value); - if (position <= 0) - return this.addHead(value); - var next = this.get(position); - return this.attach(value, next.previous, next); - }; - LinkedList.prototype.addHead = function (value) { - var node = new ListNode(value); - node.next = this.first; - if (this.first) - this.first.previous = node; - else - this.last = node; - this.first = node; - this.size++; - return node; - }; - LinkedList.prototype.addTail = function (value) { - var node = new ListNode(value); - if (this.first) { - node.previous = this.last; - this.last.next = node; - this.last = node; - } - else { - this.first = node; - this.last = node; - } - this.size++; - return node; - }; - LinkedList.prototype.addManyAfter = function (values, previousValue, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - var previous = this.find(function (node) { return compareFn(node.value, previousValue); }); - return previous ? this.attachMany(values, previous, previous.next) : this.addManyTail(values); - }; - LinkedList.prototype.addManyBefore = function (values, nextValue, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - var next = this.find(function (node) { return compareFn(node.value, nextValue); }); - return next ? this.attachMany(values, next.previous, next) : this.addManyHead(values); - }; - LinkedList.prototype.addManyByIndex = function (values, position) { - if (position < 0) - position += this.size; - if (position <= 0) - return this.addManyHead(values); - if (position >= this.size) - return this.addManyTail(values); - var next = this.get(position); - return this.attachMany(values, next.previous, next); - }; - LinkedList.prototype.addManyHead = function (values) { - var _this = this; - return values.reduceRight(function (nodes, value) { - nodes.unshift(_this.addHead(value)); - return nodes; - }, []); - }; - LinkedList.prototype.addManyTail = function (values) { - var _this = this; - return values.map(function (value) { return _this.addTail(value); }); - }; - LinkedList.prototype.drop = function () { - var _this = this; - return { - byIndex: function (position) { return _this.dropByIndex(position); }, - byValue: function () { - var params = []; - for (var _i = 0; _i < arguments.length; _i++) { - params[_i] = arguments[_i]; - } - return _this.dropByValue.apply(_this, params); - }, - byValueAll: function () { - var params = []; - for (var _i = 0; _i < arguments.length; _i++) { - params[_i] = arguments[_i]; - } - return _this.dropByValueAll.apply(_this, params); - }, - head: function () { return _this.dropHead(); }, - tail: function () { return _this.dropTail(); }, - }; - }; - LinkedList.prototype.dropMany = function (count) { - var _this = this; - return { - byIndex: function (position) { return _this.dropManyByIndex(count, position); }, - head: function () { return _this.dropManyHead(count); }, - tail: function () { return _this.dropManyTail(count); }, - }; - }; - LinkedList.prototype.dropByIndex = function (position) { - if (position < 0) - position += this.size; - var current = this.get(position); - return current ? this.detach(current) : undefined; - }; - LinkedList.prototype.dropByValue = function (value, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - var position = this.findIndex(function (node) { return compareFn(node.value, value); }); - return position < 0 ? undefined : this.dropByIndex(position); - }; - LinkedList.prototype.dropByValueAll = function (value, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - var dropped = []; - for (var current = this.first, position = 0; current; position++, current = current.next) { - if (compareFn(current.value, value)) { - dropped.push(this.dropByIndex(position - dropped.length)); - } - } - return dropped; - }; - LinkedList.prototype.dropHead = function () { - var head = this.first; - if (head) { - this.first = head.next; - if (this.first) - this.first.previous = undefined; - else - this.last = undefined; - this.size--; - return head; - } - return undefined; - }; - LinkedList.prototype.dropTail = function () { - var tail = this.last; - if (tail) { - this.last = tail.previous; - if (this.last) - this.last.next = undefined; - else - this.first = undefined; - this.size--; - return tail; - } - return undefined; - }; - LinkedList.prototype.dropManyByIndex = function (count, position) { - if (count <= 0) - return []; - if (position < 0) - position = Math.max(position + this.size, 0); - else if (position >= this.size) - return []; - count = Math.min(count, this.size - position); - var dropped = []; - while (count--) { - var current = this.get(position); - dropped.push(this.detach(current)); - } - return dropped; - }; - LinkedList.prototype.dropManyHead = function (count) { - if (count <= 0) - return []; - count = Math.min(count, this.size); - var dropped = []; - while (count--) - dropped.unshift(this.dropHead()); - return dropped; - }; - LinkedList.prototype.dropManyTail = function (count) { - if (count <= 0) - return []; - count = Math.min(count, this.size); - var dropped = []; - while (count--) - dropped.push(this.dropTail()); - return dropped; - }; - LinkedList.prototype.find = function (predicate) { - for (var current = this.first, position = 0; current; position++, current = current.next) { - if (predicate(current, position, this)) - return current; - } - return undefined; - }; - LinkedList.prototype.findIndex = function (predicate) { - for (var current = this.first, position = 0; current; position++, current = current.next) { - if (predicate(current, position, this)) - return position; - } - return -1; - }; - LinkedList.prototype.forEach = function (iteratorFn) { - for (var node = this.first, position = 0; node; position++, node = node.next) { - iteratorFn(node, position, this); - } - }; - LinkedList.prototype.get = function (position) { - return this.find(function (_, index) { return position === index; }); - }; - LinkedList.prototype.indexOf = function (value, compareFn) { - if (compareFn === void 0) { compareFn = compare; } - return this.findIndex(function (node) { return compareFn(node.value, value); }); - }; - LinkedList.prototype.toArray = function () { - var array = new Array(this.size); - this.forEach(function (node, index) { return (array[index] = node.value); }); - return array; - }; - LinkedList.prototype.toNodeArray = function () { - var array = new Array(this.size); - this.forEach(function (node, index) { return (array[index] = node); }); - return array; - }; - LinkedList.prototype.toString = function (mapperFn) { - if (mapperFn === void 0) { mapperFn = JSON.stringify; } - return this.toArray() - .map(function (value) { return mapperFn(value); }) - .join(' <-> '); - }; - // Cannot use Generator type because of ng-packagr - LinkedList.prototype[Symbol.iterator] = function () { - var node, position; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - node = this.first, position = 0; - _a.label = 1; - case 1: - if (!node) return [3 /*break*/, 4]; - return [4 /*yield*/, node.value]; - case 2: - _a.sent(); - _a.label = 3; - case 3: - position++, node = node.next; - return [3 /*break*/, 1]; - case 4: return [2 /*return*/]; - } - }); - }; - return LinkedList; - }()); - - /* - * Public API Surface of utils - */ - - /** - * Generated bundle index. Do not edit. - */ - - exports.LinkedList = LinkedList; - exports.ListNode = ListNode; - - Object.defineProperty(exports, '__esModule', { value: true }); - -}))); -//# sourceMappingURL=abp-utils.umd.js.map diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.js.map b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.js.map deleted file mode 100644 index c8cf999f3b..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"abp-utils.umd.js","sources":["../../node_modules/tslib/tslib.es6.js","../../projects/utils/src/lib/linked-list.ts","../../projects/utils/src/public-api.ts","../../projects/utils/src/abp-utils.ts"],"sourcesContent":["/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, exports) {\r\n for (var p in m) if (p !== \"default\" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","/* tslint:disable:no-non-null-assertion */\r\n\r\nimport compare from 'just-compare';\r\n\r\nexport class ListNode {\r\n next: ListNode | undefined;\r\n previous: ListNode | undefined;\r\n constructor(public readonly value: T) {}\r\n}\r\n\r\nexport class LinkedList {\r\n private first: ListNode | undefined;\r\n private last: ListNode | undefined;\r\n private size = 0;\r\n\r\n get head(): ListNode | undefined {\r\n return this.first;\r\n }\r\n get tail(): ListNode | undefined {\r\n return this.last;\r\n }\r\n get length(): number {\r\n return this.size;\r\n }\r\n\r\n private attach(\r\n value: T,\r\n previousNode: ListNode | undefined,\r\n nextNode: ListNode | undefined,\r\n ): ListNode {\r\n if (!previousNode) return this.addHead(value);\r\n\r\n if (!nextNode) return this.addTail(value);\r\n\r\n const node = new ListNode(value);\r\n node.previous = previousNode;\r\n previousNode.next = node;\r\n node.next = nextNode;\r\n nextNode.previous = node;\r\n\r\n this.size++;\r\n\r\n return node;\r\n }\r\n\r\n private attachMany(\r\n values: T[],\r\n previousNode: ListNode | undefined,\r\n nextNode: ListNode | undefined,\r\n ): ListNode[] {\r\n if (!values.length) return [];\r\n\r\n if (!previousNode) return this.addManyHead(values);\r\n\r\n if (!nextNode) return this.addManyTail(values);\r\n\r\n const list = new LinkedList();\r\n list.addManyTail(values);\r\n list.first!.previous = previousNode;\r\n previousNode.next = list.first;\r\n list.last!.next = nextNode;\r\n nextNode.previous = list.last;\r\n\r\n this.size += values.length;\r\n\r\n return list.toNodeArray();\r\n }\r\n\r\n private detach(node: ListNode) {\r\n if (!node.previous) return this.dropHead();\r\n\r\n if (!node.next) return this.dropTail();\r\n\r\n node.previous.next = node.next;\r\n node.next.previous = node.previous;\r\n\r\n this.size--;\r\n\r\n return node;\r\n }\r\n\r\n add(value: T) {\r\n return {\r\n after: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addAfter.call(this, value, ...params),\r\n before: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addBefore.call(this, value, ...params),\r\n byIndex: (position: number) => this.addByIndex(value, position),\r\n head: () => this.addHead(value),\r\n tail: () => this.addTail(value),\r\n };\r\n }\r\n\r\n addMany(values: T[]) {\r\n return {\r\n after: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addManyAfter.call(this, values, ...params),\r\n before: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addManyBefore.call(this, values, ...params),\r\n byIndex: (position: number) => this.addManyByIndex(values, position),\r\n head: () => this.addManyHead(values),\r\n tail: () => this.addManyTail(values),\r\n };\r\n }\r\n\r\n addAfter(value: T, previousValue: T): ListNode;\r\n addAfter(value: T, previousValue: any, compareFn: ListComparisonFn): ListNode;\r\n addAfter(value: T, previousValue: any, compareFn: ListComparisonFn = compare): ListNode {\r\n const previous = this.find(node => compareFn(node.value, previousValue));\r\n\r\n return previous ? this.attach(value, previous, previous.next) : this.addTail(value);\r\n }\r\n\r\n addBefore(value: T, nextValue: T): ListNode;\r\n addBefore(value: T, nextValue: any, compareFn: ListComparisonFn): ListNode;\r\n addBefore(value: T, nextValue: any, compareFn: ListComparisonFn = compare): ListNode {\r\n const next = this.find(node => compareFn(node.value, nextValue));\r\n\r\n return next ? this.attach(value, next.previous, next) : this.addHead(value);\r\n }\r\n\r\n addByIndex(value: T, position: number): ListNode {\r\n if (position < 0) position += this.size;\r\n else if (position >= this.size) return this.addTail(value);\r\n\r\n if (position <= 0) return this.addHead(value);\r\n\r\n const next = this.get(position)!;\r\n\r\n return this.attach(value, next.previous, next);\r\n }\r\n\r\n addHead(value: T): ListNode {\r\n const node = new ListNode(value);\r\n\r\n node.next = this.first;\r\n\r\n if (this.first) this.first.previous = node;\r\n else this.last = node;\r\n\r\n this.first = node;\r\n this.size++;\r\n\r\n return node;\r\n }\r\n\r\n addTail(value: T): ListNode {\r\n const node = new ListNode(value);\r\n\r\n if (this.first) {\r\n node.previous = this.last;\r\n this.last!.next = node;\r\n this.last = node;\r\n } else {\r\n this.first = node;\r\n this.last = node;\r\n }\r\n\r\n this.size++;\r\n\r\n return node;\r\n }\r\n\r\n addManyAfter(values: T[], previousValue: T): ListNode[];\r\n addManyAfter(values: T[], previousValue: any, compareFn: ListComparisonFn): ListNode[];\r\n addManyAfter(\r\n values: T[],\r\n previousValue: any,\r\n compareFn: ListComparisonFn = compare,\r\n ): ListNode[] {\r\n const previous = this.find(node => compareFn(node.value, previousValue));\r\n\r\n return previous ? this.attachMany(values, previous, previous.next) : this.addManyTail(values);\r\n }\r\n\r\n addManyBefore(values: T[], nextValue: T): ListNode[];\r\n addManyBefore(values: T[], nextValue: any, compareFn: ListComparisonFn): ListNode[];\r\n addManyBefore(\r\n values: T[],\r\n nextValue: any,\r\n compareFn: ListComparisonFn = compare,\r\n ): ListNode[] {\r\n const next = this.find(node => compareFn(node.value, nextValue));\r\n\r\n return next ? this.attachMany(values, next.previous, next) : this.addManyHead(values);\r\n }\r\n\r\n addManyByIndex(values: T[], position: number): ListNode[] {\r\n if (position < 0) position += this.size;\r\n\r\n if (position <= 0) return this.addManyHead(values);\r\n\r\n if (position >= this.size) return this.addManyTail(values);\r\n\r\n const next = this.get(position)!;\r\n\r\n return this.attachMany(values, next.previous, next);\r\n }\r\n\r\n addManyHead(values: T[]): ListNode[] {\r\n return values.reduceRight[]>((nodes, value) => {\r\n nodes.unshift(this.addHead(value));\r\n return nodes;\r\n }, []);\r\n }\r\n\r\n addManyTail(values: T[]): ListNode[] {\r\n return values.map(value => this.addTail(value));\r\n }\r\n\r\n drop() {\r\n return {\r\n byIndex: (position: number) => this.dropByIndex(position),\r\n byValue: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.dropByValue.apply(this, params),\r\n byValueAll: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.dropByValueAll.apply(this, params),\r\n head: () => this.dropHead(),\r\n tail: () => this.dropTail(),\r\n };\r\n }\r\n\r\n dropMany(count: number) {\r\n return {\r\n byIndex: (position: number) => this.dropManyByIndex(count, position),\r\n head: () => this.dropManyHead(count),\r\n tail: () => this.dropManyTail(count),\r\n };\r\n }\r\n\r\n dropByIndex(position: number): ListNode | undefined {\r\n if (position < 0) position += this.size;\r\n\r\n const current = this.get(position);\r\n\r\n return current ? this.detach(current) : undefined;\r\n }\r\n\r\n dropByValue(value: T): ListNode | undefined;\r\n dropByValue(value: any, compareFn: ListComparisonFn): ListNode | undefined;\r\n dropByValue(value: any, compareFn: ListComparisonFn = compare): ListNode | undefined {\r\n const position = this.findIndex(node => compareFn(node.value, value));\r\n\r\n return position < 0 ? undefined : this.dropByIndex(position);\r\n }\r\n\r\n dropByValueAll(value: T): ListNode[];\r\n dropByValueAll(value: any, compareFn: ListComparisonFn): ListNode[];\r\n dropByValueAll(value: any, compareFn: ListComparisonFn = compare): ListNode[] {\r\n const dropped: ListNode[] = [];\r\n\r\n for (let current = this.first, position = 0; current; position++, current = current.next) {\r\n if (compareFn(current.value, value)) {\r\n dropped.push(this.dropByIndex(position - dropped.length)!);\r\n }\r\n }\r\n\r\n return dropped;\r\n }\r\n\r\n dropHead(): ListNode | undefined {\r\n const head = this.first;\r\n\r\n if (head) {\r\n this.first = head.next;\r\n\r\n if (this.first) this.first.previous = undefined;\r\n else this.last = undefined;\r\n\r\n this.size--;\r\n\r\n return head;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n dropTail(): ListNode | undefined {\r\n const tail = this.last;\r\n\r\n if (tail) {\r\n this.last = tail.previous;\r\n\r\n if (this.last) this.last.next = undefined;\r\n else this.first = undefined;\r\n\r\n this.size--;\r\n\r\n return tail;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n dropManyByIndex(count: number, position: number): ListNode[] {\r\n if (count <= 0) return [];\r\n\r\n if (position < 0) position = Math.max(position + this.size, 0);\r\n else if (position >= this.size) return [];\r\n\r\n count = Math.min(count, this.size - position);\r\n\r\n const dropped: ListNode[] = [];\r\n\r\n while (count--) {\r\n const current = this.get(position);\r\n dropped.push(this.detach(current!)!);\r\n }\r\n\r\n return dropped;\r\n }\r\n\r\n dropManyHead(count: Exclude): ListNode[] {\r\n if (count <= 0) return [];\r\n\r\n count = Math.min(count, this.size);\r\n\r\n const dropped: ListNode[] = [];\r\n\r\n while (count--) dropped.unshift(this.dropHead()!);\r\n\r\n return dropped;\r\n }\r\n\r\n dropManyTail(count: Exclude): ListNode[] {\r\n if (count <= 0) return [];\r\n\r\n count = Math.min(count, this.size);\r\n\r\n const dropped: ListNode[] = [];\r\n\r\n while (count--) dropped.push(this.dropTail()!);\r\n\r\n return dropped;\r\n }\r\n\r\n find(predicate: ListIteratorFn): ListNode | undefined {\r\n for (let current = this.first, position = 0; current; position++, current = current.next) {\r\n if (predicate(current, position, this)) return current;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n findIndex(predicate: ListIteratorFn): number {\r\n for (let current = this.first, position = 0; current; position++, current = current.next) {\r\n if (predicate(current, position, this)) return position;\r\n }\r\n\r\n return -1;\r\n }\r\n\r\n forEach(iteratorFn: ListIteratorFn) {\r\n for (let node = this.first, position = 0; node; position++, node = node.next) {\r\n iteratorFn(node, position, this);\r\n }\r\n }\r\n\r\n get(position: number): ListNode | undefined {\r\n return this.find((_, index) => position === index);\r\n }\r\n\r\n indexOf(value: T): number;\r\n indexOf(value: any, compareFn: ListComparisonFn): number;\r\n indexOf(value: any, compareFn: ListComparisonFn = compare): number {\r\n return this.findIndex(node => compareFn(node.value, value));\r\n }\r\n\r\n toArray(): T[] {\r\n const array = new Array(this.size);\r\n\r\n this.forEach((node, index) => (array[index!] = node.value));\r\n\r\n return array;\r\n }\r\n\r\n toNodeArray(): ListNode[] {\r\n const array = new Array(this.size);\r\n\r\n this.forEach((node, index) => (array[index!] = node));\r\n\r\n return array;\r\n }\r\n\r\n toString(mapperFn: ListMapperFn = JSON.stringify): string {\r\n return this.toArray()\r\n .map(value => mapperFn(value))\r\n .join(' <-> ');\r\n }\r\n\r\n // Cannot use Generator type because of ng-packagr\r\n *[Symbol.iterator](): any {\r\n for (let node = this.first, position = 0; node; position++, node = node.next) {\r\n yield node.value;\r\n }\r\n }\r\n}\r\n\r\nexport type ListMapperFn = (value: T) => any;\r\n\r\nexport type ListComparisonFn = (value1: T, value2: any) => boolean;\r\n\r\nexport type ListIteratorFn = (\r\n node: ListNode,\r\n index?: number,\r\n list?: LinkedList,\r\n) => R;\r\n","/*\r\n * Public API Surface of utils\r\n */\r\n\r\nexport * from './lib/linked-list';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.min.js deleted file mode 100644 index 57b22e5188..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("just-compare")):"function"==typeof define&&define.amd?define("@abp/utils",["exports","just-compare"],r):r(((t=t||self).abp=t.abp||{},t.abp.utils=t.abp.utils||{},t.abp.utils.common={}),t.compare)}(this,(function(t,r){"use strict";r=r&&Object.prototype.hasOwnProperty.call(r,"default")?r.default:r;function e(t,r){var e,n,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function u(o){return function(u){return function(o){if(e)throw new TypeError("Generator is already executing.");for(;a;)try{if(e=1,n&&(i=2&o[0]?n.return:o[0]?n.throw||((i=n.return)&&i.call(n),0):n.next)&&!(i=i.call(n,o[1])).done)return i;switch(n=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,n=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=a.trys,(i=i.length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]0)&&!(n=o.next()).done;)a.push(n.value)}catch(t){i={error:t}}finally{try{n&&!n.done&&(e=o.return)&&e.call(o)}finally{if(i)throw i.error}}return a}function i(){for(var t=[],r=0;r=this.size)return this.addTail(t);if(r<=0)return this.addHead(t);var e=this.get(r);return this.attach(t,e.previous,e)},t.prototype.addHead=function(t){var r=new o(t);return r.next=this.first,this.first?this.first.previous=r:this.last=r,this.first=r,this.size++,r},t.prototype.addTail=function(t){var r=new o(t);return this.first?(r.previous=this.last,this.last.next=r,this.last=r):(this.first=r,this.last=r),this.size++,r},t.prototype.addManyAfter=function(t,e,n){void 0===n&&(n=r);var i=this.find((function(t){return n(t.value,e)}));return i?this.attachMany(t,i,i.next):this.addManyTail(t)},t.prototype.addManyBefore=function(t,e,n){void 0===n&&(n=r);var i=this.find((function(t){return n(t.value,e)}));return i?this.attachMany(t,i.previous,i):this.addManyHead(t)},t.prototype.addManyByIndex=function(t,r){if(r<0&&(r+=this.size),r<=0)return this.addManyHead(t);if(r>=this.size)return this.addManyTail(t);var e=this.get(r);return this.attachMany(t,e.previous,e)},t.prototype.addManyHead=function(t){var r=this;return t.reduceRight((function(t,e){return t.unshift(r.addHead(e)),t}),[])},t.prototype.addManyTail=function(t){var r=this;return t.map((function(t){return r.addTail(t)}))},t.prototype.drop=function(){var t=this;return{byIndex:function(r){return t.dropByIndex(r)},byValue:function(){for(var r=[],e=0;e=this.size)return[];t=Math.min(t,this.size-r);for(var e=[];t--;){var n=this.get(r);e.push(this.detach(n))}return e},t.prototype.dropManyHead=function(t){if(t<=0)return[];t=Math.min(t,this.size);for(var r=[];t--;)r.unshift(this.dropHead());return r},t.prototype.dropManyTail=function(t){if(t<=0)return[];t=Math.min(t,this.size);for(var r=[];t--;)r.push(this.dropTail());return r},t.prototype.find=function(t){for(var r=this.first,e=0;r;e++,r=r.next)if(t(r,e,this))return r},t.prototype.findIndex=function(t){for(var r=this.first,e=0;r;e++,r=r.next)if(t(r,e,this))return e;return-1},t.prototype.forEach=function(t){for(var r=this.first,e=0;r;e++,r=r.next)t(r,e,this)},t.prototype.get=function(t){return this.find((function(r,e){return t===e}))},t.prototype.indexOf=function(t,e){return void 0===e&&(e=r),this.findIndex((function(r){return e(r.value,t)}))},t.prototype.toArray=function(){var t=new Array(this.size);return this.forEach((function(r,e){return t[e]=r.value})),t},t.prototype.toNodeArray=function(){var t=new Array(this.size);return this.forEach((function(r,e){return t[e]=r})),t},t.prototype.toString=function(t){return void 0===t&&(t=JSON.stringify),this.toArray().map((function(r){return t(r)})).join(" <-> ")},t.prototype[Symbol.iterator]=function(){var t;return e(this,(function(r){switch(r.label){case 0:t=this.first,0,r.label=1;case 1:return t?[4,t.value]:[3,4];case 2:r.sent(),r.label=3;case 3:return t=t.next,[3,1];case 4:return[2]}}))},t}();t.LinkedList=a,t.ListNode=o,Object.defineProperty(t,"__esModule",{value:!0})})); -//# sourceMappingURL=abp-utils.umd.min.js.map \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.min.js.map b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.min.js.map deleted file mode 100644 index 529ba4c447..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/abp/utils/abp-utils.umd.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../../node_modules/tslib/tslib.es6.js","../../projects/utils/src/lib/linked-list.ts"],"names":["__generator","thisArg","body","f","y","t","g","_","label","sent","trys","ops","next","verb","throw","return","Symbol","iterator","this","n","v","op","TypeError","call","done","value","pop","length","push","e","step","Object","create","__read","o","m","r","i","ar","error","__spread","arguments","concat","LinkedList","size","defineProperty","prototype","first","last","attach","previousNode","nextNode","addHead","addTail","node","ListNode","previous","attachMany","values","addManyHead","addManyTail","list","toNodeArray","detach","dropTail","dropHead","add","_this","after","params","_i","_a","addAfter","apply","before","addBefore","byIndex","position","addByIndex","head","tail","addMany","addManyAfter","addManyBefore","addManyByIndex","previousValue","compareFn","compare","find","nextValue","get","reduceRight","nodes","unshift","map","drop","dropByIndex","byValue","dropByValue","byValueAll","dropByValueAll","dropMany","count","dropManyByIndex","dropManyHead","dropManyTail","current","undefined","findIndex","dropped","Math","max","min","predicate","forEach","iteratorFn","index","indexOf","toArray","array","Array","toString","mapperFn","JSON","stringify","join"],"mappings":"wYA6EgBA,EAAYC,EAASC,GACjC,IAAsGC,EAAGC,EAAGC,EAAGC,EAA3GC,EAAI,CAAEC,MAAO,EAAGC,KAAM,WAAa,GAAW,EAAPJ,EAAE,GAAQ,MAAMA,EAAE,GAAI,OAAOA,EAAE,IAAOK,KAAM,GAAIC,IAAK,IAChG,OAAOL,EAAI,CAAEM,KAAMC,EAAK,GAAIC,MAASD,EAAK,GAAIE,OAAUF,EAAK,IAAwB,mBAAXG,SAA0BV,EAAEU,OAAOC,UAAY,WAAa,OAAOC,OAAUZ,EACvJ,SAASO,EAAKM,GAAK,OAAO,SAAUC,GAAK,OACzC,SAAcC,GACV,GAAIlB,EAAG,MAAM,IAAImB,UAAU,mCAC3B,KAAOf,GAAG,IACN,GAAIJ,EAAI,EAAGC,IAAMC,EAAY,EAARgB,EAAG,GAASjB,EAAU,OAAIiB,EAAG,GAAKjB,EAAS,SAAOC,EAAID,EAAU,SAAMC,EAAEkB,KAAKnB,GAAI,GAAKA,EAAEQ,SAAWP,EAAIA,EAAEkB,KAAKnB,EAAGiB,EAAG,KAAKG,KAAM,OAAOnB,EAE3J,OADID,EAAI,EAAGC,IAAGgB,EAAK,CAAS,EAARA,EAAG,GAAQhB,EAAEoB,QACzBJ,EAAG,IACP,KAAK,EAAG,KAAK,EAAGhB,EAAIgB,EAAI,MACxB,KAAK,EAAc,OAAXd,EAAEC,QAAgB,CAAEiB,MAAOJ,EAAG,GAAIG,MAAM,GAChD,KAAK,EAAGjB,EAAEC,QAASJ,EAAIiB,EAAG,GAAIA,EAAK,CAAC,GAAI,SACxC,KAAK,EAAGA,EAAKd,EAAEI,IAAIe,MAAOnB,EAAEG,KAAKgB,MAAO,SACxC,QACI,KAAMrB,EAAIE,EAAEG,MAAML,EAAIA,EAAEsB,OAAS,GAAKtB,EAAEA,EAAEsB,OAAS,KAAkB,IAAVN,EAAG,IAAsB,IAAVA,EAAG,IAAW,CAAEd,EAAI,EAAG,SACjG,GAAc,IAAVc,EAAG,MAAchB,GAAMgB,EAAG,GAAKhB,EAAE,IAAMgB,EAAG,GAAKhB,EAAE,IAAM,CAAEE,EAAEC,MAAQa,EAAG,GAAI,MAC9E,GAAc,IAAVA,EAAG,IAAYd,EAAEC,MAAQH,EAAE,GAAI,CAAEE,EAAEC,MAAQH,EAAE,GAAIA,EAAIgB,EAAI,MAC7D,GAAIhB,GAAKE,EAAEC,MAAQH,EAAE,GAAI,CAAEE,EAAEC,MAAQH,EAAE,GAAIE,EAAEI,IAAIiB,KAAKP,GAAK,MACvDhB,EAAE,IAAIE,EAAEI,IAAIe,MAChBnB,EAAEG,KAAKgB,MAAO,SAEtBL,EAAKnB,EAAKqB,KAAKtB,EAASM,GAC1B,MAAOsB,GAAKR,EAAK,CAAC,EAAGQ,GAAIzB,EAAI,UAAeD,EAAIE,EAAI,EACtD,GAAY,EAARgB,EAAG,GAAQ,MAAMA,EAAG,GAAI,MAAO,CAAEI,MAAOJ,EAAG,GAAKA,EAAG,QAAK,EAAQG,MAAM,GArB9BM,CAAK,CAACX,EAAGC,MAyBhCW,OAAOC,gBAwBpBC,EAAOC,EAAGf,GACtB,IAAIgB,EAAsB,mBAAXnB,QAAyBkB,EAAElB,OAAOC,UACjD,IAAKkB,EAAG,OAAOD,EACf,IAAmBE,EAAYP,EAA3BQ,EAAIF,EAAEZ,KAAKW,GAAOI,EAAK,GAC3B,IACI,WAAc,IAANnB,GAAgBA,KAAM,MAAQiB,EAAIC,EAAEzB,QAAQY,MAAMc,EAAGV,KAAKQ,EAAEX,OAExE,MAAOc,GAASV,EAAI,CAAEU,MAAOA,WAEzB,IACQH,IAAMA,EAAEZ,OAASW,EAAIE,EAAU,SAAIF,EAAEZ,KAAKc,WAExC,GAAIR,EAAG,MAAMA,EAAEU,OAE7B,OAAOD,WAGKE,IACZ,IAAK,IAAIF,EAAK,GAAID,EAAI,EAAGA,EAAII,UAAUd,OAAQU,IAC3CC,EAAKA,EAAGI,OAAOT,EAAOQ,UAAUJ,KACpC,OAAOC,EA8CcP,OAAOC,aC5L9B,SAA4BP,GAAAP,KAAAO,MAAAA,gBAG9B,SAAAkB,IAGUzB,KAAA0B,KAAO,SAEfb,OAAAc,eAAIF,EAAAG,UAAA,OAAI,KAAR,WACE,OAAO5B,KAAK6B,uCAEdhB,OAAAc,eAAIF,EAAAG,UAAA,OAAI,KAAR,WACE,OAAO5B,KAAK8B,sCAEdjB,OAAAc,eAAIF,EAAAG,UAAA,SAAM,KAAV,WACE,OAAO5B,KAAK0B,sCAGND,EAAAG,UAAAG,OAAA,SACNxB,EACAyB,EACAC,GAEA,IAAKD,EAAc,OAAOhC,KAAKkC,QAAQ3B,GAEvC,IAAK0B,EAAU,OAAOjC,KAAKmC,QAAQ5B,GAEnC,IAAM6B,EAAO,IAAIC,EAAS9B,GAQ1B,OAPA6B,EAAKE,SAAWN,EAChBA,EAAatC,KAAO0C,EACpBA,EAAK1C,KAAOuC,EACZA,EAASK,SAAWF,EAEpBpC,KAAK0B,OAEEU,GAGDX,EAAAG,UAAAW,WAAA,SACNC,EACAR,EACAC,GAEA,IAAKO,EAAO/B,OAAQ,MAAO,GAE3B,IAAKuB,EAAc,OAAOhC,KAAKyC,YAAYD,GAE3C,IAAKP,EAAU,OAAOjC,KAAK0C,YAAYF,GAEvC,IAAMG,EAAO,IAAIlB,EASjB,OARAkB,EAAKD,YAAYF,GACjBG,EAAKd,MAAOS,SAAWN,EACvBA,EAAatC,KAAOiD,EAAKd,MACzBc,EAAKb,KAAMpC,KAAOuC,EAClBA,EAASK,SAAWK,EAAKb,KAEzB9B,KAAK0B,MAAQc,EAAO/B,OAEbkC,EAAKC,eAGNnB,EAAAG,UAAAiB,OAAA,SAAOT,GACb,OAAKA,EAAKE,SAELF,EAAK1C,MAEV0C,EAAKE,SAAS5C,KAAO0C,EAAK1C,KAC1B0C,EAAK1C,KAAK4C,SAAWF,EAAKE,SAE1BtC,KAAK0B,OAEEU,GAPgBpC,KAAK8C,WAFD9C,KAAK+C,YAYlCtB,EAAAG,UAAAoB,IAAA,SAAIzC,GAAJ,IAAA0C,EAAAjD,KACE,MAAO,CACLkD,MAAO,qBAACC,EAAA,GAAAC,EAAA,EAAAA,EAAA7B,UAAAd,OAAA2C,IAAAD,EAAAC,GAAA7B,UAAA6B,GACN,OAAAC,EAAAJ,EAAKK,UAASjD,KAAIkD,MAAAF,EAAA/B,EAAA,CAAC2B,EAAM1C,GAAU4C,KACrCK,OAAQ,qBAACL,EAAA,GAAAC,EAAA,EAAAA,EAAA7B,UAAAd,OAAA2C,IAAAD,EAAAC,GAAA7B,UAAA6B,GACP,OAAAC,EAAAJ,EAAKQ,WAAUpD,KAAIkD,MAAAF,EAAA/B,EAAA,CAAC2B,EAAM1C,GAAU4C,KACtCO,QAAS,SAACC,GAAqB,OAAAV,EAAKW,WAAWrD,EAAOoD,IACtDE,KAAM,WAAM,OAAAZ,EAAKf,QAAQ3B,IACzBuD,KAAM,WAAM,OAAAb,EAAKd,QAAQ5B,MAI7BkB,EAAAG,UAAAmC,QAAA,SAAQvB,GAAR,IAAAS,EAAAjD,KACE,MAAO,CACLkD,MAAO,qBAACC,EAAA,GAAAC,EAAA,EAAAA,EAAA7B,UAAAd,OAAA2C,IAAAD,EAAAC,GAAA7B,UAAA6B,GACN,OAAAC,EAAAJ,EAAKe,cAAa3D,KAAIkD,MAAAF,EAAA/B,EAAA,CAAC2B,EAAMT,GAAWW,KAC1CK,OAAQ,qBAACL,EAAA,GAAAC,EAAA,EAAAA,EAAA7B,UAAAd,OAAA2C,IAAAD,EAAAC,GAAA7B,UAAA6B,GACP,OAAAC,EAAAJ,EAAKgB,eAAc5D,KAAIkD,MAAAF,EAAA/B,EAAA,CAAC2B,EAAMT,GAAWW,KAC3CO,QAAS,SAACC,GAAqB,OAAAV,EAAKiB,eAAe1B,EAAQmB,IAC3DE,KAAM,WAAM,OAAAZ,EAAKR,YAAYD,IAC7BsB,KAAM,WAAM,OAAAb,EAAKP,YAAYF,MAMjCf,EAAAG,UAAA0B,SAAA,SAAS/C,EAAU4D,EAAoBC,QAAA,IAAAA,IAAAA,EAAAC,GACrC,IAAM/B,EAAWtC,KAAKsE,MAAK,SAAAlC,GAAQ,OAAAgC,EAAUhC,EAAK7B,MAAO4D,MAEzD,OAAO7B,EAAWtC,KAAK+B,OAAOxB,EAAO+B,EAAUA,EAAS5C,MAAQM,KAAKmC,QAAQ5B,IAK/EkB,EAAAG,UAAA6B,UAAA,SAAUlD,EAAUgE,EAAgBH,QAAA,IAAAA,IAAAA,EAAAC,GAClC,IAAM3E,EAAOM,KAAKsE,MAAK,SAAAlC,GAAQ,OAAAgC,EAAUhC,EAAK7B,MAAOgE,MAErD,OAAO7E,EAAOM,KAAK+B,OAAOxB,EAAOb,EAAK4C,SAAU5C,GAAQM,KAAKkC,QAAQ3B,IAGvEkB,EAAAG,UAAAgC,WAAA,SAAWrD,EAAUoD,GACnB,GAAIA,EAAW,EAAGA,GAAY3D,KAAK0B,UAC9B,GAAIiC,GAAY3D,KAAK0B,KAAM,OAAO1B,KAAKmC,QAAQ5B,GAEpD,GAAIoD,GAAY,EAAG,OAAO3D,KAAKkC,QAAQ3B,GAEvC,IAAMb,EAAOM,KAAKwE,IAAIb,GAEtB,OAAO3D,KAAK+B,OAAOxB,EAAOb,EAAK4C,SAAU5C,IAG3C+B,EAAAG,UAAAM,QAAA,SAAQ3B,GACN,IAAM6B,EAAO,IAAIC,EAAS9B,GAU1B,OARA6B,EAAK1C,KAAOM,KAAK6B,MAEb7B,KAAK6B,MAAO7B,KAAK6B,MAAMS,SAAWF,EACjCpC,KAAK8B,KAAOM,EAEjBpC,KAAK6B,MAAQO,EACbpC,KAAK0B,OAEEU,GAGTX,EAAAG,UAAAO,QAAA,SAAQ5B,GACN,IAAM6B,EAAO,IAAIC,EAAS9B,GAa1B,OAXIP,KAAK6B,OACPO,EAAKE,SAAWtC,KAAK8B,KACrB9B,KAAK8B,KAAMpC,KAAO0C,EAClBpC,KAAK8B,KAAOM,IAEZpC,KAAK6B,MAAQO,EACbpC,KAAK8B,KAAOM,GAGdpC,KAAK0B,OAEEU,GAKTX,EAAAG,UAAAoC,aAAA,SACExB,EACA2B,EACAC,QAAA,IAAAA,IAAAA,EAAAC,GAEA,IAAM/B,EAAWtC,KAAKsE,MAAK,SAAAlC,GAAQ,OAAAgC,EAAUhC,EAAK7B,MAAO4D,MAEzD,OAAO7B,EAAWtC,KAAKuC,WAAWC,EAAQF,EAAUA,EAAS5C,MAAQM,KAAK0C,YAAYF,IAKxFf,EAAAG,UAAAqC,cAAA,SACEzB,EACA+B,EACAH,QAAA,IAAAA,IAAAA,EAAAC,GAEA,IAAM3E,EAAOM,KAAKsE,MAAK,SAAAlC,GAAQ,OAAAgC,EAAUhC,EAAK7B,MAAOgE,MAErD,OAAO7E,EAAOM,KAAKuC,WAAWC,EAAQ9C,EAAK4C,SAAU5C,GAAQM,KAAKyC,YAAYD,IAGhFf,EAAAG,UAAAsC,eAAA,SAAe1B,EAAamB,GAG1B,GAFIA,EAAW,IAAGA,GAAY3D,KAAK0B,MAE/BiC,GAAY,EAAG,OAAO3D,KAAKyC,YAAYD,GAE3C,GAAImB,GAAY3D,KAAK0B,KAAM,OAAO1B,KAAK0C,YAAYF,GAEnD,IAAM9C,EAAOM,KAAKwE,IAAIb,GAEtB,OAAO3D,KAAKuC,WAAWC,EAAQ9C,EAAK4C,SAAU5C,IAGhD+B,EAAAG,UAAAa,YAAA,SAAYD,GAAZ,IAAAS,EAAAjD,KACE,OAAOwC,EAAOiC,aAA2B,SAACC,EAAOnE,GAE/C,OADAmE,EAAMC,QAAQ1B,EAAKf,QAAQ3B,IACpBmE,IACN,KAGLjD,EAAAG,UAAAc,YAAA,SAAYF,GAAZ,IAAAS,EAAAjD,KACE,OAAOwC,EAAOoC,KAAI,SAAArE,GAAS,OAAA0C,EAAKd,QAAQ5B,OAG1CkB,EAAAG,UAAAiD,KAAA,WAAA,IAAA5B,EAAAjD,KACE,MAAO,CACL0D,QAAS,SAACC,GAAqB,OAAAV,EAAK6B,YAAYnB,IAChDoB,QAAS,eAAC,IAAA5B,EAAA,GAAAC,EAAA,EAAAA,EAAA7B,UAAAd,OAAA2C,IAAAD,EAAAC,GAAA7B,UAAA6B,GACR,OAAAH,EAAK+B,YAAYzB,MAAMN,EAAME,IAC/B8B,WAAY,eAAC,IAAA9B,EAAA,GAAAC,EAAA,EAAAA,EAAA7B,UAAAd,OAAA2C,IAAAD,EAAAC,GAAA7B,UAAA6B,GACX,OAAAH,EAAKiC,eAAe3B,MAAMN,EAAME,IAClCU,KAAM,WAAM,OAAAZ,EAAKF,YACjBe,KAAM,WAAM,OAAAb,EAAKH,cAIrBrB,EAAAG,UAAAuD,SAAA,SAASC,GAAT,IAAAnC,EAAAjD,KACE,MAAO,CACL0D,QAAS,SAACC,GAAqB,OAAAV,EAAKoC,gBAAgBD,EAAOzB,IAC3DE,KAAM,WAAM,OAAAZ,EAAKqC,aAAaF,IAC9BtB,KAAM,WAAM,OAAAb,EAAKsC,aAAaH,MAIlC3D,EAAAG,UAAAkD,YAAA,SAAYnB,GACNA,EAAW,IAAGA,GAAY3D,KAAK0B,MAEnC,IAAM8D,EAAUxF,KAAKwE,IAAIb,GAEzB,OAAO6B,EAAUxF,KAAK6C,OAAO2C,QAAWC,GAK1ChE,EAAAG,UAAAoD,YAAA,SAAYzE,EAAY6D,QAAA,IAAAA,IAAAA,EAAAC,GACtB,IAAMV,EAAW3D,KAAK0F,WAAU,SAAAtD,GAAQ,OAAAgC,EAAUhC,EAAK7B,MAAOA,MAE9D,OAAOoD,EAAW,OAAI8B,EAAYzF,KAAK8E,YAAYnB,IAKrDlC,EAAAG,UAAAsD,eAAA,SAAe3E,EAAY6D,QAAA,IAAAA,IAAAA,EAAAC,GAGzB,IAFA,IAAMsB,EAAyB,GAEtBH,EAAUxF,KAAK6B,MAAO8B,EAAW,EAAG6B,EAAS7B,IAAY6B,EAAUA,EAAQ9F,KAC9E0E,EAAUoB,EAAQjF,MAAOA,IAC3BoF,EAAQjF,KAAKV,KAAK8E,YAAYnB,EAAWgC,EAAQlF,SAIrD,OAAOkF,GAGTlE,EAAAG,UAAAmB,SAAA,WACE,IAAMc,EAAO7D,KAAK6B,MAElB,GAAIgC,EAQF,OAPA7D,KAAK6B,MAAQgC,EAAKnE,KAEdM,KAAK6B,MAAO7B,KAAK6B,MAAMS,cAAWmD,EACjCzF,KAAK8B,UAAO2D,EAEjBzF,KAAK0B,OAEEmC,GAMXpC,EAAAG,UAAAkB,SAAA,WACE,IAAMgB,EAAO9D,KAAK8B,KAElB,GAAIgC,EAQF,OAPA9D,KAAK8B,KAAOgC,EAAKxB,SAEbtC,KAAK8B,KAAM9B,KAAK8B,KAAKpC,UAAO+F,EAC3BzF,KAAK6B,WAAQ4D,EAElBzF,KAAK0B,OAEEoC,GAMXrC,EAAAG,UAAAyD,gBAAA,SAAgBD,EAAezB,GAC7B,GAAIyB,GAAS,EAAG,MAAO,GAEvB,GAAIzB,EAAW,EAAGA,EAAWiC,KAAKC,IAAIlC,EAAW3D,KAAK0B,KAAM,QACvD,GAAIiC,GAAY3D,KAAK0B,KAAM,MAAO,GAEvC0D,EAAQQ,KAAKE,IAAIV,EAAOpF,KAAK0B,KAAOiC,GAIpC,IAFA,IAAMgC,EAAyB,GAExBP,KAAS,CACd,IAAMI,EAAUxF,KAAKwE,IAAIb,GACzBgC,EAAQjF,KAAKV,KAAK6C,OAAO2C,IAG3B,OAAOG,GAGTlE,EAAAG,UAAA0D,aAAA,SAAaF,GACX,GAAIA,GAAS,EAAG,MAAO,GAEvBA,EAAQQ,KAAKE,IAAIV,EAAOpF,KAAK0B,MAI7B,IAFA,IAAMiE,EAAyB,GAExBP,KAASO,EAAQhB,QAAQ3E,KAAK+C,YAErC,OAAO4C,GAGTlE,EAAAG,UAAA2D,aAAA,SAAaH,GACX,GAAIA,GAAS,EAAG,MAAO,GAEvBA,EAAQQ,KAAKE,IAAIV,EAAOpF,KAAK0B,MAI7B,IAFA,IAAMiE,EAAyB,GAExBP,KAASO,EAAQjF,KAAKV,KAAK8C,YAElC,OAAO6C,GAGTlE,EAAAG,UAAA0C,KAAA,SAAKyB,GACH,IAAK,IAAIP,EAAUxF,KAAK6B,MAAO8B,EAAW,EAAG6B,EAAS7B,IAAY6B,EAAUA,EAAQ9F,KAClF,GAAIqG,EAAUP,EAAS7B,EAAU3D,MAAO,OAAOwF,GAMnD/D,EAAAG,UAAA8D,UAAA,SAAUK,GACR,IAAK,IAAIP,EAAUxF,KAAK6B,MAAO8B,EAAW,EAAG6B,EAAS7B,IAAY6B,EAAUA,EAAQ9F,KAClF,GAAIqG,EAAUP,EAAS7B,EAAU3D,MAAO,OAAO2D,EAGjD,OAAQ,GAGVlC,EAAAG,UAAAoE,QAAA,SAAqBC,GACnB,IAAK,IAAI7D,EAAOpC,KAAK6B,MAAO8B,EAAW,EAAGvB,EAAMuB,IAAYvB,EAAOA,EAAK1C,KACtEuG,EAAW7D,EAAMuB,EAAU3D,OAI/ByB,EAAAG,UAAA4C,IAAA,SAAIb,GACF,OAAO3D,KAAKsE,MAAK,SAACjF,EAAG6G,GAAU,OAAAvC,IAAauC,MAK9CzE,EAAAG,UAAAuE,QAAA,SAAQ5F,EAAY6D,GAClB,YADkB,IAAAA,IAAAA,EAAAC,GACXrE,KAAK0F,WAAU,SAAAtD,GAAQ,OAAAgC,EAAUhC,EAAK7B,MAAOA,OAGtDkB,EAAAG,UAAAwE,QAAA,WACE,IAAMC,EAAQ,IAAIC,MAAMtG,KAAK0B,MAI7B,OAFA1B,KAAKgG,SAAQ,SAAC5D,EAAM8D,GAAU,OAACG,EAAMH,GAAU9D,EAAK7B,SAE7C8F,GAGT5E,EAAAG,UAAAgB,YAAA,WACE,IAAMyD,EAAQ,IAAIC,MAAMtG,KAAK0B,MAI7B,OAFA1B,KAAKgG,SAAQ,SAAC5D,EAAM8D,GAAU,OAACG,EAAMH,GAAU9D,KAExCiE,GAGT5E,EAAAG,UAAA2E,SAAA,SAASC,GACP,YADO,IAAAA,IAAAA,EAA4BC,KAAKC,WACjC1G,KAAKoG,UACTxB,KAAI,SAAArE,GAAS,OAAAiG,EAASjG,MACtBoG,KAAK,UAITlF,EAAAG,UAAC9B,OAAOC,UAAT,mEACWqC,EAAOpC,KAAK6B,MAAkB,0BAAGO,EACxC,CAAA,EAAMA,EAAK7B,OADiC,CAAA,EAAA,UAC5C8C,EAAA9D,+BAD0D6C,EAAOA,EAAK1C","sourcesContent":["/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, exports) {\r\n for (var p in m) if (p !== \"default\" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","/* tslint:disable:no-non-null-assertion */\r\n\r\nimport compare from 'just-compare';\r\n\r\nexport class ListNode {\r\n next: ListNode | undefined;\r\n previous: ListNode | undefined;\r\n constructor(public readonly value: T) {}\r\n}\r\n\r\nexport class LinkedList {\r\n private first: ListNode | undefined;\r\n private last: ListNode | undefined;\r\n private size = 0;\r\n\r\n get head(): ListNode | undefined {\r\n return this.first;\r\n }\r\n get tail(): ListNode | undefined {\r\n return this.last;\r\n }\r\n get length(): number {\r\n return this.size;\r\n }\r\n\r\n private attach(\r\n value: T,\r\n previousNode: ListNode | undefined,\r\n nextNode: ListNode | undefined,\r\n ): ListNode {\r\n if (!previousNode) return this.addHead(value);\r\n\r\n if (!nextNode) return this.addTail(value);\r\n\r\n const node = new ListNode(value);\r\n node.previous = previousNode;\r\n previousNode.next = node;\r\n node.next = nextNode;\r\n nextNode.previous = node;\r\n\r\n this.size++;\r\n\r\n return node;\r\n }\r\n\r\n private attachMany(\r\n values: T[],\r\n previousNode: ListNode | undefined,\r\n nextNode: ListNode | undefined,\r\n ): ListNode[] {\r\n if (!values.length) return [];\r\n\r\n if (!previousNode) return this.addManyHead(values);\r\n\r\n if (!nextNode) return this.addManyTail(values);\r\n\r\n const list = new LinkedList();\r\n list.addManyTail(values);\r\n list.first!.previous = previousNode;\r\n previousNode.next = list.first;\r\n list.last!.next = nextNode;\r\n nextNode.previous = list.last;\r\n\r\n this.size += values.length;\r\n\r\n return list.toNodeArray();\r\n }\r\n\r\n private detach(node: ListNode) {\r\n if (!node.previous) return this.dropHead();\r\n\r\n if (!node.next) return this.dropTail();\r\n\r\n node.previous.next = node.next;\r\n node.next.previous = node.previous;\r\n\r\n this.size--;\r\n\r\n return node;\r\n }\r\n\r\n add(value: T) {\r\n return {\r\n after: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addAfter.call(this, value, ...params),\r\n before: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addBefore.call(this, value, ...params),\r\n byIndex: (position: number) => this.addByIndex(value, position),\r\n head: () => this.addHead(value),\r\n tail: () => this.addTail(value),\r\n };\r\n }\r\n\r\n addMany(values: T[]) {\r\n return {\r\n after: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addManyAfter.call(this, values, ...params),\r\n before: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.addManyBefore.call(this, values, ...params),\r\n byIndex: (position: number) => this.addManyByIndex(values, position),\r\n head: () => this.addManyHead(values),\r\n tail: () => this.addManyTail(values),\r\n };\r\n }\r\n\r\n addAfter(value: T, previousValue: T): ListNode;\r\n addAfter(value: T, previousValue: any, compareFn: ListComparisonFn): ListNode;\r\n addAfter(value: T, previousValue: any, compareFn: ListComparisonFn = compare): ListNode {\r\n const previous = this.find(node => compareFn(node.value, previousValue));\r\n\r\n return previous ? this.attach(value, previous, previous.next) : this.addTail(value);\r\n }\r\n\r\n addBefore(value: T, nextValue: T): ListNode;\r\n addBefore(value: T, nextValue: any, compareFn: ListComparisonFn): ListNode;\r\n addBefore(value: T, nextValue: any, compareFn: ListComparisonFn = compare): ListNode {\r\n const next = this.find(node => compareFn(node.value, nextValue));\r\n\r\n return next ? this.attach(value, next.previous, next) : this.addHead(value);\r\n }\r\n\r\n addByIndex(value: T, position: number): ListNode {\r\n if (position < 0) position += this.size;\r\n else if (position >= this.size) return this.addTail(value);\r\n\r\n if (position <= 0) return this.addHead(value);\r\n\r\n const next = this.get(position)!;\r\n\r\n return this.attach(value, next.previous, next);\r\n }\r\n\r\n addHead(value: T): ListNode {\r\n const node = new ListNode(value);\r\n\r\n node.next = this.first;\r\n\r\n if (this.first) this.first.previous = node;\r\n else this.last = node;\r\n\r\n this.first = node;\r\n this.size++;\r\n\r\n return node;\r\n }\r\n\r\n addTail(value: T): ListNode {\r\n const node = new ListNode(value);\r\n\r\n if (this.first) {\r\n node.previous = this.last;\r\n this.last!.next = node;\r\n this.last = node;\r\n } else {\r\n this.first = node;\r\n this.last = node;\r\n }\r\n\r\n this.size++;\r\n\r\n return node;\r\n }\r\n\r\n addManyAfter(values: T[], previousValue: T): ListNode[];\r\n addManyAfter(values: T[], previousValue: any, compareFn: ListComparisonFn): ListNode[];\r\n addManyAfter(\r\n values: T[],\r\n previousValue: any,\r\n compareFn: ListComparisonFn = compare,\r\n ): ListNode[] {\r\n const previous = this.find(node => compareFn(node.value, previousValue));\r\n\r\n return previous ? this.attachMany(values, previous, previous.next) : this.addManyTail(values);\r\n }\r\n\r\n addManyBefore(values: T[], nextValue: T): ListNode[];\r\n addManyBefore(values: T[], nextValue: any, compareFn: ListComparisonFn): ListNode[];\r\n addManyBefore(\r\n values: T[],\r\n nextValue: any,\r\n compareFn: ListComparisonFn = compare,\r\n ): ListNode[] {\r\n const next = this.find(node => compareFn(node.value, nextValue));\r\n\r\n return next ? this.attachMany(values, next.previous, next) : this.addManyHead(values);\r\n }\r\n\r\n addManyByIndex(values: T[], position: number): ListNode[] {\r\n if (position < 0) position += this.size;\r\n\r\n if (position <= 0) return this.addManyHead(values);\r\n\r\n if (position >= this.size) return this.addManyTail(values);\r\n\r\n const next = this.get(position)!;\r\n\r\n return this.attachMany(values, next.previous, next);\r\n }\r\n\r\n addManyHead(values: T[]): ListNode[] {\r\n return values.reduceRight[]>((nodes, value) => {\r\n nodes.unshift(this.addHead(value));\r\n return nodes;\r\n }, []);\r\n }\r\n\r\n addManyTail(values: T[]): ListNode[] {\r\n return values.map(value => this.addTail(value));\r\n }\r\n\r\n drop() {\r\n return {\r\n byIndex: (position: number) => this.dropByIndex(position),\r\n byValue: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.dropByValue.apply(this, params),\r\n byValueAll: (...params: [T] | [any, ListComparisonFn]) =>\r\n this.dropByValueAll.apply(this, params),\r\n head: () => this.dropHead(),\r\n tail: () => this.dropTail(),\r\n };\r\n }\r\n\r\n dropMany(count: number) {\r\n return {\r\n byIndex: (position: number) => this.dropManyByIndex(count, position),\r\n head: () => this.dropManyHead(count),\r\n tail: () => this.dropManyTail(count),\r\n };\r\n }\r\n\r\n dropByIndex(position: number): ListNode | undefined {\r\n if (position < 0) position += this.size;\r\n\r\n const current = this.get(position);\r\n\r\n return current ? this.detach(current) : undefined;\r\n }\r\n\r\n dropByValue(value: T): ListNode | undefined;\r\n dropByValue(value: any, compareFn: ListComparisonFn): ListNode | undefined;\r\n dropByValue(value: any, compareFn: ListComparisonFn = compare): ListNode | undefined {\r\n const position = this.findIndex(node => compareFn(node.value, value));\r\n\r\n return position < 0 ? undefined : this.dropByIndex(position);\r\n }\r\n\r\n dropByValueAll(value: T): ListNode[];\r\n dropByValueAll(value: any, compareFn: ListComparisonFn): ListNode[];\r\n dropByValueAll(value: any, compareFn: ListComparisonFn = compare): ListNode[] {\r\n const dropped: ListNode[] = [];\r\n\r\n for (let current = this.first, position = 0; current; position++, current = current.next) {\r\n if (compareFn(current.value, value)) {\r\n dropped.push(this.dropByIndex(position - dropped.length)!);\r\n }\r\n }\r\n\r\n return dropped;\r\n }\r\n\r\n dropHead(): ListNode | undefined {\r\n const head = this.first;\r\n\r\n if (head) {\r\n this.first = head.next;\r\n\r\n if (this.first) this.first.previous = undefined;\r\n else this.last = undefined;\r\n\r\n this.size--;\r\n\r\n return head;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n dropTail(): ListNode | undefined {\r\n const tail = this.last;\r\n\r\n if (tail) {\r\n this.last = tail.previous;\r\n\r\n if (this.last) this.last.next = undefined;\r\n else this.first = undefined;\r\n\r\n this.size--;\r\n\r\n return tail;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n dropManyByIndex(count: number, position: number): ListNode[] {\r\n if (count <= 0) return [];\r\n\r\n if (position < 0) position = Math.max(position + this.size, 0);\r\n else if (position >= this.size) return [];\r\n\r\n count = Math.min(count, this.size - position);\r\n\r\n const dropped: ListNode[] = [];\r\n\r\n while (count--) {\r\n const current = this.get(position);\r\n dropped.push(this.detach(current!)!);\r\n }\r\n\r\n return dropped;\r\n }\r\n\r\n dropManyHead(count: Exclude): ListNode[] {\r\n if (count <= 0) return [];\r\n\r\n count = Math.min(count, this.size);\r\n\r\n const dropped: ListNode[] = [];\r\n\r\n while (count--) dropped.unshift(this.dropHead()!);\r\n\r\n return dropped;\r\n }\r\n\r\n dropManyTail(count: Exclude): ListNode[] {\r\n if (count <= 0) return [];\r\n\r\n count = Math.min(count, this.size);\r\n\r\n const dropped: ListNode[] = [];\r\n\r\n while (count--) dropped.push(this.dropTail()!);\r\n\r\n return dropped;\r\n }\r\n\r\n find(predicate: ListIteratorFn): ListNode | undefined {\r\n for (let current = this.first, position = 0; current; position++, current = current.next) {\r\n if (predicate(current, position, this)) return current;\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n findIndex(predicate: ListIteratorFn): number {\r\n for (let current = this.first, position = 0; current; position++, current = current.next) {\r\n if (predicate(current, position, this)) return position;\r\n }\r\n\r\n return -1;\r\n }\r\n\r\n forEach(iteratorFn: ListIteratorFn) {\r\n for (let node = this.first, position = 0; node; position++, node = node.next) {\r\n iteratorFn(node, position, this);\r\n }\r\n }\r\n\r\n get(position: number): ListNode | undefined {\r\n return this.find((_, index) => position === index);\r\n }\r\n\r\n indexOf(value: T): number;\r\n indexOf(value: any, compareFn: ListComparisonFn): number;\r\n indexOf(value: any, compareFn: ListComparisonFn = compare): number {\r\n return this.findIndex(node => compareFn(node.value, value));\r\n }\r\n\r\n toArray(): T[] {\r\n const array = new Array(this.size);\r\n\r\n this.forEach((node, index) => (array[index!] = node.value));\r\n\r\n return array;\r\n }\r\n\r\n toNodeArray(): ListNode[] {\r\n const array = new Array(this.size);\r\n\r\n this.forEach((node, index) => (array[index!] = node));\r\n\r\n return array;\r\n }\r\n\r\n toString(mapperFn: ListMapperFn = JSON.stringify): string {\r\n return this.toArray()\r\n .map(value => mapperFn(value))\r\n .join(' <-> ');\r\n }\r\n\r\n // Cannot use Generator type because of ng-packagr\r\n *[Symbol.iterator](): any {\r\n for (let node = this.first, position = 0; node; position++, node = node.next) {\r\n yield node.value;\r\n }\r\n }\r\n}\r\n\r\nexport type ListMapperFn = (value: T) => any;\r\n\r\nexport type ListComparisonFn = (value1: T, value2: any) => boolean;\r\n\r\nexport type ListIteratorFn = (\r\n node: ListNode,\r\n index?: number,\r\n list?: LinkedList,\r\n) => R;\r\n"]} \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.css.map b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.css.map deleted file mode 100644 index 7e08a21044..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["less/datepicker.less","build/build.less"],"names":[],"mappings":"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","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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.css b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.css deleted file mode 100644 index eb681513fb..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * 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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.js deleted file mode 100644 index 8800106e8f..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/bootstrap-datepicker.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * 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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js deleted file mode 100644 index 0aab38f3a4..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker-en-CA.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js deleted file mode 100644 index 9d70dc2fa3..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar-tn.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js deleted file mode 100644 index ece41af725..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ar.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js deleted file mode 100644 index aa1edbf4f8..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.az.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js deleted file mode 100644 index 28e8b22dcf..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bg.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(a){a.fn.datepicker.dates.bg={days:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],daysShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],daysMin:["Н","П","В","С","Ч","П","С"],months:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthsShort:["Ян","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Ное","Дек"],today:"днес"}}(jQuery); \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js deleted file mode 100644 index e0796a3ba7..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bm.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js deleted file mode 100644 index f67b5e26e7..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bn.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js deleted file mode 100644 index af3e3bd0af..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.br.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js deleted file mode 100644 index cfb06fde79..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.bs.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js deleted file mode 100644 index ac107894c4..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ca.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js deleted file mode 100644 index 42dfd1a29d..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cs.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js deleted file mode 100644 index f85ea031dd..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.cy.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js deleted file mode 100644 index 53c8180528..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.da.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js deleted file mode 100644 index 1b5d6a2474..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.de.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js deleted file mode 100644 index 046e9eb5ef..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.el.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js deleted file mode 100644 index b8d5f41cfa..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-AU.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js deleted file mode 100644 index 7b1070f74d..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-CA.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js deleted file mode 100644 index 2966f5414e..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-GB.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js deleted file mode 100644 index dc8f71c026..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-IE.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js deleted file mode 100644 index c374a8d40c..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-NZ.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js deleted file mode 100644 index 885a928cba..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.en-ZA.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js deleted file mode 100644 index 736db021ae..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eo.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js deleted file mode 100644 index f3cef5d2b9..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.es.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js deleted file mode 100644 index 34cd9c60e9..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.et.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js deleted file mode 100644 index c5aa359e48..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.eu.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js deleted file mode 100644 index 8575237a07..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fa.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js deleted file mode 100644 index 239dfb796c..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fi.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js deleted file mode 100644 index fa24e3a127..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fo.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js deleted file mode 100644 index 1c6bcdcbfc..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr-CH.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js deleted file mode 100644 index 244cfba800..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.fr.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js deleted file mode 100644 index 3d92606b3b..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.gl.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js deleted file mode 100644 index 191cb453a0..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.he.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js deleted file mode 100644 index 635baffa8b..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hi.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js deleted file mode 100644 index 8b34bce0fd..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hr.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js deleted file mode 100644 index f9decf9a2c..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hu.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js deleted file mode 100644 index a1cf653d38..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.hy.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js deleted file mode 100644 index 7c3220a643..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.id.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.is.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.is.min.js deleted file mode 100644 index f49bd18cc2..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.is.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it-CH.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it-CH.min.js deleted file mode 100644 index 7e1adbb95d..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it-CH.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it.min.js deleted file mode 100644 index cc30766ffa..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.it.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js deleted file mode 100644 index e321f04ffe..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ja.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ka.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ka.min.js deleted file mode 100644 index 84f14c0e90..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ka.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kh.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kh.min.js deleted file mode 100644 index bf2abc5d82..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kh.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kk.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kk.min.js deleted file mode 100644 index f4e2f3f1a2..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kk.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.km.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.km.min.js deleted file mode 100644 index 648d83f84f..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.km.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(a){a.fn.datepicker.dates.km={days:["អាទិត្យ","ចន្ទ","អង្គារ","ពុធ","ព្រហស្បតិ៍","សុក្រ","សៅរ៍"],daysShort:["អា.ទិ","ចន្ទ","អង្គារ","ពុធ","ព្រ.ហ","សុក្រ","សៅរ៍"],daysMin:["អា.ទិ","ចន្ទ","អង្គារ","ពុធ","ព្រ.ហ","សុក្រ","សៅរ៍"],months:["មករា","កុម្ភះ","មិនា","មេសា","ឧសភា","មិថុនា","កក្កដា","សីហា","កញ្ញា","តុលា","វិច្ឆិកា","ធ្នូ"],monthsShort:["មករា","កុម្ភះ","មិនា","មេសា","ឧសភា","មិថុនា","កក្កដា","សីហា","កញ្ញា","តុលា","វិច្ឆិកា","ធ្នូ"],today:"ថ្ងៃនេះ",clear:"សំអាត"}}(jQuery); \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ko.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ko.min.js deleted file mode 100644 index 9751ee5c22..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ko.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kr.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kr.min.js deleted file mode 100644 index 43393409e0..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.kr.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lt.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lt.min.js deleted file mode 100644 index da78ea85f3..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lt.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lv.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lv.min.js deleted file mode 100644 index 89cea00f8b..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.lv.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.me.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.me.min.js deleted file mode 100644 index c65a891646..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.me.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mk.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mk.min.js deleted file mode 100644 index 46423f7581..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mk.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mn.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mn.min.js deleted file mode 100644 index 6ebaec9d86..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.mn.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ms.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ms.min.js deleted file mode 100644 index 47efafdc2f..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ms.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl-BE.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl-BE.min.js deleted file mode 100644 index 85d3146df1..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl-BE.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl.min.js deleted file mode 100644 index af977b71ea..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.nl.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js deleted file mode 100644 index 0c5136e441..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.no.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.oc.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.oc.min.js deleted file mode 100644 index 630fa16b96..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.oc.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pl.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pl.min.js deleted file mode 100644 index ffb30ec8b1..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pl.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt-BR.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt-BR.min.js deleted file mode 100644 index 2d3f8afdac..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt-BR.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js deleted file mode 100644 index e2b4e64d77..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.pt.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ro.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ro.min.js deleted file mode 100644 index 5fff2986df..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ro.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs-latin.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs-latin.min.js deleted file mode 100644 index e520c95733..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs-latin.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs.min.js deleted file mode 100644 index ba95ae298f..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.rs.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ru.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ru.min.js deleted file mode 100644 index 52bc010b97..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ru.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.si.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.si.min.js deleted file mode 100644 index b9746b8fc1..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.si.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js deleted file mode 100644 index 79a9267fd5..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sk.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sl.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sl.min.js deleted file mode 100644 index 831cf73903..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sl.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sq.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sq.min.js deleted file mode 100644 index 8c586055a0..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sq.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr-latin.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr-latin.min.js deleted file mode 100644 index c6b7001ace..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr-latin.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr.min.js deleted file mode 100644 index 4e46dbf64b..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sr.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sv.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sv.min.js deleted file mode 100644 index 7ab6becb92..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sv.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sw.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sw.min.js deleted file mode 100644 index 454d3053cb..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.sw.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ta.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ta.min.js deleted file mode 100644 index e7909494aa..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.ta.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tg.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tg.min.js deleted file mode 100644 index 104b6dd95b..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tg.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.th.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.th.min.js deleted file mode 100644 index 1e398ba8bc..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.th.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(a){a.fn.datepicker.dates.th={days:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัส","ศุกร์","เสาร์","อาทิตย์"],daysShort:["อา","จ","อ","พ","พฤ","ศ","ส","อา"],daysMin:["อา","จ","อ","พ","พฤ","ศ","ส","อา"],months:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthsShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],today:"วันนี้"}}(jQuery); \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tk.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tk.min.js deleted file mode 100644 index 716edef2ee..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tk.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tr.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tr.min.js deleted file mode 100644 index 7889b1135d..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.tr.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uk.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uk.min.js deleted file mode 100644 index 41b02e6b28..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uk.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-cyrl.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-cyrl.min.js deleted file mode 100644 index a0a8f213ce..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-cyrl.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-latn.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-latn.min.js deleted file mode 100644 index 2f58e343ef..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.uz-latn.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.vi.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.vi.min.js deleted file mode 100644 index 3311d23f86..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.vi.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js deleted file mode 100644 index 8e6920b0c8..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-TW.min.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-TW.min.js deleted file mode 100644 index e309c1d7d6..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-datepicker/locales/bootstrap-datepicker.zh-TW.min.js +++ /dev/null @@ -1 +0,0 @@ -!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/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-daterangepicker/daterangepicker.css b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-daterangepicker/daterangepicker.css deleted file mode 100644 index 72f7acdfb3..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-daterangepicker/daterangepicker.css +++ /dev/null @@ -1,410 +0,0 @@ -.daterangepicker { - position: absolute; - color: inherit; - background-color: #fff; - border-radius: 4px; - border: 1px solid #ddd; - width: 278px; - max-width: none; - padding: 0; - margin-top: 7px; - top: 100px; - left: 20px; - z-index: 3001; - display: none; - font-family: arial; - font-size: 15px; - line-height: 1em; -} - -.daterangepicker:before, .daterangepicker:after { - position: absolute; - display: inline-block; - border-bottom-color: rgba(0, 0, 0, 0.2); - content: ''; -} - -.daterangepicker:before { - top: -7px; - border-right: 7px solid transparent; - border-left: 7px solid transparent; - border-bottom: 7px solid #ccc; -} - -.daterangepicker:after { - top: -6px; - border-right: 6px solid transparent; - border-bottom: 6px solid #fff; - border-left: 6px solid transparent; -} - -.daterangepicker.opensleft:before { - right: 9px; -} - -.daterangepicker.opensleft:after { - right: 10px; -} - -.daterangepicker.openscenter:before { - left: 0; - right: 0; - width: 0; - margin-left: auto; - margin-right: auto; -} - -.daterangepicker.openscenter:after { - left: 0; - right: 0; - width: 0; - margin-left: auto; - margin-right: auto; -} - -.daterangepicker.opensright:before { - left: 9px; -} - -.daterangepicker.opensright:after { - left: 10px; -} - -.daterangepicker.drop-up { - margin-top: -7px; -} - -.daterangepicker.drop-up:before { - top: initial; - bottom: -7px; - border-bottom: initial; - border-top: 7px solid #ccc; -} - -.daterangepicker.drop-up:after { - top: initial; - bottom: -6px; - border-bottom: initial; - border-top: 6px solid #fff; -} - -.daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar { - float: none; -} - -.daterangepicker.single .drp-selected { - display: none; -} - -.daterangepicker.show-calendar .drp-calendar { - display: block; -} - -.daterangepicker.show-calendar .drp-buttons { - display: block; -} - -.daterangepicker.auto-apply .drp-buttons { - display: none; -} - -.daterangepicker .drp-calendar { - display: none; - max-width: 270px; -} - -.daterangepicker .drp-calendar.left { - padding: 8px 0 8px 8px; -} - -.daterangepicker .drp-calendar.right { - padding: 8px; -} - -.daterangepicker .drp-calendar.single .calendar-table { - border: none; -} - -.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span { - color: #fff; - border: solid black; - border-width: 0 2px 2px 0; - border-radius: 0; - display: inline-block; - padding: 3px; -} - -.daterangepicker .calendar-table .next span { - transform: rotate(-45deg); - -webkit-transform: rotate(-45deg); -} - -.daterangepicker .calendar-table .prev span { - transform: rotate(135deg); - -webkit-transform: rotate(135deg); -} - -.daterangepicker .calendar-table th, .daterangepicker .calendar-table td { - white-space: nowrap; - text-align: center; - vertical-align: middle; - min-width: 32px; - width: 32px; - height: 24px; - line-height: 24px; - font-size: 12px; - border-radius: 4px; - border: 1px solid transparent; - white-space: nowrap; - cursor: pointer; -} - -.daterangepicker .calendar-table { - border: 1px solid #fff; - border-radius: 4px; - background-color: #fff; -} - -.daterangepicker .calendar-table table { - width: 100%; - margin: 0; - border-spacing: 0; - border-collapse: collapse; -} - -.daterangepicker td.available:hover, .daterangepicker th.available:hover { - background-color: #eee; - border-color: transparent; - color: inherit; -} - -.daterangepicker td.week, .daterangepicker th.week { - font-size: 80%; - color: #ccc; -} - -.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { - background-color: #fff; - border-color: transparent; - color: #999; -} - -.daterangepicker td.in-range { - background-color: #ebf4f8; - border-color: transparent; - color: #000; - border-radius: 0; -} - -.daterangepicker td.start-date { - border-radius: 4px 0 0 4px; -} - -.daterangepicker td.end-date { - border-radius: 0 4px 4px 0; -} - -.daterangepicker td.start-date.end-date { - border-radius: 4px; -} - -.daterangepicker td.active, .daterangepicker td.active:hover { - background-color: #357ebd; - border-color: transparent; - color: #fff; -} - -.daterangepicker th.month { - width: auto; -} - -.daterangepicker td.disabled, .daterangepicker option.disabled { - color: #999; - cursor: not-allowed; - text-decoration: line-through; -} - -.daterangepicker select.monthselect, .daterangepicker select.yearselect { - font-size: 12px; - padding: 1px; - height: auto; - margin: 0; - cursor: default; -} - -.daterangepicker select.monthselect { - margin-right: 2%; - width: 56%; -} - -.daterangepicker select.yearselect { - width: 40%; -} - -.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { - width: 50px; - margin: 0 auto; - background: #eee; - border: 1px solid #eee; - padding: 2px; - outline: 0; - font-size: 12px; -} - -.daterangepicker .calendar-time { - text-align: center; - margin: 4px auto 0 auto; - line-height: 30px; - position: relative; -} - -.daterangepicker .calendar-time select.disabled { - color: #ccc; - cursor: not-allowed; -} - -.daterangepicker .drp-buttons { - clear: both; - text-align: right; - padding: 8px; - border-top: 1px solid #ddd; - display: none; - line-height: 12px; - vertical-align: middle; -} - -.daterangepicker .drp-selected { - display: inline-block; - font-size: 12px; - padding-right: 8px; -} - -.daterangepicker .drp-buttons .btn { - margin-left: 8px; - font-size: 12px; - font-weight: bold; - padding: 4px 8px; -} - -.daterangepicker.show-ranges.single.rtl .drp-calendar.left { - border-right: 1px solid #ddd; -} - -.daterangepicker.show-ranges.single.ltr .drp-calendar.left { - border-left: 1px solid #ddd; -} - -.daterangepicker.show-ranges.rtl .drp-calendar.right { - border-right: 1px solid #ddd; -} - -.daterangepicker.show-ranges.ltr .drp-calendar.left { - border-left: 1px solid #ddd; -} - -.daterangepicker .ranges { - float: none; - text-align: left; - margin: 0; -} - -.daterangepicker.show-calendar .ranges { - margin-top: 8px; -} - -.daterangepicker .ranges ul { - list-style: none; - margin: 0 auto; - padding: 0; - width: 100%; -} - -.daterangepicker .ranges li { - font-size: 12px; - padding: 8px 12px; - cursor: pointer; -} - -.daterangepicker .ranges li:hover { - background-color: #eee; -} - -.daterangepicker .ranges li.active { - background-color: #08c; - color: #fff; -} - -/* Larger Screen Styling */ -@media (min-width: 564px) { - .daterangepicker { - width: auto; - } - - .daterangepicker .ranges ul { - width: 140px; - } - - .daterangepicker.single .ranges ul { - width: 100%; - } - - .daterangepicker.single .drp-calendar.left { - clear: none; - } - - .daterangepicker.single .ranges, .daterangepicker.single .drp-calendar { - float: left; - } - - .daterangepicker { - direction: ltr; - text-align: left; - } - - .daterangepicker .drp-calendar.left { - clear: left; - margin-right: 0; - } - - .daterangepicker .drp-calendar.left .calendar-table { - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .daterangepicker .drp-calendar.right { - margin-left: 0; - } - - .daterangepicker .drp-calendar.right .calendar-table { - border-left: none; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - - .daterangepicker .drp-calendar.left .calendar-table { - padding-right: 8px; - } - - .daterangepicker .ranges, .daterangepicker .drp-calendar { - float: left; - } -} - -@media (min-width: 730px) { - .daterangepicker .ranges { - width: auto; - } - - .daterangepicker .ranges { - float: left; - } - - .daterangepicker.rtl .ranges { - float: right; - } - - .daterangepicker .drp-calendar.left { - clear: none !important; - } -} diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-daterangepicker/daterangepicker.js b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-daterangepicker/daterangepicker.js deleted file mode 100644 index a3becee537..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap-daterangepicker/daterangepicker.js +++ /dev/null @@ -1,1578 +0,0 @@ -/** -* @version: 3.1 -* @author: Dan Grossman http://www.dangrossman.info/ -* @copyright: Copyright (c) 2012-2019 Dan Grossman. All rights reserved. -* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php -* @website: http://www.daterangepicker.com/ -*/ -// Following the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Make globaly available as well - define(['moment', 'jquery'], function (moment, jquery) { - if (!jquery.fn) jquery.fn = {}; // webpack server rendering - if (typeof moment !== 'function' && moment.hasOwnProperty('default')) moment = moment['default'] - return factory(moment, jquery); - }); - } else if (typeof module === 'object' && module.exports) { - // Node / Browserify - //isomorphic issue - var jQuery = (typeof window != 'undefined') ? window.jQuery : undefined; - if (!jQuery) { - jQuery = require('jquery'); - if (!jQuery.fn) jQuery.fn = {}; - } - var moment = (typeof window != 'undefined' && typeof window.moment != 'undefined') ? window.moment : require('moment'); - module.exports = factory(moment, jQuery); - } else { - // Browser globals - root.daterangepicker = factory(root.moment, root.jQuery); - } -}(this, function(moment, $) { - var DateRangePicker = function(element, options, cb) { - - //default settings for options - this.parentEl = 'body'; - this.element = $(element); - this.startDate = moment().startOf('day'); - this.endDate = moment().endOf('day'); - this.minDate = false; - this.maxDate = false; - this.maxSpan = false; - this.autoApply = false; - this.singleDatePicker = false; - this.showDropdowns = false; - this.minYear = moment().subtract(100, 'year').format('YYYY'); - this.maxYear = moment().add(100, 'year').format('YYYY'); - this.showWeekNumbers = false; - this.showISOWeekNumbers = false; - this.showCustomRangeLabel = true; - this.timePicker = false; - this.timePicker24Hour = false; - this.timePickerIncrement = 1; - this.timePickerSeconds = false; - this.linkedCalendars = true; - this.autoUpdateInput = true; - this.alwaysShowCalendars = false; - this.ranges = {}; - - this.opens = 'right'; - if (this.element.hasClass('pull-right')) - this.opens = 'left'; - - this.drops = 'down'; - if (this.element.hasClass('dropup')) - this.drops = 'up'; - - this.buttonClasses = 'btn btn-sm'; - this.applyButtonClasses = 'btn-primary'; - this.cancelButtonClasses = 'btn-default'; - - this.locale = { - direction: 'ltr', - format: moment.localeData().longDateFormat('L'), - separator: ' - ', - applyLabel: 'Apply', - cancelLabel: 'Cancel', - weekLabel: 'W', - customRangeLabel: 'Custom Range', - daysOfWeek: moment.weekdaysMin(), - monthNames: moment.monthsShort(), - firstDay: moment.localeData().firstDayOfWeek() - }; - - this.callback = function() { }; - - //some state information - this.isShowing = false; - this.leftCalendar = {}; - this.rightCalendar = {}; - - //custom options from user - if (typeof options !== 'object' || options === null) - options = {}; - - //allow setting options with data attributes - //data-api options will be overwritten with custom javascript options - options = $.extend(this.element.data(), options); - - //html template for the picker UI - if (typeof options.template !== 'string' && !(options.template instanceof $)) - options.template = - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + - '' + - ' ' + - '
' + - '
'; - - this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); - this.container = $(options.template).appendTo(this.parentEl); - - // - // handle all the possible options overriding defaults - // - - if (typeof options.locale === 'object') { - - if (typeof options.locale.direction === 'string') - this.locale.direction = options.locale.direction; - - if (typeof options.locale.format === 'string') - this.locale.format = options.locale.format; - - if (typeof options.locale.separator === 'string') - this.locale.separator = options.locale.separator; - - if (typeof options.locale.daysOfWeek === 'object') - this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); - - if (typeof options.locale.monthNames === 'object') - this.locale.monthNames = options.locale.monthNames.slice(); - - if (typeof options.locale.firstDay === 'number') - this.locale.firstDay = options.locale.firstDay; - - if (typeof options.locale.applyLabel === 'string') - this.locale.applyLabel = options.locale.applyLabel; - - if (typeof options.locale.cancelLabel === 'string') - this.locale.cancelLabel = options.locale.cancelLabel; - - if (typeof options.locale.weekLabel === 'string') - this.locale.weekLabel = options.locale.weekLabel; - - if (typeof options.locale.customRangeLabel === 'string'){ - //Support unicode chars in the custom range name. - var elem = document.createElement('textarea'); - elem.innerHTML = options.locale.customRangeLabel; - var rangeHtml = elem.value; - this.locale.customRangeLabel = rangeHtml; - } - } - this.container.addClass(this.locale.direction); - - if (typeof options.startDate === 'string') - this.startDate = moment(options.startDate, this.locale.format); - - if (typeof options.endDate === 'string') - this.endDate = moment(options.endDate, this.locale.format); - - if (typeof options.minDate === 'string') - this.minDate = moment(options.minDate, this.locale.format); - - if (typeof options.maxDate === 'string') - this.maxDate = moment(options.maxDate, this.locale.format); - - if (typeof options.startDate === 'object') - this.startDate = moment(options.startDate); - - if (typeof options.endDate === 'object') - this.endDate = moment(options.endDate); - - if (typeof options.minDate === 'object') - this.minDate = moment(options.minDate); - - if (typeof options.maxDate === 'object') - this.maxDate = moment(options.maxDate); - - // sanity check for bad options - if (this.minDate && this.startDate.isBefore(this.minDate)) - this.startDate = this.minDate.clone(); - - // sanity check for bad options - if (this.maxDate && this.endDate.isAfter(this.maxDate)) - this.endDate = this.maxDate.clone(); - - if (typeof options.applyButtonClasses === 'string') - this.applyButtonClasses = options.applyButtonClasses; - - if (typeof options.applyClass === 'string') //backwards compat - this.applyButtonClasses = options.applyClass; - - if (typeof options.cancelButtonClasses === 'string') - this.cancelButtonClasses = options.cancelButtonClasses; - - if (typeof options.cancelClass === 'string') //backwards compat - this.cancelButtonClasses = options.cancelClass; - - if (typeof options.maxSpan === 'object') - this.maxSpan = options.maxSpan; - - if (typeof options.dateLimit === 'object') //backwards compat - this.maxSpan = options.dateLimit; - - if (typeof options.opens === 'string') - this.opens = options.opens; - - if (typeof options.drops === 'string') - this.drops = options.drops; - - if (typeof options.showWeekNumbers === 'boolean') - this.showWeekNumbers = options.showWeekNumbers; - - if (typeof options.showISOWeekNumbers === 'boolean') - this.showISOWeekNumbers = options.showISOWeekNumbers; - - if (typeof options.buttonClasses === 'string') - this.buttonClasses = options.buttonClasses; - - if (typeof options.buttonClasses === 'object') - this.buttonClasses = options.buttonClasses.join(' '); - - if (typeof options.showDropdowns === 'boolean') - this.showDropdowns = options.showDropdowns; - - if (typeof options.minYear === 'number') - this.minYear = options.minYear; - - if (typeof options.maxYear === 'number') - this.maxYear = options.maxYear; - - if (typeof options.showCustomRangeLabel === 'boolean') - this.showCustomRangeLabel = options.showCustomRangeLabel; - - if (typeof options.singleDatePicker === 'boolean') { - this.singleDatePicker = options.singleDatePicker; - if (this.singleDatePicker) - this.endDate = this.startDate.clone(); - } - - if (typeof options.timePicker === 'boolean') - this.timePicker = options.timePicker; - - if (typeof options.timePickerSeconds === 'boolean') - this.timePickerSeconds = options.timePickerSeconds; - - if (typeof options.timePickerIncrement === 'number') - this.timePickerIncrement = options.timePickerIncrement; - - if (typeof options.timePicker24Hour === 'boolean') - this.timePicker24Hour = options.timePicker24Hour; - - if (typeof options.autoApply === 'boolean') - this.autoApply = options.autoApply; - - if (typeof options.autoUpdateInput === 'boolean') - this.autoUpdateInput = options.autoUpdateInput; - - if (typeof options.linkedCalendars === 'boolean') - this.linkedCalendars = options.linkedCalendars; - - if (typeof options.isInvalidDate === 'function') - this.isInvalidDate = options.isInvalidDate; - - if (typeof options.isCustomDate === 'function') - this.isCustomDate = options.isCustomDate; - - if (typeof options.alwaysShowCalendars === 'boolean') - this.alwaysShowCalendars = options.alwaysShowCalendars; - - // update day names order to firstDay - if (this.locale.firstDay != 0) { - var iterator = this.locale.firstDay; - while (iterator > 0) { - this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); - iterator--; - } - } - - var start, end, range; - - //if no start/end dates set, check if an input element contains initial values - if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { - if ($(this.element).is(':text')) { - var val = $(this.element).val(), - split = val.split(this.locale.separator); - - start = end = null; - - if (split.length == 2) { - start = moment(split[0], this.locale.format); - end = moment(split[1], this.locale.format); - } else if (this.singleDatePicker && val !== "") { - start = moment(val, this.locale.format); - end = moment(val, this.locale.format); - } - if (start !== null && end !== null) { - this.setStartDate(start); - this.setEndDate(end); - } - } - } - - if (typeof options.ranges === 'object') { - for (range in options.ranges) { - - if (typeof options.ranges[range][0] === 'string') - start = moment(options.ranges[range][0], this.locale.format); - else - start = moment(options.ranges[range][0]); - - if (typeof options.ranges[range][1] === 'string') - end = moment(options.ranges[range][1], this.locale.format); - else - end = moment(options.ranges[range][1]); - - // If the start or end date exceed those allowed by the minDate or maxSpan - // options, shorten the range to the allowable period. - if (this.minDate && start.isBefore(this.minDate)) - start = this.minDate.clone(); - - var maxDate = this.maxDate; - if (this.maxSpan && maxDate && start.clone().add(this.maxSpan).isAfter(maxDate)) - maxDate = start.clone().add(this.maxSpan); - if (maxDate && end.isAfter(maxDate)) - end = maxDate.clone(); - - // If the end of the range is before the minimum or the start of the range is - // after the maximum, don't display this range option at all. - if ((this.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day')) - || (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day'))) - continue; - - //Support unicode chars in the range names. - var elem = document.createElement('textarea'); - elem.innerHTML = range; - var rangeHtml = elem.value; - - this.ranges[rangeHtml] = [start, end]; - } - - var list = '
    '; - for (range in this.ranges) { - list += '
  • ' + range + '
  • '; - } - if (this.showCustomRangeLabel) { - list += '
  • ' + this.locale.customRangeLabel + '
  • '; - } - list += '
'; - this.container.find('.ranges').prepend(list); - } - - if (typeof cb === 'function') { - this.callback = cb; - } - - if (!this.timePicker) { - this.startDate = this.startDate.startOf('day'); - this.endDate = this.endDate.endOf('day'); - this.container.find('.calendar-time').hide(); - } - - //can't be used together for now - if (this.timePicker && this.autoApply) - this.autoApply = false; - - if (this.autoApply) { - this.container.addClass('auto-apply'); - } - - if (typeof options.ranges === 'object') - this.container.addClass('show-ranges'); - - if (this.singleDatePicker) { - this.container.addClass('single'); - this.container.find('.drp-calendar.left').addClass('single'); - this.container.find('.drp-calendar.left').show(); - this.container.find('.drp-calendar.right').hide(); - if (!this.timePicker && this.autoApply) { - this.container.addClass('auto-apply'); - } - } - - if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) { - this.container.addClass('show-calendar'); - } - - this.container.addClass('opens' + this.opens); - - //apply CSS classes and labels to buttons - this.container.find('.applyBtn, .cancelBtn').addClass(this.buttonClasses); - if (this.applyButtonClasses.length) - this.container.find('.applyBtn').addClass(this.applyButtonClasses); - if (this.cancelButtonClasses.length) - this.container.find('.cancelBtn').addClass(this.cancelButtonClasses); - this.container.find('.applyBtn').html(this.locale.applyLabel); - this.container.find('.cancelBtn').html(this.locale.cancelLabel); - - // - // event listeners - // - - this.container.find('.drp-calendar') - .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) - .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) - .on('mousedown.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) - .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) - .on('change.daterangepicker', 'select.yearselect', $.proxy(this.monthOrYearChanged, this)) - .on('change.daterangepicker', 'select.monthselect', $.proxy(this.monthOrYearChanged, this)) - .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.timeChanged, this)); - - this.container.find('.ranges') - .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)); - - this.container.find('.drp-buttons') - .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) - .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)); - - if (this.element.is('input') || this.element.is('button')) { - this.element.on({ - 'click.daterangepicker': $.proxy(this.show, this), - 'focus.daterangepicker': $.proxy(this.show, this), - 'keyup.daterangepicker': $.proxy(this.elementChanged, this), - 'keydown.daterangepicker': $.proxy(this.keydown, this) //IE 11 compatibility - }); - } else { - this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); - this.element.on('keydown.daterangepicker', $.proxy(this.toggle, this)); - } - - // - // if attached to a text input, set the initial value - // - - this.updateElement(); - - }; - - DateRangePicker.prototype = { - - constructor: DateRangePicker, - - setStartDate: function(startDate) { - if (typeof startDate === 'string') - this.startDate = moment(startDate, this.locale.format); - - if (typeof startDate === 'object') - this.startDate = moment(startDate); - - if (!this.timePicker) - this.startDate = this.startDate.startOf('day'); - - if (this.timePicker && this.timePickerIncrement) - this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); - - if (this.minDate && this.startDate.isBefore(this.minDate)) { - this.startDate = this.minDate.clone(); - if (this.timePicker && this.timePickerIncrement) - this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); - } - - if (this.maxDate && this.startDate.isAfter(this.maxDate)) { - this.startDate = this.maxDate.clone(); - if (this.timePicker && this.timePickerIncrement) - this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); - } - - if (!this.isShowing) - this.updateElement(); - - this.updateMonthsInView(); - }, - - setEndDate: function(endDate) { - if (typeof endDate === 'string') - this.endDate = moment(endDate, this.locale.format); - - if (typeof endDate === 'object') - this.endDate = moment(endDate); - - if (!this.timePicker) - this.endDate = this.endDate.endOf('day'); - - if (this.timePicker && this.timePickerIncrement) - this.endDate.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); - - if (this.endDate.isBefore(this.startDate)) - this.endDate = this.startDate.clone(); - - if (this.maxDate && this.endDate.isAfter(this.maxDate)) - this.endDate = this.maxDate.clone(); - - if (this.maxSpan && this.startDate.clone().add(this.maxSpan).isBefore(this.endDate)) - this.endDate = this.startDate.clone().add(this.maxSpan); - - this.previousRightTime = this.endDate.clone(); - - this.container.find('.drp-selected').html(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); - - if (!this.isShowing) - this.updateElement(); - - this.updateMonthsInView(); - }, - - isInvalidDate: function() { - return false; - }, - - isCustomDate: function() { - return false; - }, - - updateView: function() { - if (this.timePicker) { - this.renderTimePicker('left'); - this.renderTimePicker('right'); - if (!this.endDate) { - this.container.find('.right .calendar-time select').prop('disabled', true).addClass('disabled'); - } else { - this.container.find('.right .calendar-time select').prop('disabled', false).removeClass('disabled'); - } - } - if (this.endDate) - this.container.find('.drp-selected').html(this.startDate.format(this.locale.format) + this.locale.separator + this.endDate.format(this.locale.format)); - this.updateMonthsInView(); - this.updateCalendars(); - this.updateFormInputs(); - }, - - updateMonthsInView: function() { - if (this.endDate) { - - //if both dates are visible already, do nothing - if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && - (this.startDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.startDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) - && - (this.endDate.format('YYYY-MM') == this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') == this.rightCalendar.month.format('YYYY-MM')) - ) { - return; - } - - this.leftCalendar.month = this.startDate.clone().date(2); - if (!this.linkedCalendars && (this.endDate.month() != this.startDate.month() || this.endDate.year() != this.startDate.year())) { - this.rightCalendar.month = this.endDate.clone().date(2); - } else { - this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); - } - - } else { - if (this.leftCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') != this.startDate.format('YYYY-MM')) { - this.leftCalendar.month = this.startDate.clone().date(2); - this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); - } - } - if (this.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) { - this.rightCalendar.month = this.maxDate.clone().date(2); - this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month'); - } - }, - - updateCalendars: function() { - - if (this.timePicker) { - var hour, minute, second; - if (this.endDate) { - hour = parseInt(this.container.find('.left .hourselect').val(), 10); - minute = parseInt(this.container.find('.left .minuteselect').val(), 10); - if (isNaN(minute)) { - minute = parseInt(this.container.find('.left .minuteselect option:last').val(), 10); - } - second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; - if (!this.timePicker24Hour) { - var ampm = this.container.find('.left .ampmselect').val(); - if (ampm === 'PM' && hour < 12) - hour += 12; - if (ampm === 'AM' && hour === 12) - hour = 0; - } - } else { - hour = parseInt(this.container.find('.right .hourselect').val(), 10); - minute = parseInt(this.container.find('.right .minuteselect').val(), 10); - if (isNaN(minute)) { - minute = parseInt(this.container.find('.right .minuteselect option:last').val(), 10); - } - second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; - if (!this.timePicker24Hour) { - var ampm = this.container.find('.right .ampmselect').val(); - if (ampm === 'PM' && hour < 12) - hour += 12; - if (ampm === 'AM' && hour === 12) - hour = 0; - } - } - this.leftCalendar.month.hour(hour).minute(minute).second(second); - this.rightCalendar.month.hour(hour).minute(minute).second(second); - } - - this.renderCalendar('left'); - this.renderCalendar('right'); - - //highlight any predefined range matching the current start and end dates - this.container.find('.ranges li').removeClass('active'); - if (this.endDate == null) return; - - this.calculateChosenLabel(); - }, - - renderCalendar: function(side) { - - // - // Build the matrix of dates that will populate the calendar - // - - var calendar = side == 'left' ? this.leftCalendar : this.rightCalendar; - var month = calendar.month.month(); - var year = calendar.month.year(); - var hour = calendar.month.hour(); - var minute = calendar.month.minute(); - var second = calendar.month.second(); - var daysInMonth = moment([year, month]).daysInMonth(); - var firstDay = moment([year, month, 1]); - var lastDay = moment([year, month, daysInMonth]); - var lastMonth = moment(firstDay).subtract(1, 'month').month(); - var lastYear = moment(firstDay).subtract(1, 'month').year(); - var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); - var dayOfWeek = firstDay.day(); - - //initialize a 6 rows x 7 columns array for the calendar - var calendar = []; - calendar.firstDay = firstDay; - calendar.lastDay = lastDay; - - for (var i = 0; i < 6; i++) { - calendar[i] = []; - } - - //populate the calendar with date objects - var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; - if (startDay > daysInLastMonth) - startDay -= 7; - - if (dayOfWeek == this.locale.firstDay) - startDay = daysInLastMonth - 6; - - var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]); - - var col, row; - for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { - if (i > 0 && col % 7 === 0) { - col = 0; - row++; - } - calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); - curDate.hour(12); - - if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { - calendar[row][col] = this.minDate.clone(); - } - - if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { - calendar[row][col] = this.maxDate.clone(); - } - - } - - //make the calendar object available to hoverDate/clickDate - if (side == 'left') { - this.leftCalendar.calendar = calendar; - } else { - this.rightCalendar.calendar = calendar; - } - - // - // Display the calendar - // - - var minDate = side == 'left' ? this.minDate : this.startDate; - var maxDate = this.maxDate; - var selected = side == 'left' ? this.startDate : this.endDate; - var arrow = this.locale.direction == 'ltr' ? {left: 'chevron-left', right: 'chevron-right'} : {left: 'chevron-right', right: 'chevron-left'}; - - var html = ''; - html += ''; - html += ''; - - // add empty cell for week number - if (this.showWeekNumbers || this.showISOWeekNumbers) - html += ''; - - if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side == 'left')) { - html += ''; - } else { - html += ''; - } - - var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); - - if (this.showDropdowns) { - var currentMonth = calendar[1][1].month(); - var currentYear = calendar[1][1].year(); - var maxYear = (maxDate && maxDate.year()) || (this.maxYear); - var minYear = (minDate && minDate.year()) || (this.minYear); - var inMinYear = currentYear == minYear; - var inMaxYear = currentYear == maxYear; - - var monthHtml = '"; - - var yearHtml = ''; - - dateHtml = monthHtml + yearHtml; - } - - html += ''; - if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side == 'right' || this.singleDatePicker)) { - html += ''; - } else { - html += ''; - } - - html += ''; - html += ''; - - // add week number label - if (this.showWeekNumbers || this.showISOWeekNumbers) - html += ''; - - $.each(this.locale.daysOfWeek, function(index, dayOfWeek) { - html += ''; - }); - - html += ''; - html += ''; - html += ''; - - //adjust maxDate to reflect the maxSpan setting in order to - //grey out end dates beyond the maxSpan - if (this.endDate == null && this.maxSpan) { - var maxLimit = this.startDate.clone().add(this.maxSpan).endOf('day'); - if (!maxDate || maxLimit.isBefore(maxDate)) { - maxDate = maxLimit; - } - } - - for (var row = 0; row < 6; row++) { - html += ''; - - // add week number - if (this.showWeekNumbers) - html += ''; - else if (this.showISOWeekNumbers) - html += ''; - - for (var col = 0; col < 7; col++) { - - var classes = []; - - //highlight today's date - if (calendar[row][col].isSame(new Date(), "day")) - classes.push('today'); - - //highlight weekends - if (calendar[row][col].isoWeekday() > 5) - classes.push('weekend'); - - //grey out the dates in other months displayed at beginning and end of this calendar - if (calendar[row][col].month() != calendar[1][1].month()) - classes.push('off', 'ends'); - - //don't allow selection of dates before the minimum date - if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day')) - classes.push('off', 'disabled'); - - //don't allow selection of dates after the maximum date - if (maxDate && calendar[row][col].isAfter(maxDate, 'day')) - classes.push('off', 'disabled'); - - //don't allow selection of date if a custom function decides it's invalid - if (this.isInvalidDate(calendar[row][col])) - classes.push('off', 'disabled'); - - //highlight the currently selected start date - if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) - classes.push('active', 'start-date'); - - //highlight the currently selected end date - if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) - classes.push('active', 'end-date'); - - //highlight dates in-between the selected dates - if (this.endDate != null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate) - classes.push('in-range'); - - //apply custom classes for this date - var isCustom = this.isCustomDate(calendar[row][col]); - if (isCustom !== false) { - if (typeof isCustom === 'string') - classes.push(isCustom); - else - Array.prototype.push.apply(classes, isCustom); - } - - var cname = '', disabled = false; - for (var i = 0; i < classes.length; i++) { - cname += classes[i] + ' '; - if (classes[i] == 'disabled') - disabled = true; - } - if (!disabled) - cname += 'available'; - - html += ''; - - } - html += ''; - } - - html += ''; - html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][0].isoWeek() + '' + calendar[row][col].date() + '
'; - - this.container.find('.drp-calendar.' + side + ' .calendar-table').html(html); - - }, - - renderTimePicker: function(side) { - - // Don't bother updating the time picker if it's currently disabled - // because an end date hasn't been clicked yet - if (side == 'right' && !this.endDate) return; - - var html, selected, minDate, maxDate = this.maxDate; - - if (this.maxSpan && (!this.maxDate || this.startDate.clone().add(this.maxSpan).isBefore(this.maxDate))) - maxDate = this.startDate.clone().add(this.maxSpan); - - if (side == 'left') { - selected = this.startDate.clone(); - minDate = this.minDate; - } else if (side == 'right') { - selected = this.endDate.clone(); - minDate = this.startDate; - - //Preserve the time already selected - var timeSelector = this.container.find('.drp-calendar.right .calendar-time'); - if (timeSelector.html() != '') { - - selected.hour(!isNaN(selected.hour()) ? selected.hour() : timeSelector.find('.hourselect option:selected').val()); - selected.minute(!isNaN(selected.minute()) ? selected.minute() : timeSelector.find('.minuteselect option:selected').val()); - selected.second(!isNaN(selected.second()) ? selected.second() : timeSelector.find('.secondselect option:selected').val()); - - if (!this.timePicker24Hour) { - var ampm = timeSelector.find('.ampmselect option:selected').val(); - if (ampm === 'PM' && selected.hour() < 12) - selected.hour(selected.hour() + 12); - if (ampm === 'AM' && selected.hour() === 12) - selected.hour(0); - } - - } - - if (selected.isBefore(this.startDate)) - selected = this.startDate.clone(); - - if (maxDate && selected.isAfter(maxDate)) - selected = maxDate.clone(); - - } - - // - // hours - // - - html = ' '; - - // - // minutes - // - - html += ': '; - - // - // seconds - // - - if (this.timePickerSeconds) { - html += ': '; - } - - // - // AM/PM - // - - if (!this.timePicker24Hour) { - html += ''; - } - - this.container.find('.drp-calendar.' + side + ' .calendar-time').html(html); - - }, - - updateFormInputs: function() { - - if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) { - this.container.find('button.applyBtn').prop('disabled', false); - } else { - this.container.find('button.applyBtn').prop('disabled', true); - } - - }, - - move: function() { - var parentOffset = { top: 0, left: 0 }, - containerTop, - drops = this.drops; - - var parentRightEdge = $(window).width(); - if (!this.parentEl.is('body')) { - parentOffset = { - top: this.parentEl.offset().top - this.parentEl.scrollTop(), - left: this.parentEl.offset().left - this.parentEl.scrollLeft() - }; - parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; - } - - switch (drops) { - case 'auto': - containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; - if (containerTop + this.container.outerHeight() >= this.parentEl[0].scrollHeight) { - containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; - drops = 'up'; - } - break; - case 'up': - containerTop = this.element.offset().top - this.container.outerHeight() - parentOffset.top; - break; - default: - containerTop = this.element.offset().top + this.element.outerHeight() - parentOffset.top; - break; - } - - // Force the container to it's actual width - this.container.css({ - top: 0, - left: 0, - right: 'auto' - }); - var containerWidth = this.container.outerWidth(); - - this.container.toggleClass('drop-up', drops == 'up'); - - if (this.opens == 'left') { - var containerRight = parentRightEdge - this.element.offset().left - this.element.outerWidth(); - if (containerWidth + containerRight > $(window).width()) { - this.container.css({ - top: containerTop, - right: 'auto', - left: 9 - }); - } else { - this.container.css({ - top: containerTop, - right: containerRight, - left: 'auto' - }); - } - } else if (this.opens == 'center') { - var containerLeft = this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 - - containerWidth / 2; - if (containerLeft < 0) { - this.container.css({ - top: containerTop, - right: 'auto', - left: 9 - }); - } else if (containerLeft + containerWidth > $(window).width()) { - this.container.css({ - top: containerTop, - left: 'auto', - right: 0 - }); - } else { - this.container.css({ - top: containerTop, - left: containerLeft, - right: 'auto' - }); - } - } else { - var containerLeft = this.element.offset().left - parentOffset.left; - if (containerLeft + containerWidth > $(window).width()) { - this.container.css({ - top: containerTop, - left: 'auto', - right: 0 - }); - } else { - this.container.css({ - top: containerTop, - left: containerLeft, - right: 'auto' - }); - } - } - }, - - show: function(e) { - if (this.isShowing) return; - - // Create a click proxy that is private to this instance of datepicker, for unbinding - this._outsideClickProxy = $.proxy(function(e) { this.outsideClick(e); }, this); - - // Bind global datepicker mousedown for hiding and - $(document) - .on('mousedown.daterangepicker', this._outsideClickProxy) - // also support mobile devices - .on('touchend.daterangepicker', this._outsideClickProxy) - // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them - .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) - // and also close when focus changes to outside the picker (eg. tabbing between controls) - .on('focusin.daterangepicker', this._outsideClickProxy); - - // Reposition the picker if the window is resized while it's open - $(window).on('resize.daterangepicker', $.proxy(function(e) { this.move(e); }, this)); - - this.oldStartDate = this.startDate.clone(); - this.oldEndDate = this.endDate.clone(); - this.previousRightTime = this.endDate.clone(); - - this.updateView(); - this.container.show(); - this.move(); - this.element.trigger('show.daterangepicker', this); - this.isShowing = true; - }, - - hide: function(e) { - if (!this.isShowing) return; - - //incomplete date selection, revert to last values - if (!this.endDate) { - this.startDate = this.oldStartDate.clone(); - this.endDate = this.oldEndDate.clone(); - } - - //if a new date range was selected, invoke the user callback function - if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) - this.callback(this.startDate.clone(), this.endDate.clone(), this.chosenLabel); - - //if picker is attached to a text input, update it - this.updateElement(); - - $(document).off('.daterangepicker'); - $(window).off('.daterangepicker'); - this.container.hide(); - this.element.trigger('hide.daterangepicker', this); - this.isShowing = false; - }, - - toggle: function(e) { - if (this.isShowing) { - this.hide(); - } else { - this.show(); - } - }, - - outsideClick: function(e) { - var target = $(e.target); - // if the page is clicked anywhere except within the daterangerpicker/button - // itself then call this.hide() - if ( - // ie modal dialog fix - e.type == "focusin" || - target.closest(this.element).length || - target.closest(this.container).length || - target.closest('.calendar-table').length - ) return; - this.hide(); - this.element.trigger('outsideClick.daterangepicker', this); - }, - - showCalendars: function() { - this.container.addClass('show-calendar'); - this.move(); - this.element.trigger('showCalendar.daterangepicker', this); - }, - - hideCalendars: function() { - this.container.removeClass('show-calendar'); - this.element.trigger('hideCalendar.daterangepicker', this); - }, - - clickRange: function(e) { - var label = e.target.getAttribute('data-range-key'); - this.chosenLabel = label; - if (label == this.locale.customRangeLabel) { - this.showCalendars(); - } else { - var dates = this.ranges[label]; - this.startDate = dates[0]; - this.endDate = dates[1]; - - if (!this.timePicker) { - this.startDate.startOf('day'); - this.endDate.endOf('day'); - } - - if (!this.alwaysShowCalendars) - this.hideCalendars(); - this.clickApply(); - } - }, - - clickPrev: function(e) { - var cal = $(e.target).parents('.drp-calendar'); - if (cal.hasClass('left')) { - this.leftCalendar.month.subtract(1, 'month'); - if (this.linkedCalendars) - this.rightCalendar.month.subtract(1, 'month'); - } else { - this.rightCalendar.month.subtract(1, 'month'); - } - this.updateCalendars(); - }, - - clickNext: function(e) { - var cal = $(e.target).parents('.drp-calendar'); - if (cal.hasClass('left')) { - this.leftCalendar.month.add(1, 'month'); - } else { - this.rightCalendar.month.add(1, 'month'); - if (this.linkedCalendars) - this.leftCalendar.month.add(1, 'month'); - } - this.updateCalendars(); - }, - - hoverDate: function(e) { - - //ignore dates that can't be selected - if (!$(e.target).hasClass('available')) return; - - var title = $(e.target).attr('data-title'); - var row = title.substr(1, 1); - var col = title.substr(3, 1); - var cal = $(e.target).parents('.drp-calendar'); - var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; - - //highlight the dates between the start date and the date being hovered as a potential end date - var leftCalendar = this.leftCalendar; - var rightCalendar = this.rightCalendar; - var startDate = this.startDate; - if (!this.endDate) { - this.container.find('.drp-calendar tbody td').each(function(index, el) { - - //skip week numbers, only look at dates - if ($(el).hasClass('week')) return; - - var title = $(el).attr('data-title'); - var row = title.substr(1, 1); - var col = title.substr(3, 1); - var cal = $(el).parents('.drp-calendar'); - var dt = cal.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col]; - - if ((dt.isAfter(startDate) && dt.isBefore(date)) || dt.isSame(date, 'day')) { - $(el).addClass('in-range'); - } else { - $(el).removeClass('in-range'); - } - - }); - } - - }, - - clickDate: function(e) { - - if (!$(e.target).hasClass('available')) return; - - var title = $(e.target).attr('data-title'); - var row = title.substr(1, 1); - var col = title.substr(3, 1); - var cal = $(e.target).parents('.drp-calendar'); - var date = cal.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; - - // - // this function needs to do a few things: - // * alternate between selecting a start and end date for the range, - // * if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date - // * if autoapply is enabled, and an end date was chosen, apply the selection - // * if single date picker mode, and time picker isn't enabled, apply the selection immediately - // * if one of the inputs above the calendars was focused, cancel that manual input - // - - if (this.endDate || date.isBefore(this.startDate, 'day')) { //picking start - if (this.timePicker) { - var hour = parseInt(this.container.find('.left .hourselect').val(), 10); - if (!this.timePicker24Hour) { - var ampm = this.container.find('.left .ampmselect').val(); - if (ampm === 'PM' && hour < 12) - hour += 12; - if (ampm === 'AM' && hour === 12) - hour = 0; - } - var minute = parseInt(this.container.find('.left .minuteselect').val(), 10); - if (isNaN(minute)) { - minute = parseInt(this.container.find('.left .minuteselect option:last').val(), 10); - } - var second = this.timePickerSeconds ? parseInt(this.container.find('.left .secondselect').val(), 10) : 0; - date = date.clone().hour(hour).minute(minute).second(second); - } - this.endDate = null; - this.setStartDate(date.clone()); - } else if (!this.endDate && date.isBefore(this.startDate)) { - //special case: clicking the same date for start/end, - //but the time of the end date is before the start date - this.setEndDate(this.startDate.clone()); - } else { // picking end - if (this.timePicker) { - var hour = parseInt(this.container.find('.right .hourselect').val(), 10); - if (!this.timePicker24Hour) { - var ampm = this.container.find('.right .ampmselect').val(); - if (ampm === 'PM' && hour < 12) - hour += 12; - if (ampm === 'AM' && hour === 12) - hour = 0; - } - var minute = parseInt(this.container.find('.right .minuteselect').val(), 10); - if (isNaN(minute)) { - minute = parseInt(this.container.find('.right .minuteselect option:last').val(), 10); - } - var second = this.timePickerSeconds ? parseInt(this.container.find('.right .secondselect').val(), 10) : 0; - date = date.clone().hour(hour).minute(minute).second(second); - } - this.setEndDate(date.clone()); - if (this.autoApply) { - this.calculateChosenLabel(); - this.clickApply(); - } - } - - if (this.singleDatePicker) { - this.setEndDate(this.startDate); - if (!this.timePicker && this.autoApply) - this.clickApply(); - } - - this.updateView(); - - //This is to cancel the blur event handler if the mouse was in one of the inputs - e.stopPropagation(); - - }, - - calculateChosenLabel: function () { - var customRange = true; - var i = 0; - for (var range in this.ranges) { - if (this.timePicker) { - var format = this.timePickerSeconds ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD HH:mm"; - //ignore times when comparing dates if time picker seconds is not enabled - if (this.startDate.format(format) == this.ranges[range][0].format(format) && this.endDate.format(format) == this.ranges[range][1].format(format)) { - customRange = false; - this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').attr('data-range-key'); - break; - } - } else { - //ignore times when comparing dates if time picker is not enabled - if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { - customRange = false; - this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')').addClass('active').attr('data-range-key'); - break; - } - } - i++; - } - if (customRange) { - if (this.showCustomRangeLabel) { - this.chosenLabel = this.container.find('.ranges li:last').addClass('active').attr('data-range-key'); - } else { - this.chosenLabel = null; - } - this.showCalendars(); - } - }, - - clickApply: function(e) { - this.hide(); - this.element.trigger('apply.daterangepicker', this); - }, - - clickCancel: function(e) { - this.startDate = this.oldStartDate; - this.endDate = this.oldEndDate; - this.hide(); - this.element.trigger('cancel.daterangepicker', this); - }, - - monthOrYearChanged: function(e) { - var isLeft = $(e.target).closest('.drp-calendar').hasClass('left'), - leftOrRight = isLeft ? 'left' : 'right', - cal = this.container.find('.drp-calendar.'+leftOrRight); - - // Month must be Number for new moment versions - var month = parseInt(cal.find('.monthselect').val(), 10); - var year = cal.find('.yearselect').val(); - - if (!isLeft) { - if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { - month = this.startDate.month(); - year = this.startDate.year(); - } - } - - if (this.minDate) { - if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { - month = this.minDate.month(); - year = this.minDate.year(); - } - } - - if (this.maxDate) { - if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { - month = this.maxDate.month(); - year = this.maxDate.year(); - } - } - - if (isLeft) { - this.leftCalendar.month.month(month).year(year); - if (this.linkedCalendars) - this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); - } else { - this.rightCalendar.month.month(month).year(year); - if (this.linkedCalendars) - this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); - } - this.updateCalendars(); - }, - - timeChanged: function(e) { - - var cal = $(e.target).closest('.drp-calendar'), - isLeft = cal.hasClass('left'); - - var hour = parseInt(cal.find('.hourselect').val(), 10); - var minute = parseInt(cal.find('.minuteselect').val(), 10); - if (isNaN(minute)) { - minute = parseInt(cal.find('.minuteselect option:last').val(), 10); - } - var second = this.timePickerSeconds ? parseInt(cal.find('.secondselect').val(), 10) : 0; - - if (!this.timePicker24Hour) { - var ampm = cal.find('.ampmselect').val(); - if (ampm === 'PM' && hour < 12) - hour += 12; - if (ampm === 'AM' && hour === 12) - hour = 0; - } - - if (isLeft) { - var start = this.startDate.clone(); - start.hour(hour); - start.minute(minute); - start.second(second); - this.setStartDate(start); - if (this.singleDatePicker) { - this.endDate = this.startDate.clone(); - } else if (this.endDate && this.endDate.format('YYYY-MM-DD') == start.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { - this.setEndDate(start.clone()); - } - } else if (this.endDate) { - var end = this.endDate.clone(); - end.hour(hour); - end.minute(minute); - end.second(second); - this.setEndDate(end); - } - - //update the calendars so all clickable dates reflect the new time component - this.updateCalendars(); - - //update the form inputs above the calendars with the new time - this.updateFormInputs(); - - //re-render the time pickers because changing one selection can affect what's enabled in another - this.renderTimePicker('left'); - this.renderTimePicker('right'); - - }, - - elementChanged: function() { - if (!this.element.is('input')) return; - if (!this.element.val().length) return; - - var dateString = this.element.val().split(this.locale.separator), - start = null, - end = null; - - if (dateString.length === 2) { - start = moment(dateString[0], this.locale.format); - end = moment(dateString[1], this.locale.format); - } - - if (this.singleDatePicker || start === null || end === null) { - start = moment(this.element.val(), this.locale.format); - end = start; - } - - if (!start.isValid() || !end.isValid()) return; - - this.setStartDate(start); - this.setEndDate(end); - this.updateView(); - }, - - keydown: function(e) { - //hide on tab or enter - if ((e.keyCode === 9) || (e.keyCode === 13)) { - this.hide(); - } - - //hide on esc and prevent propagation - if (e.keyCode === 27) { - e.preventDefault(); - e.stopPropagation(); - - this.hide(); - } - }, - - updateElement: function() { - if (this.element.is('input') && this.autoUpdateInput) { - var newValue = this.startDate.format(this.locale.format); - if (!this.singleDatePicker) { - newValue += this.locale.separator + this.endDate.format(this.locale.format); - } - if (newValue !== this.element.val()) { - this.element.val(newValue).trigger('change'); - } - } - }, - - remove: function() { - this.container.remove(); - this.element.off('.daterangepicker'); - this.element.removeData(); - } - - }; - - $.fn.daterangepicker = function(options, callback) { - var implementOptions = $.extend(true, {}, $.fn.daterangepicker.defaultOptions, options); - this.each(function() { - var el = $(this); - if (el.data('daterangepicker')) - el.data('daterangepicker').remove(); - el.data('daterangepicker', new DateRangePicker(el, implementOptions, callback)); - }); - return this; - }; - - return DateRangePicker; - -})); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap/css/bootstrap.css b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap/css/bootstrap.css deleted file mode 100644 index f16c5be871..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap/css/bootstrap.css +++ /dev/null @@ -1,11266 +0,0 @@ -@charset "UTF-8"; -/*! - * Bootstrap v5.1.3 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -:root { - --bs-blue: #0d6efd; - --bs-indigo: #6610f2; - --bs-purple: #6f42c1; - --bs-pink: #d63384; - --bs-red: #dc3545; - --bs-orange: #fd7e14; - --bs-yellow: #ffc107; - --bs-green: #198754; - --bs-teal: #20c997; - --bs-cyan: #0dcaf0; - --bs-white: #fff; - --bs-gray: #6c757d; - --bs-gray-dark: #343a40; - --bs-gray-100: #f8f9fa; - --bs-gray-200: #e9ecef; - --bs-gray-300: #dee2e6; - --bs-gray-400: #ced4da; - --bs-gray-500: #adb5bd; - --bs-gray-600: #6c757d; - --bs-gray-700: #495057; - --bs-gray-800: #343a40; - --bs-gray-900: #212529; - --bs-primary: #0d6efd; - --bs-secondary: #6c757d; - --bs-success: #198754; - --bs-info: #0dcaf0; - --bs-warning: #ffc107; - --bs-danger: #dc3545; - --bs-light: #f8f9fa; - --bs-dark: #212529; - --bs-primary-rgb: 13, 110, 253; - --bs-secondary-rgb: 108, 117, 125; - --bs-success-rgb: 25, 135, 84; - --bs-info-rgb: 13, 202, 240; - --bs-warning-rgb: 255, 193, 7; - --bs-danger-rgb: 220, 53, 69; - --bs-light-rgb: 248, 249, 250; - --bs-dark-rgb: 33, 37, 41; - --bs-white-rgb: 255, 255, 255; - --bs-black-rgb: 0, 0, 0; - --bs-body-color-rgb: 33, 37, 41; - --bs-body-bg-rgb: 255, 255, 255; - --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); - --bs-body-font-family: var(--bs-font-sans-serif); - --bs-body-font-size: 1rem; - --bs-body-font-weight: 400; - --bs-body-line-height: 1.5; - --bs-body-color: #212529; - --bs-body-bg: #fff; -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -@media (prefers-reduced-motion: no-preference) { - :root { - scroll-behavior: smooth; - } -} - -body { - margin: 0; - font-family: var(--bs-body-font-family); - font-size: var(--bs-body-font-size); - font-weight: var(--bs-body-font-weight); - line-height: var(--bs-body-line-height); - color: var(--bs-body-color); - text-align: var(--bs-body-text-align); - background-color: var(--bs-body-bg); - -webkit-text-size-adjust: 100%; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - -hr { - margin: 1rem 0; - color: inherit; - background-color: currentColor; - border: 0; - opacity: 0.25; -} - -hr:not([size]) { - height: 1px; -} - -h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { - margin-top: 0; - margin-bottom: 0.5rem; - font-weight: 500; - line-height: 1.2; -} - -h1, .h1 { - font-size: calc(1.375rem + 1.5vw); -} -@media (min-width: 1200px) { - h1, .h1 { - font-size: 2.5rem; - } -} - -h2, .h2 { - font-size: calc(1.325rem + 0.9vw); -} -@media (min-width: 1200px) { - h2, .h2 { - font-size: 2rem; - } -} - -h3, .h3 { - font-size: calc(1.3rem + 0.6vw); -} -@media (min-width: 1200px) { - h3, .h3 { - font-size: 1.75rem; - } -} - -h4, .h4 { - font-size: calc(1.275rem + 0.3vw); -} -@media (min-width: 1200px) { - h4, .h4 { - font-size: 1.5rem; - } -} - -h5, .h5 { - font-size: 1.25rem; -} - -h6, .h6 { - font-size: 1rem; -} - -p { - margin-top: 0; - margin-bottom: 1rem; -} - -abbr[title], -abbr[data-bs-original-title] { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - cursor: help; - -webkit-text-decoration-skip-ink: none; - text-decoration-skip-ink: none; -} - -address { - margin-bottom: 1rem; - font-style: normal; - line-height: inherit; -} - -ol, -ul { - padding-left: 2rem; -} - -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: 0.5rem; - margin-left: 0; -} - -blockquote { - margin: 0 0 1rem; -} - -b, -strong { - font-weight: bolder; -} - -small, .small { - font-size: 0.875em; -} - -mark, .mark { - padding: 0.2em; - background-color: #fcf8e3; -} - -sub, -sup { - position: relative; - font-size: 0.75em; - line-height: 0; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -a { - color: #0d6efd; - text-decoration: underline; -} -a:hover { - color: #0a58ca; -} - -a:not([href]):not([class]), a:not([href]):not([class]):hover { - color: inherit; - text-decoration: none; -} - -pre, -code, -kbd, -samp { - font-family: var(--bs-font-monospace); - font-size: 1em; - direction: ltr /* rtl:ignore */; - unicode-bidi: bidi-override; -} - -pre { - display: block; - margin-top: 0; - margin-bottom: 1rem; - overflow: auto; - font-size: 0.875em; -} -pre code { - font-size: inherit; - color: inherit; - word-break: normal; -} - -code { - font-size: 0.875em; - color: #d63384; - word-wrap: break-word; -} -a > code { - color: inherit; -} - -kbd { - padding: 0.2rem 0.4rem; - font-size: 0.875em; - color: #fff; - background-color: #212529; - border-radius: 0.2rem; -} -kbd kbd { - padding: 0; - font-size: 1em; - font-weight: 700; -} - -figure { - margin: 0 0 1rem; -} - -img, -svg { - vertical-align: middle; -} - -table { - caption-side: bottom; - border-collapse: collapse; -} - -caption { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - color: #6c757d; - text-align: left; -} - -th { - text-align: inherit; - text-align: -webkit-match-parent; -} - -thead, -tbody, -tfoot, -tr, -td, -th { - border-color: inherit; - border-style: solid; - border-width: 0; -} - -label { - display: inline-block; -} - -button { - border-radius: 0; -} - -button:focus:not(:focus-visible) { - outline: 0; -} - -input, -button, -select, -optgroup, -textarea { - margin: 0; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -button, -select { - text-transform: none; -} - -[role=button] { - cursor: pointer; -} - -select { - word-wrap: normal; -} -select:disabled { - opacity: 1; -} - -[list]::-webkit-calendar-picker-indicator { - display: none; -} - -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; -} - -::-moz-focus-inner { - padding: 0; - border-style: none; -} - -textarea { - resize: vertical; -} - -fieldset { - min-width: 0; - padding: 0; - margin: 0; - border: 0; -} - -legend { - float: left; - width: 100%; - padding: 0; - margin-bottom: 0.5rem; - font-size: calc(1.275rem + 0.3vw); - line-height: inherit; -} -@media (min-width: 1200px) { - legend { - font-size: 1.5rem; - } -} -legend + * { - clear: left; -} - -::-webkit-datetime-edit-fields-wrapper, -::-webkit-datetime-edit-text, -::-webkit-datetime-edit-minute, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-year-field { - padding: 0; -} - -::-webkit-inner-spin-button { - height: auto; -} - -[type=search] { - outline-offset: -2px; - -webkit-appearance: textfield; -} - -/* rtl:raw: -[type="tel"], -[type="url"], -[type="email"], -[type="number"] { - direction: ltr; -} -*/ -::-webkit-search-decoration { - -webkit-appearance: none; -} - -::-webkit-color-swatch-wrapper { - padding: 0; -} - -::-webkit-file-upload-button { - font: inherit; -} - -::file-selector-button { - font: inherit; -} - -::-webkit-file-upload-button { - font: inherit; - -webkit-appearance: button; -} - -output { - display: inline-block; -} - -iframe { - border: 0; -} - -summary { - display: list-item; - cursor: pointer; -} - -progress { - vertical-align: baseline; -} - -[hidden] { - display: none !important; -} - -.lead { - font-size: 1.25rem; - font-weight: 300; -} - -.display-1 { - font-size: calc(1.625rem + 4.5vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-1 { - font-size: 5rem; - } -} - -.display-2 { - font-size: calc(1.575rem + 3.9vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-2 { - font-size: 4.5rem; - } -} - -.display-3 { - font-size: calc(1.525rem + 3.3vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-3 { - font-size: 4rem; - } -} - -.display-4 { - font-size: calc(1.475rem + 2.7vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-4 { - font-size: 3.5rem; - } -} - -.display-5 { - font-size: calc(1.425rem + 2.1vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-5 { - font-size: 3rem; - } -} - -.display-6 { - font-size: calc(1.375rem + 1.5vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-6 { - font-size: 2.5rem; - } -} - -.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: 0.875em; - text-transform: uppercase; -} - -.blockquote { - margin-bottom: 1rem; - font-size: 1.25rem; -} -.blockquote > :last-child { - margin-bottom: 0; -} - -.blockquote-footer { - margin-top: -1rem; - margin-bottom: 1rem; - font-size: 0.875em; - color: #6c757d; -} -.blockquote-footer::before { - content: "— "; -} - -.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: 0.875em; - color: #6c757d; -} - -.container, -.container-fluid, -.container-xxl, -.container-xl, -.container-lg, -.container-md, -.container-sm { - width: 100%; - padding-right: var(--bs-gutter-x, 0.75rem); - padding-left: var(--bs-gutter-x, 0.75rem); - margin-right: auto; - margin-left: auto; -} - -@media (min-width: 576px) { - .container-sm, .container { - max-width: 540px; - } -} -@media (min-width: 768px) { - .container-md, .container-sm, .container { - max-width: 720px; - } -} -@media (min-width: 992px) { - .container-lg, .container-md, .container-sm, .container { - max-width: 960px; - } -} -@media (min-width: 1200px) { - .container-xl, .container-lg, .container-md, .container-sm, .container { - max-width: 1140px; - } -} -@media (min-width: 1400px) { - .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { - max-width: 1320px; - } -} -.row { - --bs-gutter-x: 1.5rem; - --bs-gutter-y: 0; - display: flex; - flex-wrap: wrap; - margin-top: calc(-1 * var(--bs-gutter-y)); - margin-right: calc(-0.5 * var(--bs-gutter-x)); - margin-left: calc(-0.5 * var(--bs-gutter-x)); -} -.row > * { - flex-shrink: 0; - width: 100%; - max-width: 100%; - padding-right: calc(var(--bs-gutter-x) * 0.5); - padding-left: calc(var(--bs-gutter-x) * 0.5); - margin-top: var(--bs-gutter-y); -} - -.col { - flex: 1 0 0%; -} - -.row-cols-auto > * { - flex: 0 0 auto; - width: auto; -} - -.row-cols-1 > * { - flex: 0 0 auto; - width: 100%; -} - -.row-cols-2 > * { - flex: 0 0 auto; - width: 50%; -} - -.row-cols-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; -} - -.row-cols-4 > * { - flex: 0 0 auto; - width: 25%; -} - -.row-cols-5 > * { - flex: 0 0 auto; - width: 20%; -} - -.row-cols-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; -} - -.col-auto { - flex: 0 0 auto; - width: auto; -} - -.col-1 { - flex: 0 0 auto; - width: 8.33333333%; -} - -.col-2 { - flex: 0 0 auto; - width: 16.66666667%; -} - -.col-3 { - flex: 0 0 auto; - width: 25%; -} - -.col-4 { - flex: 0 0 auto; - width: 33.33333333%; -} - -.col-5 { - flex: 0 0 auto; - width: 41.66666667%; -} - -.col-6 { - flex: 0 0 auto; - width: 50%; -} - -.col-7 { - flex: 0 0 auto; - width: 58.33333333%; -} - -.col-8 { - flex: 0 0 auto; - width: 66.66666667%; -} - -.col-9 { - flex: 0 0 auto; - width: 75%; -} - -.col-10 { - flex: 0 0 auto; - width: 83.33333333%; -} - -.col-11 { - flex: 0 0 auto; - width: 91.66666667%; -} - -.col-12 { - flex: 0 0 auto; - width: 100%; -} - -.offset-1 { - margin-left: 8.33333333%; -} - -.offset-2 { - margin-left: 16.66666667%; -} - -.offset-3 { - margin-left: 25%; -} - -.offset-4 { - margin-left: 33.33333333%; -} - -.offset-5 { - margin-left: 41.66666667%; -} - -.offset-6 { - margin-left: 50%; -} - -.offset-7 { - margin-left: 58.33333333%; -} - -.offset-8 { - margin-left: 66.66666667%; -} - -.offset-9 { - margin-left: 75%; -} - -.offset-10 { - margin-left: 83.33333333%; -} - -.offset-11 { - margin-left: 91.66666667%; -} - -.g-0, -.gx-0 { - --bs-gutter-x: 0; -} - -.g-0, -.gy-0 { - --bs-gutter-y: 0; -} - -.g-1, -.gx-1 { - --bs-gutter-x: 0.25rem; -} - -.g-1, -.gy-1 { - --bs-gutter-y: 0.25rem; -} - -.g-2, -.gx-2 { - --bs-gutter-x: 0.5rem; -} - -.g-2, -.gy-2 { - --bs-gutter-y: 0.5rem; -} - -.g-3, -.gx-3 { - --bs-gutter-x: 1rem; -} - -.g-3, -.gy-3 { - --bs-gutter-y: 1rem; -} - -.g-4, -.gx-4 { - --bs-gutter-x: 1.5rem; -} - -.g-4, -.gy-4 { - --bs-gutter-y: 1.5rem; -} - -.g-5, -.gx-5 { - --bs-gutter-x: 3rem; -} - -.g-5, -.gy-5 { - --bs-gutter-y: 3rem; -} - -@media (min-width: 576px) { - .col-sm { - flex: 1 0 0%; - } - - .row-cols-sm-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-sm-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-sm-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-sm-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-sm-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-sm-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-sm-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - - .col-sm-auto { - flex: 0 0 auto; - width: auto; - } - - .col-sm-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - - .col-sm-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - - .col-sm-3 { - flex: 0 0 auto; - width: 25%; - } - - .col-sm-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - - .col-sm-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - - .col-sm-6 { - flex: 0 0 auto; - width: 50%; - } - - .col-sm-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - - .col-sm-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - - .col-sm-9 { - flex: 0 0 auto; - width: 75%; - } - - .col-sm-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - - .col-sm-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - - .col-sm-12 { - flex: 0 0 auto; - width: 100%; - } - - .offset-sm-0 { - margin-left: 0; - } - - .offset-sm-1 { - margin-left: 8.33333333%; - } - - .offset-sm-2 { - margin-left: 16.66666667%; - } - - .offset-sm-3 { - margin-left: 25%; - } - - .offset-sm-4 { - margin-left: 33.33333333%; - } - - .offset-sm-5 { - margin-left: 41.66666667%; - } - - .offset-sm-6 { - margin-left: 50%; - } - - .offset-sm-7 { - margin-left: 58.33333333%; - } - - .offset-sm-8 { - margin-left: 66.66666667%; - } - - .offset-sm-9 { - margin-left: 75%; - } - - .offset-sm-10 { - margin-left: 83.33333333%; - } - - .offset-sm-11 { - margin-left: 91.66666667%; - } - - .g-sm-0, -.gx-sm-0 { - --bs-gutter-x: 0; - } - - .g-sm-0, -.gy-sm-0 { - --bs-gutter-y: 0; - } - - .g-sm-1, -.gx-sm-1 { - --bs-gutter-x: 0.25rem; - } - - .g-sm-1, -.gy-sm-1 { - --bs-gutter-y: 0.25rem; - } - - .g-sm-2, -.gx-sm-2 { - --bs-gutter-x: 0.5rem; - } - - .g-sm-2, -.gy-sm-2 { - --bs-gutter-y: 0.5rem; - } - - .g-sm-3, -.gx-sm-3 { - --bs-gutter-x: 1rem; - } - - .g-sm-3, -.gy-sm-3 { - --bs-gutter-y: 1rem; - } - - .g-sm-4, -.gx-sm-4 { - --bs-gutter-x: 1.5rem; - } - - .g-sm-4, -.gy-sm-4 { - --bs-gutter-y: 1.5rem; - } - - .g-sm-5, -.gx-sm-5 { - --bs-gutter-x: 3rem; - } - - .g-sm-5, -.gy-sm-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 768px) { - .col-md { - flex: 1 0 0%; - } - - .row-cols-md-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-md-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-md-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-md-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-md-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-md-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-md-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - - .col-md-auto { - flex: 0 0 auto; - width: auto; - } - - .col-md-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - - .col-md-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - - .col-md-3 { - flex: 0 0 auto; - width: 25%; - } - - .col-md-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - - .col-md-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - - .col-md-6 { - flex: 0 0 auto; - width: 50%; - } - - .col-md-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - - .col-md-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - - .col-md-9 { - flex: 0 0 auto; - width: 75%; - } - - .col-md-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - - .col-md-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - - .col-md-12 { - flex: 0 0 auto; - width: 100%; - } - - .offset-md-0 { - margin-left: 0; - } - - .offset-md-1 { - margin-left: 8.33333333%; - } - - .offset-md-2 { - margin-left: 16.66666667%; - } - - .offset-md-3 { - margin-left: 25%; - } - - .offset-md-4 { - margin-left: 33.33333333%; - } - - .offset-md-5 { - margin-left: 41.66666667%; - } - - .offset-md-6 { - margin-left: 50%; - } - - .offset-md-7 { - margin-left: 58.33333333%; - } - - .offset-md-8 { - margin-left: 66.66666667%; - } - - .offset-md-9 { - margin-left: 75%; - } - - .offset-md-10 { - margin-left: 83.33333333%; - } - - .offset-md-11 { - margin-left: 91.66666667%; - } - - .g-md-0, -.gx-md-0 { - --bs-gutter-x: 0; - } - - .g-md-0, -.gy-md-0 { - --bs-gutter-y: 0; - } - - .g-md-1, -.gx-md-1 { - --bs-gutter-x: 0.25rem; - } - - .g-md-1, -.gy-md-1 { - --bs-gutter-y: 0.25rem; - } - - .g-md-2, -.gx-md-2 { - --bs-gutter-x: 0.5rem; - } - - .g-md-2, -.gy-md-2 { - --bs-gutter-y: 0.5rem; - } - - .g-md-3, -.gx-md-3 { - --bs-gutter-x: 1rem; - } - - .g-md-3, -.gy-md-3 { - --bs-gutter-y: 1rem; - } - - .g-md-4, -.gx-md-4 { - --bs-gutter-x: 1.5rem; - } - - .g-md-4, -.gy-md-4 { - --bs-gutter-y: 1.5rem; - } - - .g-md-5, -.gx-md-5 { - --bs-gutter-x: 3rem; - } - - .g-md-5, -.gy-md-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 992px) { - .col-lg { - flex: 1 0 0%; - } - - .row-cols-lg-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-lg-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-lg-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-lg-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-lg-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-lg-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-lg-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - - .col-lg-auto { - flex: 0 0 auto; - width: auto; - } - - .col-lg-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - - .col-lg-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - - .col-lg-3 { - flex: 0 0 auto; - width: 25%; - } - - .col-lg-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - - .col-lg-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - - .col-lg-6 { - flex: 0 0 auto; - width: 50%; - } - - .col-lg-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - - .col-lg-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - - .col-lg-9 { - flex: 0 0 auto; - width: 75%; - } - - .col-lg-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - - .col-lg-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - - .col-lg-12 { - flex: 0 0 auto; - width: 100%; - } - - .offset-lg-0 { - margin-left: 0; - } - - .offset-lg-1 { - margin-left: 8.33333333%; - } - - .offset-lg-2 { - margin-left: 16.66666667%; - } - - .offset-lg-3 { - margin-left: 25%; - } - - .offset-lg-4 { - margin-left: 33.33333333%; - } - - .offset-lg-5 { - margin-left: 41.66666667%; - } - - .offset-lg-6 { - margin-left: 50%; - } - - .offset-lg-7 { - margin-left: 58.33333333%; - } - - .offset-lg-8 { - margin-left: 66.66666667%; - } - - .offset-lg-9 { - margin-left: 75%; - } - - .offset-lg-10 { - margin-left: 83.33333333%; - } - - .offset-lg-11 { - margin-left: 91.66666667%; - } - - .g-lg-0, -.gx-lg-0 { - --bs-gutter-x: 0; - } - - .g-lg-0, -.gy-lg-0 { - --bs-gutter-y: 0; - } - - .g-lg-1, -.gx-lg-1 { - --bs-gutter-x: 0.25rem; - } - - .g-lg-1, -.gy-lg-1 { - --bs-gutter-y: 0.25rem; - } - - .g-lg-2, -.gx-lg-2 { - --bs-gutter-x: 0.5rem; - } - - .g-lg-2, -.gy-lg-2 { - --bs-gutter-y: 0.5rem; - } - - .g-lg-3, -.gx-lg-3 { - --bs-gutter-x: 1rem; - } - - .g-lg-3, -.gy-lg-3 { - --bs-gutter-y: 1rem; - } - - .g-lg-4, -.gx-lg-4 { - --bs-gutter-x: 1.5rem; - } - - .g-lg-4, -.gy-lg-4 { - --bs-gutter-y: 1.5rem; - } - - .g-lg-5, -.gx-lg-5 { - --bs-gutter-x: 3rem; - } - - .g-lg-5, -.gy-lg-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 1200px) { - .col-xl { - flex: 1 0 0%; - } - - .row-cols-xl-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-xl-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-xl-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-xl-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-xl-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-xl-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-xl-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - - .col-xl-auto { - flex: 0 0 auto; - width: auto; - } - - .col-xl-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - - .col-xl-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - - .col-xl-3 { - flex: 0 0 auto; - width: 25%; - } - - .col-xl-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - - .col-xl-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - - .col-xl-6 { - flex: 0 0 auto; - width: 50%; - } - - .col-xl-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - - .col-xl-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - - .col-xl-9 { - flex: 0 0 auto; - width: 75%; - } - - .col-xl-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - - .col-xl-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - - .col-xl-12 { - flex: 0 0 auto; - width: 100%; - } - - .offset-xl-0 { - margin-left: 0; - } - - .offset-xl-1 { - margin-left: 8.33333333%; - } - - .offset-xl-2 { - margin-left: 16.66666667%; - } - - .offset-xl-3 { - margin-left: 25%; - } - - .offset-xl-4 { - margin-left: 33.33333333%; - } - - .offset-xl-5 { - margin-left: 41.66666667%; - } - - .offset-xl-6 { - margin-left: 50%; - } - - .offset-xl-7 { - margin-left: 58.33333333%; - } - - .offset-xl-8 { - margin-left: 66.66666667%; - } - - .offset-xl-9 { - margin-left: 75%; - } - - .offset-xl-10 { - margin-left: 83.33333333%; - } - - .offset-xl-11 { - margin-left: 91.66666667%; - } - - .g-xl-0, -.gx-xl-0 { - --bs-gutter-x: 0; - } - - .g-xl-0, -.gy-xl-0 { - --bs-gutter-y: 0; - } - - .g-xl-1, -.gx-xl-1 { - --bs-gutter-x: 0.25rem; - } - - .g-xl-1, -.gy-xl-1 { - --bs-gutter-y: 0.25rem; - } - - .g-xl-2, -.gx-xl-2 { - --bs-gutter-x: 0.5rem; - } - - .g-xl-2, -.gy-xl-2 { - --bs-gutter-y: 0.5rem; - } - - .g-xl-3, -.gx-xl-3 { - --bs-gutter-x: 1rem; - } - - .g-xl-3, -.gy-xl-3 { - --bs-gutter-y: 1rem; - } - - .g-xl-4, -.gx-xl-4 { - --bs-gutter-x: 1.5rem; - } - - .g-xl-4, -.gy-xl-4 { - --bs-gutter-y: 1.5rem; - } - - .g-xl-5, -.gx-xl-5 { - --bs-gutter-x: 3rem; - } - - .g-xl-5, -.gy-xl-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 1400px) { - .col-xxl { - flex: 1 0 0%; - } - - .row-cols-xxl-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-xxl-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-xxl-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-xxl-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-xxl-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-xxl-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-xxl-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - - .col-xxl-auto { - flex: 0 0 auto; - width: auto; - } - - .col-xxl-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - - .col-xxl-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - - .col-xxl-3 { - flex: 0 0 auto; - width: 25%; - } - - .col-xxl-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - - .col-xxl-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - - .col-xxl-6 { - flex: 0 0 auto; - width: 50%; - } - - .col-xxl-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - - .col-xxl-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - - .col-xxl-9 { - flex: 0 0 auto; - width: 75%; - } - - .col-xxl-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - - .col-xxl-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - - .col-xxl-12 { - flex: 0 0 auto; - width: 100%; - } - - .offset-xxl-0 { - margin-left: 0; - } - - .offset-xxl-1 { - margin-left: 8.33333333%; - } - - .offset-xxl-2 { - margin-left: 16.66666667%; - } - - .offset-xxl-3 { - margin-left: 25%; - } - - .offset-xxl-4 { - margin-left: 33.33333333%; - } - - .offset-xxl-5 { - margin-left: 41.66666667%; - } - - .offset-xxl-6 { - margin-left: 50%; - } - - .offset-xxl-7 { - margin-left: 58.33333333%; - } - - .offset-xxl-8 { - margin-left: 66.66666667%; - } - - .offset-xxl-9 { - margin-left: 75%; - } - - .offset-xxl-10 { - margin-left: 83.33333333%; - } - - .offset-xxl-11 { - margin-left: 91.66666667%; - } - - .g-xxl-0, -.gx-xxl-0 { - --bs-gutter-x: 0; - } - - .g-xxl-0, -.gy-xxl-0 { - --bs-gutter-y: 0; - } - - .g-xxl-1, -.gx-xxl-1 { - --bs-gutter-x: 0.25rem; - } - - .g-xxl-1, -.gy-xxl-1 { - --bs-gutter-y: 0.25rem; - } - - .g-xxl-2, -.gx-xxl-2 { - --bs-gutter-x: 0.5rem; - } - - .g-xxl-2, -.gy-xxl-2 { - --bs-gutter-y: 0.5rem; - } - - .g-xxl-3, -.gx-xxl-3 { - --bs-gutter-x: 1rem; - } - - .g-xxl-3, -.gy-xxl-3 { - --bs-gutter-y: 1rem; - } - - .g-xxl-4, -.gx-xxl-4 { - --bs-gutter-x: 1.5rem; - } - - .g-xxl-4, -.gy-xxl-4 { - --bs-gutter-y: 1.5rem; - } - - .g-xxl-5, -.gx-xxl-5 { - --bs-gutter-x: 3rem; - } - - .g-xxl-5, -.gy-xxl-5 { - --bs-gutter-y: 3rem; - } -} -.table { - --bs-table-bg: transparent; - --bs-table-accent-bg: transparent; - --bs-table-striped-color: #212529; - --bs-table-striped-bg: rgba(0, 0, 0, 0.05); - --bs-table-active-color: #212529; - --bs-table-active-bg: rgba(0, 0, 0, 0.1); - --bs-table-hover-color: #212529; - --bs-table-hover-bg: rgba(0, 0, 0, 0.075); - width: 100%; - margin-bottom: 1rem; - color: #212529; - vertical-align: top; - border-color: #dee2e6; -} -.table > :not(caption) > * > * { - padding: 0.5rem 0.5rem; - background-color: var(--bs-table-bg); - border-bottom-width: 1px; - box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg); -} -.table > tbody { - vertical-align: inherit; -} -.table > thead { - vertical-align: bottom; -} -.table > :not(:first-child) { - border-top: 2px solid currentColor; -} - -.caption-top { - caption-side: top; -} - -.table-sm > :not(caption) > * > * { - padding: 0.25rem 0.25rem; -} - -.table-bordered > :not(caption) > * { - border-width: 1px 0; -} -.table-bordered > :not(caption) > * > * { - border-width: 0 1px; -} - -.table-borderless > :not(caption) > * > * { - border-bottom-width: 0; -} -.table-borderless > :not(:first-child) { - border-top-width: 0; -} - -.table-striped > tbody > tr:nth-of-type(odd) > * { - --bs-table-accent-bg: var(--bs-table-striped-bg); - color: var(--bs-table-striped-color); -} - -.table-active { - --bs-table-accent-bg: var(--bs-table-active-bg); - color: var(--bs-table-active-color); -} - -.table-hover > tbody > tr:hover > * { - --bs-table-accent-bg: var(--bs-table-hover-bg); - color: var(--bs-table-hover-color); -} - -.table-primary { - --bs-table-bg: #cfe2ff; - --bs-table-striped-bg: #c5d7f2; - --bs-table-striped-color: #000; - --bs-table-active-bg: #bacbe6; - --bs-table-active-color: #000; - --bs-table-hover-bg: #bfd1ec; - --bs-table-hover-color: #000; - color: #000; - border-color: #bacbe6; -} - -.table-secondary { - --bs-table-bg: #e2e3e5; - --bs-table-striped-bg: #d7d8da; - --bs-table-striped-color: #000; - --bs-table-active-bg: #cbccce; - --bs-table-active-color: #000; - --bs-table-hover-bg: #d1d2d4; - --bs-table-hover-color: #000; - color: #000; - border-color: #cbccce; -} - -.table-success { - --bs-table-bg: #d1e7dd; - --bs-table-striped-bg: #c7dbd2; - --bs-table-striped-color: #000; - --bs-table-active-bg: #bcd0c7; - --bs-table-active-color: #000; - --bs-table-hover-bg: #c1d6cc; - --bs-table-hover-color: #000; - color: #000; - border-color: #bcd0c7; -} - -.table-info { - --bs-table-bg: #cff4fc; - --bs-table-striped-bg: #c5e8ef; - --bs-table-striped-color: #000; - --bs-table-active-bg: #badce3; - --bs-table-active-color: #000; - --bs-table-hover-bg: #bfe2e9; - --bs-table-hover-color: #000; - color: #000; - border-color: #badce3; -} - -.table-warning { - --bs-table-bg: #fff3cd; - --bs-table-striped-bg: #f2e7c3; - --bs-table-striped-color: #000; - --bs-table-active-bg: #e6dbb9; - --bs-table-active-color: #000; - --bs-table-hover-bg: #ece1be; - --bs-table-hover-color: #000; - color: #000; - border-color: #e6dbb9; -} - -.table-danger { - --bs-table-bg: #f8d7da; - --bs-table-striped-bg: #eccccf; - --bs-table-striped-color: #000; - --bs-table-active-bg: #dfc2c4; - --bs-table-active-color: #000; - --bs-table-hover-bg: #e5c7ca; - --bs-table-hover-color: #000; - color: #000; - border-color: #dfc2c4; -} - -.table-light { - --bs-table-bg: #f8f9fa; - --bs-table-striped-bg: #ecedee; - --bs-table-striped-color: #000; - --bs-table-active-bg: #dfe0e1; - --bs-table-active-color: #000; - --bs-table-hover-bg: #e5e6e7; - --bs-table-hover-color: #000; - color: #000; - border-color: #dfe0e1; -} - -.table-dark { - --bs-table-bg: #212529; - --bs-table-striped-bg: #2c3034; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #373b3e; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #323539; - --bs-table-hover-color: #fff; - color: #fff; - border-color: #373b3e; -} - -.table-responsive { - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -@media (max-width: 575.98px) { - .table-responsive-sm { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 767.98px) { - .table-responsive-md { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 991.98px) { - .table-responsive-lg { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 1199.98px) { - .table-responsive-xl { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 1399.98px) { - .table-responsive-xxl { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -.form-label { - margin-bottom: 0.5rem; -} - -.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; -} - -.col-form-label-sm { - padding-top: calc(0.25rem + 1px); - padding-bottom: calc(0.25rem + 1px); - font-size: 0.875rem; -} - -.form-text { - margin-top: 0.25rem; - font-size: 0.875em; - color: #6c757d; -} - -.form-control { - display: block; - width: 100%; - padding: 0.375rem 0.75rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - background-color: #fff; - background-clip: padding-box; - border: 1px solid #ced4da; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - 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[type=file] { - overflow: hidden; -} -.form-control[type=file]:not(:disabled):not([readonly]) { - cursor: pointer; -} -.form-control:focus { - color: #212529; - background-color: #fff; - border-color: #86b7fe; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} -.form-control::-webkit-date-and-time-value { - height: 1.5em; -} -.form-control::-moz-placeholder { - color: #6c757d; - opacity: 1; -} -.form-control::placeholder { - color: #6c757d; - opacity: 1; -} -.form-control:disabled, .form-control[readonly] { - background-color: #e9ecef; - opacity: 1; -} -.form-control::-webkit-file-upload-button { - padding: 0.375rem 0.75rem; - margin: -0.375rem -0.75rem; - -webkit-margin-end: 0.75rem; - margin-inline-end: 0.75rem; - color: #212529; - background-color: #e9ecef; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 1px; - border-radius: 0; - -webkit-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; - 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; -} -.form-control::file-selector-button { - padding: 0.375rem 0.75rem; - margin: -0.375rem -0.75rem; - -webkit-margin-end: 0.75rem; - margin-inline-end: 0.75rem; - color: #212529; - background-color: #e9ecef; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 1px; - border-radius: 0; - 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) { - .form-control::-webkit-file-upload-button { - -webkit-transition: none; - transition: none; - } - .form-control::file-selector-button { - transition: none; - } -} -.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: #dde0e3; -} -.form-control:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: #dde0e3; -} -.form-control::-webkit-file-upload-button { - padding: 0.375rem 0.75rem; - margin: -0.375rem -0.75rem; - -webkit-margin-end: 0.75rem; - margin-inline-end: 0.75rem; - color: #212529; - background-color: #e9ecef; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 1px; - border-radius: 0; - -webkit-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; - 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) { - .form-control::-webkit-file-upload-button { - -webkit-transition: none; - transition: none; - } -} -.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: #dde0e3; -} - -.form-control-plaintext { - display: block; - width: 100%; - padding: 0.375rem 0; - margin-bottom: 0; - 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 { - min-height: calc(1.5em + 0.5rem + 2px); - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - border-radius: 0.2rem; -} -.form-control-sm::-webkit-file-upload-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - -webkit-margin-end: 0.5rem; - margin-inline-end: 0.5rem; -} -.form-control-sm::file-selector-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - -webkit-margin-end: 0.5rem; - margin-inline-end: 0.5rem; -} -.form-control-sm::-webkit-file-upload-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - -webkit-margin-end: 0.5rem; - margin-inline-end: 0.5rem; -} - -.form-control-lg { - min-height: calc(1.5em + 1rem + 2px); - padding: 0.5rem 1rem; - font-size: 1.25rem; - border-radius: 0.3rem; -} -.form-control-lg::-webkit-file-upload-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; -} -.form-control-lg::file-selector-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; -} -.form-control-lg::-webkit-file-upload-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; -} - -textarea.form-control { - min-height: calc(1.5em + 0.75rem + 2px); -} -textarea.form-control-sm { - min-height: calc(1.5em + 0.5rem + 2px); -} -textarea.form-control-lg { - min-height: calc(1.5em + 1rem + 2px); -} - -.form-control-color { - width: 3rem; - height: auto; - padding: 0.375rem; -} -.form-control-color:not(:disabled):not([readonly]) { - cursor: pointer; -} -.form-control-color::-moz-color-swatch { - height: 1.5em; - border-radius: 0.25rem; -} -.form-control-color::-webkit-color-swatch { - height: 1.5em; - border-radius: 0.25rem; -} - -.form-select { - display: block; - width: 100%; - padding: 0.375rem 2.25rem 0.375rem 0.75rem; - -moz-padding-start: calc(0.75rem - 3px); - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - background-color: #fff; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right 0.75rem center; - background-size: 16px 12px; - border: 1px solid #ced4da; - border-radius: 0.25rem; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} -@media (prefers-reduced-motion: reduce) { - .form-select { - transition: none; - } -} -.form-select:focus { - border-color: #86b7fe; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} -.form-select[multiple], .form-select[size]:not([size="1"]) { - padding-right: 0.75rem; - background-image: none; -} -.form-select:disabled { - background-color: #e9ecef; -} -.form-select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 #212529; -} - -.form-select-sm { - padding-top: 0.25rem; - padding-bottom: 0.25rem; - padding-left: 0.5rem; - font-size: 0.875rem; - border-radius: 0.2rem; -} - -.form-select-lg { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - font-size: 1.25rem; - border-radius: 0.3rem; -} - -.form-check { - display: block; - min-height: 1.5rem; - padding-left: 1.5em; - margin-bottom: 0.125rem; -} -.form-check .form-check-input { - float: left; - margin-left: -1.5em; -} - -.form-check-input { - width: 1em; - height: 1em; - margin-top: 0.25em; - vertical-align: top; - background-color: #fff; - background-repeat: no-repeat; - background-position: center; - background-size: contain; - border: 1px solid rgba(0, 0, 0, 0.25); - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - -webkit-print-color-adjust: exact; - color-adjust: exact; -} -.form-check-input[type=checkbox] { - border-radius: 0.25em; -} -.form-check-input[type=radio] { - border-radius: 50%; -} -.form-check-input:active { - filter: brightness(90%); -} -.form-check-input:focus { - border-color: #86b7fe; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} -.form-check-input:checked { - background-color: #0d6efd; - border-color: #0d6efd; -} -.form-check-input:checked[type=checkbox] { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); -} -.form-check-input:checked[type=radio] { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); -} -.form-check-input[type=checkbox]:indeterminate { - background-color: #0d6efd; - border-color: #0d6efd; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); -} -.form-check-input:disabled { - pointer-events: none; - filter: none; - opacity: 0.5; -} -.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { - opacity: 0.5; -} - -.form-switch { - padding-left: 2.5em; -} -.form-switch .form-check-input { - width: 2em; - margin-left: -2.5em; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); - background-position: left center; - border-radius: 2em; - transition: background-position 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-switch .form-check-input { - transition: none; - } -} -.form-switch .form-check-input:focus { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e"); -} -.form-switch .form-check-input:checked { - background-position: right center; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); -} - -.form-check-inline { - display: inline-block; - margin-right: 1rem; -} - -.btn-check { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; -} -.btn-check[disabled] + .btn, .btn-check:disabled + .btn { - pointer-events: none; - filter: none; - opacity: 0.65; -} - -.form-range { - width: 100%; - height: 1.5rem; - padding: 0; - background-color: transparent; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} -.form-range:focus { - outline: 0; -} -.form-range:focus::-webkit-slider-thumb { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} -.form-range:focus::-moz-range-thumb { - box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} -.form-range::-moz-focus-outer { - border: 0; -} -.form-range::-webkit-slider-thumb { - width: 1rem; - height: 1rem; - margin-top: -0.25rem; - background-color: #0d6efd; - 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) { - .form-range::-webkit-slider-thumb { - -webkit-transition: none; - transition: none; - } -} -.form-range::-webkit-slider-thumb:active { - background-color: #b6d4fe; -} -.form-range::-webkit-slider-runnable-track { - width: 100%; - height: 0.5rem; - color: transparent; - cursor: pointer; - background-color: #dee2e6; - border-color: transparent; - border-radius: 1rem; -} -.form-range::-moz-range-thumb { - width: 1rem; - height: 1rem; - background-color: #0d6efd; - 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) { - .form-range::-moz-range-thumb { - -moz-transition: none; - transition: none; - } -} -.form-range::-moz-range-thumb:active { - background-color: #b6d4fe; -} -.form-range::-moz-range-track { - width: 100%; - height: 0.5rem; - color: transparent; - cursor: pointer; - background-color: #dee2e6; - border-color: transparent; - border-radius: 1rem; -} -.form-range:disabled { - pointer-events: none; -} -.form-range:disabled::-webkit-slider-thumb { - background-color: #adb5bd; -} -.form-range:disabled::-moz-range-thumb { - background-color: #adb5bd; -} - -.form-floating { - position: relative; -} -.form-floating > .form-control, -.form-floating > .form-select { - height: calc(3.5rem + 2px); - line-height: 1.25; -} -.form-floating > label { - position: absolute; - top: 0; - left: 0; - height: 100%; - padding: 1rem 0.75rem; - pointer-events: none; - border: 1px solid transparent; - transform-origin: 0 0; - transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-floating > label { - transition: none; - } -} -.form-floating > .form-control { - padding: 1rem 0.75rem; -} -.form-floating > .form-control::-moz-placeholder { - color: transparent; -} -.form-floating > .form-control::placeholder { - color: transparent; -} -.form-floating > .form-control:not(:-moz-placeholder-shown) { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-control:-webkit-autofill { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-select { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label { - opacity: 0.65; - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} -.form-floating > .form-control:focus ~ label, -.form-floating > .form-control:not(:placeholder-shown) ~ label, -.form-floating > .form-select ~ label { - opacity: 0.65; - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} -.form-floating > .form-control:-webkit-autofill ~ label { - opacity: 0.65; - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} - -.input-group { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: stretch; - width: 100%; -} -.input-group > .form-control, -.input-group > .form-select { - position: relative; - flex: 1 1 auto; - width: 1%; - min-width: 0; -} -.input-group > .form-control:focus, -.input-group > .form-select:focus { - z-index: 3; -} -.input-group .btn { - position: relative; - z-index: 2; -} -.input-group .btn:focus { - z-index: 3; -} - -.input-group-text { - display: flex; - align-items: center; - padding: 0.375rem 0.75rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - text-align: center; - white-space: nowrap; - background-color: #e9ecef; - border: 1px solid #ced4da; - border-radius: 0.25rem; -} - -.input-group-lg > .form-control, -.input-group-lg > .form-select, -.input-group-lg > .input-group-text, -.input-group-lg > .btn { - padding: 0.5rem 1rem; - font-size: 1.25rem; - border-radius: 0.3rem; -} - -.input-group-sm > .form-control, -.input-group-sm > .form-select, -.input-group-sm > .input-group-text, -.input-group-sm > .btn { - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - border-radius: 0.2rem; -} - -.input-group-lg > .form-select, -.input-group-sm > .form-select { - padding-right: 3rem; -} - -.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu), -.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu), -.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { - margin-left: -1px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.valid-feedback { - display: none; - width: 100%; - margin-top: 0.25rem; - font-size: 0.875em; - color: #198754; -} - -.valid-tooltip { - position: absolute; - top: 100%; - z-index: 5; - display: none; - max-width: 100%; - padding: 0.25rem 0.5rem; - margin-top: 0.1rem; - font-size: 0.875rem; - color: #fff; - background-color: rgba(25, 135, 84, 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: #198754; - padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' 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: #198754; - box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 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 .form-select:valid, .form-select.is-valid { - border-color: #198754; -} -.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { - padding-right: 4.125rem; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' 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-position: right 0.75rem center, center right 2.25rem; - background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} -.was-validated .form-select:valid:focus, .form-select.is-valid:focus { - border-color: #198754; - box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25); -} - -.was-validated .form-check-input:valid, .form-check-input.is-valid { - border-color: #198754; -} -.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { - background-color: #198754; -} -.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { - box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25); -} -.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { - color: #198754; -} - -.form-check-inline .form-check-input ~ .valid-feedback { - margin-left: 0.5em; -} - -.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, -.was-validated .input-group .form-select:valid, -.input-group .form-select.is-valid { - z-index: 1; -} -.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, -.was-validated .input-group .form-select:valid:focus, -.input-group .form-select.is-valid:focus { - z-index: 3; -} - -.invalid-feedback { - display: none; - width: 100%; - margin-top: 0.25rem; - font-size: 0.875em; - color: #dc3545; -} - -.invalid-tooltip { - position: absolute; - top: 100%; - z-index: 5; - display: none; - max-width: 100%; - padding: 0.25rem 0.5rem; - margin-top: 0.1rem; - font-size: 0.875rem; - 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' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%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.25rem 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 .form-select:invalid, .form-select.is-invalid { - border-color: #dc3545; -} -.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { - padding-right: 4.125rem; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%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-position: right 0.75rem center, center right 2.25rem; - background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} -.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { - border-color: #dc3545; - box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25); -} - -.was-validated .form-check-input:invalid, .form-check-input.is-invalid { - border-color: #dc3545; -} -.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { - background-color: #dc3545; -} -.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { - box-shadow: 0 0 0 0.25rem 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; -} - -.form-check-inline .form-check-input ~ .invalid-feedback { - margin-left: 0.5em; -} - -.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, -.was-validated .input-group .form-select:invalid, -.input-group .form-select.is-invalid { - z-index: 2; -} -.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, -.was-validated .input-group .form-select:invalid:focus, -.input-group .form-select.is-invalid:focus { - z-index: 3; -} - -.btn { - display: inline-block; - font-weight: 400; - line-height: 1.5; - color: #212529; - text-align: center; - text-decoration: none; - vertical-align: middle; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - background-color: transparent; - border: 1px solid transparent; - padding: 0.375rem 0.75rem; - font-size: 1rem; - 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; -} -.btn-check:focus + .btn, .btn:focus { - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} -.btn:disabled, .btn.disabled, fieldset:disabled .btn { - pointer-events: none; - opacity: 0.65; -} - -.btn-primary { - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} -.btn-primary:hover { - color: #fff; - background-color: #0b5ed7; - border-color: #0a58ca; -} -.btn-check:focus + .btn-primary, .btn-primary:focus { - color: #fff; - background-color: #0b5ed7; - border-color: #0a58ca; - box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); -} -.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle { - color: #fff; - background-color: #0a58ca; - border-color: #0a53be; -} -.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); -} -.btn-primary:disabled, .btn-primary.disabled { - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} - -.btn-secondary { - color: #fff; - background-color: #6c757d; - border-color: #6c757d; -} -.btn-secondary:hover { - color: #fff; - background-color: #5c636a; - border-color: #565e64; -} -.btn-check:focus + .btn-secondary, .btn-secondary:focus { - color: #fff; - background-color: #5c636a; - border-color: #565e64; - box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5); -} -.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle { - color: #fff; - background-color: #565e64; - border-color: #51585e; -} -.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5); -} -.btn-secondary:disabled, .btn-secondary.disabled { - color: #fff; - background-color: #6c757d; - border-color: #6c757d; -} - -.btn-success { - color: #fff; - background-color: #198754; - border-color: #198754; -} -.btn-success:hover { - color: #fff; - background-color: #157347; - border-color: #146c43; -} -.btn-check:focus + .btn-success, .btn-success:focus { - color: #fff; - background-color: #157347; - border-color: #146c43; - box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5); -} -.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle { - color: #fff; - background-color: #146c43; - border-color: #13653f; -} -.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5); -} -.btn-success:disabled, .btn-success.disabled { - color: #fff; - background-color: #198754; - border-color: #198754; -} - -.btn-info { - color: #000; - background-color: #0dcaf0; - border-color: #0dcaf0; -} -.btn-info:hover { - color: #000; - background-color: #31d2f2; - border-color: #25cff2; -} -.btn-check:focus + .btn-info, .btn-info:focus { - color: #000; - background-color: #31d2f2; - border-color: #25cff2; - box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5); -} -.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle { - color: #000; - background-color: #3dd5f3; - border-color: #25cff2; -} -.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5); -} -.btn-info:disabled, .btn-info.disabled { - color: #000; - background-color: #0dcaf0; - border-color: #0dcaf0; -} - -.btn-warning { - color: #000; - background-color: #ffc107; - border-color: #ffc107; -} -.btn-warning:hover { - color: #000; - background-color: #ffca2c; - border-color: #ffc720; -} -.btn-check:focus + .btn-warning, .btn-warning:focus { - color: #000; - background-color: #ffca2c; - border-color: #ffc720; - box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5); -} -.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle { - color: #000; - background-color: #ffcd39; - border-color: #ffc720; -} -.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5); -} -.btn-warning:disabled, .btn-warning.disabled { - color: #000; - background-color: #ffc107; - border-color: #ffc107; -} - -.btn-danger { - color: #fff; - background-color: #dc3545; - border-color: #dc3545; -} -.btn-danger:hover { - color: #fff; - background-color: #bb2d3b; - border-color: #b02a37; -} -.btn-check:focus + .btn-danger, .btn-danger:focus { - color: #fff; - background-color: #bb2d3b; - border-color: #b02a37; - box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5); -} -.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle { - color: #fff; - background-color: #b02a37; - border-color: #a52834; -} -.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5); -} -.btn-danger:disabled, .btn-danger.disabled { - color: #fff; - background-color: #dc3545; - border-color: #dc3545; -} - -.btn-light { - color: #000; - background-color: #f8f9fa; - border-color: #f8f9fa; -} -.btn-light:hover { - color: #000; - background-color: #f9fafb; - border-color: #f9fafb; -} -.btn-check:focus + .btn-light, .btn-light:focus { - color: #000; - background-color: #f9fafb; - border-color: #f9fafb; - box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5); -} -.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle { - color: #000; - background-color: #f9fafb; - border-color: #f9fafb; -} -.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5); -} -.btn-light:disabled, .btn-light.disabled { - color: #000; - background-color: #f8f9fa; - border-color: #f8f9fa; -} - -.btn-dark { - color: #fff; - background-color: #212529; - border-color: #212529; -} -.btn-dark:hover { - color: #fff; - background-color: #1c1f23; - border-color: #1a1e21; -} -.btn-check:focus + .btn-dark, .btn-dark:focus { - color: #fff; - background-color: #1c1f23; - border-color: #1a1e21; - box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); -} -.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle { - color: #fff; - background-color: #1a1e21; - border-color: #191c1f; -} -.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5); -} -.btn-dark:disabled, .btn-dark.disabled { - color: #fff; - background-color: #212529; - border-color: #212529; -} - -.btn-outline-primary { - color: #0d6efd; - border-color: #0d6efd; -} -.btn-outline-primary:hover { - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} -.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5); -} -.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} -.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5); -} -.btn-outline-primary:disabled, .btn-outline-primary.disabled { - color: #0d6efd; - background-color: transparent; -} - -.btn-outline-secondary { - color: #6c757d; - border-color: #6c757d; -} -.btn-outline-secondary:hover { - color: #fff; - background-color: #6c757d; - border-color: #6c757d; -} -.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { - box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5); -} -.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { - color: #fff; - background-color: #6c757d; - border-color: #6c757d; -} -.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5); -} -.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { - color: #6c757d; - background-color: transparent; -} - -.btn-outline-success { - color: #198754; - border-color: #198754; -} -.btn-outline-success:hover { - color: #fff; - background-color: #198754; - border-color: #198754; -} -.btn-check:focus + .btn-outline-success, .btn-outline-success:focus { - box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5); -} -.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { - color: #fff; - background-color: #198754; - border-color: #198754; -} -.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5); -} -.btn-outline-success:disabled, .btn-outline-success.disabled { - color: #198754; - background-color: transparent; -} - -.btn-outline-info { - color: #0dcaf0; - border-color: #0dcaf0; -} -.btn-outline-info:hover { - color: #000; - background-color: #0dcaf0; - border-color: #0dcaf0; -} -.btn-check:focus + .btn-outline-info, .btn-outline-info:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5); -} -.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { - color: #000; - background-color: #0dcaf0; - border-color: #0dcaf0; -} -.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5); -} -.btn-outline-info:disabled, .btn-outline-info.disabled { - color: #0dcaf0; - background-color: transparent; -} - -.btn-outline-warning { - color: #ffc107; - border-color: #ffc107; -} -.btn-outline-warning:hover { - color: #000; - background-color: #ffc107; - border-color: #ffc107; -} -.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); -} -.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { - color: #000; - background-color: #ffc107; - border-color: #ffc107; -} -.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5); -} -.btn-outline-warning:disabled, .btn-outline-warning.disabled { - color: #ffc107; - background-color: transparent; -} - -.btn-outline-danger { - color: #dc3545; - border-color: #dc3545; -} -.btn-outline-danger:hover { - color: #fff; - background-color: #dc3545; - border-color: #dc3545; -} -.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { - box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5); -} -.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { - color: #fff; - background-color: #dc3545; - border-color: #dc3545; -} -.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5); -} -.btn-outline-danger:disabled, .btn-outline-danger.disabled { - color: #dc3545; - background-color: transparent; -} - -.btn-outline-light { - color: #f8f9fa; - border-color: #f8f9fa; -} -.btn-outline-light:hover { - color: #000; - background-color: #f8f9fa; - border-color: #f8f9fa; -} -.btn-check:focus + .btn-outline-light, .btn-outline-light:focus { - box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5); -} -.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { - color: #000; - background-color: #f8f9fa; - border-color: #f8f9fa; -} -.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5); -} -.btn-outline-light:disabled, .btn-outline-light.disabled { - color: #f8f9fa; - background-color: transparent; -} - -.btn-outline-dark { - color: #212529; - border-color: #212529; -} -.btn-outline-dark:hover { - color: #fff; - background-color: #212529; - border-color: #212529; -} -.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { - box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); -} -.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { - color: #fff; - background-color: #212529; - border-color: #212529; -} -.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5); -} -.btn-outline-dark:disabled, .btn-outline-dark.disabled { - color: #212529; - background-color: transparent; -} - -.btn-link { - font-weight: 400; - color: #0d6efd; - text-decoration: underline; -} -.btn-link:hover { - color: #0a58ca; -} -.btn-link:disabled, .btn-link.disabled { - color: #6c757d; -} - -.btn-lg, .btn-group-lg > .btn { - padding: 0.5rem 1rem; - font-size: 1.25rem; - border-radius: 0.3rem; -} - -.btn-sm, .btn-group-sm > .btn { - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - border-radius: 0.2rem; -} - -.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 { - height: 0; - overflow: hidden; - transition: height 0.35s ease; -} -@media (prefers-reduced-motion: reduce) { - .collapsing { - transition: none; - } -} -.collapsing.collapse-horizontal { - width: 0; - height: auto; - transition: width 0.35s ease; -} -@media (prefers-reduced-motion: reduce) { - .collapsing.collapse-horizontal { - transition: none; - } -} - -.dropup, -.dropend, -.dropdown, -.dropstart { - 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; - z-index: 1000; - display: none; - min-width: 10rem; - padding: 0.5rem 0; - margin: 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[data-bs-popper] { - top: 100%; - left: 0; - margin-top: 0.125rem; -} - -.dropdown-menu-start { - --bs-position: start; -} -.dropdown-menu-start[data-bs-popper] { - right: auto; - left: 0; -} - -.dropdown-menu-end { - --bs-position: end; -} -.dropdown-menu-end[data-bs-popper] { - right: 0; - left: auto; -} - -@media (min-width: 576px) { - .dropdown-menu-sm-start { - --bs-position: start; - } - .dropdown-menu-sm-start[data-bs-popper] { - right: auto; - left: 0; - } - - .dropdown-menu-sm-end { - --bs-position: end; - } - .dropdown-menu-sm-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 768px) { - .dropdown-menu-md-start { - --bs-position: start; - } - .dropdown-menu-md-start[data-bs-popper] { - right: auto; - left: 0; - } - - .dropdown-menu-md-end { - --bs-position: end; - } - .dropdown-menu-md-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 992px) { - .dropdown-menu-lg-start { - --bs-position: start; - } - .dropdown-menu-lg-start[data-bs-popper] { - right: auto; - left: 0; - } - - .dropdown-menu-lg-end { - --bs-position: end; - } - .dropdown-menu-lg-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 1200px) { - .dropdown-menu-xl-start { - --bs-position: start; - } - .dropdown-menu-xl-start[data-bs-popper] { - right: auto; - left: 0; - } - - .dropdown-menu-xl-end { - --bs-position: end; - } - .dropdown-menu-xl-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 1400px) { - .dropdown-menu-xxl-start { - --bs-position: start; - } - .dropdown-menu-xxl-start[data-bs-popper] { - right: auto; - left: 0; - } - - .dropdown-menu-xxl-end { - --bs-position: end; - } - .dropdown-menu-xxl-end[data-bs-popper] { - right: 0; - left: auto; - } -} -.dropup .dropdown-menu[data-bs-popper] { - 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; -} - -.dropend .dropdown-menu[data-bs-popper] { - top: 0; - right: auto; - left: 100%; - margin-top: 0; - margin-left: 0.125rem; -} -.dropend .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; -} -.dropend .dropdown-toggle:empty::after { - margin-left: 0; -} -.dropend .dropdown-toggle::after { - vertical-align: 0; -} - -.dropstart .dropdown-menu[data-bs-popper] { - top: 0; - right: 100%; - left: auto; - margin-top: 0; - margin-right: 0.125rem; -} -.dropstart .dropdown-toggle::after { - display: inline-block; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; -} -.dropstart .dropdown-toggle::after { - display: none; -} -.dropstart .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; -} -.dropstart .dropdown-toggle:empty::after { - margin-left: 0; -} -.dropstart .dropdown-toggle::before { - vertical-align: 0; -} - -.dropdown-divider { - height: 0; - margin: 0.5rem 0; - overflow: hidden; - border-top: 1px solid rgba(0, 0, 0, 0.15); -} - -.dropdown-item { - display: block; - width: 100%; - padding: 0.25rem 1rem; - clear: both; - font-weight: 400; - color: #212529; - text-align: inherit; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border: 0; -} -.dropdown-item:hover, .dropdown-item:focus { - color: #1e2125; - background-color: #e9ecef; -} -.dropdown-item.active, .dropdown-item:active { - color: #fff; - text-decoration: none; - background-color: #0d6efd; -} -.dropdown-item.disabled, .dropdown-item:disabled { - color: #adb5bd; - pointer-events: none; - background-color: transparent; -} - -.dropdown-menu.show { - display: block; -} - -.dropdown-header { - display: block; - padding: 0.5rem 1rem; - margin-bottom: 0; - font-size: 0.875rem; - color: #6c757d; - white-space: nowrap; -} - -.dropdown-item-text { - display: block; - padding: 0.25rem 1rem; - color: #212529; -} - -.dropdown-menu-dark { - color: #dee2e6; - background-color: #343a40; - border-color: rgba(0, 0, 0, 0.15); -} -.dropdown-menu-dark .dropdown-item { - color: #dee2e6; -} -.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus { - color: #fff; - background-color: rgba(255, 255, 255, 0.15); -} -.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active { - color: #fff; - background-color: #0d6efd; -} -.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled { - color: #adb5bd; -} -.dropdown-menu-dark .dropdown-divider { - border-color: rgba(0, 0, 0, 0.15); -} -.dropdown-menu-dark .dropdown-item-text { - color: #dee2e6; -} -.dropdown-menu-dark .dropdown-header { - color: #adb5bd; -} - -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-flex; - vertical-align: middle; -} -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - flex: 1 1 auto; -} -.btn-group > .btn-check:checked + .btn, -.btn-group > .btn-check:focus + .btn, -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn-check:checked + .btn, -.btn-group-vertical > .btn-check:focus + .btn, -.btn-group-vertical > .btn:hover, -.btn-group-vertical > .btn:focus, -.btn-group-vertical > .btn:active, -.btn-group-vertical > .btn.active { - z-index: 1; -} - -.btn-toolbar { - display: flex; - flex-wrap: wrap; - 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:nth-child(n+3), -.btn-group > :not(.btn-check) + .btn, -.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, .dropend .dropdown-toggle-split::after { - margin-left: 0; -} -.dropstart .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 { - flex-direction: column; - align-items: flex-start; - 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 ~ .btn, -.btn-group-vertical > .btn-group:not(:first-child) > .btn { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.nav { - display: flex; - flex-wrap: wrap; - padding-left: 0; - margin-bottom: 0; - list-style: none; -} - -.nav-link { - display: block; - padding: 0.5rem 1rem; - color: #0d6efd; - text-decoration: none; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .nav-link { - transition: none; - } -} -.nav-link:hover, .nav-link:focus { - color: #0a58ca; -} -.nav-link.disabled { - color: #6c757d; - pointer-events: none; - cursor: default; -} - -.nav-tabs { - border-bottom: 1px solid #dee2e6; -} -.nav-tabs .nav-link { - margin-bottom: -1px; - background: none; - 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; - isolation: isolate; -} -.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 { - background: none; - border: 0; - border-radius: 0.25rem; -} -.nav-pills .nav-link.active, -.nav-pills .show > .nav-link { - color: #fff; - background-color: #0d6efd; -} - -.nav-fill > .nav-link, -.nav-fill .nav-item { - flex: 1 1 auto; - text-align: center; -} - -.nav-justified > .nav-link, -.nav-justified .nav-item { - flex-basis: 0; - flex-grow: 1; - text-align: center; -} - -.nav-fill .nav-item .nav-link, -.nav-justified .nav-item .nav-link { - width: 100%; -} - -.tab-content > .tab-pane { - display: none; -} -.tab-content > .active { - display: block; -} - -.navbar { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} -.navbar > .container, -.navbar > .container-fluid, -.navbar > .container-sm, -.navbar > .container-md, -.navbar > .container-lg, -.navbar > .container-xl, -.navbar > .container-xxl { - display: flex; - flex-wrap: inherit; - align-items: center; - justify-content: space-between; -} -.navbar-brand { - padding-top: 0.3125rem; - padding-bottom: 0.3125rem; - margin-right: 1rem; - font-size: 1.25rem; - text-decoration: none; - white-space: nowrap; -} -.navbar-nav { - display: flex; - 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; -} - -.navbar-text { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.navbar-collapse { - flex-basis: 100%; - flex-grow: 1; - 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; - transition: box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .navbar-toggler { - transition: none; - } -} -.navbar-toggler:hover { - text-decoration: none; -} -.navbar-toggler:focus { - text-decoration: none; - outline: 0; - box-shadow: 0 0 0 0.25rem; -} - -.navbar-toggler-icon { - display: inline-block; - width: 1.5em; - height: 1.5em; - vertical-align: middle; - background-repeat: no-repeat; - background-position: center; - background-size: 100%; -} - -.navbar-nav-scroll { - max-height: var(--bs-scroll-height, 75vh); - overflow-y: auto; -} - -@media (min-width: 576px) { - .navbar-expand-sm { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-sm .navbar-nav { - 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 .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-sm .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-sm .navbar-toggler { - display: none; - } - .navbar-expand-sm .offcanvas-header { - display: none; - } - .navbar-expand-sm .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-sm .offcanvas-top, -.navbar-expand-sm .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-sm .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 768px) { - .navbar-expand-md { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-md .navbar-nav { - 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 .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-md .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-md .navbar-toggler { - display: none; - } - .navbar-expand-md .offcanvas-header { - display: none; - } - .navbar-expand-md .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-md .offcanvas-top, -.navbar-expand-md .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-md .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 992px) { - .navbar-expand-lg { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-lg .navbar-nav { - 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 .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-lg .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-lg .navbar-toggler { - display: none; - } - .navbar-expand-lg .offcanvas-header { - display: none; - } - .navbar-expand-lg .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-lg .offcanvas-top, -.navbar-expand-lg .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-lg .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 1200px) { - .navbar-expand-xl { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-xl .navbar-nav { - 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 .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-xl .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-xl .navbar-toggler { - display: none; - } - .navbar-expand-xl .offcanvas-header { - display: none; - } - .navbar-expand-xl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-xl .offcanvas-top, -.navbar-expand-xl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-xl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 1400px) { - .navbar-expand-xxl { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-xxl .navbar-nav { - flex-direction: row; - } - .navbar-expand-xxl .navbar-nav .dropdown-menu { - position: absolute; - } - .navbar-expand-xxl .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; - } - .navbar-expand-xxl .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-xxl .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-xxl .navbar-toggler { - display: none; - } - .navbar-expand-xxl .offcanvas-header { - display: none; - } - .navbar-expand-xxl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-xxl .offcanvas-top, -.navbar-expand-xxl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-xxl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -.navbar-expand { - flex-wrap: nowrap; - justify-content: flex-start; -} -.navbar-expand .navbar-nav { - 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 .navbar-nav-scroll { - overflow: visible; -} -.navbar-expand .navbar-collapse { - display: flex !important; - flex-basis: auto; -} -.navbar-expand .navbar-toggler { - display: none; -} -.navbar-expand .offcanvas-header { - display: none; -} -.navbar-expand .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; -} -.navbar-expand .offcanvas-top, -.navbar-expand .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; -} -.navbar-expand .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; -} - -.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.55); -} -.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 .nav-link.active { - color: rgba(0, 0, 0, 0.9); -} -.navbar-light .navbar-toggler { - color: rgba(0, 0, 0, 0.55); - 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' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} -.navbar-light .navbar-text { - color: rgba(0, 0, 0, 0.55); -} -.navbar-light .navbar-text a, -.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.55); -} -.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 .nav-link.active { - color: #fff; -} -.navbar-dark .navbar-toggler { - color: rgba(255, 255, 255, 0.55); - 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' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} -.navbar-dark .navbar-text { - color: rgba(255, 255, 255, 0.55); -} -.navbar-dark .navbar-text a, -.navbar-dark .navbar-text a:hover, -.navbar-dark .navbar-text a:focus { - color: #fff; -} - -.card { - position: relative; - display: flex; - 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 { - border-top: inherit; - border-bottom: inherit; -} -.card > .list-group:first-child { - border-top-width: 0; - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); -} -.card > .list-group:last-child { - border-bottom-width: 0; - border-bottom-right-radius: calc(0.25rem - 1px); - border-bottom-left-radius: calc(0.25rem - 1px); -} -.card > .card-header + .list-group, -.card > .list-group + .card-footer { - border-top: 0; -} - -.card-body { - flex: 1 1 auto; - padding: 1rem 1rem; -} - -.card-title { - margin-bottom: 0.5rem; -} - -.card-subtitle { - margin-top: -0.25rem; - margin-bottom: 0; -} - -.card-text:last-child { - margin-bottom: 0; -} - -.card-link + .card-link { - margin-left: 1rem; -} - -.card-header { - padding: 0.5rem 1rem; - 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-footer { - padding: 0.5rem 1rem; - 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.5rem; - margin-bottom: -0.5rem; - margin-left: -0.5rem; - border-bottom: 0; -} - -.card-header-pills { - margin-right: -0.5rem; - margin-left: -0.5rem; -} - -.card-img-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: 1rem; - border-radius: calc(0.25rem - 1px); -} - -.card-img, -.card-img-top, -.card-img-bottom { - 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-group > .card { - margin-bottom: 0.75rem; -} -@media (min-width: 576px) { - .card-group { - display: flex; - flex-flow: row wrap; - } - .card-group > .card { - 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; - } -} - -.accordion-button { - position: relative; - display: flex; - align-items: center; - width: 100%; - padding: 1rem 1.25rem; - font-size: 1rem; - color: #212529; - text-align: left; - background-color: #fff; - border: 0; - border-radius: 0; - overflow-anchor: none; - 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, border-radius 0.15s ease; -} -@media (prefers-reduced-motion: reduce) { - .accordion-button { - transition: none; - } -} -.accordion-button:not(.collapsed) { - color: #0c63e4; - background-color: #e7f1ff; - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125); -} -.accordion-button:not(.collapsed)::after { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - transform: rotate(-180deg); -} -.accordion-button::after { - flex-shrink: 0; - width: 1.25rem; - height: 1.25rem; - margin-left: auto; - content: ""; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-size: 1.25rem; - transition: transform 0.2s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .accordion-button::after { - transition: none; - } -} -.accordion-button:hover { - z-index: 2; -} -.accordion-button:focus { - z-index: 3; - border-color: #86b7fe; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} - -.accordion-header { - margin-bottom: 0; -} - -.accordion-item { - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.125); -} -.accordion-item:first-of-type { - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; -} -.accordion-item:first-of-type .accordion-button { - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); -} -.accordion-item:not(:first-of-type) { - border-top: 0; -} -.accordion-item:last-of-type { - border-bottom-right-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; -} -.accordion-item:last-of-type .accordion-button.collapsed { - border-bottom-right-radius: calc(0.25rem - 1px); - border-bottom-left-radius: calc(0.25rem - 1px); -} -.accordion-item:last-of-type .accordion-collapse { - border-bottom-right-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; -} - -.accordion-body { - padding: 1rem 1.25rem; -} - -.accordion-flush .accordion-collapse { - border-width: 0; -} -.accordion-flush .accordion-item { - border-right: 0; - border-left: 0; - border-radius: 0; -} -.accordion-flush .accordion-item:first-child { - border-top: 0; -} -.accordion-flush .accordion-item:last-child { - border-bottom: 0; -} -.accordion-flush .accordion-item .accordion-button { - border-radius: 0; -} - -.breadcrumb { - display: flex; - flex-wrap: wrap; - padding: 0 0; - margin-bottom: 1rem; - list-style: none; -} - -.breadcrumb-item + .breadcrumb-item { - padding-left: 0.5rem; -} -.breadcrumb-item + .breadcrumb-item::before { - float: left; - padding-right: 0.5rem; - color: #6c757d; - content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; -} -.breadcrumb-item.active { - color: #6c757d; -} - -.pagination { - display: flex; - padding-left: 0; - list-style: none; -} - -.page-link { - position: relative; - display: block; - color: #0d6efd; - text-decoration: none; - background-color: #fff; - border: 1px solid #dee2e6; - 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) { - .page-link { - transition: none; - } -} -.page-link:hover { - z-index: 2; - color: #0a58ca; - background-color: #e9ecef; - border-color: #dee2e6; -} -.page-link:focus { - z-index: 3; - color: #0a58ca; - background-color: #e9ecef; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} - -.page-item:not(:first-child) .page-link { - margin-left: -1px; -} -.page-item.active .page-link { - z-index: 3; - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} -.page-item.disabled .page-link { - color: #6c757d; - pointer-events: none; - background-color: #fff; - border-color: #dee2e6; -} - -.page-link { - padding: 0.375rem 0.75rem; -} - -.page-item:first-child .page-link { - 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; -} - -.pagination-lg .page-link { - padding: 0.75rem 1.5rem; - font-size: 1.25rem; -} -.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; -} -.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.35em 0.65em; - font-size: 0.75em; - font-weight: 700; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: 0.25rem; -} -.badge:empty { - display: none; -} - -.btn .badge { - position: relative; - top: -1px; -} - -.alert { - position: relative; - padding: 1rem 1rem; - margin-bottom: 1rem; - border: 1px solid transparent; - border-radius: 0.25rem; -} - -.alert-heading { - color: inherit; -} - -.alert-link { - font-weight: 700; -} - -.alert-dismissible { - padding-right: 3rem; -} -.alert-dismissible .btn-close { - position: absolute; - top: 0; - right: 0; - z-index: 2; - padding: 1.25rem 1rem; -} - -.alert-primary { - color: #084298; - background-color: #cfe2ff; - border-color: #b6d4fe; -} -.alert-primary .alert-link { - color: #06357a; -} - -.alert-secondary { - color: #41464b; - background-color: #e2e3e5; - border-color: #d3d6d8; -} -.alert-secondary .alert-link { - color: #34383c; -} - -.alert-success { - color: #0f5132; - background-color: #d1e7dd; - border-color: #badbcc; -} -.alert-success .alert-link { - color: #0c4128; -} - -.alert-info { - color: #055160; - background-color: #cff4fc; - border-color: #b6effb; -} -.alert-info .alert-link { - color: #04414d; -} - -.alert-warning { - color: #664d03; - background-color: #fff3cd; - border-color: #ffecb5; -} -.alert-warning .alert-link { - color: #523e02; -} - -.alert-danger { - color: #842029; - background-color: #f8d7da; - border-color: #f5c2c7; -} -.alert-danger .alert-link { - color: #6a1a21; -} - -.alert-light { - color: #636464; - background-color: #fefefe; - border-color: #fdfdfe; -} -.alert-light .alert-link { - color: #4f5050; -} - -.alert-dark { - color: #141619; - background-color: #d3d3d4; - border-color: #bcbebf; -} -.alert-dark .alert-link { - color: #101214; -} - -@-webkit-keyframes progress-bar-stripes { - 0% { - background-position-x: 1rem; - } -} - -@keyframes progress-bar-stripes { - 0% { - background-position-x: 1rem; - } -} -.progress { - display: flex; - height: 1rem; - overflow: hidden; - font-size: 0.75rem; - background-color: #e9ecef; - border-radius: 0.25rem; -} - -.progress-bar { - display: flex; - flex-direction: column; - justify-content: center; - overflow: hidden; - color: #fff; - text-align: center; - white-space: nowrap; - background-color: #0d6efd; - 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: 1s linear infinite progress-bar-stripes; - animation: 1s linear infinite progress-bar-stripes; -} -@media (prefers-reduced-motion: reduce) { - .progress-bar-animated { - -webkit-animation: none; - animation: none; - } -} - -.list-group { - display: flex; - flex-direction: column; - padding-left: 0; - margin-bottom: 0; - border-radius: 0.25rem; -} - -.list-group-numbered { - list-style-type: none; - counter-reset: section; -} -.list-group-numbered > li::before { - content: counters(section, ".") ". "; - counter-increment: section; -} - -.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.5rem 1rem; - color: #212529; - text-decoration: none; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.125); -} -.list-group-item:first-child { - border-top-left-radius: inherit; - border-top-right-radius: inherit; -} -.list-group-item:last-child { - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; -} -.list-group-item.disabled, .list-group-item:disabled { - color: #6c757d; - pointer-events: none; - background-color: #fff; -} -.list-group-item.active { - z-index: 2; - color: #fff; - background-color: #0d6efd; - border-color: #0d6efd; -} -.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 { - 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 { - 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 { - 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 { - 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 { - 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; - } -} -@media (min-width: 1400px) { - .list-group-horizontal-xxl { - flex-direction: row; - } - .list-group-horizontal-xxl > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; - border-top-right-radius: 0; - } - .list-group-horizontal-xxl > .list-group-item:last-child { - border-top-right-radius: 0.25rem; - border-bottom-left-radius: 0; - } - .list-group-horizontal-xxl > .list-group-item.active { - margin-top: 0; - } - .list-group-horizontal-xxl > .list-group-item + .list-group-item { - border-top-width: 1px; - border-left-width: 0; - } - .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; - } -} -.list-group-flush { - border-radius: 0; -} -.list-group-flush > .list-group-item { - border-width: 0 0 1px; -} -.list-group-flush > .list-group-item:last-child { - border-bottom-width: 0; -} - -.list-group-item-primary { - color: #084298; - background-color: #cfe2ff; -} -.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { - color: #084298; - background-color: #bacbe6; -} -.list-group-item-primary.list-group-item-action.active { - color: #fff; - background-color: #084298; - border-color: #084298; -} - -.list-group-item-secondary { - color: #41464b; - background-color: #e2e3e5; -} -.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { - color: #41464b; - background-color: #cbccce; -} -.list-group-item-secondary.list-group-item-action.active { - color: #fff; - background-color: #41464b; - border-color: #41464b; -} - -.list-group-item-success { - color: #0f5132; - background-color: #d1e7dd; -} -.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { - color: #0f5132; - background-color: #bcd0c7; -} -.list-group-item-success.list-group-item-action.active { - color: #fff; - background-color: #0f5132; - border-color: #0f5132; -} - -.list-group-item-info { - color: #055160; - background-color: #cff4fc; -} -.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { - color: #055160; - background-color: #badce3; -} -.list-group-item-info.list-group-item-action.active { - color: #fff; - background-color: #055160; - border-color: #055160; -} - -.list-group-item-warning { - color: #664d03; - background-color: #fff3cd; -} -.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { - color: #664d03; - background-color: #e6dbb9; -} -.list-group-item-warning.list-group-item-action.active { - color: #fff; - background-color: #664d03; - border-color: #664d03; -} - -.list-group-item-danger { - color: #842029; - background-color: #f8d7da; -} -.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { - color: #842029; - background-color: #dfc2c4; -} -.list-group-item-danger.list-group-item-action.active { - color: #fff; - background-color: #842029; - border-color: #842029; -} - -.list-group-item-light { - color: #636464; - background-color: #fefefe; -} -.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { - color: #636464; - background-color: #e5e5e5; -} -.list-group-item-light.list-group-item-action.active { - color: #fff; - background-color: #636464; - border-color: #636464; -} - -.list-group-item-dark { - color: #141619; - background-color: #d3d3d4; -} -.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { - color: #141619; - background-color: #bebebf; -} -.list-group-item-dark.list-group-item-action.active { - color: #fff; - background-color: #141619; - border-color: #141619; -} - -.btn-close { - box-sizing: content-box; - width: 1em; - height: 1em; - padding: 0.25em 0.25em; - color: #000; - background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; - border: 0; - border-radius: 0.25rem; - opacity: 0.5; -} -.btn-close:hover { - color: #000; - text-decoration: none; - opacity: 0.75; -} -.btn-close:focus { - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); - opacity: 1; -} -.btn-close:disabled, .btn-close.disabled { - pointer-events: none; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - opacity: 0.25; -} - -.btn-close-white { - filter: invert(1) grayscale(100%) brightness(200%); -} - -.toast { - width: 350px; - max-width: 100%; - font-size: 0.875rem; - pointer-events: auto; - 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.5rem 1rem rgba(0, 0, 0, 0.15); - border-radius: 0.25rem; -} -.toast.showing { - opacity: 0; -} -.toast:not(.show) { - display: none; -} - -.toast-container { - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; - max-width: 100%; - pointer-events: none; -} -.toast-container > :not(:last-child) { - margin-bottom: 0.75rem; -} - -.toast-header { - display: flex; - align-items: center; - padding: 0.5rem 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); - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); -} -.toast-header .btn-close { - margin-right: -0.375rem; - margin-left: 0.75rem; -} - -.toast-body { - padding: 0.75rem; - word-wrap: break-word; -} - -.modal { - position: fixed; - top: 0; - left: 0; - z-index: 1055; - display: none; - width: 100%; - height: 100%; - overflow-x: hidden; - overflow-y: auto; - outline: 0; -} - -.modal-dialog { - position: relative; - width: auto; - margin: 0.5rem; - pointer-events: none; -} -.modal.fade .modal-dialog { - transition: transform 0.3s ease-out; - transform: translate(0, -50px); -} -@media (prefers-reduced-motion: reduce) { - .modal.fade .modal-dialog { - transition: none; - } -} -.modal.show .modal-dialog { - transform: none; -} -.modal.modal-static .modal-dialog { - transform: scale(1.02); -} - -.modal-dialog-scrollable { - height: calc(100% - 1rem); -} -.modal-dialog-scrollable .modal-content { - max-height: 100%; - overflow: hidden; -} -.modal-dialog-scrollable .modal-body { - overflow-y: auto; -} - -.modal-dialog-centered { - display: flex; - align-items: center; - min-height: calc(100% - 1rem); -} - -.modal-content { - position: relative; - display: flex; - 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: 1050; - width: 100vw; - height: 100vh; - background-color: #000; -} -.modal-backdrop.fade { - opacity: 0; -} -.modal-backdrop.show { - opacity: 0.5; -} - -.modal-header { - display: flex; - flex-shrink: 0; - align-items: center; - 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 .btn-close { - padding: 0.5rem 0.5rem; - margin: -0.5rem -0.5rem -0.5rem auto; -} - -.modal-title { - margin-bottom: 0; - line-height: 1.5; -} - -.modal-body { - position: relative; - flex: 1 1 auto; - padding: 1rem; -} - -.modal-footer { - display: flex; - flex-wrap: wrap; - flex-shrink: 0; - align-items: center; - 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; -} - -@media (min-width: 576px) { - .modal-dialog { - max-width: 500px; - margin: 1.75rem auto; - } - - .modal-dialog-scrollable { - height: calc(100% - 3.5rem); - } - - .modal-dialog-centered { - min-height: calc(100% - 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; - } -} -.modal-fullscreen { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; -} -.modal-fullscreen .modal-content { - height: 100%; - border: 0; - border-radius: 0; -} -.modal-fullscreen .modal-header { - border-radius: 0; -} -.modal-fullscreen .modal-body { - overflow-y: auto; -} -.modal-fullscreen .modal-footer { - border-radius: 0; -} - -@media (max-width: 575.98px) { - .modal-fullscreen-sm-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-sm-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-sm-down .modal-header { - border-radius: 0; - } - .modal-fullscreen-sm-down .modal-body { - overflow-y: auto; - } - .modal-fullscreen-sm-down .modal-footer { - border-radius: 0; - } -} -@media (max-width: 767.98px) { - .modal-fullscreen-md-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-md-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-md-down .modal-header { - border-radius: 0; - } - .modal-fullscreen-md-down .modal-body { - overflow-y: auto; - } - .modal-fullscreen-md-down .modal-footer { - border-radius: 0; - } -} -@media (max-width: 991.98px) { - .modal-fullscreen-lg-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-lg-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-lg-down .modal-header { - border-radius: 0; - } - .modal-fullscreen-lg-down .modal-body { - overflow-y: auto; - } - .modal-fullscreen-lg-down .modal-footer { - border-radius: 0; - } -} -@media (max-width: 1199.98px) { - .modal-fullscreen-xl-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-xl-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-xl-down .modal-header { - border-radius: 0; - } - .modal-fullscreen-xl-down .modal-body { - overflow-y: auto; - } - .modal-fullscreen-xl-down .modal-footer { - border-radius: 0; - } -} -@media (max-width: 1399.98px) { - .modal-fullscreen-xxl-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-xxl-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-xxl-down .modal-header { - border-radius: 0; - } - .modal-fullscreen-xxl-down .modal-body { - overflow-y: auto; - } - .modal-fullscreen-xxl-down .modal-footer { - border-radius: 0; - } -} -.tooltip { - position: absolute; - z-index: 1080; - display: block; - margin: 0; - font-family: var(--bs-font-sans-serif); - 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 .tooltip-arrow { - position: absolute; - display: block; - width: 0.8rem; - height: 0.4rem; -} -.tooltip .tooltip-arrow::before { - position: absolute; - content: ""; - border-color: transparent; - border-style: solid; -} - -.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] { - padding: 0.4rem 0; -} -.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { - bottom: 0; -} -.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { - top: -1px; - border-width: 0.4rem 0.4rem 0; - border-top-color: #000; -} - -.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] { - padding: 0 0.4rem; -} -.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { - left: 0; - width: 0.4rem; - height: 0.8rem; -} -.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { - right: -1px; - border-width: 0.4rem 0.4rem 0.4rem 0; - border-right-color: #000; -} - -.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] { - padding: 0.4rem 0; -} -.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { - top: 0; -} -.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { - bottom: -1px; - border-width: 0 0.4rem 0.4rem; - border-bottom-color: #000; -} - -.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] { - padding: 0 0.4rem; -} -.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { - right: 0; - width: 0.4rem; - height: 0.8rem; -} -.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { - left: -1px; - 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 /* rtl:ignore */; - z-index: 1070; - display: block; - max-width: 276px; - font-family: var(--bs-font-sans-serif); - 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 .popover-arrow { - position: absolute; - display: block; - width: 1rem; - height: 0.5rem; -} -.popover .popover-arrow::before, .popover .popover-arrow::after { - position: absolute; - display: block; - content: ""; - border-color: transparent; - border-style: solid; -} - -.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { - bottom: calc(-0.5rem - 1px); -} -.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { - bottom: 0; - border-width: 0.5rem 0.5rem 0; - border-top-color: rgba(0, 0, 0, 0.25); -} -.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { - bottom: 1px; - border-width: 0.5rem 0.5rem 0; - border-top-color: #fff; -} - -.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { - left: calc(-0.5rem - 1px); - width: 0.5rem; - height: 1rem; -} -.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { - left: 0; - border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: rgba(0, 0, 0, 0.25); -} -.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { - left: 1px; - border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: #fff; -} - -.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { - top: calc(-0.5rem - 1px); -} -.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-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 > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-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[data-popper-placement^=bottom] .popover-header::before { - position: absolute; - top: 0; - left: 50%; - display: block; - width: 1rem; - margin-left: -0.5rem; - content: ""; - border-bottom: 1px solid #f0f0f0; -} - -.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { - right: calc(-0.5rem - 1px); - width: 0.5rem; - height: 1rem; -} -.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { - right: 0; - border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: rgba(0, 0, 0, 0.25); -} -.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { - right: 1px; - border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: #fff; -} - -.popover-header { - padding: 0.5rem 1rem; - margin-bottom: 0; - font-size: 1rem; - background-color: #f0f0f0; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); - border-top-left-radius: calc(0.3rem - 1px); - border-top-right-radius: calc(0.3rem - 1px); -} -.popover-header:empty { - display: none; -} - -.popover-body { - padding: 1rem 1rem; - color: #212529; -} - -.carousel { - position: relative; -} - -.carousel.pointer-event { - 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: 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; -} - -/* rtl:begin:ignore */ -.carousel-item-next:not(.carousel-item-start), -.active.carousel-item-end { - transform: translateX(100%); -} - -.carousel-item-prev:not(.carousel-item-end), -.active.carousel-item-start { - transform: translateX(-100%); -} - -/* rtl:end:ignore */ -.carousel-fade .carousel-item { - opacity: 0; - transition-property: opacity; - transform: none; -} -.carousel-fade .carousel-item.active, -.carousel-fade .carousel-item-next.carousel-item-start, -.carousel-fade .carousel-item-prev.carousel-item-end { - z-index: 1; - opacity: 1; -} -.carousel-fade .active.carousel-item-start, -.carousel-fade .active.carousel-item-end { - z-index: 0; - opacity: 0; - transition: opacity 0s 0.6s; -} -@media (prefers-reduced-motion: reduce) { - .carousel-fade .active.carousel-item-start, -.carousel-fade .active.carousel-item-end { - transition: none; - } -} - -.carousel-control-prev, -.carousel-control-next { - position: absolute; - top: 0; - bottom: 0; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - width: 15%; - padding: 0; - color: #fff; - text-align: center; - background: none; - border: 0; - 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: 2rem; - height: 2rem; - background-repeat: no-repeat; - background-position: 50%; - background-size: 100% 100%; -} - -/* rtl:options: { - "autoRename": true, - "stringMap":[ { - "name" : "prev-next", - "search" : "prev", - "replace" : "next" - } ] -} */ -.carousel-control-prev-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e"); -} - -.carousel-control-next-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); -} - -.carousel-indicators { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - display: flex; - justify-content: center; - padding: 0; - margin-right: 15%; - margin-bottom: 1rem; - margin-left: 15%; - list-style: none; -} -.carousel-indicators [data-bs-target] { - box-sizing: content-box; - flex: 0 1 auto; - width: 30px; - height: 3px; - padding: 0; - margin-right: 3px; - margin-left: 3px; - text-indent: -999px; - cursor: pointer; - background-color: #fff; - background-clip: padding-box; - border: 0; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - opacity: 0.5; - transition: opacity 0.6s ease; -} -@media (prefers-reduced-motion: reduce) { - .carousel-indicators [data-bs-target] { - transition: none; - } -} -.carousel-indicators .active { - opacity: 1; -} - -.carousel-caption { - position: absolute; - right: 15%; - bottom: 1.25rem; - left: 15%; - padding-top: 1.25rem; - padding-bottom: 1.25rem; - color: #fff; - text-align: center; -} - -.carousel-dark .carousel-control-prev-icon, -.carousel-dark .carousel-control-next-icon { - filter: invert(1) grayscale(100); -} -.carousel-dark .carousel-indicators [data-bs-target] { - background-color: #000; -} -.carousel-dark .carousel-caption { - color: #000; -} - -@-webkit-keyframes spinner-border { - to { - transform: rotate(360deg) /* rtl:ignore */; - } -} - -@keyframes spinner-border { - to { - transform: rotate(360deg) /* rtl:ignore */; - } -} -.spinner-border { - display: inline-block; - width: 2rem; - height: 2rem; - vertical-align: -0.125em; - border: 0.25em solid currentColor; - border-right-color: transparent; - border-radius: 50%; - -webkit-animation: 0.75s linear infinite spinner-border; - animation: 0.75s linear infinite spinner-border; -} - -.spinner-border-sm { - width: 1rem; - height: 1rem; - border-width: 0.2em; -} - -@-webkit-keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } -} - -@keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } -} -.spinner-grow { - display: inline-block; - width: 2rem; - height: 2rem; - vertical-align: -0.125em; - background-color: currentColor; - border-radius: 50%; - opacity: 0; - -webkit-animation: 0.75s linear infinite spinner-grow; - animation: 0.75s linear infinite spinner-grow; -} - -.spinner-grow-sm { - width: 1rem; - height: 1rem; -} - -@media (prefers-reduced-motion: reduce) { - .spinner-border, -.spinner-grow { - -webkit-animation-duration: 1.5s; - animation-duration: 1.5s; - } -} -.offcanvas { - position: fixed; - bottom: 0; - z-index: 1045; - display: flex; - flex-direction: column; - max-width: 100%; - visibility: hidden; - background-color: #fff; - background-clip: padding-box; - outline: 0; - transition: transform 0.3s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .offcanvas { - transition: none; - } -} - -.offcanvas-backdrop { - position: fixed; - top: 0; - left: 0; - z-index: 1040; - width: 100vw; - height: 100vh; - background-color: #000; -} -.offcanvas-backdrop.fade { - opacity: 0; -} -.offcanvas-backdrop.show { - opacity: 0.5; -} - -.offcanvas-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 1rem; -} -.offcanvas-header .btn-close { - padding: 0.5rem 0.5rem; - margin-top: -0.5rem; - margin-right: -0.5rem; - margin-bottom: -0.5rem; -} - -.offcanvas-title { - margin-bottom: 0; - line-height: 1.5; -} - -.offcanvas-body { - flex-grow: 1; - padding: 1rem 1rem; - overflow-y: auto; -} - -.offcanvas-start { - top: 0; - left: 0; - width: 400px; - border-right: 1px solid rgba(0, 0, 0, 0.2); - transform: translateX(-100%); -} - -.offcanvas-end { - top: 0; - right: 0; - width: 400px; - border-left: 1px solid rgba(0, 0, 0, 0.2); - transform: translateX(100%); -} - -.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: 30vh; - max-height: 100%; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); - transform: translateY(-100%); -} - -.offcanvas-bottom { - right: 0; - left: 0; - height: 30vh; - max-height: 100%; - border-top: 1px solid rgba(0, 0, 0, 0.2); - transform: translateY(100%); -} - -.offcanvas.show { - transform: none; -} - -.placeholder { - display: inline-block; - min-height: 1em; - vertical-align: middle; - cursor: wait; - background-color: currentColor; - opacity: 0.5; -} -.placeholder.btn::before { - display: inline-block; - content: ""; -} - -.placeholder-xs { - min-height: 0.6em; -} - -.placeholder-sm { - min-height: 0.8em; -} - -.placeholder-lg { - min-height: 1.2em; -} - -.placeholder-glow .placeholder { - -webkit-animation: placeholder-glow 2s ease-in-out infinite; - animation: placeholder-glow 2s ease-in-out infinite; -} - -@-webkit-keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} - -@keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} -.placeholder-wave { - -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); - mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); - -webkit-mask-size: 200% 100%; - mask-size: 200% 100%; - -webkit-animation: placeholder-wave 2s linear infinite; - animation: placeholder-wave 2s linear infinite; -} - -@-webkit-keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} - -@keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} -.clearfix::after { - display: block; - clear: both; - content: ""; -} - -.link-primary { - color: #0d6efd; -} -.link-primary:hover, .link-primary:focus { - color: #0a58ca; -} - -.link-secondary { - color: #6c757d; -} -.link-secondary:hover, .link-secondary:focus { - color: #565e64; -} - -.link-success { - color: #198754; -} -.link-success:hover, .link-success:focus { - color: #146c43; -} - -.link-info { - color: #0dcaf0; -} -.link-info:hover, .link-info:focus { - color: #3dd5f3; -} - -.link-warning { - color: #ffc107; -} -.link-warning:hover, .link-warning:focus { - color: #ffcd39; -} - -.link-danger { - color: #dc3545; -} -.link-danger:hover, .link-danger:focus { - color: #b02a37; -} - -.link-light { - color: #f8f9fa; -} -.link-light:hover, .link-light:focus { - color: #f9fafb; -} - -.link-dark { - color: #212529; -} -.link-dark:hover, .link-dark:focus { - color: #1a1e21; -} - -.ratio { - position: relative; - width: 100%; -} -.ratio::before { - display: block; - padding-top: var(--bs-aspect-ratio); - content: ""; -} -.ratio > * { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.ratio-1x1 { - --bs-aspect-ratio: 100%; -} - -.ratio-4x3 { - --bs-aspect-ratio: 75%; -} - -.ratio-16x9 { - --bs-aspect-ratio: 56.25%; -} - -.ratio-21x9 { - --bs-aspect-ratio: 42.8571428571%; -} - -.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; -} - -.sticky-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; -} - -@media (min-width: 576px) { - .sticky-sm-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } -} -@media (min-width: 768px) { - .sticky-md-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } -} -@media (min-width: 992px) { - .sticky-lg-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } -} -@media (min-width: 1200px) { - .sticky-xl-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } -} -@media (min-width: 1400px) { - .sticky-xxl-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } -} -.hstack { - display: flex; - flex-direction: row; - align-items: center; - align-self: stretch; -} - -.vstack { - display: flex; - flex: 1 1 auto; - flex-direction: column; - align-self: stretch; -} - -.visually-hidden, -.visually-hidden-focusable:not(:focus):not(:focus-within) { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; - border: 0 !important; -} - -.stretched-link::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1; - content: ""; -} - -.text-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.vr { - display: inline-block; - align-self: stretch; - width: 1px; - min-height: 1em; - background-color: currentColor; - opacity: 0.25; -} - -.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; -} - -.float-start { - float: left !important; -} - -.float-end { - float: right !important; -} - -.float-none { - float: none !important; -} - -.opacity-0 { - opacity: 0 !important; -} - -.opacity-25 { - opacity: 0.25 !important; -} - -.opacity-50 { - opacity: 0.5 !important; -} - -.opacity-75 { - opacity: 0.75 !important; -} - -.opacity-100 { - opacity: 1 !important; -} - -.overflow-auto { - overflow: auto !important; -} - -.overflow-hidden { - overflow: hidden !important; -} - -.overflow-visible { - overflow: visible !important; -} - -.overflow-scroll { - overflow: scroll !important; -} - -.d-inline { - display: inline !important; -} - -.d-inline-block { - display: inline-block !important; -} - -.d-block { - display: block !important; -} - -.d-grid { - display: grid !important; -} - -.d-table { - display: table !important; -} - -.d-table-row { - display: table-row !important; -} - -.d-table-cell { - display: table-cell !important; -} - -.d-flex { - display: flex !important; -} - -.d-inline-flex { - display: inline-flex !important; -} - -.d-none { - display: none !important; -} - -.shadow { - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; -} - -.shadow-sm { - box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; -} - -.shadow-lg { - box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; -} - -.shadow-none { - box-shadow: none !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; -} - -.top-0 { - top: 0 !important; -} - -.top-50 { - top: 50% !important; -} - -.top-100 { - top: 100% !important; -} - -.bottom-0 { - bottom: 0 !important; -} - -.bottom-50 { - bottom: 50% !important; -} - -.bottom-100 { - bottom: 100% !important; -} - -.start-0 { - left: 0 !important; -} - -.start-50 { - left: 50% !important; -} - -.start-100 { - left: 100% !important; -} - -.end-0 { - right: 0 !important; -} - -.end-50 { - right: 50% !important; -} - -.end-100 { - right: 100% !important; -} - -.translate-middle { - transform: translate(-50%, -50%) !important; -} - -.translate-middle-x { - transform: translateX(-50%) !important; -} - -.translate-middle-y { - transform: translateY(-50%) !important; -} - -.border { - border: 1px solid #dee2e6 !important; -} - -.border-0 { - border: 0 !important; -} - -.border-top { - border-top: 1px solid #dee2e6 !important; -} - -.border-top-0 { - border-top: 0 !important; -} - -.border-end { - border-right: 1px solid #dee2e6 !important; -} - -.border-end-0 { - border-right: 0 !important; -} - -.border-bottom { - border-bottom: 1px solid #dee2e6 !important; -} - -.border-bottom-0 { - border-bottom: 0 !important; -} - -.border-start { - border-left: 1px solid #dee2e6 !important; -} - -.border-start-0 { - border-left: 0 !important; -} - -.border-primary { - border-color: #0d6efd !important; -} - -.border-secondary { - border-color: #6c757d !important; -} - -.border-success { - border-color: #198754 !important; -} - -.border-info { - border-color: #0dcaf0 !important; -} - -.border-warning { - border-color: #ffc107 !important; -} - -.border-danger { - border-color: #dc3545 !important; -} - -.border-light { - border-color: #f8f9fa !important; -} - -.border-dark { - border-color: #212529 !important; -} - -.border-white { - border-color: #fff !important; -} - -.border-1 { - border-width: 1px !important; -} - -.border-2 { - border-width: 2px !important; -} - -.border-3 { - border-width: 3px !important; -} - -.border-4 { - border-width: 4px !important; -} - -.border-5 { - border-width: 5px !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; -} - -.mw-100 { - max-width: 100% !important; -} - -.vw-100 { - width: 100vw !important; -} - -.min-vw-100 { - min-width: 100vw !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; -} - -.mh-100 { - max-height: 100% !important; -} - -.vh-100 { - height: 100vh !important; -} - -.min-vh-100 { - min-height: 100vh !important; -} - -.flex-fill { - flex: 1 1 auto !important; -} - -.flex-row { - flex-direction: row !important; -} - -.flex-column { - flex-direction: column !important; -} - -.flex-row-reverse { - flex-direction: row-reverse !important; -} - -.flex-column-reverse { - flex-direction: column-reverse !important; -} - -.flex-grow-0 { - flex-grow: 0 !important; -} - -.flex-grow-1 { - flex-grow: 1 !important; -} - -.flex-shrink-0 { - flex-shrink: 0 !important; -} - -.flex-shrink-1 { - flex-shrink: 1 !important; -} - -.flex-wrap { - flex-wrap: wrap !important; -} - -.flex-nowrap { - flex-wrap: nowrap !important; -} - -.flex-wrap-reverse { - flex-wrap: wrap-reverse !important; -} - -.gap-0 { - gap: 0 !important; -} - -.gap-1 { - gap: 0.25rem !important; -} - -.gap-2 { - gap: 0.5rem !important; -} - -.gap-3 { - gap: 1rem !important; -} - -.gap-4 { - gap: 1.5rem !important; -} - -.gap-5 { - gap: 3rem !important; -} - -.justify-content-start { - justify-content: flex-start !important; -} - -.justify-content-end { - justify-content: flex-end !important; -} - -.justify-content-center { - justify-content: center !important; -} - -.justify-content-between { - justify-content: space-between !important; -} - -.justify-content-around { - justify-content: space-around !important; -} - -.justify-content-evenly { - justify-content: space-evenly !important; -} - -.align-items-start { - align-items: flex-start !important; -} - -.align-items-end { - align-items: flex-end !important; -} - -.align-items-center { - align-items: center !important; -} - -.align-items-baseline { - align-items: baseline !important; -} - -.align-items-stretch { - align-items: stretch !important; -} - -.align-content-start { - align-content: flex-start !important; -} - -.align-content-end { - align-content: flex-end !important; -} - -.align-content-center { - align-content: center !important; -} - -.align-content-between { - align-content: space-between !important; -} - -.align-content-around { - align-content: space-around !important; -} - -.align-content-stretch { - align-content: stretch !important; -} - -.align-self-auto { - align-self: auto !important; -} - -.align-self-start { - align-self: flex-start !important; -} - -.align-self-end { - align-self: flex-end !important; -} - -.align-self-center { - align-self: center !important; -} - -.align-self-baseline { - align-self: baseline !important; -} - -.align-self-stretch { - align-self: stretch !important; -} - -.order-first { - order: -1 !important; -} - -.order-0 { - order: 0 !important; -} - -.order-1 { - order: 1 !important; -} - -.order-2 { - order: 2 !important; -} - -.order-3 { - order: 3 !important; -} - -.order-4 { - order: 4 !important; -} - -.order-5 { - order: 5 !important; -} - -.order-last { - order: 6 !important; -} - -.m-0 { - margin: 0 !important; -} - -.m-1 { - margin: 0.25rem !important; -} - -.m-2 { - margin: 0.5rem !important; -} - -.m-3 { - margin: 1rem !important; -} - -.m-4 { - margin: 1.5rem !important; -} - -.m-5 { - margin: 3rem !important; -} - -.m-auto { - margin: auto !important; -} - -.mx-0 { - margin-right: 0 !important; - margin-left: 0 !important; -} - -.mx-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; -} - -.mx-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; -} - -.mx-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; -} - -.mx-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; -} - -.mx-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; -} - -.mx-auto { - margin-right: auto !important; - margin-left: auto !important; -} - -.my-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.my-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; -} - -.my-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; -} - -.my-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; -} - -.my-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; -} - -.my-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; -} - -.my-auto { - margin-top: auto !important; - margin-bottom: auto !important; -} - -.mt-0 { - margin-top: 0 !important; -} - -.mt-1 { - margin-top: 0.25rem !important; -} - -.mt-2 { - margin-top: 0.5rem !important; -} - -.mt-3 { - margin-top: 1rem !important; -} - -.mt-4 { - margin-top: 1.5rem !important; -} - -.mt-5 { - margin-top: 3rem !important; -} - -.mt-auto { - margin-top: auto !important; -} - -.me-0 { - margin-right: 0 !important; -} - -.me-1 { - margin-right: 0.25rem !important; -} - -.me-2 { - margin-right: 0.5rem !important; -} - -.me-3 { - margin-right: 1rem !important; -} - -.me-4 { - margin-right: 1.5rem !important; -} - -.me-5 { - margin-right: 3rem !important; -} - -.me-auto { - margin-right: auto !important; -} - -.mb-0 { - margin-bottom: 0 !important; -} - -.mb-1 { - margin-bottom: 0.25rem !important; -} - -.mb-2 { - margin-bottom: 0.5rem !important; -} - -.mb-3 { - margin-bottom: 1rem !important; -} - -.mb-4 { - margin-bottom: 1.5rem !important; -} - -.mb-5 { - margin-bottom: 3rem !important; -} - -.mb-auto { - margin-bottom: auto !important; -} - -.ms-0 { - margin-left: 0 !important; -} - -.ms-1 { - margin-left: 0.25rem !important; -} - -.ms-2 { - margin-left: 0.5rem !important; -} - -.ms-3 { - margin-left: 1rem !important; -} - -.ms-4 { - margin-left: 1.5rem !important; -} - -.ms-5 { - margin-left: 3rem !important; -} - -.ms-auto { - margin-left: auto !important; -} - -.p-0 { - padding: 0 !important; -} - -.p-1 { - padding: 0.25rem !important; -} - -.p-2 { - padding: 0.5rem !important; -} - -.p-3 { - padding: 1rem !important; -} - -.p-4 { - padding: 1.5rem !important; -} - -.p-5 { - padding: 3rem !important; -} - -.px-0 { - padding-right: 0 !important; - padding-left: 0 !important; -} - -.px-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; -} - -.px-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; -} - -.px-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; -} - -.px-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; -} - -.px-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; -} - -.py-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -.py-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; -} - -.py-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; -} - -.py-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; -} - -.py-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; -} - -.py-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; -} - -.pt-0 { - padding-top: 0 !important; -} - -.pt-1 { - padding-top: 0.25rem !important; -} - -.pt-2 { - padding-top: 0.5rem !important; -} - -.pt-3 { - padding-top: 1rem !important; -} - -.pt-4 { - padding-top: 1.5rem !important; -} - -.pt-5 { - padding-top: 3rem !important; -} - -.pe-0 { - padding-right: 0 !important; -} - -.pe-1 { - padding-right: 0.25rem !important; -} - -.pe-2 { - padding-right: 0.5rem !important; -} - -.pe-3 { - padding-right: 1rem !important; -} - -.pe-4 { - padding-right: 1.5rem !important; -} - -.pe-5 { - padding-right: 3rem !important; -} - -.pb-0 { - padding-bottom: 0 !important; -} - -.pb-1 { - padding-bottom: 0.25rem !important; -} - -.pb-2 { - padding-bottom: 0.5rem !important; -} - -.pb-3 { - padding-bottom: 1rem !important; -} - -.pb-4 { - padding-bottom: 1.5rem !important; -} - -.pb-5 { - padding-bottom: 3rem !important; -} - -.ps-0 { - padding-left: 0 !important; -} - -.ps-1 { - padding-left: 0.25rem !important; -} - -.ps-2 { - padding-left: 0.5rem !important; -} - -.ps-3 { - padding-left: 1rem !important; -} - -.ps-4 { - padding-left: 1.5rem !important; -} - -.ps-5 { - padding-left: 3rem !important; -} - -.font-monospace { - font-family: var(--bs-font-monospace) !important; -} - -.fs-1 { - font-size: calc(1.375rem + 1.5vw) !important; -} - -.fs-2 { - font-size: calc(1.325rem + 0.9vw) !important; -} - -.fs-3 { - font-size: calc(1.3rem + 0.6vw) !important; -} - -.fs-4 { - font-size: calc(1.275rem + 0.3vw) !important; -} - -.fs-5 { - font-size: 1.25rem !important; -} - -.fs-6 { - font-size: 1rem !important; -} - -.fst-italic { - font-style: italic !important; -} - -.fst-normal { - font-style: normal !important; -} - -.fw-light { - font-weight: 300 !important; -} - -.fw-lighter { - font-weight: lighter !important; -} - -.fw-normal { - font-weight: 400 !important; -} - -.fw-bold { - font-weight: 700 !important; -} - -.fw-bolder { - font-weight: bolder !important; -} - -.lh-1 { - line-height: 1 !important; -} - -.lh-sm { - line-height: 1.25 !important; -} - -.lh-base { - line-height: 1.5 !important; -} - -.lh-lg { - line-height: 2 !important; -} - -.text-start { - text-align: left !important; -} - -.text-end { - text-align: right !important; -} - -.text-center { - text-align: center !important; -} - -.text-decoration-none { - text-decoration: none !important; -} - -.text-decoration-underline { - text-decoration: underline !important; -} - -.text-decoration-line-through { - text-decoration: line-through !important; -} - -.text-lowercase { - text-transform: lowercase !important; -} - -.text-uppercase { - text-transform: uppercase !important; -} - -.text-capitalize { - text-transform: capitalize !important; -} - -.text-wrap { - white-space: normal !important; -} - -.text-nowrap { - white-space: nowrap !important; -} - -/* rtl:begin:remove */ -.text-break { - word-wrap: break-word !important; - word-break: break-word !important; -} - -/* rtl:end:remove */ -.text-primary { - --bs-text-opacity: 1; - color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; -} - -.text-secondary { - --bs-text-opacity: 1; - color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; -} - -.text-success { - --bs-text-opacity: 1; - color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; -} - -.text-info { - --bs-text-opacity: 1; - color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; -} - -.text-warning { - --bs-text-opacity: 1; - color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; -} - -.text-danger { - --bs-text-opacity: 1; - color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; -} - -.text-light { - --bs-text-opacity: 1; - color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; -} - -.text-dark { - --bs-text-opacity: 1; - color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; -} - -.text-black { - --bs-text-opacity: 1; - color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; -} - -.text-white { - --bs-text-opacity: 1; - color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; -} - -.text-body { - --bs-text-opacity: 1; - color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; -} - -.text-muted { - --bs-text-opacity: 1; - color: #6c757d !important; -} - -.text-black-50 { - --bs-text-opacity: 1; - color: rgba(0, 0, 0, 0.5) !important; -} - -.text-white-50 { - --bs-text-opacity: 1; - color: rgba(255, 255, 255, 0.5) !important; -} - -.text-reset { - --bs-text-opacity: 1; - color: inherit !important; -} - -.text-opacity-25 { - --bs-text-opacity: 0.25; -} - -.text-opacity-50 { - --bs-text-opacity: 0.5; -} - -.text-opacity-75 { - --bs-text-opacity: 0.75; -} - -.text-opacity-100 { - --bs-text-opacity: 1; -} - -.bg-primary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-secondary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-success { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-info { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-warning { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-danger { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-light { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-dark { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-black { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-white { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-body { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-transparent { - --bs-bg-opacity: 1; - background-color: transparent !important; -} - -.bg-opacity-10 { - --bs-bg-opacity: 0.1; -} - -.bg-opacity-25 { - --bs-bg-opacity: 0.25; -} - -.bg-opacity-50 { - --bs-bg-opacity: 0.5; -} - -.bg-opacity-75 { - --bs-bg-opacity: 0.75; -} - -.bg-opacity-100 { - --bs-bg-opacity: 1; -} - -.bg-gradient { - background-image: var(--bs-gradient) !important; -} - -.user-select-all { - -webkit-user-select: all !important; - -moz-user-select: all !important; - user-select: all !important; -} - -.user-select-auto { - -webkit-user-select: auto !important; - -moz-user-select: auto !important; - user-select: auto !important; -} - -.user-select-none { - -webkit-user-select: none !important; - -moz-user-select: none !important; - user-select: none !important; -} - -.pe-none { - pointer-events: none !important; -} - -.pe-auto { - pointer-events: auto !important; -} - -.rounded { - border-radius: 0.25rem !important; -} - -.rounded-0 { - border-radius: 0 !important; -} - -.rounded-1 { - border-radius: 0.2rem !important; -} - -.rounded-2 { - border-radius: 0.25rem !important; -} - -.rounded-3 { - border-radius: 0.3rem !important; -} - -.rounded-circle { - border-radius: 50% !important; -} - -.rounded-pill { - border-radius: 50rem !important; -} - -.rounded-top { - border-top-left-radius: 0.25rem !important; - border-top-right-radius: 0.25rem !important; -} - -.rounded-end { - 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-start { - border-bottom-left-radius: 0.25rem !important; - border-top-left-radius: 0.25rem !important; -} - -.visible { - visibility: visible !important; -} - -.invisible { - visibility: hidden !important; -} - -@media (min-width: 576px) { - .float-sm-start { - float: left !important; - } - - .float-sm-end { - float: right !important; - } - - .float-sm-none { - float: none !important; - } - - .d-sm-inline { - display: inline !important; - } - - .d-sm-inline-block { - display: inline-block !important; - } - - .d-sm-block { - display: block !important; - } - - .d-sm-grid { - display: grid !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: flex !important; - } - - .d-sm-inline-flex { - display: inline-flex !important; - } - - .d-sm-none { - display: none !important; - } - - .flex-sm-fill { - flex: 1 1 auto !important; - } - - .flex-sm-row { - flex-direction: row !important; - } - - .flex-sm-column { - flex-direction: column !important; - } - - .flex-sm-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-sm-column-reverse { - flex-direction: column-reverse !important; - } - - .flex-sm-grow-0 { - flex-grow: 0 !important; - } - - .flex-sm-grow-1 { - flex-grow: 1 !important; - } - - .flex-sm-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-sm-shrink-1 { - flex-shrink: 1 !important; - } - - .flex-sm-wrap { - flex-wrap: wrap !important; - } - - .flex-sm-nowrap { - flex-wrap: nowrap !important; - } - - .flex-sm-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - - .gap-sm-0 { - gap: 0 !important; - } - - .gap-sm-1 { - gap: 0.25rem !important; - } - - .gap-sm-2 { - gap: 0.5rem !important; - } - - .gap-sm-3 { - gap: 1rem !important; - } - - .gap-sm-4 { - gap: 1.5rem !important; - } - - .gap-sm-5 { - gap: 3rem !important; - } - - .justify-content-sm-start { - justify-content: flex-start !important; - } - - .justify-content-sm-end { - justify-content: flex-end !important; - } - - .justify-content-sm-center { - justify-content: center !important; - } - - .justify-content-sm-between { - justify-content: space-between !important; - } - - .justify-content-sm-around { - justify-content: space-around !important; - } - - .justify-content-sm-evenly { - justify-content: space-evenly !important; - } - - .align-items-sm-start { - align-items: flex-start !important; - } - - .align-items-sm-end { - align-items: flex-end !important; - } - - .align-items-sm-center { - align-items: center !important; - } - - .align-items-sm-baseline { - align-items: baseline !important; - } - - .align-items-sm-stretch { - align-items: stretch !important; - } - - .align-content-sm-start { - align-content: flex-start !important; - } - - .align-content-sm-end { - align-content: flex-end !important; - } - - .align-content-sm-center { - align-content: center !important; - } - - .align-content-sm-between { - align-content: space-between !important; - } - - .align-content-sm-around { - align-content: space-around !important; - } - - .align-content-sm-stretch { - align-content: stretch !important; - } - - .align-self-sm-auto { - align-self: auto !important; - } - - .align-self-sm-start { - align-self: flex-start !important; - } - - .align-self-sm-end { - align-self: flex-end !important; - } - - .align-self-sm-center { - align-self: center !important; - } - - .align-self-sm-baseline { - align-self: baseline !important; - } - - .align-self-sm-stretch { - align-self: stretch !important; - } - - .order-sm-first { - order: -1 !important; - } - - .order-sm-0 { - order: 0 !important; - } - - .order-sm-1 { - order: 1 !important; - } - - .order-sm-2 { - order: 2 !important; - } - - .order-sm-3 { - order: 3 !important; - } - - .order-sm-4 { - order: 4 !important; - } - - .order-sm-5 { - order: 5 !important; - } - - .order-sm-last { - order: 6 !important; - } - - .m-sm-0 { - margin: 0 !important; - } - - .m-sm-1 { - margin: 0.25rem !important; - } - - .m-sm-2 { - margin: 0.5rem !important; - } - - .m-sm-3 { - margin: 1rem !important; - } - - .m-sm-4 { - margin: 1.5rem !important; - } - - .m-sm-5 { - margin: 3rem !important; - } - - .m-sm-auto { - margin: auto !important; - } - - .mx-sm-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .mx-sm-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - - .mx-sm-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - - .mx-sm-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - - .mx-sm-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - - .mx-sm-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - - .mx-sm-auto { - margin-right: auto !important; - margin-left: auto !important; - } - - .my-sm-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .my-sm-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - - .my-sm-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - - .my-sm-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - - .my-sm-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - - .my-sm-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - - .my-sm-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - - .mt-sm-0 { - margin-top: 0 !important; - } - - .mt-sm-1 { - margin-top: 0.25rem !important; - } - - .mt-sm-2 { - margin-top: 0.5rem !important; - } - - .mt-sm-3 { - margin-top: 1rem !important; - } - - .mt-sm-4 { - margin-top: 1.5rem !important; - } - - .mt-sm-5 { - margin-top: 3rem !important; - } - - .mt-sm-auto { - margin-top: auto !important; - } - - .me-sm-0 { - margin-right: 0 !important; - } - - .me-sm-1 { - margin-right: 0.25rem !important; - } - - .me-sm-2 { - margin-right: 0.5rem !important; - } - - .me-sm-3 { - margin-right: 1rem !important; - } - - .me-sm-4 { - margin-right: 1.5rem !important; - } - - .me-sm-5 { - margin-right: 3rem !important; - } - - .me-sm-auto { - margin-right: auto !important; - } - - .mb-sm-0 { - margin-bottom: 0 !important; - } - - .mb-sm-1 { - margin-bottom: 0.25rem !important; - } - - .mb-sm-2 { - margin-bottom: 0.5rem !important; - } - - .mb-sm-3 { - margin-bottom: 1rem !important; - } - - .mb-sm-4 { - margin-bottom: 1.5rem !important; - } - - .mb-sm-5 { - margin-bottom: 3rem !important; - } - - .mb-sm-auto { - margin-bottom: auto !important; - } - - .ms-sm-0 { - margin-left: 0 !important; - } - - .ms-sm-1 { - margin-left: 0.25rem !important; - } - - .ms-sm-2 { - margin-left: 0.5rem !important; - } - - .ms-sm-3 { - margin-left: 1rem !important; - } - - .ms-sm-4 { - margin-left: 1.5rem !important; - } - - .ms-sm-5 { - margin-left: 3rem !important; - } - - .ms-sm-auto { - margin-left: auto !important; - } - - .p-sm-0 { - padding: 0 !important; - } - - .p-sm-1 { - padding: 0.25rem !important; - } - - .p-sm-2 { - padding: 0.5rem !important; - } - - .p-sm-3 { - padding: 1rem !important; - } - - .p-sm-4 { - padding: 1.5rem !important; - } - - .p-sm-5 { - padding: 3rem !important; - } - - .px-sm-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .px-sm-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - - .px-sm-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - - .px-sm-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - - .px-sm-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - - .px-sm-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - - .py-sm-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .py-sm-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - - .py-sm-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - - .py-sm-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - - .py-sm-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - - .py-sm-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - - .pt-sm-0 { - padding-top: 0 !important; - } - - .pt-sm-1 { - padding-top: 0.25rem !important; - } - - .pt-sm-2 { - padding-top: 0.5rem !important; - } - - .pt-sm-3 { - padding-top: 1rem !important; - } - - .pt-sm-4 { - padding-top: 1.5rem !important; - } - - .pt-sm-5 { - padding-top: 3rem !important; - } - - .pe-sm-0 { - padding-right: 0 !important; - } - - .pe-sm-1 { - padding-right: 0.25rem !important; - } - - .pe-sm-2 { - padding-right: 0.5rem !important; - } - - .pe-sm-3 { - padding-right: 1rem !important; - } - - .pe-sm-4 { - padding-right: 1.5rem !important; - } - - .pe-sm-5 { - padding-right: 3rem !important; - } - - .pb-sm-0 { - padding-bottom: 0 !important; - } - - .pb-sm-1 { - padding-bottom: 0.25rem !important; - } - - .pb-sm-2 { - padding-bottom: 0.5rem !important; - } - - .pb-sm-3 { - padding-bottom: 1rem !important; - } - - .pb-sm-4 { - padding-bottom: 1.5rem !important; - } - - .pb-sm-5 { - padding-bottom: 3rem !important; - } - - .ps-sm-0 { - padding-left: 0 !important; - } - - .ps-sm-1 { - padding-left: 0.25rem !important; - } - - .ps-sm-2 { - padding-left: 0.5rem !important; - } - - .ps-sm-3 { - padding-left: 1rem !important; - } - - .ps-sm-4 { - padding-left: 1.5rem !important; - } - - .ps-sm-5 { - padding-left: 3rem !important; - } - - .text-sm-start { - text-align: left !important; - } - - .text-sm-end { - text-align: right !important; - } - - .text-sm-center { - text-align: center !important; - } -} -@media (min-width: 768px) { - .float-md-start { - float: left !important; - } - - .float-md-end { - float: right !important; - } - - .float-md-none { - float: none !important; - } - - .d-md-inline { - display: inline !important; - } - - .d-md-inline-block { - display: inline-block !important; - } - - .d-md-block { - display: block !important; - } - - .d-md-grid { - display: grid !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: flex !important; - } - - .d-md-inline-flex { - display: inline-flex !important; - } - - .d-md-none { - display: none !important; - } - - .flex-md-fill { - flex: 1 1 auto !important; - } - - .flex-md-row { - flex-direction: row !important; - } - - .flex-md-column { - flex-direction: column !important; - } - - .flex-md-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-md-column-reverse { - flex-direction: column-reverse !important; - } - - .flex-md-grow-0 { - flex-grow: 0 !important; - } - - .flex-md-grow-1 { - flex-grow: 1 !important; - } - - .flex-md-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-md-shrink-1 { - flex-shrink: 1 !important; - } - - .flex-md-wrap { - flex-wrap: wrap !important; - } - - .flex-md-nowrap { - flex-wrap: nowrap !important; - } - - .flex-md-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - - .gap-md-0 { - gap: 0 !important; - } - - .gap-md-1 { - gap: 0.25rem !important; - } - - .gap-md-2 { - gap: 0.5rem !important; - } - - .gap-md-3 { - gap: 1rem !important; - } - - .gap-md-4 { - gap: 1.5rem !important; - } - - .gap-md-5 { - gap: 3rem !important; - } - - .justify-content-md-start { - justify-content: flex-start !important; - } - - .justify-content-md-end { - justify-content: flex-end !important; - } - - .justify-content-md-center { - justify-content: center !important; - } - - .justify-content-md-between { - justify-content: space-between !important; - } - - .justify-content-md-around { - justify-content: space-around !important; - } - - .justify-content-md-evenly { - justify-content: space-evenly !important; - } - - .align-items-md-start { - align-items: flex-start !important; - } - - .align-items-md-end { - align-items: flex-end !important; - } - - .align-items-md-center { - align-items: center !important; - } - - .align-items-md-baseline { - align-items: baseline !important; - } - - .align-items-md-stretch { - align-items: stretch !important; - } - - .align-content-md-start { - align-content: flex-start !important; - } - - .align-content-md-end { - align-content: flex-end !important; - } - - .align-content-md-center { - align-content: center !important; - } - - .align-content-md-between { - align-content: space-between !important; - } - - .align-content-md-around { - align-content: space-around !important; - } - - .align-content-md-stretch { - align-content: stretch !important; - } - - .align-self-md-auto { - align-self: auto !important; - } - - .align-self-md-start { - align-self: flex-start !important; - } - - .align-self-md-end { - align-self: flex-end !important; - } - - .align-self-md-center { - align-self: center !important; - } - - .align-self-md-baseline { - align-self: baseline !important; - } - - .align-self-md-stretch { - align-self: stretch !important; - } - - .order-md-first { - order: -1 !important; - } - - .order-md-0 { - order: 0 !important; - } - - .order-md-1 { - order: 1 !important; - } - - .order-md-2 { - order: 2 !important; - } - - .order-md-3 { - order: 3 !important; - } - - .order-md-4 { - order: 4 !important; - } - - .order-md-5 { - order: 5 !important; - } - - .order-md-last { - order: 6 !important; - } - - .m-md-0 { - margin: 0 !important; - } - - .m-md-1 { - margin: 0.25rem !important; - } - - .m-md-2 { - margin: 0.5rem !important; - } - - .m-md-3 { - margin: 1rem !important; - } - - .m-md-4 { - margin: 1.5rem !important; - } - - .m-md-5 { - margin: 3rem !important; - } - - .m-md-auto { - margin: auto !important; - } - - .mx-md-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .mx-md-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - - .mx-md-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - - .mx-md-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - - .mx-md-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - - .mx-md-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - - .mx-md-auto { - margin-right: auto !important; - margin-left: auto !important; - } - - .my-md-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .my-md-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - - .my-md-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - - .my-md-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - - .my-md-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - - .my-md-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - - .my-md-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - - .mt-md-0 { - margin-top: 0 !important; - } - - .mt-md-1 { - margin-top: 0.25rem !important; - } - - .mt-md-2 { - margin-top: 0.5rem !important; - } - - .mt-md-3 { - margin-top: 1rem !important; - } - - .mt-md-4 { - margin-top: 1.5rem !important; - } - - .mt-md-5 { - margin-top: 3rem !important; - } - - .mt-md-auto { - margin-top: auto !important; - } - - .me-md-0 { - margin-right: 0 !important; - } - - .me-md-1 { - margin-right: 0.25rem !important; - } - - .me-md-2 { - margin-right: 0.5rem !important; - } - - .me-md-3 { - margin-right: 1rem !important; - } - - .me-md-4 { - margin-right: 1.5rem !important; - } - - .me-md-5 { - margin-right: 3rem !important; - } - - .me-md-auto { - margin-right: auto !important; - } - - .mb-md-0 { - margin-bottom: 0 !important; - } - - .mb-md-1 { - margin-bottom: 0.25rem !important; - } - - .mb-md-2 { - margin-bottom: 0.5rem !important; - } - - .mb-md-3 { - margin-bottom: 1rem !important; - } - - .mb-md-4 { - margin-bottom: 1.5rem !important; - } - - .mb-md-5 { - margin-bottom: 3rem !important; - } - - .mb-md-auto { - margin-bottom: auto !important; - } - - .ms-md-0 { - margin-left: 0 !important; - } - - .ms-md-1 { - margin-left: 0.25rem !important; - } - - .ms-md-2 { - margin-left: 0.5rem !important; - } - - .ms-md-3 { - margin-left: 1rem !important; - } - - .ms-md-4 { - margin-left: 1.5rem !important; - } - - .ms-md-5 { - margin-left: 3rem !important; - } - - .ms-md-auto { - margin-left: auto !important; - } - - .p-md-0 { - padding: 0 !important; - } - - .p-md-1 { - padding: 0.25rem !important; - } - - .p-md-2 { - padding: 0.5rem !important; - } - - .p-md-3 { - padding: 1rem !important; - } - - .p-md-4 { - padding: 1.5rem !important; - } - - .p-md-5 { - padding: 3rem !important; - } - - .px-md-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .px-md-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - - .px-md-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - - .px-md-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - - .px-md-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - - .px-md-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - - .py-md-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .py-md-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - - .py-md-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - - .py-md-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - - .py-md-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - - .py-md-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - - .pt-md-0 { - padding-top: 0 !important; - } - - .pt-md-1 { - padding-top: 0.25rem !important; - } - - .pt-md-2 { - padding-top: 0.5rem !important; - } - - .pt-md-3 { - padding-top: 1rem !important; - } - - .pt-md-4 { - padding-top: 1.5rem !important; - } - - .pt-md-5 { - padding-top: 3rem !important; - } - - .pe-md-0 { - padding-right: 0 !important; - } - - .pe-md-1 { - padding-right: 0.25rem !important; - } - - .pe-md-2 { - padding-right: 0.5rem !important; - } - - .pe-md-3 { - padding-right: 1rem !important; - } - - .pe-md-4 { - padding-right: 1.5rem !important; - } - - .pe-md-5 { - padding-right: 3rem !important; - } - - .pb-md-0 { - padding-bottom: 0 !important; - } - - .pb-md-1 { - padding-bottom: 0.25rem !important; - } - - .pb-md-2 { - padding-bottom: 0.5rem !important; - } - - .pb-md-3 { - padding-bottom: 1rem !important; - } - - .pb-md-4 { - padding-bottom: 1.5rem !important; - } - - .pb-md-5 { - padding-bottom: 3rem !important; - } - - .ps-md-0 { - padding-left: 0 !important; - } - - .ps-md-1 { - padding-left: 0.25rem !important; - } - - .ps-md-2 { - padding-left: 0.5rem !important; - } - - .ps-md-3 { - padding-left: 1rem !important; - } - - .ps-md-4 { - padding-left: 1.5rem !important; - } - - .ps-md-5 { - padding-left: 3rem !important; - } - - .text-md-start { - text-align: left !important; - } - - .text-md-end { - text-align: right !important; - } - - .text-md-center { - text-align: center !important; - } -} -@media (min-width: 992px) { - .float-lg-start { - float: left !important; - } - - .float-lg-end { - float: right !important; - } - - .float-lg-none { - float: none !important; - } - - .d-lg-inline { - display: inline !important; - } - - .d-lg-inline-block { - display: inline-block !important; - } - - .d-lg-block { - display: block !important; - } - - .d-lg-grid { - display: grid !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: flex !important; - } - - .d-lg-inline-flex { - display: inline-flex !important; - } - - .d-lg-none { - display: none !important; - } - - .flex-lg-fill { - flex: 1 1 auto !important; - } - - .flex-lg-row { - flex-direction: row !important; - } - - .flex-lg-column { - flex-direction: column !important; - } - - .flex-lg-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-lg-column-reverse { - flex-direction: column-reverse !important; - } - - .flex-lg-grow-0 { - flex-grow: 0 !important; - } - - .flex-lg-grow-1 { - flex-grow: 1 !important; - } - - .flex-lg-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-lg-shrink-1 { - flex-shrink: 1 !important; - } - - .flex-lg-wrap { - flex-wrap: wrap !important; - } - - .flex-lg-nowrap { - flex-wrap: nowrap !important; - } - - .flex-lg-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - - .gap-lg-0 { - gap: 0 !important; - } - - .gap-lg-1 { - gap: 0.25rem !important; - } - - .gap-lg-2 { - gap: 0.5rem !important; - } - - .gap-lg-3 { - gap: 1rem !important; - } - - .gap-lg-4 { - gap: 1.5rem !important; - } - - .gap-lg-5 { - gap: 3rem !important; - } - - .justify-content-lg-start { - justify-content: flex-start !important; - } - - .justify-content-lg-end { - justify-content: flex-end !important; - } - - .justify-content-lg-center { - justify-content: center !important; - } - - .justify-content-lg-between { - justify-content: space-between !important; - } - - .justify-content-lg-around { - justify-content: space-around !important; - } - - .justify-content-lg-evenly { - justify-content: space-evenly !important; - } - - .align-items-lg-start { - align-items: flex-start !important; - } - - .align-items-lg-end { - align-items: flex-end !important; - } - - .align-items-lg-center { - align-items: center !important; - } - - .align-items-lg-baseline { - align-items: baseline !important; - } - - .align-items-lg-stretch { - align-items: stretch !important; - } - - .align-content-lg-start { - align-content: flex-start !important; - } - - .align-content-lg-end { - align-content: flex-end !important; - } - - .align-content-lg-center { - align-content: center !important; - } - - .align-content-lg-between { - align-content: space-between !important; - } - - .align-content-lg-around { - align-content: space-around !important; - } - - .align-content-lg-stretch { - align-content: stretch !important; - } - - .align-self-lg-auto { - align-self: auto !important; - } - - .align-self-lg-start { - align-self: flex-start !important; - } - - .align-self-lg-end { - align-self: flex-end !important; - } - - .align-self-lg-center { - align-self: center !important; - } - - .align-self-lg-baseline { - align-self: baseline !important; - } - - .align-self-lg-stretch { - align-self: stretch !important; - } - - .order-lg-first { - order: -1 !important; - } - - .order-lg-0 { - order: 0 !important; - } - - .order-lg-1 { - order: 1 !important; - } - - .order-lg-2 { - order: 2 !important; - } - - .order-lg-3 { - order: 3 !important; - } - - .order-lg-4 { - order: 4 !important; - } - - .order-lg-5 { - order: 5 !important; - } - - .order-lg-last { - order: 6 !important; - } - - .m-lg-0 { - margin: 0 !important; - } - - .m-lg-1 { - margin: 0.25rem !important; - } - - .m-lg-2 { - margin: 0.5rem !important; - } - - .m-lg-3 { - margin: 1rem !important; - } - - .m-lg-4 { - margin: 1.5rem !important; - } - - .m-lg-5 { - margin: 3rem !important; - } - - .m-lg-auto { - margin: auto !important; - } - - .mx-lg-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .mx-lg-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - - .mx-lg-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - - .mx-lg-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - - .mx-lg-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - - .mx-lg-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - - .mx-lg-auto { - margin-right: auto !important; - margin-left: auto !important; - } - - .my-lg-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .my-lg-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - - .my-lg-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - - .my-lg-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - - .my-lg-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - - .my-lg-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - - .my-lg-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - - .mt-lg-0 { - margin-top: 0 !important; - } - - .mt-lg-1 { - margin-top: 0.25rem !important; - } - - .mt-lg-2 { - margin-top: 0.5rem !important; - } - - .mt-lg-3 { - margin-top: 1rem !important; - } - - .mt-lg-4 { - margin-top: 1.5rem !important; - } - - .mt-lg-5 { - margin-top: 3rem !important; - } - - .mt-lg-auto { - margin-top: auto !important; - } - - .me-lg-0 { - margin-right: 0 !important; - } - - .me-lg-1 { - margin-right: 0.25rem !important; - } - - .me-lg-2 { - margin-right: 0.5rem !important; - } - - .me-lg-3 { - margin-right: 1rem !important; - } - - .me-lg-4 { - margin-right: 1.5rem !important; - } - - .me-lg-5 { - margin-right: 3rem !important; - } - - .me-lg-auto { - margin-right: auto !important; - } - - .mb-lg-0 { - margin-bottom: 0 !important; - } - - .mb-lg-1 { - margin-bottom: 0.25rem !important; - } - - .mb-lg-2 { - margin-bottom: 0.5rem !important; - } - - .mb-lg-3 { - margin-bottom: 1rem !important; - } - - .mb-lg-4 { - margin-bottom: 1.5rem !important; - } - - .mb-lg-5 { - margin-bottom: 3rem !important; - } - - .mb-lg-auto { - margin-bottom: auto !important; - } - - .ms-lg-0 { - margin-left: 0 !important; - } - - .ms-lg-1 { - margin-left: 0.25rem !important; - } - - .ms-lg-2 { - margin-left: 0.5rem !important; - } - - .ms-lg-3 { - margin-left: 1rem !important; - } - - .ms-lg-4 { - margin-left: 1.5rem !important; - } - - .ms-lg-5 { - margin-left: 3rem !important; - } - - .ms-lg-auto { - margin-left: auto !important; - } - - .p-lg-0 { - padding: 0 !important; - } - - .p-lg-1 { - padding: 0.25rem !important; - } - - .p-lg-2 { - padding: 0.5rem !important; - } - - .p-lg-3 { - padding: 1rem !important; - } - - .p-lg-4 { - padding: 1.5rem !important; - } - - .p-lg-5 { - padding: 3rem !important; - } - - .px-lg-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .px-lg-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - - .px-lg-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - - .px-lg-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - - .px-lg-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - - .px-lg-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - - .py-lg-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .py-lg-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - - .py-lg-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - - .py-lg-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - - .py-lg-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - - .py-lg-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - - .pt-lg-0 { - padding-top: 0 !important; - } - - .pt-lg-1 { - padding-top: 0.25rem !important; - } - - .pt-lg-2 { - padding-top: 0.5rem !important; - } - - .pt-lg-3 { - padding-top: 1rem !important; - } - - .pt-lg-4 { - padding-top: 1.5rem !important; - } - - .pt-lg-5 { - padding-top: 3rem !important; - } - - .pe-lg-0 { - padding-right: 0 !important; - } - - .pe-lg-1 { - padding-right: 0.25rem !important; - } - - .pe-lg-2 { - padding-right: 0.5rem !important; - } - - .pe-lg-3 { - padding-right: 1rem !important; - } - - .pe-lg-4 { - padding-right: 1.5rem !important; - } - - .pe-lg-5 { - padding-right: 3rem !important; - } - - .pb-lg-0 { - padding-bottom: 0 !important; - } - - .pb-lg-1 { - padding-bottom: 0.25rem !important; - } - - .pb-lg-2 { - padding-bottom: 0.5rem !important; - } - - .pb-lg-3 { - padding-bottom: 1rem !important; - } - - .pb-lg-4 { - padding-bottom: 1.5rem !important; - } - - .pb-lg-5 { - padding-bottom: 3rem !important; - } - - .ps-lg-0 { - padding-left: 0 !important; - } - - .ps-lg-1 { - padding-left: 0.25rem !important; - } - - .ps-lg-2 { - padding-left: 0.5rem !important; - } - - .ps-lg-3 { - padding-left: 1rem !important; - } - - .ps-lg-4 { - padding-left: 1.5rem !important; - } - - .ps-lg-5 { - padding-left: 3rem !important; - } - - .text-lg-start { - text-align: left !important; - } - - .text-lg-end { - text-align: right !important; - } - - .text-lg-center { - text-align: center !important; - } -} -@media (min-width: 1200px) { - .float-xl-start { - float: left !important; - } - - .float-xl-end { - float: right !important; - } - - .float-xl-none { - float: none !important; - } - - .d-xl-inline { - display: inline !important; - } - - .d-xl-inline-block { - display: inline-block !important; - } - - .d-xl-block { - display: block !important; - } - - .d-xl-grid { - display: grid !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: flex !important; - } - - .d-xl-inline-flex { - display: inline-flex !important; - } - - .d-xl-none { - display: none !important; - } - - .flex-xl-fill { - flex: 1 1 auto !important; - } - - .flex-xl-row { - flex-direction: row !important; - } - - .flex-xl-column { - flex-direction: column !important; - } - - .flex-xl-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-xl-column-reverse { - flex-direction: column-reverse !important; - } - - .flex-xl-grow-0 { - flex-grow: 0 !important; - } - - .flex-xl-grow-1 { - flex-grow: 1 !important; - } - - .flex-xl-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-xl-shrink-1 { - flex-shrink: 1 !important; - } - - .flex-xl-wrap { - flex-wrap: wrap !important; - } - - .flex-xl-nowrap { - flex-wrap: nowrap !important; - } - - .flex-xl-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - - .gap-xl-0 { - gap: 0 !important; - } - - .gap-xl-1 { - gap: 0.25rem !important; - } - - .gap-xl-2 { - gap: 0.5rem !important; - } - - .gap-xl-3 { - gap: 1rem !important; - } - - .gap-xl-4 { - gap: 1.5rem !important; - } - - .gap-xl-5 { - gap: 3rem !important; - } - - .justify-content-xl-start { - justify-content: flex-start !important; - } - - .justify-content-xl-end { - justify-content: flex-end !important; - } - - .justify-content-xl-center { - justify-content: center !important; - } - - .justify-content-xl-between { - justify-content: space-between !important; - } - - .justify-content-xl-around { - justify-content: space-around !important; - } - - .justify-content-xl-evenly { - justify-content: space-evenly !important; - } - - .align-items-xl-start { - align-items: flex-start !important; - } - - .align-items-xl-end { - align-items: flex-end !important; - } - - .align-items-xl-center { - align-items: center !important; - } - - .align-items-xl-baseline { - align-items: baseline !important; - } - - .align-items-xl-stretch { - align-items: stretch !important; - } - - .align-content-xl-start { - align-content: flex-start !important; - } - - .align-content-xl-end { - align-content: flex-end !important; - } - - .align-content-xl-center { - align-content: center !important; - } - - .align-content-xl-between { - align-content: space-between !important; - } - - .align-content-xl-around { - align-content: space-around !important; - } - - .align-content-xl-stretch { - align-content: stretch !important; - } - - .align-self-xl-auto { - align-self: auto !important; - } - - .align-self-xl-start { - align-self: flex-start !important; - } - - .align-self-xl-end { - align-self: flex-end !important; - } - - .align-self-xl-center { - align-self: center !important; - } - - .align-self-xl-baseline { - align-self: baseline !important; - } - - .align-self-xl-stretch { - align-self: stretch !important; - } - - .order-xl-first { - order: -1 !important; - } - - .order-xl-0 { - order: 0 !important; - } - - .order-xl-1 { - order: 1 !important; - } - - .order-xl-2 { - order: 2 !important; - } - - .order-xl-3 { - order: 3 !important; - } - - .order-xl-4 { - order: 4 !important; - } - - .order-xl-5 { - order: 5 !important; - } - - .order-xl-last { - order: 6 !important; - } - - .m-xl-0 { - margin: 0 !important; - } - - .m-xl-1 { - margin: 0.25rem !important; - } - - .m-xl-2 { - margin: 0.5rem !important; - } - - .m-xl-3 { - margin: 1rem !important; - } - - .m-xl-4 { - margin: 1.5rem !important; - } - - .m-xl-5 { - margin: 3rem !important; - } - - .m-xl-auto { - margin: auto !important; - } - - .mx-xl-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .mx-xl-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - - .mx-xl-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - - .mx-xl-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - - .mx-xl-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - - .mx-xl-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - - .mx-xl-auto { - margin-right: auto !important; - margin-left: auto !important; - } - - .my-xl-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .my-xl-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - - .my-xl-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - - .my-xl-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - - .my-xl-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - - .my-xl-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - - .my-xl-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - - .mt-xl-0 { - margin-top: 0 !important; - } - - .mt-xl-1 { - margin-top: 0.25rem !important; - } - - .mt-xl-2 { - margin-top: 0.5rem !important; - } - - .mt-xl-3 { - margin-top: 1rem !important; - } - - .mt-xl-4 { - margin-top: 1.5rem !important; - } - - .mt-xl-5 { - margin-top: 3rem !important; - } - - .mt-xl-auto { - margin-top: auto !important; - } - - .me-xl-0 { - margin-right: 0 !important; - } - - .me-xl-1 { - margin-right: 0.25rem !important; - } - - .me-xl-2 { - margin-right: 0.5rem !important; - } - - .me-xl-3 { - margin-right: 1rem !important; - } - - .me-xl-4 { - margin-right: 1.5rem !important; - } - - .me-xl-5 { - margin-right: 3rem !important; - } - - .me-xl-auto { - margin-right: auto !important; - } - - .mb-xl-0 { - margin-bottom: 0 !important; - } - - .mb-xl-1 { - margin-bottom: 0.25rem !important; - } - - .mb-xl-2 { - margin-bottom: 0.5rem !important; - } - - .mb-xl-3 { - margin-bottom: 1rem !important; - } - - .mb-xl-4 { - margin-bottom: 1.5rem !important; - } - - .mb-xl-5 { - margin-bottom: 3rem !important; - } - - .mb-xl-auto { - margin-bottom: auto !important; - } - - .ms-xl-0 { - margin-left: 0 !important; - } - - .ms-xl-1 { - margin-left: 0.25rem !important; - } - - .ms-xl-2 { - margin-left: 0.5rem !important; - } - - .ms-xl-3 { - margin-left: 1rem !important; - } - - .ms-xl-4 { - margin-left: 1.5rem !important; - } - - .ms-xl-5 { - margin-left: 3rem !important; - } - - .ms-xl-auto { - margin-left: auto !important; - } - - .p-xl-0 { - padding: 0 !important; - } - - .p-xl-1 { - padding: 0.25rem !important; - } - - .p-xl-2 { - padding: 0.5rem !important; - } - - .p-xl-3 { - padding: 1rem !important; - } - - .p-xl-4 { - padding: 1.5rem !important; - } - - .p-xl-5 { - padding: 3rem !important; - } - - .px-xl-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .px-xl-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - - .px-xl-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - - .px-xl-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - - .px-xl-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - - .px-xl-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - - .py-xl-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .py-xl-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - - .py-xl-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - - .py-xl-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - - .py-xl-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - - .py-xl-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - - .pt-xl-0 { - padding-top: 0 !important; - } - - .pt-xl-1 { - padding-top: 0.25rem !important; - } - - .pt-xl-2 { - padding-top: 0.5rem !important; - } - - .pt-xl-3 { - padding-top: 1rem !important; - } - - .pt-xl-4 { - padding-top: 1.5rem !important; - } - - .pt-xl-5 { - padding-top: 3rem !important; - } - - .pe-xl-0 { - padding-right: 0 !important; - } - - .pe-xl-1 { - padding-right: 0.25rem !important; - } - - .pe-xl-2 { - padding-right: 0.5rem !important; - } - - .pe-xl-3 { - padding-right: 1rem !important; - } - - .pe-xl-4 { - padding-right: 1.5rem !important; - } - - .pe-xl-5 { - padding-right: 3rem !important; - } - - .pb-xl-0 { - padding-bottom: 0 !important; - } - - .pb-xl-1 { - padding-bottom: 0.25rem !important; - } - - .pb-xl-2 { - padding-bottom: 0.5rem !important; - } - - .pb-xl-3 { - padding-bottom: 1rem !important; - } - - .pb-xl-4 { - padding-bottom: 1.5rem !important; - } - - .pb-xl-5 { - padding-bottom: 3rem !important; - } - - .ps-xl-0 { - padding-left: 0 !important; - } - - .ps-xl-1 { - padding-left: 0.25rem !important; - } - - .ps-xl-2 { - padding-left: 0.5rem !important; - } - - .ps-xl-3 { - padding-left: 1rem !important; - } - - .ps-xl-4 { - padding-left: 1.5rem !important; - } - - .ps-xl-5 { - padding-left: 3rem !important; - } - - .text-xl-start { - text-align: left !important; - } - - .text-xl-end { - text-align: right !important; - } - - .text-xl-center { - text-align: center !important; - } -} -@media (min-width: 1400px) { - .float-xxl-start { - float: left !important; - } - - .float-xxl-end { - float: right !important; - } - - .float-xxl-none { - float: none !important; - } - - .d-xxl-inline { - display: inline !important; - } - - .d-xxl-inline-block { - display: inline-block !important; - } - - .d-xxl-block { - display: block !important; - } - - .d-xxl-grid { - display: grid !important; - } - - .d-xxl-table { - display: table !important; - } - - .d-xxl-table-row { - display: table-row !important; - } - - .d-xxl-table-cell { - display: table-cell !important; - } - - .d-xxl-flex { - display: flex !important; - } - - .d-xxl-inline-flex { - display: inline-flex !important; - } - - .d-xxl-none { - display: none !important; - } - - .flex-xxl-fill { - flex: 1 1 auto !important; - } - - .flex-xxl-row { - flex-direction: row !important; - } - - .flex-xxl-column { - flex-direction: column !important; - } - - .flex-xxl-row-reverse { - flex-direction: row-reverse !important; - } - - .flex-xxl-column-reverse { - flex-direction: column-reverse !important; - } - - .flex-xxl-grow-0 { - flex-grow: 0 !important; - } - - .flex-xxl-grow-1 { - flex-grow: 1 !important; - } - - .flex-xxl-shrink-0 { - flex-shrink: 0 !important; - } - - .flex-xxl-shrink-1 { - flex-shrink: 1 !important; - } - - .flex-xxl-wrap { - flex-wrap: wrap !important; - } - - .flex-xxl-nowrap { - flex-wrap: nowrap !important; - } - - .flex-xxl-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - - .gap-xxl-0 { - gap: 0 !important; - } - - .gap-xxl-1 { - gap: 0.25rem !important; - } - - .gap-xxl-2 { - gap: 0.5rem !important; - } - - .gap-xxl-3 { - gap: 1rem !important; - } - - .gap-xxl-4 { - gap: 1.5rem !important; - } - - .gap-xxl-5 { - gap: 3rem !important; - } - - .justify-content-xxl-start { - justify-content: flex-start !important; - } - - .justify-content-xxl-end { - justify-content: flex-end !important; - } - - .justify-content-xxl-center { - justify-content: center !important; - } - - .justify-content-xxl-between { - justify-content: space-between !important; - } - - .justify-content-xxl-around { - justify-content: space-around !important; - } - - .justify-content-xxl-evenly { - justify-content: space-evenly !important; - } - - .align-items-xxl-start { - align-items: flex-start !important; - } - - .align-items-xxl-end { - align-items: flex-end !important; - } - - .align-items-xxl-center { - align-items: center !important; - } - - .align-items-xxl-baseline { - align-items: baseline !important; - } - - .align-items-xxl-stretch { - align-items: stretch !important; - } - - .align-content-xxl-start { - align-content: flex-start !important; - } - - .align-content-xxl-end { - align-content: flex-end !important; - } - - .align-content-xxl-center { - align-content: center !important; - } - - .align-content-xxl-between { - align-content: space-between !important; - } - - .align-content-xxl-around { - align-content: space-around !important; - } - - .align-content-xxl-stretch { - align-content: stretch !important; - } - - .align-self-xxl-auto { - align-self: auto !important; - } - - .align-self-xxl-start { - align-self: flex-start !important; - } - - .align-self-xxl-end { - align-self: flex-end !important; - } - - .align-self-xxl-center { - align-self: center !important; - } - - .align-self-xxl-baseline { - align-self: baseline !important; - } - - .align-self-xxl-stretch { - align-self: stretch !important; - } - - .order-xxl-first { - order: -1 !important; - } - - .order-xxl-0 { - order: 0 !important; - } - - .order-xxl-1 { - order: 1 !important; - } - - .order-xxl-2 { - order: 2 !important; - } - - .order-xxl-3 { - order: 3 !important; - } - - .order-xxl-4 { - order: 4 !important; - } - - .order-xxl-5 { - order: 5 !important; - } - - .order-xxl-last { - order: 6 !important; - } - - .m-xxl-0 { - margin: 0 !important; - } - - .m-xxl-1 { - margin: 0.25rem !important; - } - - .m-xxl-2 { - margin: 0.5rem !important; - } - - .m-xxl-3 { - margin: 1rem !important; - } - - .m-xxl-4 { - margin: 1.5rem !important; - } - - .m-xxl-5 { - margin: 3rem !important; - } - - .m-xxl-auto { - margin: auto !important; - } - - .mx-xxl-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - - .mx-xxl-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - - .mx-xxl-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - - .mx-xxl-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - - .mx-xxl-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - - .mx-xxl-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - - .mx-xxl-auto { - margin-right: auto !important; - margin-left: auto !important; - } - - .my-xxl-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - - .my-xxl-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - - .my-xxl-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - - .my-xxl-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - - .my-xxl-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - - .my-xxl-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - - .my-xxl-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - - .mt-xxl-0 { - margin-top: 0 !important; - } - - .mt-xxl-1 { - margin-top: 0.25rem !important; - } - - .mt-xxl-2 { - margin-top: 0.5rem !important; - } - - .mt-xxl-3 { - margin-top: 1rem !important; - } - - .mt-xxl-4 { - margin-top: 1.5rem !important; - } - - .mt-xxl-5 { - margin-top: 3rem !important; - } - - .mt-xxl-auto { - margin-top: auto !important; - } - - .me-xxl-0 { - margin-right: 0 !important; - } - - .me-xxl-1 { - margin-right: 0.25rem !important; - } - - .me-xxl-2 { - margin-right: 0.5rem !important; - } - - .me-xxl-3 { - margin-right: 1rem !important; - } - - .me-xxl-4 { - margin-right: 1.5rem !important; - } - - .me-xxl-5 { - margin-right: 3rem !important; - } - - .me-xxl-auto { - margin-right: auto !important; - } - - .mb-xxl-0 { - margin-bottom: 0 !important; - } - - .mb-xxl-1 { - margin-bottom: 0.25rem !important; - } - - .mb-xxl-2 { - margin-bottom: 0.5rem !important; - } - - .mb-xxl-3 { - margin-bottom: 1rem !important; - } - - .mb-xxl-4 { - margin-bottom: 1.5rem !important; - } - - .mb-xxl-5 { - margin-bottom: 3rem !important; - } - - .mb-xxl-auto { - margin-bottom: auto !important; - } - - .ms-xxl-0 { - margin-left: 0 !important; - } - - .ms-xxl-1 { - margin-left: 0.25rem !important; - } - - .ms-xxl-2 { - margin-left: 0.5rem !important; - } - - .ms-xxl-3 { - margin-left: 1rem !important; - } - - .ms-xxl-4 { - margin-left: 1.5rem !important; - } - - .ms-xxl-5 { - margin-left: 3rem !important; - } - - .ms-xxl-auto { - margin-left: auto !important; - } - - .p-xxl-0 { - padding: 0 !important; - } - - .p-xxl-1 { - padding: 0.25rem !important; - } - - .p-xxl-2 { - padding: 0.5rem !important; - } - - .p-xxl-3 { - padding: 1rem !important; - } - - .p-xxl-4 { - padding: 1.5rem !important; - } - - .p-xxl-5 { - padding: 3rem !important; - } - - .px-xxl-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - - .px-xxl-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - - .px-xxl-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - - .px-xxl-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - - .px-xxl-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - - .px-xxl-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - - .py-xxl-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - - .py-xxl-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - - .py-xxl-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - - .py-xxl-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - - .py-xxl-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - - .py-xxl-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - - .pt-xxl-0 { - padding-top: 0 !important; - } - - .pt-xxl-1 { - padding-top: 0.25rem !important; - } - - .pt-xxl-2 { - padding-top: 0.5rem !important; - } - - .pt-xxl-3 { - padding-top: 1rem !important; - } - - .pt-xxl-4 { - padding-top: 1.5rem !important; - } - - .pt-xxl-5 { - padding-top: 3rem !important; - } - - .pe-xxl-0 { - padding-right: 0 !important; - } - - .pe-xxl-1 { - padding-right: 0.25rem !important; - } - - .pe-xxl-2 { - padding-right: 0.5rem !important; - } - - .pe-xxl-3 { - padding-right: 1rem !important; - } - - .pe-xxl-4 { - padding-right: 1.5rem !important; - } - - .pe-xxl-5 { - padding-right: 3rem !important; - } - - .pb-xxl-0 { - padding-bottom: 0 !important; - } - - .pb-xxl-1 { - padding-bottom: 0.25rem !important; - } - - .pb-xxl-2 { - padding-bottom: 0.5rem !important; - } - - .pb-xxl-3 { - padding-bottom: 1rem !important; - } - - .pb-xxl-4 { - padding-bottom: 1.5rem !important; - } - - .pb-xxl-5 { - padding-bottom: 3rem !important; - } - - .ps-xxl-0 { - padding-left: 0 !important; - } - - .ps-xxl-1 { - padding-left: 0.25rem !important; - } - - .ps-xxl-2 { - padding-left: 0.5rem !important; - } - - .ps-xxl-3 { - padding-left: 1rem !important; - } - - .ps-xxl-4 { - padding-left: 1.5rem !important; - } - - .ps-xxl-5 { - padding-left: 3rem !important; - } - - .text-xxl-start { - text-align: left !important; - } - - .text-xxl-end { - text-align: right !important; - } - - .text-xxl-center { - text-align: center !important; - } -} -@media (min-width: 1200px) { - .fs-1 { - font-size: 2.5rem !important; - } - - .fs-2 { - font-size: 2rem !important; - } - - .fs-3 { - font-size: 1.75rem !important; - } - - .fs-4 { - font-size: 1.5rem !important; - } -} -@media print { - .d-print-inline { - display: inline !important; - } - - .d-print-inline-block { - display: inline-block !important; - } - - .d-print-block { - display: block !important; - } - - .d-print-grid { - display: grid !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: flex !important; - } - - .d-print-inline-flex { - display: inline-flex !important; - } - - .d-print-none { - display: none !important; - } -} - -/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap/css/bootstrap.css.map b/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap/css/bootstrap.css.map deleted file mode 100644 index 39c26aaa2c..0000000000 --- a/modules/openiddict/app/OpenIddict.Demo.Server/wwwroot/libs/bootstrap/css/bootstrap.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["bootstrap.css","../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","../../scss/vendor/_rfs.scss","../../scss/_variables.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACAhB;;;;;EAAA;ACAA;EAQI,kBAAA;EAAA,oBAAA;EAAA,oBAAA;EAAA,kBAAA;EAAA,iBAAA;EAAA,oBAAA;EAAA,oBAAA;EAAA,mBAAA;EAAA,kBAAA;EAAA,kBAAA;EAAA,gBAAA;EAAA,kBAAA;EAAA,uBAAA;EAIA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAAA,sBAAA;EAIA,qBAAA;EAAA,uBAAA;EAAA,qBAAA;EAAA,kBAAA;EAAA,qBAAA;EAAA,oBAAA;EAAA,mBAAA;EAAA,kBAAA;EAIA,8BAAA;EAAA,iCAAA;EAAA,6BAAA;EAAA,2BAAA;EAAA,6BAAA;EAAA,4BAAA;EAAA,6BAAA;EAAA,yBAAA;EAGF,6BAAA;EACA,uBAAA;EACA,+BAAA;EACA,+BAAA;EAMA,qNAAA;EACA,yGAAA;EACA,yFAAA;EAQA,gDAAA;EACA,yBAAA;EACA,0BAAA;EACA,0BAAA;EACA,wBAAA;EAIA,kBAAA;AFSF;;AG5CA;;;EAGE,sBAAA;AH+CF;;AGhCI;EANJ;IAOM,uBAAA;EHoCJ;AACF;;AGvBA;EACE,SAAA;EACA,uCAAA;ECmPI,mCALI;ED5OR,uCAAA;EACA,uCAAA;EACA,2BAAA;EACA,qCAAA;EACA,mCAAA;EACA,8BAAA;EACA,6CAAA;AH0BF;;AGhBA;EACE,cAAA;EACA,cE+kB4B;EF9kB5B,8BAAA;EACA,SAAA;EACA,aE8kB4B;AL3jB9B;;AGhBA;EACE,WEwb4B;ALra9B;;AGTA;EACE,aAAA;EACA,qBEohB4B;EFjhB5B,gBEohB4B;EFnhB5B,gBEohB4B;AL1gB9B;;AGNA;ECwMQ,iCAAA;AJ9LR;AI4BI;EDtCJ;IC+MQ,iBAAA;EJjMN;AACF;;AGVA;ECmMQ,iCAAA;AJrLR;AImBI;EDjCJ;IC0MQ,eAAA;EJxLN;AACF;;AGdA;EC8LQ,+BAAA;AJ5KR;AIUI;ED5BJ;ICqMQ,kBAAA;EJ/KN;AACF;;AGlBA;ECyLQ,iCAAA;AJnKR;AICI;EDvBJ;ICgMQ,iBAAA;EJtKN;AACF;;AGtBA;ECgLM,kBALI;AJjJV;;AGrBA;EC2KM,eALI;AJ7IV;;AGdA;EACE,aAAA;EACA,mBEkU0B;ALjT5B;;AGNA;;EAEE,yCAAA;EAAA,iCAAA;EACA,YAAA;EACA,sCAAA;EAAA,8BAAA;AHSF;;AGHA;EACE,mBAAA;EACA,kBAAA;EACA,oBAAA;AHMF;;AGAA;;EAEE,kBAAA;AHGF;;AGAA;;;EAGE,aAAA;EACA,mBAAA;AHGF;;AGAA;;;;EAIE,gBAAA;AHGF;;AGAA;EACE,gBEuZ4B;ALpZ9B;;AGEA;EACE,qBAAA;EACA,cAAA;AHCF;;AGKA;EACE,gBAAA;AHFF;;AGUA;;EAEE,mBEgY4B;ALvY9B;;AGeA;EC4EM,kBALI;AJlFV;;AGkBA;EACE,cE4b4B;EF3b5B,yBEmc4B;ALld9B;;AGwBA;;EAEE,kBAAA;ECwDI,iBALI;EDjDR,cAAA;EACA,wBAAA;AHrBF;;AGwBA;EAAM,eAAA;AHpBN;;AGqBA;EAAM,WAAA;AHjBN;;AGsBA;EACE,cEpNQ;EFqNR,0BEkMwC;ALrN1C;AGqBE;EACE,cEiMsC;ALpN1C;;AG8BE;EAEE,cAAA;EACA,qBAAA;AH5BJ;;AGmCA;;;;EAIE,qCE6S4B;ED/RxB,cALI;EDPR,+BAAA;EACA,2BAAA;AHhCF;;AGuCA;EACE,cAAA;EACA,aAAA;EACA,mBAAA;EACA,cAAA;ECAI,kBALI;AJ9BV;AGwCE;ECLI,kBALI;EDYN,cAAA;EACA,kBAAA;AHtCJ;;AG0CA;ECZM,kBALI;EDmBR,cE1QQ;EF2QR,qBAAA;AHvCF;AG0CE;EACE,cAAA;AHxCJ;;AG4CA;EACE,sBAAA;ECxBI,kBALI;ED+BR,WEvTS;EFwTT,yBE/SS;ECEP,qBAAA;ANqQJ;AG2CE;EACE,UAAA;EC/BE,cALI;EDsCN,gBE0Q0B;ALnT9B;;AGkDA;EACE,gBAAA;AH/CF;;AGqDA;;EAEE,sBAAA;AHlDF;;AG0DA;EACE,oBAAA;EACA,yBAAA;AHvDF;;AG0DA;EACE,mBEwU4B;EFvU5B,sBEuU4B;EFtU5B,cE1VS;EF2VT,gBAAA;AHvDF;;AG8DA;EAEE,mBAAA;EACA,gCAAA;AH5DF;;AG+DA;;;;;;EAME,qBAAA;EACA,mBAAA;EACA,eAAA;AH5DF;;AGoEA;EACE,qBAAA;AHjEF;;AGuEA;EAEE,gBAAA;AHrEF;;AG6EA;EACE,UAAA;AH1EF;;AG+EA;;;;;EAKE,SAAA;EACA,oBAAA;EC9HI,kBALI;EDqIR,oBAAA;AH5EF;;AGgFA;;EAEE,oBAAA;AH7EF;;AGkFA;EACE,eAAA;AH/EF;;AGkFA;EAGE,iBAAA;AHjFF;AGoFE;EACE,UAAA;AHlFJ;;AGyFA;EACE,aAAA;AHtFF;;AG8FA;;;;EAIE,0BAAA;AH3FF;AG8FI;;;;EACE,eAAA;AHzFN;;AGgGA;EACE,UAAA;EACA,kBAAA;AH7FF;;AGkGA;EACE,gBAAA;AH/FF;;AGyGA;EACE,YAAA;EACA,UAAA;EACA,SAAA;EACA,SAAA;AHtGF;;AG8GA;EACE,WAAA;EACA,WAAA;EACA,UAAA;EACA,qBE6J4B;EDhXtB,iCAAA;EDsNN,oBAAA;AH5GF;AI5QI;EDiXJ;ICxMQ,iBAAA;EJuGN;AACF;AGyGE;EACE,WAAA;AHvGJ;;AG8GA;;;;;;;EAOE,UAAA;AH3GF;;AG8GA;EACE,YAAA;AH3GF;;AGoHA;EACE,oBAAA;EACA,6BAAA;AHjHF;;AGyHA;;;;;;;CAAA;AAWA;EACE,wBAAA;AHzHF;;AG8HA;EACE,UAAA;AH3HF;;AGiIA;EACE,aAAA;AH9HF;;AG6HA;EACE,aAAA;AH9HF;;AGoIA;EACE,aAAA;EACA,0BAAA;AHjIF;;AGsIA;EACE,qBAAA;AHnIF;;AGwIA;EACE,SAAA;AHrIF;;AG4IA;EACE,kBAAA;EACA,eAAA;AHzIF;;AGiJA;EACE,wBAAA;AH9IF;;AGsJA;EACE,wBAAA;AHnJF;;AOhcA;EHyQM,kBALI;EGlQR,gBFumB4B;ALpK9B;;AO9bE;EHsQM,iCAAA;EGpQJ,gBF0lBkB;EEzlBlB,gBF2kB0B;AL1I9B;AIhWI;EGpGF;IH6QM,eAAA;EJ2LN;AACF;;AOzcE;EHsQM,iCAAA;EGpQJ,gBF0lBkB;EEzlBlB,gBF2kB0B;AL/H9B;AI3WI;EGpGF;IH6QM,iBAAA;EJsMN;AACF;;AOpdE;EHsQM,iCAAA;EGpQJ,gBF0lBkB;EEzlBlB,gBF2kB0B;ALpH9B;AItXI;EGpGF;IH6QM,eAAA;EJiNN;AACF;;AO/dE;EHsQM,iCAAA;EGpQJ,gBF0lBkB;EEzlBlB,gBF2kB0B;ALzG9B;AIjYI;EGpGF;IH6QM,iBAAA;EJ4NN;AACF;;AO1eE;EHsQM,iCAAA;EGpQJ,gBF0lBkB;EEzlBlB,gBF2kB0B;AL9F9B;AI5YI;EGpGF;IH6QM,eAAA;EJuON;AACF;;AOrfE;EHsQM,iCAAA;EGpQJ,gBF0lBkB;EEzlBlB,gBF2kB0B;ALnF9B;AIvZI;EGpGF;IH6QM,iBAAA;EJkPN;AACF;;AO1eA;ECrDE,eAAA;EACA,gBAAA;ARmiBF;;AO1eA;EC1DE,eAAA;EACA,gBAAA;ARwiBF;;AO5eA;EACE,qBAAA;AP+eF;AO7eE;EACE,oBF8lB0B;AL/G9B;;AOreA;EHsNM,kBALI;EG/MR,yBAAA;APweF;;AOpeA;EACE,mBFgSO;EDjFH,kBALI;AJ8RV;AOreE;EACE,gBAAA;APueJ;;AOneA;EACE,iBAAA;EACA,mBFsRO;EDjFH,kBALI;EG9LR,cFpFS;AL0jBX;AOpeE;EACE,aAAA;APseJ;;ASpkBA;ECIE,eAAA;EAGA,YAAA;AVkkBF;;ASnkBA;EACE,gBJs9CkC;EIr9ClC,sBJPS;EIQT,yBAAA;EHGE,sBAAA;EIRF,eAAA;EAGA,YAAA;AV2kBF;;AS7jBA;EAEE,qBAAA;AT+jBF;;AS5jBA;EACE,qBAAA;EACA,cAAA;AT+jBF;;AS5jBA;EL+PM,kBALI;EKxPR,cJ1BS;ALylBX;;AWjmBE;;;;;;;ECHA,WAAA;EACA,0CAAA;EACA,yCAAA;EACA,kBAAA;EACA,iBAAA;AZ8mBF;;AatjBI;EF5CE;IACE,gBNide;ELqJrB;AACF;Aa5jBI;EF5CE;IACE,gBNide;EL0JrB;AACF;AajkBI;EF5CE;IACE,gBNide;EL+JrB;AACF;AatkBI;EF5CE;IACE,iBNide;ELoKrB;AACF;Aa3kBI;EF5CE;IACE,iBNide;ELyKrB;AACF;Ac1oBE;ECAA,qBAAA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EAEA,yCAAA;EACA,6CAAA;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6nEJ;AoB3nEI;EACE,eAAA;ApB6nEN;AoBxnEE;EACE,cfjBO;EekBP,sBf3BO;Ee4BP,qBf8zBoC;Ee7zBpC,UAAA;EAKE,kDfusB0B;AL+6ChC;AoB/mEE;EAEE,aAAA;ApBgnEJ;AoB5mEE;EACE,cf1CO;Ee4CP,UAAA;ApB6mEJ;AoBhnEE;EACE,cf1CO;Ee4CP,UAAA;ApB6mEJ;AoBrmEE;EAEE,yBf1DO;Ee6DP,UAAA;ApBomEJ;AoBhmEE;EACE,yBAAA;EACA,0BAAA;EACA,2Bf0pB0B;Ee1pB1B,0Bf0pB0B;EezpB1B,cf9DO;EiBbT,yBjBMS;EeuEP,oBAAA;EACA,qBAAA;EACA,mBAAA;EACA,eAAA;EACA,4Bfgb0B;Ee/a1B,gBAAA;ECtEE,6IDuEF;ECvEE,qIDuEF;ApBkmEJ;AoB9mEE;EACE,yBAAA;EACA,0BAAA;EACA,2Bf0pB0B;Ee1pB1B,0Bf0pB0B;EezpB1B,cf9DO;EiBbT,yBjBMS;EeuEP,oBAAA;EACA,qBAAA;EACA,mBAAA;EACA,eAAA;EACA,4Bfgb0B;Ee/a1B,gBAAA;ECtEE,qIDuEF;ApBkmEJ;AqBrqEM;EDuDJ;ICtDM,wBAAA;IAAA,gBAAA;ErBwqEN;EoBlnEA;ICtDM,gBAAA;ErBwqEN;AACF;AoBpmEE;EACE,yBf+5B8B;ALusClC;AoBvmEE;EACE,yBf+5B8B;ALusClC;AoBnmEE;EACE,yBAAA;EACA,0BAAA;EACA,2BfuoB0B;EevoB1B,0BfuoB0B;EetoB1B,cfjFO;EiBbT,yBjBMS;Ee0FP,oBAAA;EACA,qBAAA;EACA,mBAAA;EACA,eAAA;EACA,4Bf6Z0B;Ee5Z1B,gBAAA;ECzFE,6ID0FF;EC1FE,qID0FF;ApBqmEJ;AqB3rEM;ED0EJ;ICzEM,wBAAA;IAAA,gBAAA;ErB8rEN;AACF;AoBvmEE;EACE,yBf44B8B;AL6tClC;;AoBhmEA;EACE,cAAA;EACA,WAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBfyd4B;Eexd5B,cf5GS;Ee6GT,6BAAA;EACA,yBAAA;EACA,mBAAA;ApBmmEF;AoBjmEE;EAEE,gBAAA;EACA,eAAA;ApBkmEJ;;AoBvlEA;EACE,sCfguBsC;Ee/tBtC,uBAAA;EhBmJI,mBALI;EE7QN,qBAAA;AN2tEJ;AoBxlEE;EACE,uBAAA;EACA,wBAAA;EACA,0Bf2lB0B;Ee3lB1B,yBf2lB0B;AL+/C9B;AoB7lEE;EACE,uBAAA;EACA,wBAAA;EACA,0Bf2lB0B;Ee3lB1B,yBf2lB0B;AL+/C9B;AoBvlEE;EACE,uBAAA;EACA,wBAAA;EACA,0BfqlB0B;EerlB1B,yBfqlB0B;ALogD9B;;AoBrlEA;EACE,oCf8sBsC;Ee7sBtC,oBAAA;EhBgII,kBALI;EE7QN,qBAAA;AN4uEJ;AoBtlEE;EACE,oBAAA;EACA,qBAAA;EACA,wBf4kB0B;Ee5kB1B,uBf4kB0B;AL4gD9B;AoB3lEE;EACE,oBAAA;EACA,qBAAA;EACA,wBf4kB0B;Ee5kB1B,uBf4kB0B;AL4gD9B;AoBrlEE;EACE,oBAAA;EACA,qBAAA;EACA,wBfskB0B;EetkB1B,uBfskB0B;ALihD9B;;AoB/kEE;EACE,uCfqrBoC;AL65CxC;AoB/kEE;EACE,sCfkrBoC;AL+5CxC;AoB9kEE;EACE,oCf+qBoC;ALi6CxC;;AoB3kEA;EACE,Wf6qBsC;Ee5qBtC,YAAA;EACA,iBf4hB4B;ALkjD9B;AoB5kEE;EACE,eAAA;ApB8kEJ;AoB3kEE;EACE,aAAA;Ed/LA,sBAAA;AN6wEJ;AoB1kEE;EACE,aAAA;EdpMA,sBAAA;ANixEJ;;AuB/xEA;EACE,cAAA;EACA,WAAA;EACA,0CAAA;EAEA,uCAAA;EnB2RI,eALI;EmBnRR,gBlBkkB4B;EkBjkB5B,gBlBukB4B;EkBtkB5B,clBES;EkBDT,sBlBRS;EkBST,iPAAA;EACA,4BAAA;EACA,yClBg7BkC;EkB/6BlC,0BlBg7BkC;EkB/6BlC,yBAAA;EjBFE,sBAAA;EeHE,wEEQJ;EACA,wBAAA;EAAA,qBAAA;EAAA,gBAAA;AvB+xEF;AqBpyEM;EEfN;IFgBQ,gBAAA;ErBuyEN;AACF;AuBlyEE;EACE,qBlBs0BoC;EkBr0BpC,UAAA;EAKE,kDlBi7B4B;AL+2ClC;AuB5xEE;EAEE,sBlBgsB0B;EkB/rB1B,sBAAA;AvB6xEJ;AuB1xEE;EAEE,yBlBpCO;AL+zEX;AuBtxEE;EACE,kBAAA;EACA,0BAAA;AvBwxEJ;;AuBpxEA;EACE,oBlByrB4B;EkBxrB5B,uBlBwrB4B;EkBvrB5B,oBlBwrB4B;ED/cxB,mBALI;EE7QN,qBAAA;ANk0EJ;;AuBpxEA;EACE,mBlBqrB4B;EkBprB5B,sBlBorB4B;EkBnrB5B,kBlBorB4B;EDndxB,kBALI;EE7QN,qBAAA;AN00EJ;;AwBz1EA;EACE,cAAA;EACA,kBnBq3BwC;EmBp3BxC,mBnBq3BwC;EmBp3BxC,uBnBq3BwC;ALu+C1C;AwB11EE;EACE,WAAA;EACA,mBAAA;AxB41EJ;;AwBx1EA;EACE,UnBy2BwC;EmBx2BxC,WnBw2BwC;EmBv2BxC,kBAAA;EACA,mBAAA;EACA,sBnBbS;EmBcT,4BAAA;EACA,2BAAA;EACA,wBAAA;EACA,qCnB42BwC;EmB32BxC,wBAAA;EAAA,qBAAA;EAAA,gBAAA;EACA,iCAAA;EAAA,mBAAA;AxB21EF;AwBx1EE;ElBXE,qBAAA;ANs2EJ;AwBv1EE;EAEE,kBnBm2BsC;ALq/C1C;AwBr1EE;EACE,uBnB01BsC;AL6/C1C;AwBp1EE;EACE,qBnBszBoC;EmBrzBpC,UAAA;EACA,kDnBmsB4B;ALmpDhC;AwBn1EE;EACE,yBnBZM;EmBaN,qBnBbM;ALk2EV;AwBn1EI;EAII,+OAAA;AxBk1ER;AwB90EI;EAII,uJAAA;AxB60ER;AwBx0EE;EACE,yBnBjCM;EmBkCN,qBnBlCM;EmBuCJ,yOAAA;AxBs0EN;AwBl0EE;EACE,oBAAA;EACA,YAAA;EACA,YnBk0BuC;ALkgD3C;AwB7zEI;EACE,YnB0zBqC;ALqgD3C;;AwBjzEA;EACE,mBnBqzBgC;AL+/ClC;AwBlzEE;EACE,UnBizB8B;EmBhzB9B,mBAAA;EACA,wKAAA;EACA,gCAAA;ElB9FA,kBAAA;EeHE,iDGmGF;AxBozEJ;AqBn5EM;EGyFJ;IHxFM,gBAAA;ErBs5EN;AACF;AwBvzEI;EACE,0JAAA;AxByzEN;AwBtzEI;EACE,iCnBgzB4B;EmB3yB1B,uJAAA;AxBozER;;AwB9yEA;EACE,qBAAA;EACA,kBnBmxBgC;AL8hDlC;;AwB9yEA;EACE,kBAAA;EACA,sBAAA;EACA,oBAAA;AxBizEF;AwB7yEI;EACE,oBAAA;EACA,YAAA;EACA,anBqoBwB;AL0qD9B;;AyB77EA;EACE,WAAA;EACA,cAAA;EACA,UAAA;EACA,6BAAA;EACA,wBAAA;EAAA,qBAAA;EAAA,gBAAA;AzBg8EF;AyB97EE;EACE,UAAA;AzBg8EJ;AyB57EI;EAA0B,kEpB89Ba;ALi+C3C;AyB97EI;EAA0B,kEpB69Ba;ALo+C3C;AyB97EE;EACE,SAAA;AzBg8EJ;AyB77EE;EACE,WpB+8BuC;EoB98BvC,YpB88BuC;EoB78BvC,oBAAA;EHzBF,yBjBkCQ;EoBPN,SpB88BuC;EC19BvC,mBAAA;EeHE,oHIkBF;EJlBE,4GIkBF;EACA,wBAAA;EAAA,gBAAA;AzB87EJ;AqB78EM;EIMJ;IJLM,wBAAA;IAAA,gBAAA;ErBg9EN;AACF;AyBj8EI;EHjCF,yBjB8+ByC;ALu/C3C;AyB/7EE;EACE,WpBw7B8B;EoBv7B9B,cpBw7B8B;EoBv7B9B,kBAAA;EACA,epBu7B8B;EoBt7B9B,yBpBpCO;EoBqCP,yBAAA;EnB7BA,mBAAA;AN+9EJ;AyB77EE;EACE,WpBo7BuC;EoBn7BvC,YpBm7BuC;EiBt+BzC,yBjBkCQ;EoBmBN,SpBo7BuC;EC19BvC,mBAAA;EeHE,iHI4CF;EJ5CE,4GI4CF;EACA,qBAAA;EAAA,gBAAA;AzB87EJ;AqBv+EM;EIiCJ;IJhCM,qBAAA;IAAA,gBAAA;ErB0+EN;AACF;AyBj8EI;EH3DF,yBjB8+ByC;ALihD3C;AyB/7EE;EACE,WpB85B8B;EoB75B9B,cpB85B8B;EoB75B9B,kBAAA;EACA,epB65B8B;EoB55B9B,yBpB9DO;EoB+DP,yBAAA;EnBvDA,mBAAA;ANy/EJ;AyB77EE;EACE,oBAAA;AzB+7EJ;AyB77EI;EACE,yBpBtEK;ALqgFX;AyB57EI;EACE,yBpB1EK;ALwgFX;;A0BrhFA;EACE,kBAAA;A1BwhFF;A0BthFE;;EAEE,0BrBy/B8B;EqBx/B9B,iBrBy/B8B;AL+hDlC;A0BrhFE;EACE,kBAAA;EACA,MAAA;EACA,OAAA;EACA,YAAA;EACA,qBAAA;EACA,oBAAA;EACA,6BAAA;EACA,qBAAA;ELDE,gEKEF;A1BuhFJ;AqBrhFM;EKXJ;ILYM,gBAAA;ErBwhFN;AACF;A0BxhFE;EACE,qBAAA;A1B0hFJ;A0BxhFI;EACE,kBAAA;A1B0hFN;A0B3hFI;EACE,kBAAA;A1B0hFN;A0BvhFI;EAEE,qBrBm+B4B;EqBl+B5B,wBrBm+B4B;ALqjDlC;A0B3hFI;EAEE,qBrBm+B4B;EqBl+B5B,wBrBm+B4B;ALqjDlC;A0BrhFI;EACE,qBrB89B4B;EqB79B5B,wBrB89B4B;ALyjDlC;A0BnhFE;EACE,qBrBw9B8B;EqBv9B9B,wBrBw9B8B;AL6jDlC;A0B/gFI;EACE,arBk9B4B;EqBj9B5B,8DrBk9B4B;ALikDlC;A0BrhFI;;;EACE,arBk9B4B;EqBj9B5B,8DrBk9B4B;ALikDlC;A0B9gFI;EACE,arB28B4B;EqB18B5B,8DrB28B4B;ALqkDlC;;A2BtkFA;EACE,kBAAA;EACA,aAAA;EACA,eAAA;EACA,oBAAA;EACA,WAAA;A3BykFF;A2BvkFE;;EAEE,kBAAA;EACA,cAAA;EACA,SAAA;EACA,YAAA;A3BykFJ;A2BrkFE;;EAEE,UAAA;A3BukFJ;A2BjkFE;EACE,kBAAA;EACA,UAAA;A3BmkFJ;A2BjkFI;EACE,UAAA;A3BmkFN;;A2BxjFA;EACE,aAAA;EACA,mBAAA;EACA,yBAAA;EvBsPI,eALI;EuB/OR,gBtB8hB4B;EsB7hB5B,gBtBmiB4B;EsBliB5B,ctBlCS;EsBmCT,kBAAA;EACA,mBAAA;EACA,yBtB5CS;EsB6CT,yBAAA;ErBpCE,sBAAA;ANgmFJ;;A2BljFA;;;;EAIE,oBAAA;EvBgOI,kBALI;EE7QN,qBAAA;ANymFJ;;A2BljFA;;;;EAIE,uBAAA;EvBuNI,mBALI;EE7QN,qBAAA;ANknFJ;;A2BljFA;;EAEE,mBAAA;A3BqjFF;;A2BxiFI;;ErB/DA,0BAAA;EACA,6BAAA;AN4mFJ;A2BviFI;;ErBtEA,0BAAA;EACA,6BAAA;ANinFJ;A2BjiFE;EACE,iBAAA;ErBpEA,yBAAA;EACA,4BAAA;ANwmFJ;;A4BjoFE;EACE,aAAA;EACA,WAAA;EACA,mBvByxBoC;EDhhBlC,kBALI;EwBjQN,cvB0/BqB;ALyoDzB;;A4BhoFE;EACE,kBAAA;EACA,SAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;EACA,uBAAA;EACA,kBAAA;ExB4PE,mBALI;EwBpPN,WAvBc;EAwBd,wCAvBiB;EtBHjB,sBAAA;AN6pFJ;;A4B9nFI;;;;EAEE,cAAA;A5BmoFN;;A4BjrFI;EAoDE,qBvB+9BmB;EuB59BjB,oCvB+yBgC;EuB9yBhC,4PAAA;EACA,4BAAA;EACA,2DAAA;EACA,gEAAA;A5B+nFR;A4B5nFM;EACE,qBvBo9BiB;EuBn9BjB,iDA/Ca;A5B6qFrB;;A4B9rFI;EAyEI,oCvB6xBgC;EuB5xBhC,kFAAA;A5BynFR;;A4BnsFI;EAiFE,qBvBk8BmB;ALorDzB;A4BnnFQ;EAEE,uBvB42B8B;EuB32B9B,6dAAA;EACA,+DAAA;EACA,2EAAA;A5BonFV;A4BhnFM;EACE,qBvBq7BiB;EuBp7BjB,iDA9Ea;A5BgsFrB;;A4BjtFI;EAsGE,qBvB66BmB;ALksDzB;A4B7mFM;EACE,yBvB06BiB;ALqsDzB;A4B5mFM;EACE,iDA5Fa;A5B0sFrB;A4B3mFM;EACE,cvBk6BiB;AL2sDzB;;A4BxmFI;EACE,kBAAA;A5B2mFN;;A4BluFI;;;EA+HI,UAAA;A5BymFR;A4BrmFM;;;EACE,UAAA;A5BymFR;;A4B1tFE;EACE,aAAA;EACA,WAAA;EACA,mBvByxBoC;EDhhBlC,kBALI;EwBjQN,cvB0/BqB;ALkuDzB;;A4BztFE;EACE,kBAAA;EACA,SAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;EACA,uBAAA;EACA,kBAAA;ExB4PE,mBALI;EwBpPN,WAvBc;EAwBd,wCAvBiB;EtBHjB,sBAAA;ANsvFJ;;A4BvtFI;;;;EAEE,cAAA;A5B4tFN;;A4B1wFI;EAoDE,qBvB+9BmB;EuB59BjB,oCvB+yBgC;EuB9yBhC,4UAAA;EACA,4BAAA;EACA,2DAAA;EACA,gEAAA;A5BwtFR;A4BrtFM;EACE,qBvBo9BiB;EuBn9BjB,iDA/Ca;A5BswFrB;;A4BvxFI;EAyEI,oCvB6xBgC;EuB5xBhC,kFAAA;A5BktFR;;A4B5xFI;EAiFE,qBvBk8BmB;AL6wDzB;A4B5sFQ;EAEE,uBvB42B8B;EuB32B9B,6iBAAA;EACA,+DAAA;EACA,2EAAA;A5B6sFV;A4BzsFM;EACE,qBvBq7BiB;EuBp7BjB,iDA9Ea;A5ByxFrB;;A4B1yFI;EAsGE,qBvB66BmB;AL2xDzB;A4BtsFM;EACE,yBvB06BiB;AL8xDzB;A4BrsFM;EACE,iDA5Fa;A5BmyFrB;A4BpsFM;EACE,cvBk6BiB;ALoyDzB;;A4BjsFI;EACE,kBAAA;A5BosFN;;A4B3zFI;;;EAiII,UAAA;A5BgsFR;A4B9rFM;;;EACE,UAAA;A5BksFR;;A6Bx0FA;EACE,qBAAA;EAEA,gBxBwkB4B;EwBvkB5B,gBxB6kB4B;EwB5kB5B,cxBQS;EwBPT,kBAAA;EACA,qBAAA;EAEA,sBAAA;EACA,eAAA;EACA,yBAAA;EAAA,sBAAA;EAAA,iBAAA;EACA,6BAAA;EACA,6BAAA;EC8GA,yBAAA;E1BsKI,eALI;EE7QN,sBAAA;EeHE,qIQGJ;A7B20FF;AqB10FM;EQhBN;IRiBQ,gBAAA;ErB60FN;AACF;A6B90FE;EACE,cxBLO;ALq1FX;A6B50FE;EAEE,UAAA;EACA,kDxBotB4B;ALynEhC;A6B/zFE;EAGE,oBAAA;EACA,axB0uB0B;ALqlE9B;;A6BnzFE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;ALoyFf;A8B31FE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9B82FjB;A8B11FE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,iDAAA;A9Bu1FN;A8Bn1FE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9Bs3FlB;A8B70FI;EAKI,iDAAA;A9B20FR;A8Bt0FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;AL4zFf;;A6Bj1FE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;ALk0Ff;A8Bz3FE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9B44FjB;A8Bx3FE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,kDAAA;A9Bq3FN;A8Bj3FE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9Bo5FlB;A8B32FI;EAKI,kDAAA;A9By2FR;A8Bp2FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;AL01Ff;;A6B/2FE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;ALg2Ff;A8Bv5FE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9B06FjB;A8Bt5FE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,iDAAA;A9Bm5FN;A8B/4FE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9Bk7FlB;A8Bz4FI;EAKI,iDAAA;A9Bu4FR;A8Bl4FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;ALw3Ff;;A6B74FE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;AL83Ff;A8Br7FE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9Bw8FjB;A8Bp7FE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,iDAAA;A9Bi7FN;A8B76FE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9Bg9FlB;A8Bv6FI;EAKI,iDAAA;A9Bq6FR;A8Bh6FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;ALs5Ff;;A6B36FE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;AL45Ff;A8Bn9FE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9Bs+FjB;A8Bl9FE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,gDAAA;A9B+8FN;A8B38FE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9B8+FlB;A8Br8FI;EAKI,gDAAA;A9Bm8FR;A8B97FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;ALo7Ff;;A6Bz8FE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;AL07Ff;A8Bj/FE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9BogGjB;A8Bh/FE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,gDAAA;A9B6+FN;A8Bz+FE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9B4gGlB;A8Bn+FI;EAKI,gDAAA;A9Bi+FR;A8B59FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;ALk9Ff;;A6Bv+FE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;ALw9Ff;A8B/gGE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9BkiGjB;A8B9gGE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,kDAAA;A9B2gGN;A8BvgGE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9B0iGlB;A8BjgGI;EAKI,kDAAA;A9B+/FR;A8B1/FE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;ALg/Ff;;A6BrgGE;ECvCA,WAXQ;ERLR,yBjB4Ea;EyB1Db,qBzB0Da;ALs/Ff;A8B7iGE;EACE,WAdY;ERRd,yBQMmB;EAkBjB,qBAjBa;A9BgkGjB;A8B5iGE;EAEE,WArBY;ERRd,yBQMmB;EAyBjB,qBAxBa;EA6BX,+CAAA;A9ByiGN;A8BriGE;EAKE,WAlCa;EAmCb,yBArCkB;EAwClB,qBAvCc;A9BwkGlB;A8B/hGI;EAKI,+CAAA;A9B6hGR;A8BxhGE;EAEE,WAjDe;EAkDf,yBzBYW;EyBTX,qBzBSW;AL8gGf;;A6B7hGE;ECmBA,czBJa;EyBKb,qBzBLa;ALmhGf;A8B5gGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;ALwhGf;A8B3gGE;EAEE,iDAAA;A9B4gGJ;A8BzgGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;ALgiGf;A8BrgGI;EAKI,iDAAA;A9BmgGR;A8B9/FE;EAEE,czBvCW;EyBwCX,6BAAA;A9B+/FJ;;A6BtjGE;ECmBA,czBJa;EyBKb,qBzBLa;AL4iGf;A8BriGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;ALijGf;A8BpiGE;EAEE,kDAAA;A9BqiGJ;A8BliGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;ALyjGf;A8B9hGI;EAKI,kDAAA;A9B4hGR;A8BvhGE;EAEE,czBvCW;EyBwCX,6BAAA;A9BwhGJ;;A6B/kGE;ECmBA,czBJa;EyBKb,qBzBLa;ALqkGf;A8B9jGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;AL0kGf;A8B7jGE;EAEE,gDAAA;A9B8jGJ;A8B3jGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;ALklGf;A8BvjGI;EAKI,gDAAA;A9BqjGR;A8BhjGE;EAEE,czBvCW;EyBwCX,6BAAA;A9BijGJ;;A6BxmGE;ECmBA,czBJa;EyBKb,qBzBLa;AL8lGf;A8BvlGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;ALmmGf;A8BtlGE;EAEE,iDAAA;A9BulGJ;A8BplGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;AL2mGf;A8BhlGI;EAKI,iDAAA;A9B8kGR;A8BzkGE;EAEE,czBvCW;EyBwCX,6BAAA;A9B0kGJ;;A6BjoGE;ECmBA,czBJa;EyBKb,qBzBLa;ALunGf;A8BhnGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;AL4nGf;A8B/mGE;EAEE,gDAAA;A9BgnGJ;A8B7mGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;ALooGf;A8BzmGI;EAKI,gDAAA;A9BumGR;A8BlmGE;EAEE,czBvCW;EyBwCX,6BAAA;A9BmmGJ;;A6B1pGE;ECmBA,czBJa;EyBKb,qBzBLa;ALgpGf;A8BzoGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;ALqpGf;A8BxoGE;EAEE,gDAAA;A9ByoGJ;A8BtoGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;AL6pGf;A8BloGI;EAKI,gDAAA;A9BgoGR;A8B3nGE;EAEE,czBvCW;EyBwCX,6BAAA;A9B4nGJ;;A6BnrGE;ECmBA,czBJa;EyBKb,qBzBLa;ALyqGf;A8BlqGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;AL8qGf;A8BjqGE;EAEE,kDAAA;A9BkqGJ;A8B/pGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;ALsrGf;A8B3pGI;EAKI,kDAAA;A9BypGR;A8BppGE;EAEE,czBvCW;EyBwCX,6BAAA;A9BqpGJ;;A6B5sGE;ECmBA,czBJa;EyBKb,qBzBLa;ALksGf;A8B3rGE;EACE,WATY;EAUZ,yBzBTW;EyBUX,qBzBVW;ALusGf;A8B1rGE;EAEE,+CAAA;A9B2rGJ;A8BxrGE;EAKE,WArBa;EAsBb,yBzBxBW;EyByBX,qBzBzBW;AL+sGf;A8BprGI;EAKI,+CAAA;A9BkrGR;A8B7qGE;EAEE,czBvCW;EyBwCX,6BAAA;A9B8qGJ;;A6BztGA;EACE,gBxBigB4B;EwBhgB5B,cxBzCQ;EwB0CR,0BxB6WwC;AL+2F1C;A6B1tGE;EACE,cxB4WsC;ALg3F1C;A6BptGE;EAEE,cxB/EO;ALoyGX;;A6B1sGA;ECuBE,oBAAA;E1BsKI,kBALI;EE7QN,qBAAA;ANqyGJ;;A6B5sGA;ECmBE,uBAAA;E1BsKI,mBALI;EE7QN,qBAAA;AN2yGJ;;A+B9zGA;EVgBM,gCUfJ;A/Bi0GF;AqB9yGM;EUpBN;IVqBQ,gBAAA;ErBizGN;AACF;A+Bp0GE;EACE,UAAA;A/Bs0GJ;;A+Bh0GE;EACE,aAAA;A/Bm0GJ;;A+B/zGA;EACE,SAAA;EACA,gBAAA;EVDI,6BUEJ;A/Bk0GF;AqBh0GM;EULN;IVMQ,gBAAA;ErBm0GN;AACF;A+Br0GE;EACE,QAAA;EACA,YAAA;EVNE,4BUOF;A/Bu0GJ;AqB10GM;EUAJ;IVCM,gBAAA;ErB60GN;AACF;;AgCl2GA;;;;EAIE,kBAAA;AhCq2GF;;AgCl2GA;EACE,mBAAA;AhCq2GF;AiCh1GI;EACE,qBAAA;EACA,oB5BqgBwB;E4BpgBxB,uB5BmgBwB;E4BlgBxB,WAAA;EAhCJ,uBAAA;EACA,qCAAA;EACA,gBAAA;EACA,oCAAA;AjCm3GF;AiC9zGI;EACE,cAAA;AjCg0GN;;AgC32GA;EACE,kBAAA;EACA,a3ByhCkC;E2BxhClC,aAAA;EACA,gB3B+mCkC;E2B9mClC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CA,gBAAA;ErCuwIJ;EqCrwII;IACE,cAAA;IACA,cAAA;ErCuwIN;EqClwIM;I/BpJJ,0BAAA;IACA,6BAAA;ENy5IF;EqCnwIQ;;IAGE,0BAAA;ErCowIV;EqClwIQ;;IAGE,6BAAA;ErCmwIV;EqC/vIM;I/BrJJ,yBAAA;IACA,4BAAA;ENu5IF;EqChwIQ;;IAGE,yBAAA;ErCiwIV;EqC/vIQ;;IAGE,4BAAA;ErCgwIV;AACF;;AsC98IA;EACE,kBAAA;EACA,aAAA;EACA,mBAAA;EACA,WAAA;EACA,qBAAA;ElC4RI,eALI;EkCrRR,cjCMS;EiCLT,gBAAA;EACA,sBjCLS;EiCMT,SAAA;EhCKE,gBAAA;EgCHF,qBAAA;EjBAI,+JiBCJ;AtCi9IF;AqB98IM;EiBhBN;IjBiBQ,gBAAA;ErBi9IN;AACF;AsCp9IE;EACE,cjCyvCsC;EiCxvCtC,yBjCuvCsC;EiCtvCtC,+CAAA;AtCs9IJ;AsCp9II;EACE,iSAAA;EACA,0BjC4vCoC;AL0tG1C;AsCj9IE;EACE,cAAA;EACA,cjCivCsC;EiChvCtC,ejCgvCsC;EiC/uCtC,iBAAA;EACA,WAAA;EACA,iSAAA;EACA,4BAAA;EACA,wBjC2uCsC;EgBlwCpC,sCiBwBF;AtCm9IJ;AqBv+IM;EiBWJ;IjBVM,gBAAA;ErB0+IN;AACF;AsCr9IE;EACE,UAAA;AtCu9IJ;AsCp9IE;EACE,UAAA;EACA,qBjCizBoC;EiChzBpC,UAAA;EACA,kDjC8rB4B;ALwxHhC;;AsCl9IA;EACE,gBAAA;AtCq9IF;;AsCl9IA;EACE,sBjCpDS;EiCqDT,sCAAA;AtCq9IF;AsCn9IE;EhCnCE,+BAAA;EACA,gCAAA;ANy/IJ;AsCp9II;EhCtCA,2CAAA;EACA,4CAAA;AN6/IJ;AsCn9IE;EACE,aAAA;AtCq9IJ;AsCj9IE;EhClCE,mCAAA;EACA,kCAAA;ANs/IJ;AsCj9IM;EhCtCF,+CAAA;EACA,8CAAA;AN0/IJ;AsCh9II;EhC3CA,mCAAA;EACA,kCAAA;AN8/IJ;;AsC98IA;EACE,qBAAA;AtCi9IF;;AsCx8IE;EACE,eAAA;AtC28IJ;AsCx8IE;EACE,eAAA;EACA,cAAA;EhCxFA,gBAAA;ANmiJJ;AsCx8II;EAAgB,aAAA;AtC28IpB;AsC18II;EAAe,gBAAA;AtC68InB;AsC38II;EhC9FA,gBAAA;AN4iJJ;;AuC/jJA;EACE,aAAA;EACA,eAAA;EACA,YAAA;EACA,mBlCw/CkC;EkCt/ClC,gBAAA;AvCikJF;;AuC1jJE;EACE,oBlC6+CgC;ALglGpC;AuC3jJI;EACE,WAAA;EACA,qBlCy+C8B;EkCx+C9B,clCLK;EkCML,uFAAA;AvC6jJN;AuCzjJE;EACE,clCXO;ALskJX;;AwCplJA;EACE,aAAA;EhCGA,eAAA;EACA,gBAAA;ARqlJF;;AwCrlJA;EACE,kBAAA;EACA,cAAA;EACA,cnC8BQ;EmC7BR,qBAAA;EACA,sBnCFS;EmCGT,yBAAA;EnBKI,qImBJJ;AxCwlJF;AqBhlJM;EmBfN;InBgBQ,gBAAA;ErBmlJN;AACF;AwC3lJE;EACE,UAAA;EACA,cnC+asC;EmC7atC,yBnCRO;EmCSP,qBnCRO;ALomJX;AwCzlJE;EACE,UAAA;EACA,cnCuasC;EmCtatC,yBnCfO;EmCgBP,UnC4qCgC;EmC3qChC,kDnCstB4B;ALq4HhC;;AwCtlJE;EACE,iBnC+pCgC;AL07GpC;AwCtlJE;EACE,UAAA;EACA,WnC9BO;EiBJT,yBjBkCQ;EmCEN,qBnCFM;AL0lJV;AwCrlJE;EACE,cnC9BO;EmC+BP,oBAAA;EACA,sBnCtCO;EmCuCP,qBnCpCO;AL2nJX;;AyCloJE;EACE,yBAAA;AzCqoJJ;;AyC9nJQ;EnCqCJ,+BAAA;EACA,kCAAA;AN6lJJ;AyC7nJQ;EnCiBJ,gCAAA;EACA,mCAAA;AN+mJJ;;AyC/oJE;EACE,uBAAA;ErCgSE,kBALI;AJw3IV;AyC5oJQ;EnCqCJ,8BAAA;EACA,iCAAA;AN0mJJ;AyC1oJQ;EnCiBJ,+BAAA;EACA,kCAAA;AN4nJJ;;AyC5pJE;EACE,uBAAA;ErCgSE,mBALI;AJq4IV;AyCzpJQ;EnCqCJ,8BAAA;EACA,iCAAA;ANunJJ;AyCvpJQ;EnCiBJ,+BAAA;EACA,kCAAA;ANyoJJ;;A0CxqJA;EACE,qBAAA;EACA,sBAAA;EtC8RI,iBALI;EsCvRR,gBrCukB4B;EqCtkB5B,cAAA;EACA,WrCHS;EqCIT,kBAAA;EACA,mBAAA;EACA,wBAAA;EpCKE,sBAAA;ANuqJJ;A0CvqJE;EACE,aAAA;A1CyqJJ;;A0CpqJA;EACE,kBAAA;EACA,SAAA;A1CuqJF;;A2C9rJA;EACE,kBAAA;EACA,kBAAA;EACA,mBtCk6C8B;EsCj6C9B,6BAAA;ErCWE,sBAAA;ANurJJ;;A2C7rJA;EAEE,cAAA;A3C+rJF;;A2C3rJA;EACE,gBtC4jB4B;ALkoI9B;;A2CtrJA;EACE,mBtCm5C8B;ALsyGhC;A2CtrJE;EACE,kBAAA;EACA,MAAA;EACA,QAAA;EACA,UAAA;EACA,qBAAA;A3CwrJJ;;A2CzqJE;EClDA,cD8Cc;ErB5Cd,yBqB0CmB;EC1CnB,qBD2Ce;A3CorJjB;A4C7tJE;EACE,cAAA;A5C+tJJ;;A2ClrJE;EClDA,cD8Cc;ErB5Cd,yBqB0CmB;EC1CnB,qBD2Ce;A3C6rJjB;A4CtuJE;EACE,cAAA;A5CwuJJ;;A2C3rJE;EClDA,cD8Cc;ErB5Cd,yBqB0CmB;EC1CnB,qBD2Ce;A3CssJjB;A4C/uJE;EACE,cAAA;A5CivJJ;;A2CpsJE;EClDA,cDgDgB;ErB9ChB,yBqB0CmB;EC1CnB,qBD2Ce;A3C+sJjB;A4CxvJE;EACE,cAAA;A5C0vJJ;;A2C7sJE;EClDA,cDgDgB;ErB9ChB,yBqB0CmB;EC1CnB,qBD2Ce;A3CwtJjB;A4CjwJE;EACE,cAAA;A5CmwJJ;;A2CttJE;EClDA,cD8Cc;ErB5Cd,yBqB0CmB;EC1CnB,qBD2Ce;A3CiuJjB;A4C1wJE;EACE,cAAA;A5C4wJJ;;A2C/tJE;EClDA,cDgDgB;ErB9ChB,yBqB0CmB;EC1CnB,qBD2Ce;A3C0uJjB;A4CnxJE;EACE,cAAA;A5CqxJJ;;A2CxuJE;EClDA,cD8Cc;ErB5Cd,yBqB0CmB;EC1CnB,qBD2Ce;A3CmvJjB;A4C5xJE;EACE,cAAA;A5C8xJJ;;A6CjyJE;EACE;IAAK,2BxCk7C2B;ELm3GlC;AACF;;A6CvyJE;EACE;IAAK,2BxCk7C2B;ELm3GlC;AACF;A6CjyJA;EACE,aAAA;EACA,YxC26CkC;EwC16ClC,gBAAA;EzCwRI,kBALI;EyCjRR,yBxCLS;ECSP,sBAAA;ANgyJJ;;A6C/xJA;EACE,aAAA;EACA,sBAAA;EACA,uBAAA;EACA,gBAAA;EACA,WxCjBS;EwCkBT,kBAAA;EACA,mBAAA;EACA,yBxCUQ;EgBtBJ,2BwBaJ;A7CkyJF;AqB3yJM;EwBAN;IxBCQ,gBAAA;ErB8yJN;AACF;;A6CpyJA;EvBYE,qMAAA;EuBVA,0BAAA;A7CuyJF;;A6CnyJE;EACE,0DAAA;EAAA,kDAAA;A7CsyJJ;A6CnyJM;EAJJ;IAKM,uBAAA;IAAA,eAAA;E7CsyJN;AACF;;A8C90JA;EACE,aAAA;EACA,sBAAA;EAGA,eAAA;EACA,gBAAA;ExCSE,sBAAA;ANu0JJ;;A8C50JA;EACE,qBAAA;EACA,sBAAA;A9C+0JF;A8C70JE;EAEE,oCAAA;EACA,0BAAA;A9C80JJ;;A8Cp0JA;EACE,WAAA;EACA,czClBS;EyCmBT,mBAAA;A9Cu0JF;A8Cp0JE;EAEE,UAAA;EACA,czCzBO;EyC0BP,qBAAA;EACA,yBzCjCO;ALs2JX;A8Cl0JE;EACE,czC7BO;EyC8BP,yBzCrCO;ALy2JX;;A8C3zJA;EACE,kBAAA;EACA,cAAA;EACA,oBAAA;EACA,czC3CS;EyC4CT,qBAAA;EACA,sBzCtDS;EyCuDT,sCAAA;A9C8zJF;A8C5zJE;ExCrCE,+BAAA;EACA,gCAAA;ANo2JJ;A8C5zJE;ExC3BE,mCAAA;EACA,kCAAA;AN01JJ;A8C5zJE;EAEE,czC7DO;EyC8DP,oBAAA;EACA,sBzCrEO;ALk4JX;A8CzzJE;EACE,UAAA;EACA,WzC3EO;EyC4EP,yBzC9CM;EyC+CN,qBzC/CM;AL02JV;A8CxzJE;EACE,mBAAA;A9C0zJJ;A8CxzJI;EACE,gBAAA;EACA,qBzCwawB;ALk5I9B;;A8C5yJI;EACE,mBAAA;A9C+yJN;A8C5yJQ;ExCrCJ,kCAAA;EAZA,0BAAA;ANi2JJ;A8C3yJQ;ExCtDJ,gCAAA;EAYA,4BAAA;ANy1JJ;A8C1yJQ;EACE,aAAA;A9C4yJV;A8CzyJQ;EACE,qBzCuYoB;EyCtYpB,oBAAA;A9C2yJV;A8CzyJU;EACE,iBAAA;EACA,sBzCkYkB;ALy6I9B;;Aa/2JI;EiC4CA;IACE,mBAAA;E9Cu0JJ;E8Cp0JM;IxCrCJ,kCAAA;IAZA,0BAAA;ENy3JF;E8Cn0JM;IxCtDJ,gCAAA;IAYA,4BAAA;ENi3JF;E8Cl0JM;IACE,aAAA;E9Co0JR;E8Cj0JM;IACE,qBzCuYoB;IyCtYpB,oBAAA;E9Cm0JR;E8Cj0JQ;IACE,iBAAA;IACA,sBzCkYkB;ELi8I5B;AACF;Aax4JI;EiC4CA;IACE,mBAAA;E9C+1JJ;E8C51JM;IxCrCJ,kCAAA;IAZA,0BAAA;ENi5JF;E8C31JM;IxCtDJ,gCAAA;IAYA,4BAAA;ENy4JF;E8C11JM;IACE,aAAA;E9C41JR;E8Cz1JM;IACE,qBzCuYoB;IyCtYpB,oBAAA;E9C21JR;E8Cz1JQ;IACE,iBAAA;IACA,sBzCkYkB;ELy9I5B;AACF;Aah6JI;EiC4CA;IACE,mBAAA;E9Cu3JJ;E8Cp3JM;IxCrCJ,kCAAA;IAZA,0BAAA;ENy6JF;E8Cn3JM;IxCtDJ,gCAAA;IAYA,4BAAA;ENi6JF;E8Cl3JM;IACE,aAAA;E9Co3JR;E8Cj3JM;IACE,qBzCuYoB;IyCtYpB,oBAAA;E9Cm3JR;E8Cj3JQ;IACE,iBAAA;IACA,sBzCkYkB;ELi/I5B;AACF;Aax7JI;EiC4CA;IACE,mBAAA;E9C+4JJ;E8C54JM;IxCrCJ,kCAAA;IAZA,0BAAA;ENi8JF;E8C34JM;IxCtDJ,gCAAA;IAYA,4BAAA;ENy7JF;E8C14JM;IACE,aAAA;E9C44JR;E8Cz4JM;IACE,qBzCuYoB;IyCtYpB,oBAAA;E9C24JR;E8Cz4JQ;IACE,iBAAA;IACA,sBzCkYkB;ELygJ5B;AACF;Aah9JI;EiC4CA;IACE,mBAAA;E9Cu6JJ;E8Cp6JM;IxCrCJ,kCAAA;IAZA,0BAAA;ENy9JF;E8Cn6JM;IxCtDJ,gCAAA;IAYA,4BAAA;ENi9JF;E8Cl6JM;IACE,aAAA;E9Co6JR;E8Cj6JM;IACE,qBzCuYoB;IyCtYpB,oBAAA;E9Cm6JR;E8Cj6JQ;IACE,iBAAA;IACA,sBzCkYkB;ELiiJ5B;AACF;A8Ct5JA;ExC9HI,gBAAA;ANuhKJ;A8Ct5JE;EACE,qBAAA;A9Cw5JJ;A8Ct5JI;EACE,sBAAA;A9Cw5JN;;A+C5iKE;EACE,cDiKyB;EChKzB,yBD+JsB;A9Cg5J1B;A+C5iKM;EAEE,cD2JqB;EC1JrB,yBAAA;A/C6iKR;A+C1iKM;EACE,W1CRG;E0CSH,yBDqJqB;ECpJrB,qBDoJqB;A9Cw5J7B;;A+C1jKE;EACE,cDiKyB;EChKzB,yBD+JsB;A9C85J1B;A+C1jKM;EAEE,cD2JqB;EC1JrB,yBAAA;A/C2jKR;A+CxjKM;EACE,W1CRG;E0CSH,yBDqJqB;ECpJrB,qBDoJqB;A9Cs6J7B;;A+CxkKE;EACE,cDiKyB;EChKzB,yBD+JsB;A9C46J1B;A+CxkKM;EAEE,cD2JqB;EC1JrB,yBAAA;A/CykKR;A+CtkKM;EACE,W1CRG;E0CSH,yBDqJqB;ECpJrB,qBDoJqB;A9Co7J7B;;A+CtlKE;EACE,cDmK2B;EClK3B,yBD+JsB;A9C07J1B;A+CtlKM;EAEE,cD6JuB;EC5JvB,yBAAA;A/CulKR;A+CplKM;EACE,W1CRG;E0CSH,yBDuJuB;ECtJvB,qBDsJuB;A9Cg8J/B;;A+CpmKE;EACE,cDmK2B;EClK3B,yBD+JsB;A9Cw8J1B;A+CpmKM;EAEE,cD6JuB;EC5JvB,yBAAA;A/CqmKR;A+ClmKM;EACE,W1CRG;E0CSH,yBDuJuB;ECtJvB,qBDsJuB;A9C88J/B;;A+ClnKE;EACE,cDiKyB;EChKzB,yBD+JsB;A9Cs9J1B;A+ClnKM;EAEE,cD2JqB;EC1JrB,yBAAA;A/CmnKR;A+ChnKM;EACE,W1CRG;E0CSH,yBDqJqB;ECpJrB,qBDoJqB;A9C89J7B;;A+ChoKE;EACE,cDmK2B;EClK3B,yBD+JsB;A9Co+J1B;A+ChoKM;EAEE,cD6JuB;EC5JvB,yBAAA;A/CioKR;A+C9nKM;EACE,W1CRG;E0CSH,yBDuJuB;ECtJvB,qBDsJuB;A9C0+J/B;;A+C9oKE;EACE,cDiKyB;EChKzB,yBD+JsB;A9Ck/J1B;A+C9oKM;EAEE,cD2JqB;EC1JrB,yBAAA;A/C+oKR;A+C5oKM;EACE,W1CRG;E0CSH,yBDqJqB;ECpJrB,qB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BAAA;AtE+/MZ;;AsEtgNQ;EAOI,gCAAA;AtEmgNZ;;AsE1gNQ;EAOI,gCAAA;AtEugNZ;;AsE9gNQ;EAOI,gCAAA;AtE2gNZ;;AsElhNQ;EAOI,gCAAA;AtE+gNZ;;AsEthNQ;EAOI,gCAAA;AtEmhNZ;;AsE1hNQ;EAOI,gCAAA;AtEuhNZ;;AsE9hNQ;EAOI,gCAAA;AtE2hNZ;;AsEliNQ;EAOI,gCAAA;AtE+hNZ;;AsEtiNQ;EAOI,6BAAA;AtEmiNZ;;AsE1iNQ;EAOI,4BAAA;AtEuiNZ;;AsE9iNQ;EAOI,4BAAA;AtE2iNZ;;AsEljNQ;EAOI,4BAAA;AtE+iNZ;;AsEtjNQ;EAOI,4BAAA;AtEmjNZ;;AsE1jNQ;EAOI,4BAAA;AtEujNZ;;AsE9jNQ;EAOI,qBAAA;AtE2jNZ;;AsElkNQ;EAOI,qBAAA;AtE+jNZ;;AsEtkNQ;EAOI,qBAAA;AtEmkNZ;;AsE1kNQ;EAOI,sBAAA;AtEukNZ;;AsE9kNQ;EAOI,sBAAA;AtE2kNZ;;AsEllNQ;EAOI,0BAAA;AtE+kNZ;;AsEtlNQ;EAOI,uBAAA;AtEmlNZ;;AsE1lNQ;EAOI,2BAAA;AtEulNZ;;AsE9lNQ;EAOI,sBAAA;AtE2lNZ;;AsElmNQ;EAOI,sBAAA;AtE+lNZ;;AsEtmNQ;EAOI,sBAAA;AtEmmNZ;;AsE1mNQ;EAOI,uBAAA;AtEumNZ;;AsE9mNQ;EAOI,uBAAA;AtE2mNZ;;AsElnNQ;EAOI,2BAAA;AtE+mNZ;;AsEtnNQ;EAOI,wBAAA;AtEmnNZ;;AsE1nNQ;EAOI,4BAAA;AtEunNZ;;AsE9nNQ;EAOI,yBAAA;AtE2nNZ;;AsEloNQ;EAOI,8BAAA;AtE+nNZ;;AsEtoNQ;EAOI,iCAAA;AtEmoNZ;;AsE1oNQ;EAOI,sCAAA;AtEuoNZ;;AsE9oNQ;EAOI,yCAAA;AtE2oNZ;;AsElpNQ;EAOI,uBAAA;AtE+oNZ;;AsEtpNQ;EAOI,uBAAA;AtEmpNZ;;AsE1pNQ;EAOI,yBAAA;AtEupNZ;;AsE9pNQ;EAOI,yBAAA;AtE2pNZ;;AsElqNQ;EAOI,0BAAA;AtE+pNZ;;AsEtqNQ;EAOI,4BAAA;AtEmqNZ;;AsE1qNQ;EAOI,kCAAA;AtEuqNZ;;AsE9qNQ;EAOI,iBAAA;AtE2qNZ;;AsElrNQ;EAOI,uBAAA;AtE+qNZ;;AsEtrNQ;EAOI,sBAAA;AtEmrNZ;;AsE1rNQ;EAOI,oBAAA;AtEurNZ;;AsE9rNQ;EAOI,sBAAA;AtE2rNZ;;AsElsNQ;EAOI,oBAAA;AtE+rNZ;;AsEtsNQ;EAOI,sCAAA;AtEmsNZ;;AsE1sNQ;EAOI,oCAAA;AtEusNZ;;AsE9sNQ;EAOI,kCAAA;AtE2sNZ;;AsEltNQ;EAOI,yCAAA;AtE+sNZ;;AsEttNQ;EAOI,wCAAA;AtEmtNZ;;AsE1tNQ;EAOI,wCAAA;AtEutNZ;;AsE9tNQ;EAOI,kCAAA;AtE2tNZ;;AsEluNQ;EAOI,gCAAA;AtE+tNZ;;AsEtuNQ;EAOI,8BAAA;AtEmuNZ;;AsE1uNQ;EAOI,gCAAA;AtEuuNZ;;AsE9uNQ;EAOI,+BAAA;AtE2uNZ;;AsElvNQ;EAOI,oCAAA;AtE+uNZ;;AsEtvNQ;EAOI,kCAAA;AtEmvNZ;;AsE1vNQ;EAOI,gCAAA;AtEuvNZ;;AsE9vNQ;EAOI,uCAAA;AtE2vNZ;;AsElwNQ;EAOI,sCAAA;AtE+vNZ;;AsEtwNQ;EAOI,iCAAA;AtEmwNZ;;AsE1wNQ;EAOI,2BAAA;AtEuwNZ;;AsE9wNQ;EAOI,iCAAA;AtE2wNZ;;AsElxNQ;EAOI,+BAAA;AtE+wNZ;;AsEtxNQ;EAOI,6BAAA;AtEmxNZ;;AsE1xNQ;EAOI,+BAAA;AtEuxNZ;;AsE9xNQ;EAOI,8BAAA;AtE2xNZ;;AsElyNQ;EAOI,oBAAA;AtE+xNZ;;AsEtyNQ;EAOI,mBAAA;AtEmyNZ;;AsE1yNQ;EAOI,mBAAA;AtEuyNZ;;AsE9yNQ;EAOI,mBAAA;AtE2yNZ;;AsElzNQ;EAOI,mBAAA;AtE+yNZ;;AsEtzNQ;EAOI,mBAAA;AtEmzNZ;;AsE1zNQ;EAOI,mBAAA;AtEuzNZ;;AsE9zNQ;EAOI,mBAAA;AtE2zNZ;;AsEl0NQ;EAOI,oBAAA;AtE+zNZ;;AsEt0NQ;EAOI,0BAAA;AtEm0NZ;;AsE10NQ;EAOI,yBAAA;AtEu0NZ;;AsE90NQ;EAOI,uBAAA;AtE20NZ;;AsEl1NQ;EAOI,yBAAA;AtE+0NZ;;AsEt1NQ;EAOI,uBAAA;AtEm1NZ;;AsE11N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OI,uBAAA;EtEooPV;;EsE3oPM;IAOI,uBAAA;EtEwoPV;;EsE/oPM;IAOI,yBAAA;EtE4oPV;;EsEnpPM;IAOI,yBAAA;EtEgpPV;;EsEvpPM;IAOI,0BAAA;EtEopPV;;EsE3pPM;IAOI,4BAAA;EtEwpPV;;EsE/pPM;IAOI,kCAAA;EtE4pPV;;EsEnqPM;IAOI,iBAAA;EtEgqPV;;EsEvqPM;IAOI,uBAAA;EtEoqPV;;EsE3qPM;IAOI,sBAAA;EtEwqPV;;EsE/qPM;IAOI,oBAAA;EtE4qPV;;EsEnrPM;IAOI,sBAAA;EtEgrPV;;EsEvrPM;IAOI,oBAAA;EtEorPV;;EsE3rPM;IAOI,sCAAA;EtEwrPV;;EsE/rPM;IAOI,oCAAA;EtE4rPV;;EsEnsPM;IAOI,kCAAA;EtEgsPV;;EsEvsPM;IAOI,yCAAA;EtEosPV;;EsE3sPM;IAOI,wCAAA;EtEwsPV;;EsE/sPM;IAOI,wCAAA;EtE4sPV;;EsEntPM;IAOI,kCAAA;EtEgtPV;;EsEvtPM;IAOI,gCAAA;EtEotPV;;EsE3tPM;IAOI,8BAAA;EtEwtPV;;EsE/tPM;IAOI,gCAAA;EtE4tPV;;EsEnuPM;IAOI,+BAAA;EtEguPV;;EsEvuPM;IAOI,oCAAA;EtEouPV;;EsE3uPM;IAOI,kCAAA;EtEwuPV;;EsE/uPM;IAOI,gCAAA;EtE4uPV;;EsEnvPM;IAOI,uCAAA;EtEgvPV;;EsEvvPM;IAOI,sCAAA;EtEovPV;;EsE3vPM;IAOI,iCAAA;EtEwvPV;;EsE/vPM;IAOI,2BAAA;EtE4vPV;;EsEnwPM;IAOI,iCAAA;EtEgwPV;;EsEvwPM;IAOI,+BAAA;EtEowPV;;EsE3wPM;IAOI,6BAAA;EtEwwPV;;EsE/wPM;IAOI,+BAAA;EtE4wPV;;EsEnxPM;IAOI,8BAAA;EtEgxPV;;EsEvxPM;IAOI,oBAAA;EtEoxPV;;EsE3xPM;IAOI,mBAAA;EtEwxPV;;EsE/xPM;IAOI,mBAAA;EtE4xPV;;EsEnyPM;IAOI,mBAAA;EtEgyPV;;EsEvyPM;IAOI,mBAAA;EtEoyPV;;EsE3yPM;IAOI,mBAAA;EtEwyPV;;EsE/yPM;IAOI,mBAAA;EtE4yPV;;EsEnzP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tEsnQV;;EsE7nQM;IAOI,gCAAA;EtE0nQV;;EsEjoQM;IAOI,8BAAA;EtE8nQV;;EsEroQM;IAOI,gCAAA;EtEkoQV;;EsEzoQM;IAOI,8BAAA;EtEsoQV;;EsE7oQM;IAOI,4BAAA;EtE0oQV;;EsEjpQM;IAOI,kCAAA;EtE8oQV;;EsErpQM;IAOI,iCAAA;EtEkpQV;;EsEzpQM;IAOI,+BAAA;EtEspQV;;EsE7pQM;IAOI,iCAAA;EtE0pQV;;EsEjqQM;IAOI,+BAAA;EtE8pQV;;EsErqQM;IAOI,0BAAA;EtEkqQV;;EsEzqQM;IAOI,gCAAA;EtEsqQV;;EsE7qQM;IAOI,+BAAA;EtE0qQV;;EsEjrQM;IAOI,6BAAA;EtE8qQV;;EsErrQM;IAOI,+BAAA;EtEkrQV;;EsEzrQM;IAOI,6BAAA;EtEsrQV;;EsE7rQM;IAOI,2BAAA;EtE0rQV;;EsEjsQM;IAOI,4BAAA;EtE8rQV;;EsErsQM;IAOI,6BAAA;EtEksQV;AACF;Aa1sQI;EyDAI;IAOI,sBAAA;EtEusQV;;EsE9sQM;IAOI,uBAAA;EtE2sQV;;EsEltQM;IAOI,sBAAA;EtE+sQV;;EsEttQM;IAOI,0BAAA;EtEmtQV;;EsE1tQM;IAOI,gCAAA;EtEutQV;;EsE9tQM;IAOI,yBAAA;EtE2tQV;;EsEluQM;IAOI,wBAAA;EtE+tQV;;EsEtuQM;IAOI,yBAAA;EtEmuQV;;EsE1uQM;IAOI,6BAAA;EtEuuQV;;EsE9uQM;IAOI,8BAAA;EtE2uQV;;EsElvQM;IAOI,wBAAA;EtE+uQV;;EsEtvQM;IAOI,+BAAA;EtEmvQV;;EsE1vQM;IAOI,wBAAA;EtEuvQV;;EsE9vQM;IAOI,yBAAA;EtE2vQV;;EsElwQM;IAOI,8BAAA;EtE+vQV;;EsEtwQM;IAOI,iCAAA;EtEmwQV;;EsE1wQM;IAOI,sCAAA;EtEuwQV;;EsE9wQM;IAOI,yCAAA;EtE2wQV;;EsElxQM;IAOI,uBAAA;EtE+wQV;;EsEtxQM;IAOI,uBAAA;EtEmxQV;;EsE1xQM;IAOI,yBAAA;EtEuxQV;;EsE9xQM;IAOI,yBAAA;EtE2xQV;;EsElyQM;IAOI,0BAAA;EtE+xQV;;EsEtyQM;IAOI,4BAAA;EtEmyQV;;EsE1yQM;IAOI,kCAAA;EtEuyQV;;EsE9yQM;IAOI,iBAAA;EtE2yQV;;EsElzQM;IAOI,uBAAA;EtE+yQV;;EsEtzQM;IAOI,sBAAA;EtEmzQV;;EsE1zQM;IAOI,oBAAA;EtEuzQV;;EsE9zQM;IAOI,sBAAA;EtE2zQV;;EsEl0QM;IAOI,oBAAA;EtE+zQV;;EsEt0QM;IAOI,sCAAA;EtEm0QV;;EsE10QM;IAOI,oCAAA;EtEu0QV;;EsE90QM;IAOI,kCAAA;EtE20QV;;EsEl1QM;IAOI,yCAAA;EtE+0QV;;EsEt1QM;IAOI,wCAAA;EtEm1QV;;EsE11QM;IAOI,wCAAA;EtEu1QV;;EsE91QM;IAOI,kCAAA;EtE21QV;;EsEl2QM;IAOI,gCAAA;EtE+1QV;;EsEt2QM;IAOI,8BAAA;EtEm2QV;;EsE12QM;IAOI,gCAAA;EtEu2QV;;EsE92QM;IAOI,+BAAA;EtE22QV;;EsEl3QM;IAOI,oCAAA;EtE+2QV;;EsEt3QM;IAOI,kCAAA;EtEm3QV;;EsE13QM;IAOI,gCAAA;EtEu3QV;;EsE93QM;IAOI,uCAAA;EtE23QV;;EsEl4QM;IAOI,sCAAA;EtE+3QV;;EsEt4QM;IAOI,iCAAA;EtEm4QV;;EsE14QM;IAOI,2BAAA;EtEu4QV;;EsE94QM;IAOI,iCAAA;EtE24QV;;EsEl5QM;IAOI,+BAAA;EtE+4QV;;EsEt5QM;IAOI,6BAAA;EtEm5QV;;EsE15QM;IAOI,+BAAA;EtEu5QV;;EsE95QM;IAOI,8BAAA;EtE25QV;;EsEl6QM;IAOI,oBAAA;EtE+5QV;;EsEt6QM;IAOI,mBAAA;EtEm6QV;;EsE16QM;IAOI,mBAAA;EtEu6QV;;EsE96QM;IAOI,mBAAA;EtE26QV;;EsEl7QM;IAOI,mBAAA;EtE+6QV;;EsEt7QM;IAOI,mBAAA;EtEm7QV;;EsE17QM;IAOI,mBAAA;EtEu7QV;;EsE97QM;IAOI,mBAAA;EtE27QV;;EsEl8QM;IAOI,oBAAA;EtE+7QV;;EsEt8QM;IAOI,0BAAA;EtEm8QV;;EsE18QM;IAOI,yBAAA;EtEu8QV;;EsE98QM;IAOI,uBAAA;EtE28QV;;EsE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A;EtEqxRV;;EsE5xRM;IAOI,kCAAA;EtEyxRV;;EsEhyRM;IAOI,iCAAA;EtE6xRV;;EsEpyRM;IAOI,+BAAA;EtEiyRV;;EsExyRM;IAOI,iCAAA;EtEqyRV;;EsE5yRM;IAOI,+BAAA;EtEyyRV;;EsEhzRM;IAOI,0BAAA;EtE6yRV;;EsEpzRM;IAOI,gCAAA;EtEizRV;;EsExzRM;IAOI,+BAAA;EtEqzRV;;EsE5zRM;IAOI,6BAAA;EtEyzRV;;EsEh0RM;IAOI,+BAAA;EtE6zRV;;EsEp0RM;IAOI,6BAAA;EtEi0RV;;EsEx0RM;IAOI,2BAAA;EtEq0RV;;EsE50RM;IAOI,4BAAA;EtEy0RV;;EsEh1RM;IAOI,6BAAA;EtE60RV;AACF;Aar1RI;EyDAI;IAOI,sBAAA;EtEk1RV;;EsEz1RM;IAOI,uBAAA;EtEs1RV;;EsE71RM;IAOI,sBAAA;EtE01RV;;EsEj2RM;IAOI,0BAAA;EtE81RV;;EsEr2RM;IAOI,gCAAA;EtEk2RV;;EsEz2RM;IAOI,yBAAA;EtEs2RV;;EsE72RM;IAOI,wBAAA;EtE02RV;;EsEj3RM;IAOI,yBAAA;EtE82RV;;EsEr3RM;IAOI,6BAAA;EtEk3RV;;EsEz3RM;IAOI,8BAAA;EtEs3RV;;EsE73RM;IAOI,wBAAA;EtE03RV;;EsEj4RM;IAOI,+BAAA;EtE83RV;;EsEr4RM;IAOI,wBAAA;EtEk4RV;;EsEz4RM;IAOI,yBAAA;EtEs4RV;;EsE74RM;IAOI,8BAAA;EtE04RV;;EsEj5RM;IAOI,iCAAA;EtE84RV;;EsEr5RM;IAOI,sCAAA;EtEk5RV;;EsEz5RM;IAOI,yCAAA;EtEs5RV;;EsE75RM;IAOI,uBAAA;EtE05RV;;EsEj6RM;IAOI,uBAAA;EtE85RV;;EsEr6RM;IAOI,yBAAA;EtEk6RV;;EsEz6RM;IAOI,yBAAA;EtEs6RV;;EsE76RM;IAOI,0BAAA;EtE06RV;;EsEj7RM;IAOI,4BAAA;EtE86RV;;EsEr7RM;IAOI,kCAAA;EtEk7RV;;EsEz7RM;IAOI,iBAAA;EtEs7RV;;EsE77RM;IAOI,uBAAA;EtE07RV;;EsEj8RM;IAOI,sBAAA;EtE87RV;;EsEr8RM;IAOI,oBAAA;EtEk8RV;;EsEz8RM;IAOI,sBAAA;EtEs8RV;;EsE78RM;IAOI,oBAAA;EtE08RV;;EsEj9RM;IAOI,sCAAA;EtE88RV;;EsEr9RM;IAOI,oCAAA;EtEk9RV;;EsEz9RM;IAOI,kCAAA;EtEs9RV;;EsE79RM;IAOI,yCAAA;EtE09RV;;EsEj+RM;IAOI,wCAAA;EtE89RV;;EsEr+RM;IAOI,wCAAA;EtEk+RV;;EsEz+RM;IAOI,kCAAA;EtEs+RV;;EsE7+RM;IAOI,gCAAA;EtE0+RV;;EsEj/RM;IAOI,8BAAA;EtE8+RV;;EsEr/RM;IAOI,gCAAA;EtEk/RV;;EsEz/RM;IAOI,+BAAA;EtEs/RV;;EsE7/RM;IAOI,oCAAA;EtE0/RV;;EsEjgSM;IAOI,kCAAA;EtE8/RV;;EsErgSM;IAOI,gCAAA;EtEkgSV;;EsEzgSM;IAOI,uCAAA;EtEsgSV;;EsE7gSM;IAOI,sCAAA;EtE0gSV;;EsEjhSM;IAOI,iCAAA;EtE8gSV;;EsErhSM;IAOI,2BAAA;EtEkhSV;;EsEzhSM;IAOI,iCAAA;EtEshSV;;EsE7hSM;IAOI,+BAAA;EtE0hSV;;EsEjiSM;IAOI,6BAAA;EtE8hSV;;EsEriSM;IAOI,+BAAA;EtEkiSV;;EsEziSM;IAOI,8BAAA;EtEsiSV;;EsE7iSM;IAOI,oBAAA;EtE0iSV;;EsEjjSM;IAOI,mBAAA;EtE8iSV;;EsErjSM;IAOI,mBAAA;EtEkjSV;;EsEzjSM;IAOI,mBAAA;EtEsjSV;;EsE7jSM;IAOI,mBAAA;EtE0jSV;;EsEjkSM;IAOI,mBAAA;EtE8jSV;;EsErkSM;IAOI,mBAAA;EtEkkSV;;EsEzkSM;IAOI,mBAAA;EtEskSV;;EsE7kSM;IAOI,oBAAA;EtE0kSV;;EsEjlSM;IAOI,0BAAA;EtE8kSV;;EsErlSM;IAOI,yBAAA;EtEklSV;;EsEzlSM;IAOI,uBAAA;EtEslSV;;EsE7lSM;IAOI,yBAAA;EtE0lSV;;EsEjmSM;IAOI,uBAAA;EtE8lSV;;EsErmSM;IAOI,uBAAA;EtEkmSV;;EsEzmSM;IAOI,0BAAA;IAAA,yBAAA;EtEumSV;;EsE9mSM;IAOI,gCAAA;IAAA,+BAAA;EtE4mSV;;EsEnnSM;IAOI,+BAAA;IAAA,8BAAA;EtEinSV;;EsExnSM;IAOI,6BAAA;IAAA,4BAAA;EtEsnSV;;EsE7nSM;IAOI,+BAAA;IAAA,8BAAA;EtE2nSV;;EsEloSM;IAOI,6BAAA;IAAA,4BAAA;EtEgoSV;;EsEvoSM;IAOI,6BAAA;IAAA,4BAAA;EtEqoSV;;EsE5oSM;IAOI,wBAAA;IAAA,2BAAA;EtE0oSV;;EsEjpSM;IAOI,8BAAA;IAAA,iCAAA;EtE+oSV;;EsEtpSM;IAOI,6BAAA;IAAA,gCAAA;EtEopSV;;EsE3pSM;IAOI,2BAAA;IAAA,8BAAA;EtEypSV;;EsEhqSM;IAOI,6BAAA;IAAA,gCAAA;EtE8pSV;;EsErqSM;IAOI,2BAAA;IAAA,8BAAA;EtEmqSV;;EsE1qSM;IAOI,2BAAA;IAAA,8BAAA;EtEwqSV;;EsE/qSM;IAOI,wBAAA;EtE4qSV;;EsEnrSM;IAOI,8BAAA;EtEgrSV;;EsEvrSM;IAOI,6BAAA;EtEorSV;;EsE3rSM;IAOI,2BAAA;EtEwrSV;;EsE/rSM;IAOI,6BAAA;EtE4rSV;;EsEnsSM;IAOI,2BAAA;EtEgsSV;;EsEvsSM;IAOI,2BAAA;EtEosSV;;EsE3sSM;IAOI,0BAAA;EtEwsSV;;EsE/sSM;IAOI,gCAAA;EtE4sSV;;EsEntSM;IAOI,+BAAA;EtEgtSV;;EsEvtSM;IAOI,6BAAA;EtEotSV;;EsE3tSM;IAOI,+BAAA;EtEwtSV;;EsE/tSM;IAOI,6BAAA;EtE4tSV;;EsEnuSM;IAOI,6BAAA;EtEguSV;;EsEvuSM;IAOI,2BAAA;EtEouSV;;EsE3uSM;IAOI,iCAAA;EtEwuSV;;EsE/uSM;IAOI,gCAAA;EtE4uSV;;EsEnvSM;IAOI,8BAAA;EtEgvSV;;EsEvvSM;IAOI,gCAAA;EtEovSV;;EsE3vSM;IAOI,8BAAA;EtEwvSV;;EsE/vSM;IAOI,8BAAA;EtE4vSV;;EsEnwSM;IAOI,yBAAA;EtEgwSV;;EsEvwSM;IAOI,+BAAA;EtEowSV;;EsE3wSM;IAOI,8BAAA;EtEwwSV;;EsE/wSM;IAOI,4BAAA;EtE4wSV;;EsEnxSM;IAOI,8BAAA;EtEgxSV;;EsEvxSM;IAOI,4BAAA;EtEoxSV;;EsE3xSM;IAOI,4BAAA;EtEwxSV;;EsE/xSM;IAOI,qBAAA;EtE4xSV;;EsEnySM;IAOI,2BAAA;EtEgySV;;EsEvySM;IAOI,0BAAA;EtEoySV;;EsE3ySM;IAOI,wBAAA;EtEwySV;;EsE/ySM;IAOI,0BAAA;EtE4ySV;;EsEnzSM;IAOI,wBAAA;EtEgzSV;;EsEvzSM;IAOI,2BAAA;IAAA,0BAAA;EtEqzSV;;EsE5zSM;IAOI,iCAAA;IAAA,gCAAA;EtE0zSV;;EsEj0SM;IAOI,gCAAA;IAAA,+BAAA;EtE+zSV;;EsEt0SM;IAOI,8BAAA;IAAA,6BAAA;EtEo0SV;;EsE30SM;IAOI,gCAAA;IAAA,+BAAA;EtEy0SV;;EsEh1SM;IAOI,8BAAA;IAAA,6BAAA;EtE80SV;;EsEr1SM;IAOI,yBAAA;IAAA,4BAAA;EtEm1SV;;EsE11SM;IAOI,+BAAA;IAAA,kCAAA;EtEw1SV;;EsE/1SM;IAOI,8BAAA;IAAA,iCAAA;EtE61SV;;EsEp2SM;IAOI,4BAAA;IAAA,+BAAA;EtEk2SV;;EsEz2SM;IAOI,8BAAA;IAAA,iCAAA;EtEu2SV;;EsE92SM;IAOI,4BAAA;IAAA,+BAAA;EtE42SV;;EsEn3SM;IAOI,yBAAA;EtEg3SV;;EsEv3SM;IAOI,+BAAA;EtEo3SV;;EsE33SM;IAOI,8BAAA;EtEw3SV;;EsE/3SM;IAOI,4BAAA;EtE43SV;;EsEn4SM;IAOI,8BAAA;EtEg4SV;;EsEv4SM;IAOI,4BAAA;EtEo4SV;;EsE34SM;IAOI,2BAAA;EtEw4SV;;EsE/4SM;IAOI,iCAAA;EtE44SV;;EsEn5SM;IAOI,gCAAA;EtEg5SV;;EsEv5SM;IAOI,8BAAA;EtEo5SV;;EsE35SM;IAOI,gCAAA;EtEw5SV;;EsE/5SM;IAOI,8BAAA;EtE45SV;;EsEn6SM;IAOI,4BAAA;EtEg6SV;;EsEv6SM;IAOI,kCAAA;EtEo6SV;;EsE36SM;IAOI,iCAAA;EtEw6SV;;EsE/6SM;IAOI,+BAAA;EtE46SV;;EsEn7SM;IAOI,iCAAA;EtEg7SV;;EsEv7SM;IAOI,+BAAA;EtEo7SV;;EsE37SM;IAOI,0BAAA;EtEw7SV;;EsE/7SM;IAOI,gCAAA;EtE47SV;;EsEn8SM;IAOI,+BAAA;EtEg8SV;;EsEv8SM;IAOI,6BAAA;EtEo8SV;;EsE38SM;IAOI,+BAAA;EtEw8SV;;EsE/8SM;IAOI,6BAAA;EtE48SV;;EsEn9SM;IAOI,2BAAA;EtEg9SV;;EsEv9SM;IAOI,4BAAA;EtEo9SV;;EsE39SM;IAOI,6BAAA;EtEw9SV;AACF;Aah+SI;EyDAI;IAOI,sBAAA;EtE69SV;;EsEp+SM;IAOI,uBAAA;EtEi+SV;;EsEx+SM;IAOI,sBAAA;EtEq+SV;;EsE5+SM;IAOI,0BAAA;EtEy+SV;;EsEh/SM;IAOI,gCAAA;EtE6+SV;;EsEp/SM;IAOI,yBAAA;EtEi/SV;;EsEx/SM;IAOI,wBAAA;EtEq/SV;;EsE5/SM;IAOI,yBAAA;EtEy/SV;;EsEhgTM;IAOI,6BAAA;EtE6/SV;;EsEpgTM;IAOI,8BAAA;EtEigTV;;EsExgTM;IAOI,wBAAA;EtEqgTV;;EsE5gTM;IAOI,+BAAA;EtEygTV;;EsEhhTM;IAOI,wBAAA;EtE6gTV;;EsEphTM;IAOI,yBAAA;EtEihTV;;EsExhTM;IAOI,8BAAA;EtEqhTV;;EsE5hTM;IAOI,iCAAA;EtEyhTV;;EsEhiTM;IAOI,sCAAA;EtE6hTV;;EsEpiTM;IAOI,yCAAA;EtEiiTV;;EsExiTM;IAOI,uBAAA;EtEqiTV;;EsE5iTM;IAOI,uBAAA;EtEyiTV;;EsEhjTM;IAOI,yBAAA;EtE6iTV;;EsEpjTM;IAOI,yBAAA;EtEijTV;;EsExjTM;IAOI,0BAAA;EtEqjTV;;EsE5jTM;IAOI,4BAAA;EtEyjTV;;EsEhkTM;IAOI,kCAAA;EtE6jTV;;EsEpkTM;IAOI,iBAAA;EtEikTV;;EsExkTM;IAOI,uBAAA;EtEqkTV;;EsE5kTM;IAOI,sBAAA;EtEykTV;;EsEhlTM;IAOI,oBAAA;EtE6kTV;;EsEplTM;IAOI,sBAAA;EtEilTV;;EsExlTM;IAOI,oBAAA;EtEqlTV;;EsE5lTM;IAOI,sCAAA;EtEylTV;;EsEhmTM;IAOI,oCAAA;EtE6lTV;;E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I,+BAAA;EtEmlUV;;EsE1lUM;IAOI,6BAAA;EtEulUV;;EsE9lUM;IAOI,2BAAA;EtE2lUV;;EsElmUM;IAOI,4BAAA;EtE+lUV;;EsEtmUM;IAOI,6BAAA;EtEmmUV;AACF;Aa3mUI;EyDAI;IAOI,sBAAA;EtEwmUV;;EsE/mUM;IAOI,uBAAA;EtE4mUV;;EsEnnUM;IAOI,sBAAA;EtEgnUV;;EsEvnUM;IAOI,0BAAA;EtEonUV;;EsE3nUM;IAOI,gCAAA;EtEwnUV;;EsE/nUM;IAOI,yBAAA;EtE4nUV;;EsEnoUM;IAOI,wBAAA;EtEgoUV;;EsEvoUM;IAOI,yBAAA;EtEooUV;;EsE3oUM;IAOI,6BAAA;EtEwoUV;;EsE/oUM;IAOI,8BAAA;EtE4oUV;;EsEnpUM;IAOI,wBAAA;EtEgpUV;;EsEvpUM;IAOI,+BAAA;EtEopUV;;EsE3pUM;IAOI,wBAAA;EtEwpUV;;EsE/pUM;IAOI,yBAAA;EtE4pUV;;EsEnqUM;IAOI,8BAAA;EtEgqUV;;EsEvqUM;IAOI,iCAAA;EtEoqUV;;EsE3qUM;IAOI,sCAAA;EtEwqUV;;EsE/qUM;IAOI,yCAAA;EtE4qUV;;EsEnrUM;IAOI,uBAAA;EtEgrUV;;EsEvrUM;IAOI,uBAAA;EtEorUV;;EsE3rUM;IAOI,yBAAA;EtEwrUV;;EsE/rUM;IAOI,yBAAA;EtE4rUV;;EsEnsUM;IAOI,0BAAA;EtEgsUV;;EsEvsUM;IAOI,4BAAA;EtEosUV;;EsE3sUM;IAOI,kCAAA;EtEwsUV;;EsE/sUM;IAOI,iBAAA;EtE4sUV;;EsEntUM;IAOI,uBAAA;EtEgtUV;;EsEvtUM;IAOI,sBAAA;EtEotUV;;EsE3tUM;IAOI,oBAAA;EtEwtUV;;EsE/tUM;IAOI,sBAAA;EtE4tUV;;EsEnuUM;IAOI,oBAAA;EtEguUV;;EsEvuUM;IAOI,sCAAA;EtEouUV;;EsE3uUM;IAOI,oCAAA;EtEwuUV;;EsE/uUM;IAOI,kCAAA;EtE4uUV;;EsEnvUM;IAOI,yCAAA;EtEgvUV;;EsEvvUM;IAOI,wCAAA;EtEovUV;;EsE3vUM;IAOI,wCAAA;EtEwvUV;;EsE/vUM;IAOI,kCAAA;EtE4vUV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","file":"bootstrap.css","sourcesContent":["@charset \"UTF-8\";\n/*!\n * Bootstrap v5.1.3 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n:root {\n --bs-blue: #0d6efd;\n --bs-indigo: #6610f2;\n --bs-purple: #6f42c1;\n --bs-pink: #d63384;\n --bs-red: #dc3545;\n --bs-orange: #fd7e14;\n --bs-yellow: #ffc107;\n --bs-green: #198754;\n --bs-teal: #20c997;\n --bs-cyan: #0dcaf0;\n --bs-white: #fff;\n --bs-gray: #6c757d;\n --bs-gray-dark: #343a40;\n --bs-gray-100: #f8f9fa;\n --bs-gray-200: #e9ecef;\n --bs-gray-300: #dee2e6;\n --bs-gray-400: #ced4da;\n --bs-gray-500: #adb5bd;\n --bs-gray-600: #6c757d;\n --bs-gray-700: #495057;\n --bs-gray-800: #343a40;\n --bs-gray-900: #212529;\n --bs-primary: #0d6efd;\n --bs-secondary: #6c757d;\n --bs-success: #198754;\n --bs-info: #0dcaf0;\n --bs-warning: #ffc107;\n --bs-danger: #dc3545;\n --bs-light: #f8f9fa;\n --bs-dark: #212529;\n --bs-primary-rgb: 13, 110, 253;\n --bs-secondary-rgb: 108, 117, 125;\n --bs-success-rgb: 25, 135, 84;\n --bs-info-rgb: 13, 202, 240;\n --bs-warning-rgb: 255, 193, 7;\n --bs-danger-rgb: 220, 53, 69;\n --bs-light-rgb: 248, 249, 250;\n --bs-dark-rgb: 33, 37, 41;\n --bs-white-rgb: 255, 255, 255;\n --bs-black-rgb: 0, 0, 0;\n --bs-body-color-rgb: 33, 37, 41;\n --bs-body-bg-rgb: 255, 255, 255;\n --bs-font-sans-serif: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", \"Liberation Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));\n --bs-body-font-family: var(--bs-font-sans-serif);\n --bs-body-font-size: 1rem;\n --bs-body-font-weight: 400;\n --bs-body-line-height: 1.5;\n --bs-body-color: #212529;\n --bs-body-bg: #fff;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n :root {\n scroll-behavior: smooth;\n }\n}\n\nbody {\n margin: 0;\n font-family: var(--bs-body-font-family);\n font-size: var(--bs-body-font-size);\n font-weight: var(--bs-body-font-weight);\n line-height: var(--bs-body-line-height);\n color: var(--bs-body-color);\n text-align: var(--bs-body-text-align);\n background-color: var(--bs-body-bg);\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\nhr {\n margin: 1rem 0;\n color: inherit;\n background-color: currentColor;\n border: 0;\n opacity: 0.25;\n}\n\nhr:not([size]) {\n height: 1px;\n}\n\nh6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: calc(1.375rem + 1.5vw);\n}\n@media (min-width: 1200px) {\n h1, .h1 {\n font-size: 2.5rem;\n }\n}\n\nh2, .h2 {\n font-size: calc(1.325rem + 0.9vw);\n}\n@media (min-width: 1200px) {\n h2, .h2 {\n font-size: 2rem;\n }\n}\n\nh3, .h3 {\n font-size: calc(1.3rem + 0.6vw);\n}\n@media (min-width: 1200px) {\n h3, .h3 {\n font-size: 1.75rem;\n }\n}\n\nh4, .h4 {\n font-size: calc(1.275rem + 0.3vw);\n}\n@media (min-width: 1200px) {\n h4, .h4 {\n font-size: 1.5rem;\n }\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-bs-original-title] {\n text-decoration: underline dotted;\n cursor: help;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: 0.5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall, .small {\n font-size: 0.875em;\n}\n\nmark, .mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 0.75em;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\na {\n color: #0d6efd;\n text-decoration: underline;\n}\na:hover {\n color: #0a58ca;\n}\n\na:not([href]):not([class]), a:not([href]):not([class]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: var(--bs-font-monospace);\n font-size: 1em;\n direction: ltr /* rtl:ignore */;\n unicode-bidi: bidi-override;\n}\n\npre {\n display: block;\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n font-size: 0.875em;\n}\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\ncode {\n font-size: 0.875em;\n color: #d63384;\n word-wrap: break-word;\n}\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 0.875em;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\nkbd kbd {\n padding: 0;\n font-size: 1em;\n font-weight: 700;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n color: #6c757d;\n text-align: left;\n}\n\nth {\n text-align: inherit;\n text-align: -webkit-match-parent;\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\nlabel {\n display: inline-block;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=button] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\nselect:disabled {\n opacity: 1;\n}\n\n[list]::-webkit-calendar-picker-indicator {\n display: none;\n}\n\nbutton,\n[type=button],\n[type=reset],\n[type=submit] {\n -webkit-appearance: button;\n}\nbutton:not(:disabled),\n[type=button]:not(:disabled),\n[type=reset]:not(:disabled),\n[type=submit]:not(:disabled) {\n cursor: pointer;\n}\n\n::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ntextarea {\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n float: left;\n width: 100%;\n padding: 0;\n margin-bottom: 0.5rem;\n font-size: calc(1.275rem + 0.3vw);\n line-height: inherit;\n}\n@media (min-width: 1200px) {\n legend {\n font-size: 1.5rem;\n }\n}\nlegend + * {\n clear: left;\n}\n\n::-webkit-datetime-edit-fields-wrapper,\n::-webkit-datetime-edit-text,\n::-webkit-datetime-edit-minute,\n::-webkit-datetime-edit-hour-field,\n::-webkit-datetime-edit-day-field,\n::-webkit-datetime-edit-month-field,\n::-webkit-datetime-edit-year-field {\n padding: 0;\n}\n\n::-webkit-inner-spin-button {\n height: auto;\n}\n\n[type=search] {\n outline-offset: -2px;\n -webkit-appearance: textfield;\n}\n\n/* rtl:raw:\n[type=\"tel\"],\n[type=\"url\"],\n[type=\"email\"],\n[type=\"number\"] {\n direction: ltr;\n}\n*/\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-color-swatch-wrapper {\n padding: 0;\n}\n\n::file-selector-button {\n font: inherit;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\niframe {\n border: 0;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[hidden] {\n display: none !important;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: calc(1.625rem + 4.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-1 {\n font-size: 5rem;\n }\n}\n\n.display-2 {\n font-size: calc(1.575rem + 3.9vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-2 {\n font-size: 4.5rem;\n }\n}\n\n.display-3 {\n font-size: calc(1.525rem + 3.3vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-3 {\n font-size: 4rem;\n }\n}\n\n.display-4 {\n font-size: calc(1.475rem + 2.7vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-4 {\n font-size: 3.5rem;\n }\n}\n\n.display-5 {\n font-size: calc(1.425rem + 2.1vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-5 {\n font-size: 3rem;\n }\n}\n\n.display-6 {\n font-size: calc(1.375rem + 1.5vw);\n font-weight: 300;\n line-height: 1.2;\n}\n@media (min-width: 1200px) {\n .display-6 {\n font-size: 2.5rem;\n }\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 0.875em;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n.blockquote > :last-child {\n margin-bottom: 0;\n}\n\n.blockquote-footer {\n margin-top: -1rem;\n margin-bottom: 1rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n.blockquote-footer::before {\n content: \"— \";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 0.875em;\n color: #6c757d;\n}\n\n.container,\n.container-fluid,\n.container-xxl,\n.container-xl,\n.container-lg,\n.container-md,\n.container-sm {\n width: 100%;\n padding-right: var(--bs-gutter-x, 0.75rem);\n padding-left: var(--bs-gutter-x, 0.75rem);\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container-sm, .container {\n max-width: 540px;\n }\n}\n@media (min-width: 768px) {\n .container-md, .container-sm, .container {\n max-width: 720px;\n }\n}\n@media (min-width: 992px) {\n .container-lg, .container-md, .container-sm, .container {\n max-width: 960px;\n }\n}\n@media (min-width: 1200px) {\n .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1140px;\n }\n}\n@media (min-width: 1400px) {\n .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {\n max-width: 1320px;\n }\n}\n.row {\n --bs-gutter-x: 1.5rem;\n --bs-gutter-y: 0;\n display: flex;\n flex-wrap: wrap;\n margin-top: calc(-1 * var(--bs-gutter-y));\n margin-right: calc(-0.5 * var(--bs-gutter-x));\n margin-left: calc(-0.5 * var(--bs-gutter-x));\n}\n.row > * {\n flex-shrink: 0;\n width: 100%;\n max-width: 100%;\n padding-right: calc(var(--bs-gutter-x) * 0.5);\n padding-left: calc(var(--bs-gutter-x) * 0.5);\n margin-top: var(--bs-gutter-y);\n}\n\n.col {\n flex: 1 0 0%;\n}\n\n.row-cols-auto > * {\n flex: 0 0 auto;\n width: auto;\n}\n\n.row-cols-1 > * {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 auto;\n width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n}\n\n.col-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n}\n\n.col-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n}\n\n.col-3 {\n flex: 0 0 auto;\n width: 25%;\n}\n\n.col-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n}\n\n.col-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n}\n\n.col-6 {\n flex: 0 0 auto;\n width: 50%;\n}\n\n.col-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n}\n\n.col-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n}\n\n.col-9 {\n flex: 0 0 auto;\n width: 75%;\n}\n\n.col-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n}\n\n.col-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n}\n\n.col-12 {\n flex: 0 0 auto;\n width: 100%;\n}\n\n.offset-1 {\n margin-left: 8.33333333%;\n}\n\n.offset-2 {\n margin-left: 16.66666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.33333333%;\n}\n\n.offset-5 {\n margin-left: 41.66666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.33333333%;\n}\n\n.offset-8 {\n margin-left: 66.66666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.33333333%;\n}\n\n.offset-11 {\n margin-left: 91.66666667%;\n}\n\n.g-0,\n.gx-0 {\n --bs-gutter-x: 0;\n}\n\n.g-0,\n.gy-0 {\n --bs-gutter-y: 0;\n}\n\n.g-1,\n.gx-1 {\n --bs-gutter-x: 0.25rem;\n}\n\n.g-1,\n.gy-1 {\n --bs-gutter-y: 0.25rem;\n}\n\n.g-2,\n.gx-2 {\n --bs-gutter-x: 0.5rem;\n}\n\n.g-2,\n.gy-2 {\n --bs-gutter-y: 0.5rem;\n}\n\n.g-3,\n.gx-3 {\n --bs-gutter-x: 1rem;\n}\n\n.g-3,\n.gy-3 {\n --bs-gutter-y: 1rem;\n}\n\n.g-4,\n.gx-4 {\n --bs-gutter-x: 1.5rem;\n}\n\n.g-4,\n.gy-4 {\n --bs-gutter-y: 1.5rem;\n}\n\n.g-5,\n.gx-5 {\n --bs-gutter-x: 3rem;\n}\n\n.g-5,\n.gy-5 {\n --bs-gutter-y: 3rem;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex: 1 0 0%;\n }\n\n .row-cols-sm-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-sm-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-sm-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-sm-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-sm-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-sm-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-sm-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-sm-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-sm-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-sm-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-sm-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-sm-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-sm-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-sm-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-sm-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-sm-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-sm-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-sm-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-sm-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-sm-0 {\n margin-left: 0;\n }\n\n .offset-sm-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-sm-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-sm-3 {\n margin-left: 25%;\n }\n\n .offset-sm-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-sm-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-sm-6 {\n margin-left: 50%;\n }\n\n .offset-sm-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-sm-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-sm-9 {\n margin-left: 75%;\n }\n\n .offset-sm-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-sm-11 {\n margin-left: 91.66666667%;\n }\n\n .g-sm-0,\n.gx-sm-0 {\n --bs-gutter-x: 0;\n }\n\n .g-sm-0,\n.gy-sm-0 {\n --bs-gutter-y: 0;\n }\n\n .g-sm-1,\n.gx-sm-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-sm-1,\n.gy-sm-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-sm-2,\n.gx-sm-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-sm-2,\n.gy-sm-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-sm-3,\n.gx-sm-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-sm-3,\n.gy-sm-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-sm-4,\n.gx-sm-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-sm-4,\n.gy-sm-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-sm-5,\n.gx-sm-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-sm-5,\n.gy-sm-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 768px) {\n .col-md {\n flex: 1 0 0%;\n }\n\n .row-cols-md-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-md-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-md-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-md-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-md-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-md-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-md-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-md-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-md-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-md-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-md-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-md-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-md-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-md-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-md-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-md-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-md-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-md-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-md-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-md-0 {\n margin-left: 0;\n }\n\n .offset-md-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-md-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-md-3 {\n margin-left: 25%;\n }\n\n .offset-md-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-md-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-md-6 {\n margin-left: 50%;\n }\n\n .offset-md-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-md-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-md-9 {\n margin-left: 75%;\n }\n\n .offset-md-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-md-11 {\n margin-left: 91.66666667%;\n }\n\n .g-md-0,\n.gx-md-0 {\n --bs-gutter-x: 0;\n }\n\n .g-md-0,\n.gy-md-0 {\n --bs-gutter-y: 0;\n }\n\n .g-md-1,\n.gx-md-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-md-1,\n.gy-md-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-md-2,\n.gx-md-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-md-2,\n.gy-md-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-md-3,\n.gx-md-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-md-3,\n.gy-md-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-md-4,\n.gx-md-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-md-4,\n.gy-md-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-md-5,\n.gx-md-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-md-5,\n.gy-md-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 992px) {\n .col-lg {\n flex: 1 0 0%;\n }\n\n .row-cols-lg-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-lg-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-lg-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-lg-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-lg-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-lg-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-lg-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-lg-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-lg-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-lg-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-lg-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-lg-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-lg-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-lg-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-lg-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-lg-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-lg-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-lg-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-lg-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-lg-0 {\n margin-left: 0;\n }\n\n .offset-lg-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-lg-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-lg-3 {\n margin-left: 25%;\n }\n\n .offset-lg-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-lg-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-lg-6 {\n margin-left: 50%;\n }\n\n .offset-lg-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-lg-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-lg-9 {\n margin-left: 75%;\n }\n\n .offset-lg-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-lg-11 {\n margin-left: 91.66666667%;\n }\n\n .g-lg-0,\n.gx-lg-0 {\n --bs-gutter-x: 0;\n }\n\n .g-lg-0,\n.gy-lg-0 {\n --bs-gutter-y: 0;\n }\n\n .g-lg-1,\n.gx-lg-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-lg-1,\n.gy-lg-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-lg-2,\n.gx-lg-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-lg-2,\n.gy-lg-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-lg-3,\n.gx-lg-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-lg-3,\n.gy-lg-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-lg-4,\n.gx-lg-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-lg-4,\n.gy-lg-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-lg-5,\n.gx-lg-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-lg-5,\n.gy-lg-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1200px) {\n .col-xl {\n flex: 1 0 0%;\n }\n\n .row-cols-xl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xl-0 {\n margin-left: 0;\n }\n\n .offset-xl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xl-3 {\n margin-left: 25%;\n }\n\n .offset-xl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xl-6 {\n margin-left: 50%;\n }\n\n .offset-xl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xl-9 {\n margin-left: 75%;\n }\n\n .offset-xl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xl-0,\n.gx-xl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xl-0,\n.gy-xl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xl-1,\n.gx-xl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xl-1,\n.gy-xl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xl-2,\n.gx-xl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xl-2,\n.gy-xl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xl-3,\n.gx-xl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xl-3,\n.gy-xl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xl-4,\n.gx-xl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xl-4,\n.gy-xl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xl-5,\n.gx-xl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xl-5,\n.gy-xl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n@media (min-width: 1400px) {\n .col-xxl {\n flex: 1 0 0%;\n }\n\n .row-cols-xxl-auto > * {\n flex: 0 0 auto;\n width: auto;\n }\n\n .row-cols-xxl-1 > * {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .row-cols-xxl-2 > * {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .row-cols-xxl-3 > * {\n flex: 0 0 auto;\n width: 33.3333333333%;\n }\n\n .row-cols-xxl-4 > * {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .row-cols-xxl-5 > * {\n flex: 0 0 auto;\n width: 20%;\n }\n\n .row-cols-xxl-6 > * {\n flex: 0 0 auto;\n width: 16.6666666667%;\n }\n\n .col-xxl-auto {\n flex: 0 0 auto;\n width: auto;\n }\n\n .col-xxl-1 {\n flex: 0 0 auto;\n width: 8.33333333%;\n }\n\n .col-xxl-2 {\n flex: 0 0 auto;\n width: 16.66666667%;\n }\n\n .col-xxl-3 {\n flex: 0 0 auto;\n width: 25%;\n }\n\n .col-xxl-4 {\n flex: 0 0 auto;\n width: 33.33333333%;\n }\n\n .col-xxl-5 {\n flex: 0 0 auto;\n width: 41.66666667%;\n }\n\n .col-xxl-6 {\n flex: 0 0 auto;\n width: 50%;\n }\n\n .col-xxl-7 {\n flex: 0 0 auto;\n width: 58.33333333%;\n }\n\n .col-xxl-8 {\n flex: 0 0 auto;\n width: 66.66666667%;\n }\n\n .col-xxl-9 {\n flex: 0 0 auto;\n width: 75%;\n }\n\n .col-xxl-10 {\n flex: 0 0 auto;\n width: 83.33333333%;\n }\n\n .col-xxl-11 {\n flex: 0 0 auto;\n width: 91.66666667%;\n }\n\n .col-xxl-12 {\n flex: 0 0 auto;\n width: 100%;\n }\n\n .offset-xxl-0 {\n margin-left: 0;\n }\n\n .offset-xxl-1 {\n margin-left: 8.33333333%;\n }\n\n .offset-xxl-2 {\n margin-left: 16.66666667%;\n }\n\n .offset-xxl-3 {\n margin-left: 25%;\n }\n\n .offset-xxl-4 {\n margin-left: 33.33333333%;\n }\n\n .offset-xxl-5 {\n margin-left: 41.66666667%;\n }\n\n .offset-xxl-6 {\n margin-left: 50%;\n }\n\n .offset-xxl-7 {\n margin-left: 58.33333333%;\n }\n\n .offset-xxl-8 {\n margin-left: 66.66666667%;\n }\n\n .offset-xxl-9 {\n margin-left: 75%;\n }\n\n .offset-xxl-10 {\n margin-left: 83.33333333%;\n }\n\n .offset-xxl-11 {\n margin-left: 91.66666667%;\n }\n\n .g-xxl-0,\n.gx-xxl-0 {\n --bs-gutter-x: 0;\n }\n\n .g-xxl-0,\n.gy-xxl-0 {\n --bs-gutter-y: 0;\n }\n\n .g-xxl-1,\n.gx-xxl-1 {\n --bs-gutter-x: 0.25rem;\n }\n\n .g-xxl-1,\n.gy-xxl-1 {\n --bs-gutter-y: 0.25rem;\n }\n\n .g-xxl-2,\n.gx-xxl-2 {\n --bs-gutter-x: 0.5rem;\n }\n\n .g-xxl-2,\n.gy-xxl-2 {\n --bs-gutter-y: 0.5rem;\n }\n\n .g-xxl-3,\n.gx-xxl-3 {\n --bs-gutter-x: 1rem;\n }\n\n .g-xxl-3,\n.gy-xxl-3 {\n --bs-gutter-y: 1rem;\n }\n\n .g-xxl-4,\n.gx-xxl-4 {\n --bs-gutter-x: 1.5rem;\n }\n\n .g-xxl-4,\n.gy-xxl-4 {\n --bs-gutter-y: 1.5rem;\n }\n\n .g-xxl-5,\n.gx-xxl-5 {\n --bs-gutter-x: 3rem;\n }\n\n .g-xxl-5,\n.gy-xxl-5 {\n --bs-gutter-y: 3rem;\n }\n}\n.table {\n --bs-table-bg: transparent;\n --bs-table-accent-bg: transparent;\n --bs-table-striped-color: #212529;\n --bs-table-striped-bg: rgba(0, 0, 0, 0.05);\n --bs-table-active-color: #212529;\n --bs-table-active-bg: rgba(0, 0, 0, 0.1);\n --bs-table-hover-color: #212529;\n --bs-table-hover-bg: rgba(0, 0, 0, 0.075);\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n vertical-align: top;\n border-color: #dee2e6;\n}\n.table > :not(caption) > * > * {\n padding: 0.5rem 0.5rem;\n background-color: var(--bs-table-bg);\n border-bottom-width: 1px;\n box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);\n}\n.table > tbody {\n vertical-align: inherit;\n}\n.table > thead {\n vertical-align: bottom;\n}\n.table > :not(:first-child) {\n border-top: 2px solid currentColor;\n}\n\n.caption-top {\n caption-side: top;\n}\n\n.table-sm > :not(caption) > * > * {\n padding: 0.25rem 0.25rem;\n}\n\n.table-bordered > :not(caption) > * {\n border-width: 1px 0;\n}\n.table-bordered > :not(caption) > * > * {\n border-width: 0 1px;\n}\n\n.table-borderless > :not(caption) > * > * {\n border-bottom-width: 0;\n}\n.table-borderless > :not(:first-child) {\n border-top-width: 0;\n}\n\n.table-striped > tbody > tr:nth-of-type(odd) > * {\n --bs-table-accent-bg: var(--bs-table-striped-bg);\n color: var(--bs-table-striped-color);\n}\n\n.table-active {\n --bs-table-accent-bg: var(--bs-table-active-bg);\n color: var(--bs-table-active-color);\n}\n\n.table-hover > tbody > tr:hover > * {\n --bs-table-accent-bg: var(--bs-table-hover-bg);\n color: var(--bs-table-hover-color);\n}\n\n.table-primary {\n --bs-table-bg: #cfe2ff;\n --bs-table-striped-bg: #c5d7f2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bacbe6;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfd1ec;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #bacbe6;\n}\n\n.table-secondary {\n --bs-table-bg: #e2e3e5;\n --bs-table-striped-bg: #d7d8da;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #cbccce;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #d1d2d4;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #cbccce;\n}\n\n.table-success {\n --bs-table-bg: #d1e7dd;\n --bs-table-striped-bg: #c7dbd2;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #bcd0c7;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #c1d6cc;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #bcd0c7;\n}\n\n.table-info {\n --bs-table-bg: #cff4fc;\n --bs-table-striped-bg: #c5e8ef;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #badce3;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #bfe2e9;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #badce3;\n}\n\n.table-warning {\n --bs-table-bg: #fff3cd;\n --bs-table-striped-bg: #f2e7c3;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #e6dbb9;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #ece1be;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #e6dbb9;\n}\n\n.table-danger {\n --bs-table-bg: #f8d7da;\n --bs-table-striped-bg: #eccccf;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfc2c4;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5c7ca;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #dfc2c4;\n}\n\n.table-light {\n --bs-table-bg: #f8f9fa;\n --bs-table-striped-bg: #ecedee;\n --bs-table-striped-color: #000;\n --bs-table-active-bg: #dfe0e1;\n --bs-table-active-color: #000;\n --bs-table-hover-bg: #e5e6e7;\n --bs-table-hover-color: #000;\n color: #000;\n border-color: #dfe0e1;\n}\n\n.table-dark {\n --bs-table-bg: #212529;\n --bs-table-striped-bg: #2c3034;\n --bs-table-striped-color: #fff;\n --bs-table-active-bg: #373b3e;\n --bs-table-active-color: #fff;\n --bs-table-hover-bg: #323539;\n --bs-table-hover-color: #fff;\n color: #fff;\n border-color: #373b3e;\n}\n\n.table-responsive {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 767.98px) {\n .table-responsive-md {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n@media (max-width: 1399.98px) {\n .table-responsive-xxl {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n}\n.form-label {\n margin-bottom: 0.5rem;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n}\n\n.form-text {\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #6c757d;\n}\n\n.form-control {\n display: block;\n width: 100%;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n appearance: none;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n.form-control[type=file] {\n overflow: hidden;\n}\n.form-control[type=file]:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control:focus {\n color: #212529;\n background-color: #fff;\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-control::-webkit-date-and-time-value {\n height: 1.5em;\n}\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n.form-control::file-selector-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n margin-inline-end: 0.75rem;\n color: #212529;\n background-color: #e9ecef;\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: 1px;\n border-radius: 0;\n 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;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::file-selector-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::file-selector-button {\n background-color: #dde0e3;\n}\n.form-control::-webkit-file-upload-button {\n padding: 0.375rem 0.75rem;\n margin: -0.375rem -0.75rem;\n margin-inline-end: 0.75rem;\n color: #212529;\n background-color: #e9ecef;\n pointer-events: none;\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n border-inline-end-width: 1px;\n border-radius: 0;\n 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;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-control::-webkit-file-upload-button {\n transition: none;\n }\n}\n.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {\n background-color: #dde0e3;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n min-height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n.form-control-sm::file-selector-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n margin-inline-end: 0.5rem;\n}\n.form-control-sm::-webkit-file-upload-button {\n padding: 0.25rem 0.5rem;\n margin: -0.25rem -0.5rem;\n margin-inline-end: 0.5rem;\n}\n\n.form-control-lg {\n min-height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n.form-control-lg::file-selector-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n margin-inline-end: 1rem;\n}\n.form-control-lg::-webkit-file-upload-button {\n padding: 0.5rem 1rem;\n margin: -0.5rem -1rem;\n margin-inline-end: 1rem;\n}\n\ntextarea.form-control {\n min-height: calc(1.5em + 0.75rem + 2px);\n}\ntextarea.form-control-sm {\n min-height: calc(1.5em + 0.5rem + 2px);\n}\ntextarea.form-control-lg {\n min-height: calc(1.5em + 1rem + 2px);\n}\n\n.form-control-color {\n width: 3rem;\n height: auto;\n padding: 0.375rem;\n}\n.form-control-color:not(:disabled):not([readonly]) {\n cursor: pointer;\n}\n.form-control-color::-moz-color-swatch {\n height: 1.5em;\n border-radius: 0.25rem;\n}\n.form-control-color::-webkit-color-swatch {\n height: 1.5em;\n border-radius: 0.25rem;\n}\n\n.form-select {\n display: block;\n width: 100%;\n padding: 0.375rem 2.25rem 0.375rem 0.75rem;\n -moz-padding-start: calc(0.75rem - 3px);\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n background-color: #fff;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right 0.75rem center;\n background-size: 16px 12px;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-select {\n transition: none;\n }\n}\n.form-select:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-select[multiple], .form-select[size]:not([size=\"1\"]) {\n padding-right: 0.75rem;\n background-image: none;\n}\n.form-select:disabled {\n background-color: #e9ecef;\n}\n.form-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #212529;\n}\n\n.form-select-sm {\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.form-select-lg {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.form-check {\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5em;\n margin-bottom: 0.125rem;\n}\n.form-check .form-check-input {\n float: left;\n margin-left: -1.5em;\n}\n\n.form-check-input {\n width: 1em;\n height: 1em;\n margin-top: 0.25em;\n vertical-align: top;\n background-color: #fff;\n background-repeat: no-repeat;\n background-position: center;\n background-size: contain;\n border: 1px solid rgba(0, 0, 0, 0.25);\n appearance: none;\n color-adjust: exact;\n}\n.form-check-input[type=checkbox] {\n border-radius: 0.25em;\n}\n.form-check-input[type=radio] {\n border-radius: 50%;\n}\n.form-check-input:active {\n filter: brightness(90%);\n}\n.form-check-input:focus {\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-check-input:checked {\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.form-check-input:checked[type=checkbox] {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e\");\n}\n.form-check-input:checked[type=radio] {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e\");\n}\n.form-check-input[type=checkbox]:indeterminate {\n background-color: #0d6efd;\n border-color: #0d6efd;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e\");\n}\n.form-check-input:disabled {\n pointer-events: none;\n filter: none;\n opacity: 0.5;\n}\n.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {\n opacity: 0.5;\n}\n\n.form-switch {\n padding-left: 2.5em;\n}\n.form-switch .form-check-input {\n width: 2em;\n margin-left: -2.5em;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e\");\n background-position: left center;\n border-radius: 2em;\n transition: background-position 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-switch .form-check-input {\n transition: none;\n }\n}\n.form-switch .form-check-input:focus {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e\");\n}\n.form-switch .form-check-input:checked {\n background-position: right center;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e\");\n}\n\n.form-check-inline {\n display: inline-block;\n margin-right: 1rem;\n}\n\n.btn-check {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.btn-check[disabled] + .btn, .btn-check:disabled + .btn {\n pointer-events: none;\n filter: none;\n opacity: 0.65;\n}\n\n.form-range {\n width: 100%;\n height: 1.5rem;\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n.form-range:focus {\n outline: 0;\n}\n.form-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.form-range::-moz-focus-outer {\n border: 0;\n}\n.form-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n.form-range::-webkit-slider-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #0d6efd;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-range::-moz-range-thumb {\n transition: none;\n }\n}\n.form-range::-moz-range-thumb:active {\n background-color: #b6d4fe;\n}\n.form-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n.form-range:disabled {\n pointer-events: none;\n}\n.form-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n.form-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.form-floating {\n position: relative;\n}\n.form-floating > .form-control,\n.form-floating > .form-select {\n height: calc(3.5rem + 2px);\n line-height: 1.25;\n}\n.form-floating > label {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n padding: 1rem 0.75rem;\n pointer-events: none;\n border: 1px solid transparent;\n transform-origin: 0 0;\n transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .form-floating > label {\n transition: none;\n }\n}\n.form-floating > .form-control {\n padding: 1rem 0.75rem;\n}\n.form-floating > .form-control::placeholder {\n color: transparent;\n}\n.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:-webkit-autofill {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-select {\n padding-top: 1.625rem;\n padding-bottom: 0.625rem;\n}\n.form-floating > .form-control:focus ~ label,\n.form-floating > .form-control:not(:placeholder-shown) ~ label,\n.form-floating > .form-select ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n.form-floating > .form-control:-webkit-autofill ~ label {\n opacity: 0.65;\n transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n.input-group > .form-control,\n.input-group > .form-select {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n}\n.input-group > .form-control:focus,\n.input-group > .form-select:focus {\n z-index: 3;\n}\n.input-group .btn {\n position: relative;\n z-index: 2;\n}\n.input-group .btn:focus {\n z-index: 3;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .form-select,\n.input-group-lg > .input-group-text,\n.input-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .form-select,\n.input-group-sm > .input-group-text,\n.input-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .form-select,\n.input-group-sm > .form-select {\n padding-right: 3rem;\n}\n\n.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu),\n.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu),\n.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {\n margin-left: -1px;\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #198754;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: rgba(25, 135, 84, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #198754;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' 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\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #198754;\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:valid, .form-select.is-valid {\n border-color: #198754;\n}\n.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size=\"1\"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size=\"1\"] {\n padding-right: 4.125rem;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\"), url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' 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\");\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:valid:focus, .form-select.is-valid:focus {\n border-color: #198754;\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n\n.was-validated .form-check-input:valid, .form-check-input.is-valid {\n border-color: #198754;\n}\n.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {\n background-color: #198754;\n}\n.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);\n}\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #198754;\n}\n\n.form-check-inline .form-check-input ~ .valid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,\n.was-validated .input-group .form-select:valid,\n.input-group .form-select.is-valid {\n z-index: 1;\n}\n.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus,\n.was-validated .input-group .form-select:valid:focus,\n.input-group .form-select.is-valid:focus {\n z-index: 3;\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 0.875em;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: 0.1rem;\n font-size: 0.875rem;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%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\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .form-select:invalid, .form-select.is-invalid {\n border-color: #dc3545;\n}\n.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size=\"1\"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size=\"1\"] {\n padding-right: 4.125rem;\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e\"), url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%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\");\n background-position: right 0.75rem center, center right 2.25rem;\n background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid, .form-check-input.is-invalid {\n border-color: #dc3545;\n}\n.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {\n background-color: #dc3545;\n}\n.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);\n}\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.form-check-inline .form-check-input ~ .invalid-feedback {\n margin-left: 0.5em;\n}\n\n.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,\n.was-validated .input-group .form-select:invalid,\n.input-group .form-select.is-invalid {\n z-index: 2;\n}\n.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus,\n.was-validated .input-group .form-select:invalid:focus,\n.input-group .form-select.is-invalid:focus {\n z-index: 3;\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: center;\n text-decoration: none;\n vertical-align: middle;\n cursor: pointer;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n border-radius: 0.25rem;\n 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;\n}\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n.btn:hover {\n color: #212529;\n}\n.btn-check:focus + .btn, .btn:focus {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n.btn:disabled, .btn.disabled, fieldset:disabled .btn {\n pointer-events: none;\n opacity: 0.65;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #0b5ed7;\n border-color: #0a58ca;\n}\n.btn-check:focus + .btn-primary, .btn-primary:focus {\n color: #fff;\n background-color: #0b5ed7;\n border-color: #0a58ca;\n box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);\n}\n.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0a58ca;\n border-color: #0a53be;\n}\n.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);\n}\n.btn-primary:disabled, .btn-primary.disabled {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-secondary:hover {\n color: #fff;\n background-color: #5c636a;\n border-color: #565e64;\n}\n.btn-check:focus + .btn-secondary, .btn-secondary:focus {\n color: #fff;\n background-color: #5c636a;\n border-color: #565e64;\n box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);\n}\n.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #565e64;\n border-color: #51585e;\n}\n.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);\n}\n.btn-secondary:disabled, .btn-secondary.disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-success {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #157347;\n border-color: #146c43;\n}\n.btn-check:focus + .btn-success, .btn-success:focus {\n color: #fff;\n background-color: #157347;\n border-color: #146c43;\n box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);\n}\n.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #146c43;\n border-color: #13653f;\n}\n.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);\n}\n.btn-success:disabled, .btn-success.disabled {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n\n.btn-info {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-info:hover {\n color: #000;\n background-color: #31d2f2;\n border-color: #25cff2;\n}\n.btn-check:focus + .btn-info, .btn-info:focus {\n color: #000;\n background-color: #31d2f2;\n border-color: #25cff2;\n box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);\n}\n.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle {\n color: #000;\n background-color: #3dd5f3;\n border-color: #25cff2;\n}\n.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);\n}\n.btn-info:disabled, .btn-info.disabled {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n\n.btn-warning {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-warning:hover {\n color: #000;\n background-color: #ffca2c;\n border-color: #ffc720;\n}\n.btn-check:focus + .btn-warning, .btn-warning:focus {\n color: #000;\n background-color: #ffca2c;\n border-color: #ffc720;\n box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);\n}\n.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle {\n color: #000;\n background-color: #ffcd39;\n border-color: #ffc720;\n}\n.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);\n}\n.btn-warning:disabled, .btn-warning.disabled {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #bb2d3b;\n border-color: #b02a37;\n}\n.btn-check:focus + .btn-danger, .btn-danger:focus {\n color: #fff;\n background-color: #bb2d3b;\n border-color: #b02a37;\n box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);\n}\n.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #b02a37;\n border-color: #a52834;\n}\n.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);\n}\n.btn-danger:disabled, .btn-danger.disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-light {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-light:hover {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n}\n.btn-check:focus + .btn-light, .btn-light:focus {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);\n}\n.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle {\n color: #000;\n background-color: #f9fafb;\n border-color: #f9fafb;\n}\n.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);\n}\n.btn-light:disabled, .btn-light.disabled {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-dark {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-dark:hover {\n color: #fff;\n background-color: #1c1f23;\n border-color: #1a1e21;\n}\n.btn-check:focus + .btn-dark, .btn-dark:focus {\n color: #fff;\n background-color: #1c1f23;\n border-color: #1a1e21;\n box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);\n}\n.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1a1e21;\n border-color: #191c1f;\n}\n.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);\n}\n.btn-dark:disabled, .btn-dark.disabled {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n\n.btn-outline-primary {\n color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);\n}\n.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show {\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);\n}\n.btn-outline-primary:disabled, .btn-outline-primary.disabled {\n color: #0d6efd;\n background-color: transparent;\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus {\n box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);\n}\n.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);\n}\n.btn-outline-secondary:disabled, .btn-outline-secondary.disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-success {\n color: #198754;\n border-color: #198754;\n}\n.btn-outline-success:hover {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-check:focus + .btn-outline-success, .btn-outline-success:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);\n}\n.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show {\n color: #fff;\n background-color: #198754;\n border-color: #198754;\n}\n.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);\n}\n.btn-outline-success:disabled, .btn-outline-success.disabled {\n color: #198754;\n background-color: transparent;\n}\n\n.btn-outline-info {\n color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-outline-info:hover {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-check:focus + .btn-outline-info, .btn-outline-info:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);\n}\n.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show {\n color: #000;\n background-color: #0dcaf0;\n border-color: #0dcaf0;\n}\n.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);\n}\n.btn-outline-info:disabled, .btn-outline-info.disabled {\n color: #0dcaf0;\n background-color: transparent;\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n.btn-outline-warning:hover {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus {\n box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);\n}\n.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show {\n color: #000;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);\n}\n.btn-outline-warning:disabled, .btn-outline-warning.disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);\n}\n.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);\n}\n.btn-outline-danger:disabled, .btn-outline-danger.disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-outline-light:hover {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-check:focus + .btn-outline-light, .btn-outline-light:focus {\n box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);\n}\n.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show {\n color: #000;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);\n}\n.btn-outline-light:disabled, .btn-outline-light.disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-dark {\n color: #212529;\n border-color: #212529;\n}\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus {\n box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);\n}\n.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show {\n color: #fff;\n background-color: #212529;\n border-color: #212529;\n}\n.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus {\n box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);\n}\n.btn-outline-dark:disabled, .btn-outline-dark.disabled {\n color: #212529;\n background-color: transparent;\n}\n\n.btn-link {\n font-weight: 400;\n color: #0d6efd;\n text-decoration: underline;\n}\n.btn-link:hover {\n color: #0a58ca;\n}\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n border-radius: 0.2rem;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n.collapsing.collapse-horizontal {\n width: 0;\n height: auto;\n transition: width 0.35s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .collapsing.collapse-horizontal {\n transition: none;\n }\n}\n\n.dropup,\n.dropend,\n.dropdown,\n.dropstart {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n z-index: 1000;\n display: none;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n.dropdown-menu[data-bs-popper] {\n top: 100%;\n left: 0;\n margin-top: 0.125rem;\n}\n\n.dropdown-menu-start {\n --bs-position: start;\n}\n.dropdown-menu-start[data-bs-popper] {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-end {\n --bs-position: end;\n}\n.dropdown-menu-end[data-bs-popper] {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-start {\n --bs-position: start;\n }\n .dropdown-menu-sm-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-sm-end {\n --bs-position: end;\n }\n .dropdown-menu-sm-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 768px) {\n .dropdown-menu-md-start {\n --bs-position: start;\n }\n .dropdown-menu-md-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-md-end {\n --bs-position: end;\n }\n .dropdown-menu-md-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 992px) {\n .dropdown-menu-lg-start {\n --bs-position: start;\n }\n .dropdown-menu-lg-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-lg-end {\n --bs-position: end;\n }\n .dropdown-menu-lg-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1200px) {\n .dropdown-menu-xl-start {\n --bs-position: start;\n }\n .dropdown-menu-xl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-xl-end {\n --bs-position: end;\n }\n .dropdown-menu-xl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n@media (min-width: 1400px) {\n .dropdown-menu-xxl-start {\n --bs-position: start;\n }\n .dropdown-menu-xxl-start[data-bs-popper] {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu-xxl-end {\n --bs-position: end;\n }\n .dropdown-menu-xxl-end[data-bs-popper] {\n right: 0;\n left: auto;\n }\n}\n.dropup .dropdown-menu[data-bs-popper] {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropend .dropdown-menu[data-bs-popper] {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n.dropend .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n.dropend .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropend .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropstart .dropdown-menu[data-bs-popper] {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n.dropstart .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n.dropstart .dropdown-toggle::after {\n display: none;\n}\n.dropstart .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n.dropstart .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid rgba(0, 0, 0, 0.15);\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n text-decoration: none;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n.dropdown-item:hover, .dropdown-item:focus {\n color: #1e2125;\n background-color: #e9ecef;\n}\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #0d6efd;\n}\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #adb5bd;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1rem;\n color: #212529;\n}\n\n.dropdown-menu-dark {\n color: #dee2e6;\n background-color: #343a40;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.dropdown-menu-dark .dropdown-item {\n color: #dee2e6;\n}\n.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.15);\n}\n.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active {\n color: #fff;\n background-color: #0d6efd;\n}\n.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled {\n color: #adb5bd;\n}\n.dropdown-menu-dark .dropdown-divider {\n border-color: rgba(0, 0, 0, 0.15);\n}\n.dropdown-menu-dark .dropdown-item-text {\n color: #dee2e6;\n}\n.dropdown-menu-dark .dropdown-header {\n color: #adb5bd;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n.btn-group > .btn-check:checked + .btn,\n.btn-group > .btn-check:focus + .btn,\n.btn-group > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn-check:checked + .btn,\n.btn-group-vertical > .btn-check:focus + .btn,\n.btn-group-vertical > .btn:hover,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:nth-child(n+3),\n.btn-group > :not(.btn-check) + .btn,\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {\n margin-left: 0;\n}\n.dropstart .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn ~ .btn,\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n color: #0d6efd;\n text-decoration: none;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .nav-link {\n transition: none;\n }\n}\n.nav-link:hover, .nav-link:focus {\n color: #0a58ca;\n}\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n.nav-tabs .nav-link {\n margin-bottom: -1px;\n background: none;\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n isolation: isolate;\n}\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n background: none;\n border: 0;\n border-radius: 0.25rem;\n}\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #0d6efd;\n}\n\n.nav-fill > .nav-link,\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified > .nav-link,\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.nav-fill .nav-item .nav-link,\n.nav-justified .nav-item .nav-link {\n width: 100%;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n.navbar > .container,\n.navbar > .container-fluid,\n.navbar > .container-sm,\n.navbar > .container-md,\n.navbar > .container-lg,\n.navbar > .container-xl,\n.navbar > .container-xxl {\n display: flex;\n flex-wrap: inherit;\n align-items: center;\n justify-content: space-between;\n}\n.navbar-brand {\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n text-decoration: none;\n white-space: nowrap;\n}\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n.navbar-nav .dropdown-menu {\n position: static;\n}\n\n.navbar-text {\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n transition: box-shadow 0.15s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .navbar-toggler {\n transition: none;\n }\n}\n.navbar-toggler:hover {\n text-decoration: none;\n}\n.navbar-toggler:focus {\n text-decoration: none;\n outline: 0;\n box-shadow: 0 0 0 0.25rem;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 100%;\n}\n\n.navbar-nav-scroll {\n max-height: var(--bs-scroll-height, 75vh);\n overflow-y: auto;\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n .navbar-expand-sm .offcanvas-header {\n display: none;\n }\n .navbar-expand-sm .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-sm .offcanvas-top,\n.navbar-expand-sm .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-sm .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n .navbar-expand-md .offcanvas-header {\n display: none;\n }\n .navbar-expand-md .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-md .offcanvas-top,\n.navbar-expand-md .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-md .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n .navbar-expand-lg .offcanvas-header {\n display: none;\n }\n .navbar-expand-lg .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-lg .offcanvas-top,\n.navbar-expand-lg .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-lg .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xl .offcanvas-header {\n display: none;\n }\n .navbar-expand-xl .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-xl .offcanvas-top,\n.navbar-expand-xl .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-xl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n@media (min-width: 1400px) {\n .navbar-expand-xxl {\n flex-wrap: nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xxl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xxl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xxl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xxl .navbar-nav-scroll {\n overflow: visible;\n }\n .navbar-expand-xxl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xxl .navbar-toggler {\n display: none;\n }\n .navbar-expand-xxl .offcanvas-header {\n display: none;\n }\n .navbar-expand-xxl .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n }\n .navbar-expand-xxl .offcanvas-top,\n.navbar-expand-xxl .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n }\n .navbar-expand-xxl .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n }\n}\n.navbar-expand {\n flex-wrap: nowrap;\n justify-content: flex-start;\n}\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n.navbar-expand .navbar-nav-scroll {\n overflow: visible;\n}\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n.navbar-expand .navbar-toggler {\n display: none;\n}\n.navbar-expand .offcanvas-header {\n display: none;\n}\n.navbar-expand .offcanvas {\n position: inherit;\n bottom: 0;\n z-index: 1000;\n flex-grow: 1;\n visibility: visible !important;\n background-color: transparent;\n border-right: 0;\n border-left: 0;\n transition: none;\n transform: none;\n}\n.navbar-expand .offcanvas-top,\n.navbar-expand .offcanvas-bottom {\n height: auto;\n border-top: 0;\n border-bottom: 0;\n}\n.navbar-expand .offcanvas-body {\n display: flex;\n flex-grow: 0;\n padding: 0;\n overflow-y: visible;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.55);\n}\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.55);\n border-color: rgba(0, 0, 0, 0.1);\n}\n.navbar-light .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.55);\n}\n.navbar-light .navbar-text a,\n.navbar-light .navbar-text a:hover,\n.navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.55);\n}\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.55);\n border-color: rgba(255, 255, 255, 0.1);\n}\n.navbar-dark .navbar-toggler-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\");\n}\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.55);\n}\n.navbar-dark .navbar-text a,\n.navbar-dark .navbar-text a:hover,\n.navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n.card > .card-header + .list-group,\n.card > .list-group + .card-footer {\n border-top: 0;\n}\n\n.card-body {\n flex: 1 1 auto;\n padding: 1rem 1rem;\n}\n\n.card-title {\n margin-bottom: 0.5rem;\n}\n\n.card-subtitle {\n margin-top: -0.25rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link + .card-link {\n margin-left: 1rem;\n}\n\n.card-header {\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-footer {\n padding: 0.5rem 1rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.5rem;\n margin-bottom: -0.5rem;\n margin-left: -0.5rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.5rem;\n margin-left: -0.5rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1rem;\n border-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-group > .card {\n margin-bottom: 0.75rem;\n}\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n.card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n.card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n.card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n.card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.accordion-button {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n padding: 1rem 1.25rem;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n background-color: #fff;\n border: 0;\n border-radius: 0;\n overflow-anchor: none;\n 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, border-radius 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button {\n transition: none;\n }\n}\n.accordion-button:not(.collapsed) {\n color: #0c63e4;\n background-color: #e7f1ff;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);\n}\n.accordion-button:not(.collapsed)::after {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n transform: rotate(-180deg);\n}\n.accordion-button::after {\n flex-shrink: 0;\n width: 1.25rem;\n height: 1.25rem;\n margin-left: auto;\n content: \"\";\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-size: 1.25rem;\n transition: transform 0.2s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .accordion-button::after {\n transition: none;\n }\n}\n.accordion-button:hover {\n z-index: 2;\n}\n.accordion-button:focus {\n z-index: 3;\n border-color: #86b7fe;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n\n.accordion-header {\n margin-bottom: 0;\n}\n\n.accordion-item {\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n.accordion-item:first-of-type {\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n.accordion-item:first-of-type .accordion-button {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.accordion-item:not(:first-of-type) {\n border-top: 0;\n}\n.accordion-item:last-of-type {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n.accordion-item:last-of-type .accordion-button.collapsed {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n.accordion-item:last-of-type .accordion-collapse {\n border-bottom-right-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.accordion-body {\n padding: 1rem 1.25rem;\n}\n\n.accordion-flush .accordion-collapse {\n border-width: 0;\n}\n.accordion-flush .accordion-item {\n border-right: 0;\n border-left: 0;\n border-radius: 0;\n}\n.accordion-flush .accordion-item:first-child {\n border-top: 0;\n}\n.accordion-flush .accordion-item:last-child {\n border-bottom: 0;\n}\n.accordion-flush .accordion-item .accordion-button {\n border-radius: 0;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0 0;\n margin-bottom: 1rem;\n list-style: none;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n.breadcrumb-item + .breadcrumb-item::before {\n float: left;\n padding-right: 0.5rem;\n color: #6c757d;\n content: var(--bs-breadcrumb-divider, \"/\") /* rtl: var(--bs-breadcrumb-divider, \"/\") */;\n}\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n}\n\n.page-link {\n position: relative;\n display: block;\n color: #0d6efd;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #dee2e6;\n 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;\n}\n@media (prefers-reduced-motion: reduce) {\n .page-link {\n transition: none;\n }\n}\n.page-link:hover {\n z-index: 2;\n color: #0a58ca;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n.page-link:focus {\n z-index: 3;\n color: #0a58ca;\n background-color: #e9ecef;\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n}\n\n.page-item:not(:first-child) .page-link {\n margin-left: -1px;\n}\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.page-link {\n padding: 0.375rem 0.75rem;\n}\n\n.page-item:first-child .page-link {\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n}\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n}\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.35em 0.65em;\n font-size: 0.75em;\n font-weight: 700;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n}\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.alert {\n position: relative;\n padding: 1rem 1rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 3rem;\n}\n.alert-dismissible .btn-close {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n padding: 1.25rem 1rem;\n}\n\n.alert-primary {\n color: #084298;\n background-color: #cfe2ff;\n border-color: #b6d4fe;\n}\n.alert-primary .alert-link {\n color: #06357a;\n}\n\n.alert-secondary {\n color: #41464b;\n background-color: #e2e3e5;\n border-color: #d3d6d8;\n}\n.alert-secondary .alert-link {\n color: #34383c;\n}\n\n.alert-success {\n color: #0f5132;\n background-color: #d1e7dd;\n border-color: #badbcc;\n}\n.alert-success .alert-link {\n color: #0c4128;\n}\n\n.alert-info {\n color: #055160;\n background-color: #cff4fc;\n border-color: #b6effb;\n}\n.alert-info .alert-link {\n color: #04414d;\n}\n\n.alert-warning {\n color: #664d03;\n background-color: #fff3cd;\n border-color: #ffecb5;\n}\n.alert-warning .alert-link {\n color: #523e02;\n}\n\n.alert-danger {\n color: #842029;\n background-color: #f8d7da;\n border-color: #f5c2c7;\n}\n.alert-danger .alert-link {\n color: #6a1a21;\n}\n\n.alert-light {\n color: #636464;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n.alert-light .alert-link {\n color: #4f5050;\n}\n\n.alert-dark {\n color: #141619;\n background-color: #d3d3d4;\n border-color: #bcbebf;\n}\n.alert-dark .alert-link {\n color: #101214;\n}\n\n@keyframes progress-bar-stripes {\n 0% {\n background-position-x: 1rem;\n }\n}\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #0d6efd;\n transition: width 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n 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);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: 1s linear infinite progress-bar-stripes;\n}\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: 0.25rem;\n}\n\n.list-group-numbered {\n list-style-type: none;\n counter-reset: section;\n}\n.list-group-numbered > li::before {\n content: counters(section, \".\") \". \";\n counter-increment: section;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.5rem 1rem;\n color: #212529;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #0d6efd;\n border-color: #0d6efd;\n}\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n.list-group-horizontal > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n.list-group-horizontal > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n@media (min-width: 1400px) {\n .list-group-horizontal-xxl {\n flex-direction: row;\n }\n .list-group-horizontal-xxl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xxl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n.list-group-flush {\n border-radius: 0;\n}\n.list-group-flush > .list-group-item {\n border-width: 0 0 1px;\n}\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #084298;\n background-color: #cfe2ff;\n}\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #084298;\n background-color: #bacbe6;\n}\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #084298;\n border-color: #084298;\n}\n\n.list-group-item-secondary {\n color: #41464b;\n background-color: #e2e3e5;\n}\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #41464b;\n background-color: #cbccce;\n}\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #41464b;\n border-color: #41464b;\n}\n\n.list-group-item-success {\n color: #0f5132;\n background-color: #d1e7dd;\n}\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #0f5132;\n background-color: #bcd0c7;\n}\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #0f5132;\n border-color: #0f5132;\n}\n\n.list-group-item-info {\n color: #055160;\n background-color: #cff4fc;\n}\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #055160;\n background-color: #badce3;\n}\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #055160;\n border-color: #055160;\n}\n\n.list-group-item-warning {\n color: #664d03;\n background-color: #fff3cd;\n}\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #664d03;\n background-color: #e6dbb9;\n}\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #664d03;\n border-color: #664d03;\n}\n\n.list-group-item-danger {\n color: #842029;\n background-color: #f8d7da;\n}\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #842029;\n background-color: #dfc2c4;\n}\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #842029;\n border-color: #842029;\n}\n\n.list-group-item-light {\n color: #636464;\n background-color: #fefefe;\n}\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #636464;\n background-color: #e5e5e5;\n}\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #636464;\n border-color: #636464;\n}\n\n.list-group-item-dark {\n color: #141619;\n background-color: #d3d3d4;\n}\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #141619;\n background-color: #bebebf;\n}\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #141619;\n border-color: #141619;\n}\n\n.btn-close {\n box-sizing: content-box;\n width: 1em;\n height: 1em;\n padding: 0.25em 0.25em;\n color: #000;\n background: transparent url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e\") center/1em auto no-repeat;\n border: 0;\n border-radius: 0.25rem;\n opacity: 0.5;\n}\n.btn-close:hover {\n color: #000;\n text-decoration: none;\n opacity: 0.75;\n}\n.btn-close:focus {\n outline: 0;\n box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n opacity: 1;\n}\n.btn-close:disabled, .btn-close.disabled {\n pointer-events: none;\n user-select: none;\n opacity: 0.25;\n}\n\n.btn-close-white {\n filter: invert(1) grayscale(100%) brightness(200%);\n}\n\n.toast {\n width: 350px;\n max-width: 100%;\n font-size: 0.875rem;\n pointer-events: auto;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n.toast.showing {\n opacity: 0;\n}\n.toast:not(.show) {\n display: none;\n}\n\n.toast-container {\n width: max-content;\n max-width: 100%;\n pointer-events: none;\n}\n.toast-container > :not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.5rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n.toast-header .btn-close {\n margin-right: -0.375rem;\n margin-left: 0.75rem;\n}\n\n.toast-body {\n padding: 0.75rem;\n word-wrap: break-word;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1055;\n display: none;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n.modal.show .modal-dialog {\n transform: none;\n}\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n height: calc(100% - 1rem);\n}\n.modal-dialog-scrollable .modal-content {\n max-height: 100%;\n overflow: hidden;\n}\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n}\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n.modal-header .btn-close {\n padding: 0.5rem 0.5rem;\n margin: -0.5rem -0.5rem -0.5rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n flex-shrink: 0;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n\n .modal-dialog-scrollable {\n height: calc(100% - 3.5rem);\n }\n\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n\n .modal-sm {\n max-width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg,\n.modal-xl {\n max-width: 800px;\n }\n}\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n.modal-fullscreen {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n}\n.modal-fullscreen .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n}\n.modal-fullscreen .modal-header {\n border-radius: 0;\n}\n.modal-fullscreen .modal-body {\n overflow-y: auto;\n}\n.modal-fullscreen .modal-footer {\n border-radius: 0;\n}\n\n@media (max-width: 575.98px) {\n .modal-fullscreen-sm-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-sm-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-sm-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-sm-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 767.98px) {\n .modal-fullscreen-md-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-md-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-md-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-md-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 991.98px) {\n .modal-fullscreen-lg-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-lg-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-lg-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-lg-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 1199.98px) {\n .modal-fullscreen-xl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-xl-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-xl-down .modal-footer {\n border-radius: 0;\n }\n}\n@media (max-width: 1399.98px) {\n .modal-fullscreen-xxl-down {\n width: 100vw;\n max-width: none;\n height: 100%;\n margin: 0;\n }\n .modal-fullscreen-xxl-down .modal-content {\n height: 100%;\n border: 0;\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-header {\n border-radius: 0;\n }\n .modal-fullscreen-xxl-down .modal-body {\n overflow-y: auto;\n }\n .modal-fullscreen-xxl-down .modal-footer {\n border-radius: 0;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1080;\n display: block;\n margin: 0;\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n.tooltip.show {\n opacity: 0.9;\n}\n.tooltip .tooltip-arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n.tooltip .tooltip-arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] {\n padding: 0.4rem 0;\n}\n.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {\n bottom: 0;\n}\n.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {\n top: -1px;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] {\n padding: 0 0.4rem;\n}\n.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {\n right: -1px;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] {\n padding: 0.4rem 0;\n}\n.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {\n top: 0;\n}\n.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {\n bottom: -1px;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] {\n padding: 0 0.4rem;\n}\n.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {\n left: -1px;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0 /* rtl:ignore */;\n z-index: 1070;\n display: block;\n max-width: 276px;\n font-family: var(--bs-font-sans-serif);\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n.popover .popover-arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n}\n.popover .popover-arrow::before, .popover .popover-arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {\n bottom: calc(-0.5rem - 1px);\n}\n.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n}\n.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {\n top: calc(-0.5rem - 1px);\n}\n.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f0f0f0;\n}\n\n.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n}\n.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 1rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f0f0f0;\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 1rem 1rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n/* rtl:begin:ignore */\n.carousel-item-next:not(.carousel-item-start),\n.active.carousel-item-end {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-end),\n.active.carousel-item-start {\n transform: translateX(-100%);\n}\n\n/* rtl:end:ignore */\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-start,\n.carousel-fade .carousel-item-prev.carousel-item-end {\n z-index: 1;\n opacity: 1;\n}\n.carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-start,\n.carousel-fade .active.carousel-item-end {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n padding: 0;\n color: #fff;\n text-align: center;\n background: none;\n border: 0;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n.carousel-control-next {\n transition: none;\n }\n}\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n background-repeat: no-repeat;\n background-position: 50%;\n background-size: 100% 100%;\n}\n\n/* rtl:options: {\n \"autoRename\": true,\n \"stringMap\":[ {\n \"name\" : \"prev-next\",\n \"search\" : \"prev\",\n \"replace\" : \"next\"\n } ]\n} */\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 2;\n display: flex;\n justify-content: center;\n padding: 0;\n margin-right: 15%;\n margin-bottom: 1rem;\n margin-left: 15%;\n list-style: none;\n}\n.carousel-indicators [data-bs-target] {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n padding: 0;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border: 0;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: 0.5;\n transition: opacity 0.6s ease;\n}\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators [data-bs-target] {\n transition: none;\n }\n}\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 1.25rem;\n left: 15%;\n padding-top: 1.25rem;\n padding-bottom: 1.25rem;\n color: #fff;\n text-align: center;\n}\n\n.carousel-dark .carousel-control-prev-icon,\n.carousel-dark .carousel-control-next-icon {\n filter: invert(1) grayscale(100);\n}\n.carousel-dark .carousel-indicators [data-bs-target] {\n background-color: #000;\n}\n.carousel-dark .carousel-caption {\n color: #000;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg) /* rtl:ignore */;\n }\n}\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: -0.125em;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: 0.75s linear infinite spinner-border;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: -0.125em;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: 0.75s linear infinite spinner-grow;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .spinner-border,\n.spinner-grow {\n animation-duration: 1.5s;\n }\n}\n.offcanvas {\n position: fixed;\n bottom: 0;\n z-index: 1045;\n display: flex;\n flex-direction: column;\n max-width: 100%;\n visibility: hidden;\n background-color: #fff;\n background-clip: padding-box;\n outline: 0;\n transition: transform 0.3s ease-in-out;\n}\n@media (prefers-reduced-motion: reduce) {\n .offcanvas {\n transition: none;\n }\n}\n\n.offcanvas-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n.offcanvas-backdrop.fade {\n opacity: 0;\n}\n.offcanvas-backdrop.show {\n opacity: 0.5;\n}\n\n.offcanvas-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1rem;\n}\n.offcanvas-header .btn-close {\n padding: 0.5rem 0.5rem;\n margin-top: -0.5rem;\n margin-right: -0.5rem;\n margin-bottom: -0.5rem;\n}\n\n.offcanvas-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.offcanvas-body {\n flex-grow: 1;\n padding: 1rem 1rem;\n overflow-y: auto;\n}\n\n.offcanvas-start {\n top: 0;\n left: 0;\n width: 400px;\n border-right: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateX(-100%);\n}\n\n.offcanvas-end {\n top: 0;\n right: 0;\n width: 400px;\n border-left: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateX(100%);\n}\n\n.offcanvas-top {\n top: 0;\n right: 0;\n left: 0;\n height: 30vh;\n max-height: 100%;\n border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateY(-100%);\n}\n\n.offcanvas-bottom {\n right: 0;\n left: 0;\n height: 30vh;\n max-height: 100%;\n border-top: 1px solid rgba(0, 0, 0, 0.2);\n transform: translateY(100%);\n}\n\n.offcanvas.show {\n transform: none;\n}\n\n.placeholder {\n display: inline-block;\n min-height: 1em;\n vertical-align: middle;\n cursor: wait;\n background-color: currentColor;\n opacity: 0.5;\n}\n.placeholder.btn::before {\n display: inline-block;\n content: \"\";\n}\n\n.placeholder-xs {\n min-height: 0.6em;\n}\n\n.placeholder-sm {\n min-height: 0.8em;\n}\n\n.placeholder-lg {\n min-height: 1.2em;\n}\n\n.placeholder-glow .placeholder {\n animation: placeholder-glow 2s ease-in-out infinite;\n}\n\n@keyframes placeholder-glow {\n 50% {\n opacity: 0.2;\n }\n}\n.placeholder-wave {\n mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);\n mask-size: 200% 100%;\n animation: placeholder-wave 2s linear infinite;\n}\n\n@keyframes placeholder-wave {\n 100% {\n mask-position: -200% 0%;\n }\n}\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.link-primary {\n color: #0d6efd;\n}\n.link-primary:hover, .link-primary:focus {\n color: #0a58ca;\n}\n\n.link-secondary {\n color: #6c757d;\n}\n.link-secondary:hover, .link-secondary:focus {\n color: #565e64;\n}\n\n.link-success {\n color: #198754;\n}\n.link-success:hover, .link-success:focus {\n color: #146c43;\n}\n\n.link-info {\n color: #0dcaf0;\n}\n.link-info:hover, .link-info:focus {\n color: #3dd5f3;\n}\n\n.link-warning {\n color: #ffc107;\n}\n.link-warning:hover, .link-warning:focus {\n color: #ffcd39;\n}\n\n.link-danger {\n color: #dc3545;\n}\n.link-danger:hover, .link-danger:focus {\n color: #b02a37;\n}\n\n.link-light {\n color: #f8f9fa;\n}\n.link-light:hover, .link-light:focus {\n color: #f9fafb;\n}\n\n.link-dark {\n color: #212529;\n}\n.link-dark:hover, .link-dark:focus {\n color: #1a1e21;\n}\n\n.ratio {\n position: relative;\n width: 100%;\n}\n.ratio::before {\n display: block;\n padding-top: var(--bs-aspect-ratio);\n content: \"\";\n}\n.ratio > * {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n}\n\n.ratio-1x1 {\n --bs-aspect-ratio: 100%;\n}\n\n.ratio-4x3 {\n --bs-aspect-ratio: 75%;\n}\n\n.ratio-16x9 {\n --bs-aspect-ratio: 56.25%;\n}\n\n.ratio-21x9 {\n --bs-aspect-ratio: 42.8571428571%;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n.sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n}\n\n@media (min-width: 576px) {\n .sticky-sm-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 768px) {\n .sticky-md-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 992px) {\n .sticky-lg-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1200px) {\n .sticky-xl-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n@media (min-width: 1400px) {\n .sticky-xxl-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n.hstack {\n display: flex;\n flex-direction: row;\n align-items: center;\n align-self: stretch;\n}\n\n.vstack {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n align-self: stretch;\n}\n\n.visually-hidden,\n.visually-hidden-focusable:not(:focus):not(:focus-within) {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0, 0, 0, 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n content: \"\";\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.vr {\n display: inline-block;\n align-self: stretch;\n width: 1px;\n min-height: 1em;\n background-color: currentColor;\n opacity: 0.25;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.float-start {\n float: left !important;\n}\n\n.float-end {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n.opacity-0 {\n opacity: 0 !important;\n}\n\n.opacity-25 {\n opacity: 0.25 !important;\n}\n\n.opacity-50 {\n opacity: 0.5 !important;\n}\n\n.opacity-75 {\n opacity: 0.75 !important;\n}\n\n.opacity-100 {\n opacity: 1 !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.overflow-visible {\n overflow: visible !important;\n}\n\n.overflow-scroll {\n overflow: scroll !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-grid {\n display: grid !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n.d-none {\n display: none !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.top-0 {\n top: 0 !important;\n}\n\n.top-50 {\n top: 50% !important;\n}\n\n.top-100 {\n top: 100% !important;\n}\n\n.bottom-0 {\n bottom: 0 !important;\n}\n\n.bottom-50 {\n bottom: 50% !important;\n}\n\n.bottom-100 {\n bottom: 100% !important;\n}\n\n.start-0 {\n left: 0 !important;\n}\n\n.start-50 {\n left: 50% !important;\n}\n\n.start-100 {\n left: 100% !important;\n}\n\n.end-0 {\n right: 0 !important;\n}\n\n.end-50 {\n right: 50% !important;\n}\n\n.end-100 {\n right: 100% !important;\n}\n\n.translate-middle {\n transform: translate(-50%, -50%) !important;\n}\n\n.translate-middle-x {\n transform: translateX(-50%) !important;\n}\n\n.translate-middle-y {\n transform: translateY(-50%) !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-end {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-end-0 {\n border-right: 0 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-start {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-start-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #0d6efd !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #198754 !important;\n}\n\n.border-info {\n border-color: #0dcaf0 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #212529 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.border-1 {\n border-width: 1px !important;\n}\n\n.border-2 {\n border-width: 2px !important;\n}\n\n.border-3 {\n border-width: 3px !important;\n}\n\n.border-4 {\n border-width: 4px !important;\n}\n\n.border-5 {\n border-width: 5px !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.gap-0 {\n gap: 0 !important;\n}\n\n.gap-1 {\n gap: 0.25rem !important;\n}\n\n.gap-2 {\n gap: 0.5rem !important;\n}\n\n.gap-3 {\n gap: 1rem !important;\n}\n\n.gap-4 {\n gap: 1.5rem !important;\n}\n\n.gap-5 {\n gap: 3rem !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.justify-content-evenly {\n justify-content: space-evenly !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n.order-first {\n order: -1 !important;\n}\n\n.order-0 {\n order: 0 !important;\n}\n\n.order-1 {\n order: 1 !important;\n}\n\n.order-2 {\n order: 2 !important;\n}\n\n.order-3 {\n order: 3 !important;\n}\n\n.order-4 {\n order: 4 !important;\n}\n\n.order-5 {\n order: 5 !important;\n}\n\n.order-last {\n order: 6 !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mx-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n}\n\n.mx-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n}\n\n.mx-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n}\n\n.mx-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n}\n\n.mx-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n}\n\n.mx-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n}\n\n.mx-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n}\n\n.my-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n}\n\n.my-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n}\n\n.my-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n}\n\n.my-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n}\n\n.my-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n}\n\n.my-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n}\n\n.my-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n}\n\n.mt-0 {\n margin-top: 0 !important;\n}\n\n.mt-1 {\n margin-top: 0.25rem !important;\n}\n\n.mt-2 {\n margin-top: 0.5rem !important;\n}\n\n.mt-3 {\n margin-top: 1rem !important;\n}\n\n.mt-4 {\n margin-top: 1.5rem !important;\n}\n\n.mt-5 {\n margin-top: 3rem !important;\n}\n\n.mt-auto {\n margin-top: auto !important;\n}\n\n.me-0 {\n margin-right: 0 !important;\n}\n\n.me-1 {\n margin-right: 0.25rem !important;\n}\n\n.me-2 {\n margin-right: 0.5rem !important;\n}\n\n.me-3 {\n margin-right: 1rem !important;\n}\n\n.me-4 {\n margin-right: 1.5rem !important;\n}\n\n.me-5 {\n margin-right: 3rem !important;\n}\n\n.me-auto {\n margin-right: auto !important;\n}\n\n.mb-0 {\n margin-bottom: 0 !important;\n}\n\n.mb-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.mb-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.mb-3 {\n margin-bottom: 1rem !important;\n}\n\n.mb-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.mb-5 {\n margin-bottom: 3rem !important;\n}\n\n.mb-auto {\n margin-bottom: auto !important;\n}\n\n.ms-0 {\n margin-left: 0 !important;\n}\n\n.ms-1 {\n margin-left: 0.25rem !important;\n}\n\n.ms-2 {\n margin-left: 0.5rem !important;\n}\n\n.ms-3 {\n margin-left: 1rem !important;\n}\n\n.ms-4 {\n margin-left: 1.5rem !important;\n}\n\n.ms-5 {\n margin-left: 3rem !important;\n}\n\n.ms-auto {\n margin-left: auto !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.px-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n}\n\n.px-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n}\n\n.px-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n}\n\n.px-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n}\n\n.px-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n}\n\n.px-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n}\n\n.py-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n}\n\n.py-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n}\n\n.py-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n}\n\n.py-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n}\n\n.py-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n}\n\n.py-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n}\n\n.pt-0 {\n padding-top: 0 !important;\n}\n\n.pt-1 {\n padding-top: 0.25rem !important;\n}\n\n.pt-2 {\n padding-top: 0.5rem !important;\n}\n\n.pt-3 {\n padding-top: 1rem !important;\n}\n\n.pt-4 {\n padding-top: 1.5rem !important;\n}\n\n.pt-5 {\n padding-top: 3rem !important;\n}\n\n.pe-0 {\n padding-right: 0 !important;\n}\n\n.pe-1 {\n padding-right: 0.25rem !important;\n}\n\n.pe-2 {\n padding-right: 0.5rem !important;\n}\n\n.pe-3 {\n padding-right: 1rem !important;\n}\n\n.pe-4 {\n padding-right: 1.5rem !important;\n}\n\n.pe-5 {\n padding-right: 3rem !important;\n}\n\n.pb-0 {\n padding-bottom: 0 !important;\n}\n\n.pb-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pb-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pb-3 {\n padding-bottom: 1rem !important;\n}\n\n.pb-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pb-5 {\n padding-bottom: 3rem !important;\n}\n\n.ps-0 {\n padding-left: 0 !important;\n}\n\n.ps-1 {\n padding-left: 0.25rem !important;\n}\n\n.ps-2 {\n padding-left: 0.5rem !important;\n}\n\n.ps-3 {\n padding-left: 1rem !important;\n}\n\n.ps-4 {\n padding-left: 1.5rem !important;\n}\n\n.ps-5 {\n padding-left: 3rem !important;\n}\n\n.font-monospace {\n font-family: var(--bs-font-monospace) !important;\n}\n\n.fs-1 {\n font-size: calc(1.375rem + 1.5vw) !important;\n}\n\n.fs-2 {\n font-size: calc(1.325rem + 0.9vw) !important;\n}\n\n.fs-3 {\n font-size: calc(1.3rem + 0.6vw) !important;\n}\n\n.fs-4 {\n font-size: calc(1.275rem + 0.3vw) !important;\n}\n\n.fs-5 {\n font-size: 1.25rem !important;\n}\n\n.fs-6 {\n font-size: 1rem !important;\n}\n\n.fst-italic {\n font-style: italic !important;\n}\n\n.fst-normal {\n font-style: normal !important;\n}\n\n.fw-light {\n font-weight: 300 !important;\n}\n\n.fw-lighter {\n font-weight: lighter !important;\n}\n\n.fw-normal {\n font-weight: 400 !important;\n}\n\n.fw-bold {\n font-weight: 700 !important;\n}\n\n.fw-bolder {\n font-weight: bolder !important;\n}\n\n.lh-1 {\n line-height: 1 !important;\n}\n\n.lh-sm {\n line-height: 1.25 !important;\n}\n\n.lh-base {\n line-height: 1.5 !important;\n}\n\n.lh-lg {\n line-height: 2 !important;\n}\n\n.text-start {\n text-align: left !important;\n}\n\n.text-end {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-decoration-underline {\n text-decoration: underline !important;\n}\n\n.text-decoration-line-through {\n text-decoration: line-through !important;\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n/* rtl:begin:remove */\n.text-break {\n word-wrap: break-word !important;\n word-break: break-word !important;\n}\n\n/* rtl:end:remove */\n.text-primary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-secondary {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-success {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-info {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-warning {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-danger {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-light {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-dark {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-black {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-white {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-body {\n --bs-text-opacity: 1;\n color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;\n}\n\n.text-muted {\n --bs-text-opacity: 1;\n color: #6c757d !important;\n}\n\n.text-black-50 {\n --bs-text-opacity: 1;\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n --bs-text-opacity: 1;\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-reset {\n --bs-text-opacity: 1;\n color: inherit !important;\n}\n\n.text-opacity-25 {\n --bs-text-opacity: 0.25;\n}\n\n.text-opacity-50 {\n --bs-text-opacity: 0.5;\n}\n\n.text-opacity-75 {\n --bs-text-opacity: 0.75;\n}\n\n.text-opacity-100 {\n --bs-text-opacity: 1;\n}\n\n.bg-primary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-secondary {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-success {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-info {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-warning {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-danger {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-light {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-dark {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-black {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-white {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-body {\n --bs-bg-opacity: 1;\n background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;\n}\n\n.bg-transparent {\n --bs-bg-opacity: 1;\n background-color: transparent !important;\n}\n\n.bg-opacity-10 {\n --bs-bg-opacity: 0.1;\n}\n\n.bg-opacity-25 {\n --bs-bg-opacity: 0.25;\n}\n\n.bg-opacity-50 {\n --bs-bg-opacity: 0.5;\n}\n\n.bg-opacity-75 {\n --bs-bg-opacity: 0.75;\n}\n\n.bg-opacity-100 {\n --bs-bg-opacity: 1;\n}\n\n.bg-gradient {\n background-image: var(--bs-gradient) !important;\n}\n\n.user-select-all {\n user-select: all !important;\n}\n\n.user-select-auto {\n user-select: auto !important;\n}\n\n.user-select-none {\n user-select: none !important;\n}\n\n.pe-none {\n pointer-events: none !important;\n}\n\n.pe-auto {\n pointer-events: auto !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.rounded-1 {\n border-radius: 0.2rem !important;\n}\n\n.rounded-2 {\n border-radius: 0.25rem !important;\n}\n\n.rounded-3 {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-end {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-start {\n border-bottom-left-radius: 0.25rem !important;\n border-top-left-radius: 0.25rem !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-start {\n float: left !important;\n }\n\n .float-sm-end {\n float: right !important;\n }\n\n .float-sm-none {\n float: none !important;\n }\n\n .d-sm-inline {\n display: inline !important;\n }\n\n .d-sm-inline-block {\n display: inline-block !important;\n }\n\n .d-sm-block {\n display: block !important;\n }\n\n .d-sm-grid {\n display: grid !important;\n }\n\n .d-sm-table {\n display: table !important;\n }\n\n .d-sm-table-row {\n display: table-row !important;\n }\n\n .d-sm-table-cell {\n display: table-cell !important;\n }\n\n .d-sm-flex {\n display: flex !important;\n }\n\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n\n .d-sm-none {\n display: none !important;\n }\n\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-sm-row {\n flex-direction: row !important;\n }\n\n .flex-sm-column {\n flex-direction: column !important;\n }\n\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-sm-0 {\n gap: 0 !important;\n }\n\n .gap-sm-1 {\n gap: 0.25rem !important;\n }\n\n .gap-sm-2 {\n gap: 0.5rem !important;\n }\n\n .gap-sm-3 {\n gap: 1rem !important;\n }\n\n .gap-sm-4 {\n gap: 1.5rem !important;\n }\n\n .gap-sm-5 {\n gap: 3rem !important;\n }\n\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-sm-center {\n justify-content: center !important;\n }\n\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n\n .justify-content-sm-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n\n .align-items-sm-center {\n align-items: center !important;\n }\n\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n\n .align-content-sm-center {\n align-content: center !important;\n }\n\n .align-content-sm-between {\n align-content: space-between !important;\n }\n\n .align-content-sm-around {\n align-content: space-around !important;\n }\n\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n\n .align-self-sm-auto {\n align-self: auto !important;\n }\n\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n\n .align-self-sm-center {\n align-self: center !important;\n }\n\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n\n .order-sm-first {\n order: -1 !important;\n }\n\n .order-sm-0 {\n order: 0 !important;\n }\n\n .order-sm-1 {\n order: 1 !important;\n }\n\n .order-sm-2 {\n order: 2 !important;\n }\n\n .order-sm-3 {\n order: 3 !important;\n }\n\n .order-sm-4 {\n order: 4 !important;\n }\n\n .order-sm-5 {\n order: 5 !important;\n }\n\n .order-sm-last {\n order: 6 !important;\n }\n\n .m-sm-0 {\n margin: 0 !important;\n }\n\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n\n .m-sm-3 {\n margin: 1rem !important;\n }\n\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n\n .m-sm-5 {\n margin: 3rem !important;\n }\n\n .m-sm-auto {\n margin: auto !important;\n }\n\n .mx-sm-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-sm-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-sm-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-sm-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-sm-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-sm-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-sm-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-sm-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-sm-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-sm-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-sm-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-sm-0 {\n margin-top: 0 !important;\n }\n\n .mt-sm-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-sm-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-sm-3 {\n margin-top: 1rem !important;\n }\n\n .mt-sm-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-sm-5 {\n margin-top: 3rem !important;\n }\n\n .mt-sm-auto {\n margin-top: auto !important;\n }\n\n .me-sm-0 {\n margin-right: 0 !important;\n }\n\n .me-sm-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-sm-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-sm-3 {\n margin-right: 1rem !important;\n }\n\n .me-sm-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-sm-5 {\n margin-right: 3rem !important;\n }\n\n .me-sm-auto {\n margin-right: auto !important;\n }\n\n .mb-sm-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-sm-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-sm-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-sm-auto {\n margin-bottom: auto !important;\n }\n\n .ms-sm-0 {\n margin-left: 0 !important;\n }\n\n .ms-sm-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-sm-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-sm-3 {\n margin-left: 1rem !important;\n }\n\n .ms-sm-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-sm-5 {\n margin-left: 3rem !important;\n }\n\n .ms-sm-auto {\n margin-left: auto !important;\n }\n\n .p-sm-0 {\n padding: 0 !important;\n }\n\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n\n .p-sm-3 {\n padding: 1rem !important;\n }\n\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n\n .p-sm-5 {\n padding: 3rem !important;\n }\n\n .px-sm-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-sm-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-sm-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-sm-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-sm-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-sm-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-sm-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-sm-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-sm-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-sm-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-sm-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-sm-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-sm-0 {\n padding-top: 0 !important;\n }\n\n .pt-sm-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-sm-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-sm-3 {\n padding-top: 1rem !important;\n }\n\n .pt-sm-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-sm-5 {\n padding-top: 3rem !important;\n }\n\n .pe-sm-0 {\n padding-right: 0 !important;\n }\n\n .pe-sm-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-sm-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-sm-3 {\n padding-right: 1rem !important;\n }\n\n .pe-sm-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-sm-5 {\n padding-right: 3rem !important;\n }\n\n .pb-sm-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-sm-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-sm-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-sm-0 {\n padding-left: 0 !important;\n }\n\n .ps-sm-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-sm-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-sm-3 {\n padding-left: 1rem !important;\n }\n\n .ps-sm-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-sm-5 {\n padding-left: 3rem !important;\n }\n\n .text-sm-start {\n text-align: left !important;\n }\n\n .text-sm-end {\n text-align: right !important;\n }\n\n .text-sm-center {\n text-align: center !important;\n }\n}\n@media (min-width: 768px) {\n .float-md-start {\n float: left !important;\n }\n\n .float-md-end {\n float: right !important;\n }\n\n .float-md-none {\n float: none !important;\n }\n\n .d-md-inline {\n display: inline !important;\n }\n\n .d-md-inline-block {\n display: inline-block !important;\n }\n\n .d-md-block {\n display: block !important;\n }\n\n .d-md-grid {\n display: grid !important;\n }\n\n .d-md-table {\n display: table !important;\n }\n\n .d-md-table-row {\n display: table-row !important;\n }\n\n .d-md-table-cell {\n display: table-cell !important;\n }\n\n .d-md-flex {\n display: flex !important;\n }\n\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n\n .d-md-none {\n display: none !important;\n }\n\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-md-row {\n flex-direction: row !important;\n }\n\n .flex-md-column {\n flex-direction: column !important;\n }\n\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-md-0 {\n gap: 0 !important;\n }\n\n .gap-md-1 {\n gap: 0.25rem !important;\n }\n\n .gap-md-2 {\n gap: 0.5rem !important;\n }\n\n .gap-md-3 {\n gap: 1rem !important;\n }\n\n .gap-md-4 {\n gap: 1.5rem !important;\n }\n\n .gap-md-5 {\n gap: 3rem !important;\n }\n\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-md-center {\n justify-content: center !important;\n }\n\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n\n .justify-content-md-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-md-start {\n align-items: flex-start !important;\n }\n\n .align-items-md-end {\n align-items: flex-end !important;\n }\n\n .align-items-md-center {\n align-items: center !important;\n }\n\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n\n .align-content-md-start {\n align-content: flex-start !important;\n }\n\n .align-content-md-end {\n align-content: flex-end !important;\n }\n\n .align-content-md-center {\n align-content: center !important;\n }\n\n .align-content-md-between {\n align-content: space-between !important;\n }\n\n .align-content-md-around {\n align-content: space-around !important;\n }\n\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n\n .align-self-md-auto {\n align-self: auto !important;\n }\n\n .align-self-md-start {\n align-self: flex-start !important;\n }\n\n .align-self-md-end {\n align-self: flex-end !important;\n }\n\n .align-self-md-center {\n align-self: center !important;\n }\n\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n\n .order-md-first {\n order: -1 !important;\n }\n\n .order-md-0 {\n order: 0 !important;\n }\n\n .order-md-1 {\n order: 1 !important;\n }\n\n .order-md-2 {\n order: 2 !important;\n }\n\n .order-md-3 {\n order: 3 !important;\n }\n\n .order-md-4 {\n order: 4 !important;\n }\n\n .order-md-5 {\n order: 5 !important;\n }\n\n .order-md-last {\n order: 6 !important;\n }\n\n .m-md-0 {\n margin: 0 !important;\n }\n\n .m-md-1 {\n margin: 0.25rem !important;\n }\n\n .m-md-2 {\n margin: 0.5rem !important;\n }\n\n .m-md-3 {\n margin: 1rem !important;\n }\n\n .m-md-4 {\n margin: 1.5rem !important;\n }\n\n .m-md-5 {\n margin: 3rem !important;\n }\n\n .m-md-auto {\n margin: auto !important;\n }\n\n .mx-md-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-md-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-md-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-md-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-md-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-md-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-md-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-md-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-md-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-md-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-md-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-md-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-md-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-md-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-md-0 {\n margin-top: 0 !important;\n }\n\n .mt-md-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-md-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-md-3 {\n margin-top: 1rem !important;\n }\n\n .mt-md-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-md-5 {\n margin-top: 3rem !important;\n }\n\n .mt-md-auto {\n margin-top: auto !important;\n }\n\n .me-md-0 {\n margin-right: 0 !important;\n }\n\n .me-md-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-md-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-md-3 {\n margin-right: 1rem !important;\n }\n\n .me-md-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-md-5 {\n margin-right: 3rem !important;\n }\n\n .me-md-auto {\n margin-right: auto !important;\n }\n\n .mb-md-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-md-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-md-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-md-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-md-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-md-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-md-auto {\n margin-bottom: auto !important;\n }\n\n .ms-md-0 {\n margin-left: 0 !important;\n }\n\n .ms-md-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-md-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-md-3 {\n margin-left: 1rem !important;\n }\n\n .ms-md-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-md-5 {\n margin-left: 3rem !important;\n }\n\n .ms-md-auto {\n margin-left: auto !important;\n }\n\n .p-md-0 {\n padding: 0 !important;\n }\n\n .p-md-1 {\n padding: 0.25rem !important;\n }\n\n .p-md-2 {\n padding: 0.5rem !important;\n }\n\n .p-md-3 {\n padding: 1rem !important;\n }\n\n .p-md-4 {\n padding: 1.5rem !important;\n }\n\n .p-md-5 {\n padding: 3rem !important;\n }\n\n .px-md-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-md-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-md-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-md-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-md-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-md-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-md-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-md-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-md-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-md-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-md-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-md-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-md-0 {\n padding-top: 0 !important;\n }\n\n .pt-md-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-md-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-md-3 {\n padding-top: 1rem !important;\n }\n\n .pt-md-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-md-5 {\n padding-top: 3rem !important;\n }\n\n .pe-md-0 {\n padding-right: 0 !important;\n }\n\n .pe-md-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-md-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-md-3 {\n padding-right: 1rem !important;\n }\n\n .pe-md-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-md-5 {\n padding-right: 3rem !important;\n }\n\n .pb-md-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-md-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-md-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-md-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-md-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-md-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-md-0 {\n padding-left: 0 !important;\n }\n\n .ps-md-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-md-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-md-3 {\n padding-left: 1rem !important;\n }\n\n .ps-md-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-md-5 {\n padding-left: 3rem !important;\n }\n\n .text-md-start {\n text-align: left !important;\n }\n\n .text-md-end {\n text-align: right !important;\n }\n\n .text-md-center {\n text-align: center !important;\n }\n}\n@media (min-width: 992px) {\n .float-lg-start {\n float: left !important;\n }\n\n .float-lg-end {\n float: right !important;\n }\n\n .float-lg-none {\n float: none !important;\n }\n\n .d-lg-inline {\n display: inline !important;\n }\n\n .d-lg-inline-block {\n display: inline-block !important;\n }\n\n .d-lg-block {\n display: block !important;\n }\n\n .d-lg-grid {\n display: grid !important;\n }\n\n .d-lg-table {\n display: table !important;\n }\n\n .d-lg-table-row {\n display: table-row !important;\n }\n\n .d-lg-table-cell {\n display: table-cell !important;\n }\n\n .d-lg-flex {\n display: flex !important;\n }\n\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n\n .d-lg-none {\n display: none !important;\n }\n\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-lg-row {\n flex-direction: row !important;\n }\n\n .flex-lg-column {\n flex-direction: column !important;\n }\n\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-lg-0 {\n gap: 0 !important;\n }\n\n .gap-lg-1 {\n gap: 0.25rem !important;\n }\n\n .gap-lg-2 {\n gap: 0.5rem !important;\n }\n\n .gap-lg-3 {\n gap: 1rem !important;\n }\n\n .gap-lg-4 {\n gap: 1.5rem !important;\n }\n\n .gap-lg-5 {\n gap: 3rem !important;\n }\n\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-lg-center {\n justify-content: center !important;\n }\n\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n\n .justify-content-lg-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n\n .align-items-lg-center {\n align-items: center !important;\n }\n\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n\n .align-content-lg-center {\n align-content: center !important;\n }\n\n .align-content-lg-between {\n align-content: space-between !important;\n }\n\n .align-content-lg-around {\n align-content: space-around !important;\n }\n\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n\n .align-self-lg-auto {\n align-self: auto !important;\n }\n\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n\n .align-self-lg-center {\n align-self: center !important;\n }\n\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n\n .order-lg-first {\n order: -1 !important;\n }\n\n .order-lg-0 {\n order: 0 !important;\n }\n\n .order-lg-1 {\n order: 1 !important;\n }\n\n .order-lg-2 {\n order: 2 !important;\n }\n\n .order-lg-3 {\n order: 3 !important;\n }\n\n .order-lg-4 {\n order: 4 !important;\n }\n\n .order-lg-5 {\n order: 5 !important;\n }\n\n .order-lg-last {\n order: 6 !important;\n }\n\n .m-lg-0 {\n margin: 0 !important;\n }\n\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n\n .m-lg-3 {\n margin: 1rem !important;\n }\n\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n\n .m-lg-5 {\n margin: 3rem !important;\n }\n\n .m-lg-auto {\n margin: auto !important;\n }\n\n .mx-lg-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-lg-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-lg-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-lg-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-lg-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-lg-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-lg-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-lg-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-lg-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-lg-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-lg-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-lg-0 {\n margin-top: 0 !important;\n }\n\n .mt-lg-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-lg-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-lg-3 {\n margin-top: 1rem !important;\n }\n\n .mt-lg-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-lg-5 {\n margin-top: 3rem !important;\n }\n\n .mt-lg-auto {\n margin-top: auto !important;\n }\n\n .me-lg-0 {\n margin-right: 0 !important;\n }\n\n .me-lg-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-lg-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-lg-3 {\n margin-right: 1rem !important;\n }\n\n .me-lg-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-lg-5 {\n margin-right: 3rem !important;\n }\n\n .me-lg-auto {\n margin-right: auto !important;\n }\n\n .mb-lg-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-lg-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-lg-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-lg-auto {\n margin-bottom: auto !important;\n }\n\n .ms-lg-0 {\n margin-left: 0 !important;\n }\n\n .ms-lg-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-lg-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-lg-3 {\n margin-left: 1rem !important;\n }\n\n .ms-lg-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-lg-5 {\n margin-left: 3rem !important;\n }\n\n .ms-lg-auto {\n margin-left: auto !important;\n }\n\n .p-lg-0 {\n padding: 0 !important;\n }\n\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n\n .p-lg-3 {\n padding: 1rem !important;\n }\n\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n\n .p-lg-5 {\n padding: 3rem !important;\n }\n\n .px-lg-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-lg-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-lg-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-lg-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-lg-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-lg-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-lg-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-lg-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-lg-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-lg-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-lg-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-lg-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-lg-0 {\n padding-top: 0 !important;\n }\n\n .pt-lg-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-lg-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-lg-3 {\n padding-top: 1rem !important;\n }\n\n .pt-lg-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-lg-5 {\n padding-top: 3rem !important;\n }\n\n .pe-lg-0 {\n padding-right: 0 !important;\n }\n\n .pe-lg-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-lg-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-lg-3 {\n padding-right: 1rem !important;\n }\n\n .pe-lg-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-lg-5 {\n padding-right: 3rem !important;\n }\n\n .pb-lg-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-lg-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-lg-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-lg-0 {\n padding-left: 0 !important;\n }\n\n .ps-lg-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-lg-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-lg-3 {\n padding-left: 1rem !important;\n }\n\n .ps-lg-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-lg-5 {\n padding-left: 3rem !important;\n }\n\n .text-lg-start {\n text-align: left !important;\n }\n\n .text-lg-end {\n text-align: right !important;\n }\n\n .text-lg-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .float-xl-start {\n float: left !important;\n }\n\n .float-xl-end {\n float: right !important;\n }\n\n .float-xl-none {\n float: none !important;\n }\n\n .d-xl-inline {\n display: inline !important;\n }\n\n .d-xl-inline-block {\n display: inline-block !important;\n }\n\n .d-xl-block {\n display: block !important;\n }\n\n .d-xl-grid {\n display: grid !important;\n }\n\n .d-xl-table {\n display: table !important;\n }\n\n .d-xl-table-row {\n display: table-row !important;\n }\n\n .d-xl-table-cell {\n display: table-cell !important;\n }\n\n .d-xl-flex {\n display: flex !important;\n }\n\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xl-none {\n display: none !important;\n }\n\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xl-row {\n flex-direction: row !important;\n }\n\n .flex-xl-column {\n flex-direction: column !important;\n }\n\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-xl-0 {\n gap: 0 !important;\n }\n\n .gap-xl-1 {\n gap: 0.25rem !important;\n }\n\n .gap-xl-2 {\n gap: 0.5rem !important;\n }\n\n .gap-xl-3 {\n gap: 1rem !important;\n }\n\n .gap-xl-4 {\n gap: 1.5rem !important;\n }\n\n .gap-xl-5 {\n gap: 3rem !important;\n }\n\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xl-center {\n justify-content: center !important;\n }\n\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xl-center {\n align-items: center !important;\n }\n\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xl-center {\n align-content: center !important;\n }\n\n .align-content-xl-between {\n align-content: space-between !important;\n }\n\n .align-content-xl-around {\n align-content: space-around !important;\n }\n\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xl-auto {\n align-self: auto !important;\n }\n\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xl-center {\n align-self: center !important;\n }\n\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n\n .order-xl-first {\n order: -1 !important;\n }\n\n .order-xl-0 {\n order: 0 !important;\n }\n\n .order-xl-1 {\n order: 1 !important;\n }\n\n .order-xl-2 {\n order: 2 !important;\n }\n\n .order-xl-3 {\n order: 3 !important;\n }\n\n .order-xl-4 {\n order: 4 !important;\n }\n\n .order-xl-5 {\n order: 5 !important;\n }\n\n .order-xl-last {\n order: 6 !important;\n }\n\n .m-xl-0 {\n margin: 0 !important;\n }\n\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xl-3 {\n margin: 1rem !important;\n }\n\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xl-5 {\n margin: 3rem !important;\n }\n\n .m-xl-auto {\n margin: auto !important;\n }\n\n .mx-xl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xl-auto {\n margin-top: auto !important;\n }\n\n .me-xl-0 {\n margin-right: 0 !important;\n }\n\n .me-xl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xl-auto {\n margin-right: auto !important;\n }\n\n .mb-xl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xl-auto {\n margin-left: auto !important;\n }\n\n .p-xl-0 {\n padding: 0 !important;\n }\n\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xl-3 {\n padding: 1rem !important;\n }\n\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xl-5 {\n padding: 3rem !important;\n }\n\n .px-xl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xl-5 {\n padding-left: 3rem !important;\n }\n\n .text-xl-start {\n text-align: left !important;\n }\n\n .text-xl-end {\n text-align: right !important;\n }\n\n .text-xl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1400px) {\n .float-xxl-start {\n float: left !important;\n }\n\n .float-xxl-end {\n float: right !important;\n }\n\n .float-xxl-none {\n float: none !important;\n }\n\n .d-xxl-inline {\n display: inline !important;\n }\n\n .d-xxl-inline-block {\n display: inline-block !important;\n }\n\n .d-xxl-block {\n display: block !important;\n }\n\n .d-xxl-grid {\n display: grid !important;\n }\n\n .d-xxl-table {\n display: table !important;\n }\n\n .d-xxl-table-row {\n display: table-row !important;\n }\n\n .d-xxl-table-cell {\n display: table-cell !important;\n }\n\n .d-xxl-flex {\n display: flex !important;\n }\n\n .d-xxl-inline-flex {\n display: inline-flex !important;\n }\n\n .d-xxl-none {\n display: none !important;\n }\n\n .flex-xxl-fill {\n flex: 1 1 auto !important;\n }\n\n .flex-xxl-row {\n flex-direction: row !important;\n }\n\n .flex-xxl-column {\n flex-direction: column !important;\n }\n\n .flex-xxl-row-reverse {\n flex-direction: row-reverse !important;\n }\n\n .flex-xxl-column-reverse {\n flex-direction: column-reverse !important;\n }\n\n .flex-xxl-grow-0 {\n flex-grow: 0 !important;\n }\n\n .flex-xxl-grow-1 {\n flex-grow: 1 !important;\n }\n\n .flex-xxl-shrink-0 {\n flex-shrink: 0 !important;\n }\n\n .flex-xxl-shrink-1 {\n flex-shrink: 1 !important;\n }\n\n .flex-xxl-wrap {\n flex-wrap: wrap !important;\n }\n\n .flex-xxl-nowrap {\n flex-wrap: nowrap !important;\n }\n\n .flex-xxl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n\n .gap-xxl-0 {\n gap: 0 !important;\n }\n\n .gap-xxl-1 {\n gap: 0.25rem !important;\n }\n\n .gap-xxl-2 {\n gap: 0.5rem !important;\n }\n\n .gap-xxl-3 {\n gap: 1rem !important;\n }\n\n .gap-xxl-4 {\n gap: 1.5rem !important;\n }\n\n .gap-xxl-5 {\n gap: 3rem !important;\n }\n\n .justify-content-xxl-start {\n justify-content: flex-start !important;\n }\n\n .justify-content-xxl-end {\n justify-content: flex-end !important;\n }\n\n .justify-content-xxl-center {\n justify-content: center !important;\n }\n\n .justify-content-xxl-between {\n justify-content: space-between !important;\n }\n\n .justify-content-xxl-around {\n justify-content: space-around !important;\n }\n\n .justify-content-xxl-evenly {\n justify-content: space-evenly !important;\n }\n\n .align-items-xxl-start {\n align-items: flex-start !important;\n }\n\n .align-items-xxl-end {\n align-items: flex-end !important;\n }\n\n .align-items-xxl-center {\n align-items: center !important;\n }\n\n .align-items-xxl-baseline {\n align-items: baseline !important;\n }\n\n .align-items-xxl-stretch {\n align-items: stretch !important;\n }\n\n .align-content-xxl-start {\n align-content: flex-start !important;\n }\n\n .align-content-xxl-end {\n align-content: flex-end !important;\n }\n\n .align-content-xxl-center {\n align-content: center !important;\n }\n\n .align-content-xxl-between {\n align-content: space-between !important;\n }\n\n .align-content-xxl-around {\n align-content: space-around !important;\n }\n\n .align-content-xxl-stretch {\n align-content: stretch !important;\n }\n\n .align-self-xxl-auto {\n align-self: auto !important;\n }\n\n .align-self-xxl-start {\n align-self: flex-start !important;\n }\n\n .align-self-xxl-end {\n align-self: flex-end !important;\n }\n\n .align-self-xxl-center {\n align-self: center !important;\n }\n\n .align-self-xxl-baseline {\n align-self: baseline !important;\n }\n\n .align-self-xxl-stretch {\n align-self: stretch !important;\n }\n\n .order-xxl-first {\n order: -1 !important;\n }\n\n .order-xxl-0 {\n order: 0 !important;\n }\n\n .order-xxl-1 {\n order: 1 !important;\n }\n\n .order-xxl-2 {\n order: 2 !important;\n }\n\n .order-xxl-3 {\n order: 3 !important;\n }\n\n .order-xxl-4 {\n order: 4 !important;\n }\n\n .order-xxl-5 {\n order: 5 !important;\n }\n\n .order-xxl-last {\n order: 6 !important;\n }\n\n .m-xxl-0 {\n margin: 0 !important;\n }\n\n .m-xxl-1 {\n margin: 0.25rem !important;\n }\n\n .m-xxl-2 {\n margin: 0.5rem !important;\n }\n\n .m-xxl-3 {\n margin: 1rem !important;\n }\n\n .m-xxl-4 {\n margin: 1.5rem !important;\n }\n\n .m-xxl-5 {\n margin: 3rem !important;\n }\n\n .m-xxl-auto {\n margin: auto !important;\n }\n\n .mx-xxl-0 {\n margin-right: 0 !important;\n margin-left: 0 !important;\n }\n\n .mx-xxl-1 {\n margin-right: 0.25rem !important;\n margin-left: 0.25rem !important;\n }\n\n .mx-xxl-2 {\n margin-right: 0.5rem !important;\n margin-left: 0.5rem !important;\n }\n\n .mx-xxl-3 {\n margin-right: 1rem !important;\n margin-left: 1rem !important;\n }\n\n .mx-xxl-4 {\n margin-right: 1.5rem !important;\n margin-left: 1.5rem !important;\n }\n\n .mx-xxl-5 {\n margin-right: 3rem !important;\n margin-left: 3rem !important;\n }\n\n .mx-xxl-auto {\n margin-right: auto !important;\n margin-left: auto !important;\n }\n\n .my-xxl-0 {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n\n .my-xxl-1 {\n margin-top: 0.25rem !important;\n margin-bottom: 0.25rem !important;\n }\n\n .my-xxl-2 {\n margin-top: 0.5rem !important;\n margin-bottom: 0.5rem !important;\n }\n\n .my-xxl-3 {\n margin-top: 1rem !important;\n margin-bottom: 1rem !important;\n }\n\n .my-xxl-4 {\n margin-top: 1.5rem !important;\n margin-bottom: 1.5rem !important;\n }\n\n .my-xxl-5 {\n margin-top: 3rem !important;\n margin-bottom: 3rem !important;\n }\n\n .my-xxl-auto {\n margin-top: auto !important;\n margin-bottom: auto !important;\n }\n\n .mt-xxl-0 {\n margin-top: 0 !important;\n }\n\n .mt-xxl-1 {\n margin-top: 0.25rem !important;\n }\n\n .mt-xxl-2 {\n margin-top: 0.5rem !important;\n }\n\n .mt-xxl-3 {\n margin-top: 1rem !important;\n }\n\n .mt-xxl-4 {\n margin-top: 1.5rem !important;\n }\n\n .mt-xxl-5 {\n margin-top: 3rem !important;\n }\n\n .mt-xxl-auto {\n margin-top: auto !important;\n }\n\n .me-xxl-0 {\n margin-right: 0 !important;\n }\n\n .me-xxl-1 {\n margin-right: 0.25rem !important;\n }\n\n .me-xxl-2 {\n margin-right: 0.5rem !important;\n }\n\n .me-xxl-3 {\n margin-right: 1rem !important;\n }\n\n .me-xxl-4 {\n margin-right: 1.5rem !important;\n }\n\n .me-xxl-5 {\n margin-right: 3rem !important;\n }\n\n .me-xxl-auto {\n margin-right: auto !important;\n }\n\n .mb-xxl-0 {\n margin-bottom: 0 !important;\n }\n\n .mb-xxl-1 {\n margin-bottom: 0.25rem !important;\n }\n\n .mb-xxl-2 {\n margin-bottom: 0.5rem !important;\n }\n\n .mb-xxl-3 {\n margin-bottom: 1rem !important;\n }\n\n .mb-xxl-4 {\n margin-bottom: 1.5rem !important;\n }\n\n .mb-xxl-5 {\n margin-bottom: 3rem !important;\n }\n\n .mb-xxl-auto {\n margin-bottom: auto !important;\n }\n\n .ms-xxl-0 {\n margin-left: 0 !important;\n }\n\n .ms-xxl-1 {\n margin-left: 0.25rem !important;\n }\n\n .ms-xxl-2 {\n margin-left: 0.5rem !important;\n }\n\n .ms-xxl-3 {\n margin-left: 1rem !important;\n }\n\n .ms-xxl-4 {\n margin-left: 1.5rem !important;\n }\n\n .ms-xxl-5 {\n margin-left: 3rem !important;\n }\n\n .ms-xxl-auto {\n margin-left: auto !important;\n }\n\n .p-xxl-0 {\n padding: 0 !important;\n }\n\n .p-xxl-1 {\n padding: 0.25rem !important;\n }\n\n .p-xxl-2 {\n padding: 0.5rem !important;\n }\n\n .p-xxl-3 {\n padding: 1rem !important;\n }\n\n .p-xxl-4 {\n padding: 1.5rem !important;\n }\n\n .p-xxl-5 {\n padding: 3rem !important;\n }\n\n .px-xxl-0 {\n padding-right: 0 !important;\n padding-left: 0 !important;\n }\n\n .px-xxl-1 {\n padding-right: 0.25rem !important;\n padding-left: 0.25rem !important;\n }\n\n .px-xxl-2 {\n padding-right: 0.5rem !important;\n padding-left: 0.5rem !important;\n }\n\n .px-xxl-3 {\n padding-right: 1rem !important;\n padding-left: 1rem !important;\n }\n\n .px-xxl-4 {\n padding-right: 1.5rem !important;\n padding-left: 1.5rem !important;\n }\n\n .px-xxl-5 {\n padding-right: 3rem !important;\n padding-left: 3rem !important;\n }\n\n .py-xxl-0 {\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n }\n\n .py-xxl-1 {\n padding-top: 0.25rem !important;\n padding-bottom: 0.25rem !important;\n }\n\n .py-xxl-2 {\n padding-top: 0.5rem !important;\n padding-bottom: 0.5rem !important;\n }\n\n .py-xxl-3 {\n padding-top: 1rem !important;\n padding-bottom: 1rem !important;\n }\n\n .py-xxl-4 {\n padding-top: 1.5rem !important;\n padding-bottom: 1.5rem !important;\n }\n\n .py-xxl-5 {\n padding-top: 3rem !important;\n padding-bottom: 3rem !important;\n }\n\n .pt-xxl-0 {\n padding-top: 0 !important;\n }\n\n .pt-xxl-1 {\n padding-top: 0.25rem !important;\n }\n\n .pt-xxl-2 {\n padding-top: 0.5rem !important;\n }\n\n .pt-xxl-3 {\n padding-top: 1rem !important;\n }\n\n .pt-xxl-4 {\n padding-top: 1.5rem !important;\n }\n\n .pt-xxl-5 {\n padding-top: 3rem !important;\n }\n\n .pe-xxl-0 {\n padding-right: 0 !important;\n }\n\n .pe-xxl-1 {\n padding-right: 0.25rem !important;\n }\n\n .pe-xxl-2 {\n padding-right: 0.5rem !important;\n }\n\n .pe-xxl-3 {\n padding-right: 1rem !important;\n }\n\n .pe-xxl-4 {\n padding-right: 1.5rem !important;\n }\n\n .pe-xxl-5 {\n padding-right: 3rem !important;\n }\n\n .pb-xxl-0 {\n padding-bottom: 0 !important;\n }\n\n .pb-xxl-1 {\n padding-bottom: 0.25rem !important;\n }\n\n .pb-xxl-2 {\n padding-bottom: 0.5rem !important;\n }\n\n .pb-xxl-3 {\n padding-bottom: 1rem !important;\n }\n\n .pb-xxl-4 {\n padding-bottom: 1.5rem !important;\n }\n\n .pb-xxl-5 {\n padding-bottom: 3rem !important;\n }\n\n .ps-xxl-0 {\n padding-left: 0 !important;\n }\n\n .ps-xxl-1 {\n padding-left: 0.25rem !important;\n }\n\n .ps-xxl-2 {\n padding-left: 0.5rem !important;\n }\n\n .ps-xxl-3 {\n padding-left: 1rem !important;\n }\n\n .ps-xxl-4 {\n padding-left: 1.5rem !important;\n }\n\n .ps-xxl-5 {\n padding-left: 3rem !important;\n }\n\n .text-xxl-start {\n text-align: left !important;\n }\n\n .text-xxl-end {\n text-align: right !important;\n }\n\n .text-xxl-center {\n text-align: center !important;\n }\n}\n@media (min-width: 1200px) {\n .fs-1 {\n font-size: 2.5rem !important;\n }\n\n .fs-2 {\n font-size: 2rem !important;\n }\n\n .fs-3 {\n font-size: 1.75rem !important;\n }\n\n .fs-4 {\n font-size: 1.5rem !important;\n }\n}\n@media print {\n .d-print-inline {\n display: inline !important;\n }\n\n .d-print-inline-block {\n display: inline-block !important;\n }\n\n .d-print-block {\n display: block !important;\n }\n\n .d-print-grid {\n display: grid !important;\n }\n\n .d-print-table {\n display: table !important;\n }\n\n .d-print-table-row {\n display: table-row !important;\n }\n\n .d-print-table-cell {\n display: table-cell !important;\n }\n\n .d-print-flex {\n display: flex !important;\n }\n\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n\n .d-print-none {\n display: none !important;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */\n","/*!\n * Bootstrap v5.1.3 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-color-rgb: #{to-rgb($body-color)};\n --#{$variable-prefix}body-bg-rgb: #{to-rgb($body-bg)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`