diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4b5b456a93..d296a49ed4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,6 +3,8 @@ "allow": [ "Bash(yarn nx g:*)", "Bash(npx vitest:*)", + "Bash(xargs:*)", + "Bash(dotnet build:*)", "Bash(git show:*)" ] } diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index e3d6cffb7a..9f3ddb4289 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -1,13 +1,13 @@ -name: Merge branch rel-10.4 with rel-10.3 +name: Merge branch rel-10.5 with rel-10.4 on: push: branches: - - rel-10.3 + - rel-10.4 permissions: contents: read jobs: - merge-rel-10-4-with-rel-10-3: + merge-rel-10-5-with-rel-10-4: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR @@ -15,17 +15,17 @@ jobs: steps: - uses: actions/checkout@v2 with: - ref: rel-10.4 + ref: rel-10.5 - name: Reset promotion branch run: | - git fetch origin rel-10.3:rel-10.3 - git reset --hard rel-10.3 + git fetch origin rel-10.4:rel-10.4 + git reset --hard rel-10.4 - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: - branch: auto-merge/rel-10-3/${{github.run_number}} - title: Merge branch rel-10.4 with rel-10.3 - body: This PR generated automatically to merge rel-10.4 with rel-10.3. Please review the changed files before merging to prevent any errors that may occur. + branch: auto-merge/rel-10-4/${{github.run_number}} + title: Merge branch rel-10.5 with rel-10.4 + body: This PR generated automatically to merge rel-10.5 with rel-10.4. Please review the changed files before merging to prevent any errors that may occur. draft: true token: ${{ github.token }} - name: Merge Pull Request @@ -33,5 +33,5 @@ jobs: GH_TOKEN: ${{ secrets.BOT_SECRET }} run: | gh pr ready - gh pr review auto-merge/rel-10-3/${{github.run_number}} --approve - gh pr merge auto-merge/rel-10-3/${{github.run_number}} --merge --auto --delete-branch + gh pr review auto-merge/rel-10-4/${{github.run_number}} --approve + gh pr merge auto-merge/rel-10-4/${{github.run_number}} --merge --auto --delete-branch diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b1bc675156..6c3dce1854 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -86,7 +86,7 @@ jobs: - name: Codecov if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: use_oidc: true fail_ci_if_error: true diff --git a/.github/workflows/update-studio-docs.yml b/.github/workflows/update-studio-docs.yml index 9e16b07280..fd258051e6 100644 --- a/.github/workflows/update-studio-docs.yml +++ b/.github/workflows/update-studio-docs.yml @@ -213,17 +213,18 @@ jobs: CRITICAL RULES: 1. Extract ONLY essential, user-facing changes - 2. Format as bullet points starting with "- " - 3. Keep it concise and professional + 2. Format as markdown bullet points starting with "* " + 3. Keep it concise, friendly and easy to scan 4. Match the style of existing release notes 5. Skip internal/technical details unless critical 6. Return ONLY the bullet points (no version header, no date) 7. One change per line + 8. Prefer short action-oriented summaries like "AI Agent Upgrades: Added browser automation tools" Output example: - - Fixed books sample for blazor-webapp tiered solution - - Enhanced Module Installation UI - - Added AI Management option to Startup Templates + * AI Agent Upgrades: Added browser automation tools + * Module Setup Improvements: Added guidance for modularity options + * UI Polish: Improved sidebar icons and visual consistency Return ONLY the formatted bullet points. @@ -240,56 +241,85 @@ jobs: echo "✅ Using AI-formatted release notes" echo "$AI_RESPONSE" > .tmp/final-notes.txt else - echo "⚠️ AI unavailable - using aggressive cleaning on raw release notes" - - # Clean and format raw notes with aggressive filtering - echo "$RAW_NOTES" | while IFS= read -r line; do - # Skip empty lines - [ -z "$line" ] && continue - - # Skip section headers - [[ "$line" =~ ^#+.*What.*Changed ]] && continue - [[ "$line" =~ ^##[[:space:]] ]] && continue - - # Skip full changelog links - [[ "$line" =~ ^\*\*Full\ Changelog ]] && continue - [[ "$line" =~ ^Full\ Changelog ]] && continue - - # Remove leading bullet/asterisk - line=$(echo "$line" | sed 's/^[[:space:]]*[*-][[:space:]]*//') - - # Aggressive cleaning: remove entire " by @user in https://..." suffix - line=$(echo "$line" | sed 's/[[:space:]]*by @[a-zA-Z0-9_-]*[[:space:]]*in https:\/\/github\.com\/[^[:space:]]*//g') - - # Remove remaining "by @username" or "by username" - line=$(echo "$line" | sed 's/[[:space:]]*by @[a-zA-Z0-9_-]*[[:space:]]*$//g') - line=$(echo "$line" | sed 's/[[:space:]]*by [a-zA-Z0-9_-]*[[:space:]]*$//g') - - # Remove standalone @mentions - line=$(echo "$line" | sed 's/@[a-zA-Z0-9_-]*//g') - - # Clean trailing periods if orphaned - line=$(echo "$line" | sed 's/\.[[:space:]]*$//') - - # Trim all whitespace - line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - - # Skip if line is empty or too short - [ -z "$line" ] && continue - [ ${#line} -lt 5 ] && continue - - # Capitalize first letter if lowercase - line="$(echo ${line:0:1} | tr '[:lower:]' '[:upper:]')${line:1}" - - # Add clean bullet and output - echo "- $line" - done > .tmp/final-notes.txt + echo "⚠️ AI unavailable - generating concise user-friendly summaries from raw notes" + + python3 <<'PYTHON_EOF' + import os + import re + + raw = os.environ.get("RAW_NOTES", "") + lines = raw.splitlines() + + output = [] + seen = set() + + def clean_line(text: str) -> str: + text = text.strip() + if not text: + return "" + + # Drop markdown headers/changelog lines. + if re.match(r"^#+\s", text, flags=re.I): + return "" + if re.match(r"^\*\*?\s*full\s+changelog", text, flags=re.I): + return "" + if re.match(r"^full\s+changelog", text, flags=re.I): + return "" + + text = re.sub(r"^[\s\-*•]+", "", text) + text = re.sub(r"\s+by\s+@?[a-zA-Z0-9_-]+\s+in\s+https?://\S+", "", text) + text = re.sub(r"\s+by\s+@?[a-zA-Z0-9_-]+\s*$", "", text) + text = re.sub(r"@([a-zA-Z0-9_-]+)", "", text) + text = re.sub(r"\s*\([^)]*#\d+\)\s*$", "", text) + text = re.sub(r"\s+#\d+\s*$", "", text) + text = re.sub(r"\s+", " ", text).strip(" .:-") + + if len(text) < 8: + return "" + + # Make user-friendly short title + summary when possible. + if ":" in text: + left, right = [p.strip() for p in text.split(":", 1)] + left = left[:40].rstrip(" .") + right_words = right.split() + right = " ".join(right_words[:14]).rstrip(" .") + text = f"{left}: {right}" if right else left + else: + words = text.split() + if len(words) > 16: + text = " ".join(words[:16]).rstrip(" .") + + return text + + for line in lines: + cleaned = clean_line(line) + if not cleaned: + continue + + # Normalize casing and deduplicate. + cleaned = cleaned[0].upper() + cleaned[1:] if cleaned else cleaned + key = cleaned.lower() + if key in seen: + continue + seen.add(key) + + output.append(f"* {cleaned}") + if len(output) >= 8: + break + + with open('.tmp/final-notes.txt', 'w', encoding='utf-8') as f: + f.write("\n".join(output)) + PYTHON_EOF fi + + # Normalize bullets to "* " even if AI returns "- ". + sed -E 's/^[[:space:]]*-[[:space:]]+/* /' .tmp/final-notes.txt > .tmp/final-notes.normalized.txt + mv .tmp/final-notes.normalized.txt .tmp/final-notes.txt # Safety check: verify we have content if [ ! -s .tmp/final-notes.txt ]; then echo "⚠️ No valid release notes extracted, using minimal fallback" - echo "- Release ${{ steps.payload.outputs.version }}" > .tmp/final-notes.txt + echo "* Release ${{ steps.payload.outputs.version }}" > .tmp/final-notes.txt fi echo "=== Final release notes ===" diff --git a/Directory.Packages.props b/Directory.Packages.props index 6750b6eeb2..fa7901052f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,6 +23,7 @@ + @@ -54,66 +55,66 @@ - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -122,7 +123,7 @@ - + @@ -132,11 +133,11 @@ - - - - - + + + + + @@ -150,7 +151,7 @@ - + @@ -169,17 +170,17 @@ - + - - + + - - - + + + @@ -194,6 +195,6 @@ - + \ No newline at end of file diff --git a/common.props b/common.props index 179253321d..2548b7c9af 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 10.3.0 - 5.3.0 + 10.4.1 + 5.4.1 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/POST.md b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/POST.md new file mode 100644 index 0000000000..95a83adec6 --- /dev/null +++ b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/POST.md @@ -0,0 +1,214 @@ +# ABP Platform 10.4 RC Has Been Released + +We are happy to release [ABP](https://abp.io) version **10.4 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.4! Thanks to you in advance. + +## Get Started with the 10.4 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). + +> The v10.4 RC versions of ABP Studio and the ABP CLI are still being tested and will be released shortly. + +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](studio-switch-to-preview.png) + +## Migration Guide + +There are no explicitly marked breaking changes in this version. However, there are still some important migration notes for specific scenarios. Please check the migration guide if you are upgrading from v10.3 or earlier: [ABP Version 10.4 Migration Guide](https://abp.io/docs/10.4/release-info/migration-guides/abp-10-4). + +## What's New with ABP v10.4? + +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: + +- URL-Based Localization +- Localization File Splitting +- Blazor UI: MudBlazor Support +- Identity: Single-Use Email/SMS 2FA Token Providers +- Account Pro: Passwordless Email Login +- AI Management: MCP Server Enhancements +- LeptonX: URL-Based Localization and Theme Improvements +- Dependency and Security Updates + +### URL-Based Localization + +ABP v10.4 introduces URL-based localization support. You can now embed the culture directly in the URL path, such as `/tr/products` or `/en/about`. + +This is especially useful for public websites, documentation sites, e-commerce applications, and any application that needs SEO-friendly and shareable localized URLs. Instead of relying only on query string, cookie, or browser language detection, the selected culture can be part of the URL itself. + +You can enable it with a single configuration: + +```csharp +Configure(options => +{ + options.UseRouteBasedCulture = true; +}); +``` + +When enabled, ABP automatically handles route registration, URL generation, menu links, and language switching for MVC/Razor Pages, Blazor, and Angular UIs. + +For Angular applications, route trees can be wrapped with `withOptionalRouteCulturePrefix` so the same route configuration can handle both `/identity/users` and `/en/identity/users`: + +```typescript +import { Routes } from '@angular/router'; +import { withOptionalRouteCulturePrefix } from '@abp/ng.core'; + +const appRoutesCore: Routes = [ + // ... your routes +]; + +export const appRoutes = withOptionalRouteCulturePrefix(appRoutesCore); +``` + +For Blazor applications, ABP built-in module pages already include culture-aware route variants. If you have your own Blazor pages, add culture route variants manually: + +```razor +@page "/Products" +@page "/{culture}/Products" +``` + +> See the [URL-Based Localization](https://abp.io/docs/10.4/framework/fundamentals/url-based-localization) documentation and [#25174](https://github.com/abpframework/abp/pull/25174) for details. + +### Localization File Splitting + +ABP localization resources can now use multiple JSON files for the same culture. This is useful for large modules or applications where keeping all localization texts in a single `en.json` file becomes difficult to maintain. + +For example, you can split a resource by feature: + +```text +Localization/ ++-- MyResource/ + +-- en.json + +-- en_Authors.json + +-- en_Books.json + +-- en_Users.json +``` + +ABP merges these files into the same localization dictionary. Files are sorted by name before merging, and if the same key exists in multiple files, the value from the last file wins. + +> See the [Localization](https://abp.io/docs/10.4/framework/fundamentals/localization) documentation and [#25227](https://github.com/abpframework/abp/pull/25227) for details. + +### Blazor UI: MudBlazor Support + +ABP v10.4 starts the [MudBlazor](https://mudblazor.com/) integration work for the Blazor UI stack. + +This release adds MudBlazor-based package infrastructure, template integration, and module/theme support needed to build ABP Blazor applications with MudBlazor. Blazorise and MudBlazor are now supported side by side, the LeptonX theme works with both UI libraries, and when creating a new Blazor project you can pick which UI library to use. + +This is a major UI foundation change, so we especially encourage Blazor users to try the RC and share feedback before the stable release. + +***Selecting the UI library when creating a new Blazor project in ABP Studio:*** + +![mud-studio](mud-studio.png) + +***MudBlazor-based application home page:*** + +![mud-index](mud-index.png) + +***MudBlazor-based Identity management page:*** + +![mud-identity](mud-identity.png) + +> See [#25235](https://github.com/abpframework/abp/pull/25235) for details. + +### Identity: Single-Use Email/SMS 2FA Token Providers + +ABP v10.4 improves the security model for email and SMS two-factor authentication codes. + +Email and phone verification codes now use ABP's single-use token providers. Generated codes are encrypted, stored with an absolute expiration time, and consumed after successful validation. Generating a new code invalidates the previous one. + +You can configure token lifetime and code length: + +```csharp +Configure(options => +{ + options.TokenLifespan = TimeSpan.FromMinutes(5); + options.CodeLength = 8; +}); + +Configure(options => +{ + options.TokenLifespan = TimeSpan.FromMinutes(2); +}); +``` + +The authenticator app provider is not affected and continues to use the standard TOTP approach. + +> See the [Two Factor Authentication](https://abp.io/docs/10.4/modules/identity/two-factor-authentication) documentation and [#25316](https://github.com/abpframework/abp/pull/25316) for details. + +### Account Pro: Passwordless Email Login + +ABP Commercial v10.4 RC introduces passwordless email login for the Account Pro module. + +Users can sign in by receiving an email login link and/or a one-time password (OTP), depending on the configured login type. Administrators can enable the feature, choose the login mode, and configure token lifetime from the account settings. + +![account-settings](account-settings.png) + +The feature is designed with security in mind: + +- Login links and OTPs are single-use. +- Resending a login email invalidates previous tokens. +- Token operations respect the current tenant context. +- Rate limiting helps protect against brute-force and email spam scenarios. +- Email enumeration behavior follows the existing account security setting. + +This feature is especially useful for applications that want a smoother sign-in experience without removing the tenant-aware and security-focused account flow of ABP. + +***"Login via email":*** + +![login-via-email](login-via-email.png) + +***Type the One-time Password (OTP) to login:*** + +![login-via-email2](login-via-email2.png) + +### AI Management: MCP Server Enhancements + +The [AI Management module](https://abp.io/docs/latest/modules/ai-management) continues to improve its MCP (Model Context Protocol) support. + +In this release, MCP server configuration has been enhanced for `stdio` transport scenarios and workspace relationships. This makes it easier to connect local or process-based MCP servers to AI workspaces and use their tools from the chat playground. + +### LeptonX: URL-Based Localization and Theme Improvements + +LeptonX has been updated to work with the new URL-based localization flow across UI types, including Angular language switching and culture-aware navigation. + +This release also includes several theme improvements and fixes, such as PathBase-safe menu links, improved custom select synchronization, sidebar menu re-binding after async rendering, and MudBlazor-related theme support. + +### Dependency and Security Updates + +ABP v10.4 RC includes several dependency updates and security-related package bumps: + +- OpenIddict upgraded to **7.5.0** +- MongoDB.Driver upgraded to **3.8.0** +- Microsoft/System package updates for CVE-2026-40372 +- `System.Security.Cryptography.Xml` upgraded to **10.0.6** +- `@abp/lodash` lodash dependency updated + +> Check [Package Version Changes](https://abp.io/docs/10.4/package-version-changes) document for all updates. + +### Other Improvements and Enhancements + +- **Virtual File System**: `ReplaceEmbeddedByPhysical` can now receive exclusion filters, which gives developers more control over included/excluded physical files during development ([#25284](https://github.com/abpframework/abp/pull/25284)). +- **Exception logging**: Complex objects in exception data are now serialized more clearly in logs ([#25267](https://github.com/abpframework/abp/pull/25267)). +- **Feature management**: Improved batch state checker performance and added `RequireFeaturesSimpleBatchStateChecker` ([#25276](https://github.com/abpframework/abp/pull/25276)). +- **RabbitMQ**: Fixed a potential hang while acquiring a closed channel after RabbitMQ restart ([#25311](https://github.com/abpframework/abp/pull/25311)). +- **Shared user accounts**: Improved shared-user lookup and two-factor authentication flows for shared user scenarios. +- **Account and SaaS modules**: Improved shared-user invitation and account-page flows in tenant user sharing scenarios. + +## Community News + +### New ABP Community Articles + +As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: + +- [Stop Sprinkling [RequiresFeature] Everywhere — A Centralized Feature Gate for ABP.IO](https://abp.io/community/articles/stop-sprinkling-requiresfeature-everywhere-a-centralized-7znie818) by [Mohammad AlMohammad AlMahmoud](https://abp.io/community/members/Mohammad97Dev) +- [Top AI Coding Models in 2026: Which One Should Developers Actually Use?](https://abp.io/community/articles/top-ai-coding-models-in-2026-which-one-should-developers-use-rivh8x15) by [Alper Ebiçoğlu](https://abp.io/community/members/alper) + +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.4/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.4 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! \ No newline at end of file diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/account-settings.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/account-settings.png new file mode 100644 index 0000000000..49f483f795 Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/account-settings.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/cover-image.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/cover-image.png new file mode 100644 index 0000000000..9ebe8ca13a Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/login-via-email.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/login-via-email.png new file mode 100644 index 0000000000..026c1af5be Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/login-via-email.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/login-via-email2.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/login-via-email2.png new file mode 100644 index 0000000000..02a5702b98 Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/login-via-email2.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-identity.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-identity.png new file mode 100644 index 0000000000..76f3cdf37c Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-identity.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-index.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-index.png new file mode 100644 index 0000000000..b123c69f13 Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-index.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-studio.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-studio.png new file mode 100644 index 0000000000..8c8cde3732 Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/mud-studio.png differ diff --git a/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/studio-switch-to-preview.png b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/studio-switch-to-preview.png new file mode 100644 index 0000000000..62fd4d165e Binary files /dev/null and b/docs/en/Blog-Posts/2026-04-29 v10_4_Preview/studio-switch-to-preview.png 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 index 43dee7c712..c54b50c881 100644 --- a/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/article.md +++ b/docs/en/Community-Articles/2026-03-10-Tutorial-Validator/article.md @@ -1,4 +1,6 @@ -# Automatically Validate Your Documentation: How We Built a Tutorial Validator +# Automatically Validate Your Documentation: How We Built an AI Tutorial Validator + +> If you're in a hurry and want to quickly check the repository, you can find the source code of the AI Tutorial Validator here 👉 [github.com/abpframework/ai-tutorial-validator](https://github.com/abpframework/ai-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. @@ -34,7 +36,7 @@ It treats tutorials like testable workflows, ensuring that every step works exac ## How the Tutorial Validator Works? -the tutorial validator validates tutorials using a three-stage pipeline: +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 @@ -43,7 +45,7 @@ the tutorial validator validates tutorials using a three-stage pipeline: ![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. +The key idea is simple: if a developer needs 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) @@ -108,6 +110,6 @@ If your team maintains technical tutorials, this project can give you a practica --- -You can find the source-code of the tutorial validator at this repo 👉 https://github.com/abpframework/tutorial-validator +You can find the source code of the tutorial validator at this repo 👉 [github.com/abpframework/ai-tutorial-validator](https://github.com/abpframework/ai-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-17-Shared-User-Accounts-in-ABP/POST.md b/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/POST.md index dcb69c289d..4dc27218c0 100644 --- 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 @@ -50,6 +50,8 @@ After signing into a tenant, a tenant switcher appears in the user menu — clic 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. +The same soft removal is available to a tenant admin from the user list — a **Remove from tenant** action that takes a user off the tenant without touching the global account. Useful for the obvious case: an employee leaves the company, the admin removes them from the tenant, but their account (and any other tenant they belong to) stays intact. + 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 diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/POST.md b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/POST.md new file mode 100644 index 0000000000..e8a5d16015 --- /dev/null +++ b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/POST.md @@ -0,0 +1,167 @@ +# SEO-Friendly Localized URLs in ABP with a Single Line of Configuration + +ABP has always supported language switching via the `?culture=en` query string and the culture cookie. That works fine for most applications — but it has a limitation that shows up quickly once SEO or link-sharing matters. + +Consider a book-store app where users browse in their language: + +- A Spanish user shares a product link. The recipient opens it in English because the cookie on *their* machine says `en`. +- Search engines crawl the same URL in every language, making it impossible to create separate sitemaps per locale. +- A user shares a link like `/Books/Detail?id=42&culture=es`. When the server processes the request, it sets the culture cookie and then redirects to `/Books/Detail?id=42` — stripping the `?culture=` parameter. The shared link no longer carries the intended language. + +Embedding the culture in the URL path — `/es/books`, `/zh-Hans/about` — solves all three. Each language has its own stable URL, readable by humans and index-friendly for search engines. + +ABP supports this out of the box. You opt in with a single configuration property, and the framework takes care of routing, URL generation, menu links, and language switching automatically. + +## Enabling URL-Based Localization + +In your ABP module class, add: + +```csharp +Configure(options => +{ + options.UseRouteBasedCulture = true; +}); +``` + +That is the only change you need to make. + +## MVC / Razor Pages + +MVC and Razor Pages have the most complete support — everything works automatically. No code changes needed in your pages or controllers. + +![MVC sample — English](images/mvc-home-en.png) + +![MVC sample — Turkish](images/mvc-home-tr.png) + +## What Happens Automatically + +When you set `UseRouteBasedCulture = true`, ABP automatically: + +- Registers ASP.NET Core's built-in [`RouteDataRequestCultureProvider`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider) to detect culture from the URL path. +- Adds a `{culture}/{controller}/{action}` conventional route for MVC controllers, with a route constraint to prevent non-culture URL segments (like `/enterprise/products`) from matching. +- Adds `{culture}/...` route selectors to all Razor Pages at startup. +- Injects the current culture into all `Url.Page()` and `Url.Action()` calls, so generated URLs automatically include the culture prefix. +- Prepends the culture prefix to navigation menu item URLs. + +You do not need to configure these individually. + +## URL Generation Just Works + +In a Razor Page or view running under a culture-prefixed URL (say, `/zh-Hans/Books`), you do not need to pass a `culture` parameter anywhere: + +```cshtml +@Url.Page("/Books/Detail", new { id = book.Id }) +@* Generates: /zh-Hans/Books/Detail?id=42 *@ + +@Url.Action("About", "Home") +@* Generates: /zh-Hans/Home/About *@ +``` + +If you explicitly pass a different `culture` value, that takes precedence — so cross-language links are also straightforward: + +```cshtml +@Url.Page("/Books/Index", new { culture = "tr" }) +@* Generates: /tr/Books *@ +``` + +## Language Switching + +The built-in ABP language switcher already works with route-based culture. When a user switches language, the culture segment in the URL is automatically replaced: + +| Current URL | Switch to | Redirect to | +|---|---|---| +| `/tr/books` | `en` | `/en/books` | +| `/zh-Hans/about` | `en` | `/en/about` | +| `/tenant-a/zh-Hans/about` | `en` | `/tenant-a/en/about` | + +No theme changes, no language switcher changes — the existing UI component just works. + +## Blazor Support + +Blazor Server and Blazor WebAssembly (WebApp) both support URL-based localization. Culture detection and cookie persistence work automatically on the initial page load (SSR). Menu URLs and language switching also work automatically. + +![Blazor Server sample](images/blazor-server-zh-hans.png) + +![Blazor WebApp sample](images/blazor-webapp-tr.png) + +ABP's built-in module pages (Identity, Settings, etc.) also work with URL-based localization out of the box: + +![Identity module — User Management](images/module-identity-users.png) + +### Manual step: Blazor component routes + +The only manual step for Blazor is adding `@page "/{culture}/..."` routes to your own pages. ASP.NET Core does not support automatically adding route selectors to Blazor components (unlike Razor Pages), so you must add them explicitly: + +```razor +@page "/" +@page "/{culture}" + +@code { + [Parameter] + public string? Culture { get; set; } +} +``` + +```razor +@page "/Products" +@page "/{culture}/Products" + +@code { + [Parameter] + public string? Culture { get; set; } +} +``` + +> **ABP's built-in module pages** (Identity, Tenant Management, Settings, Account, etc.) already ship with `@page "/{culture}/..."` route variants. You only need to add these routes to your own application pages. + +### Blazor WebApp (WASM) configuration + +The WASM client project does not need any `UseRouteBasedCulture` configuration. It reads the setting from the server automatically. + +```csharp +// Server project — the only place you need to configure +Configure(options => +{ + options.UseRouteBasedCulture = true; +}); +``` + +## Multi-Tenancy + +URL-based localization is fully compatible with ABP's multi-tenant routing. Language switching supports tenant-prefixed URLs, so `/tenant-a/zh-Hans/About` correctly switches to `/tenant-a/en/About` without any additional configuration. + +## UI Framework Support Overview + +| UI Framework | Route Registration | URL Generation | Menu URLs | Language Switch | Manual Work | +|---|---|---|---|---|---| +| **MVC / Razor Pages** | Automatic | Automatic | Automatic | Automatic | None | +| **Blazor Server** | Manual `@page` routes | N/A | Automatic | Automatic | Add `{culture}` route to pages | +| **Blazor WebApp (WASM)** | Manual `@page` routes | N/A | Automatic | Automatic | Add `{culture}` route to pages | + +## Running the Sample + +A runnable sample is available at [abp-samples/UrlBasedLocalization](https://github.com/abpframework/abp-samples/tree/master/UrlBasedLocalization), with three projects: + +| Project | UI Type | URL | Command | +|---|---|---|---| +| `BookStore.Mvc` | MVC / Razor Pages | `https://localhost:44335` | `dotnet run --project src/BookStore.Mvc` | +| `BookStore.Blazor.Server` | Blazor Server | `https://localhost:44336` | `dotnet run --project src/BookStore.Blazor.Server` | +| `BookStore.Blazor.WebApp` | Blazor WebApp (InteractiveAuto) | `https://localhost:44337` | `dotnet run --project src/BookStore.Blazor.WebApp` | + +Supported languages: English, Türkçe, Français, 简体中文. + +## Summary + +To add SEO-friendly localized URL paths to your ABP application: + +1. Set `options.UseRouteBasedCulture = true` in your module. +2. For **Blazor** projects, add `@page "/{culture}/..."` routes to your own pages. + +Everything else — route registration, URL generation, menu links, and language switching — is handled automatically. + +## References + +- [URL-Based Localization — ABP Documentation](https://abp.io/docs/latest/framework/fundamentals/url-based-localization) +- [Localization — ABP Documentation](https://abp.io/docs/latest/framework/fundamentals/localization) +- [abp-samples/UrlBasedLocalization — GitHub](https://github.com/abpframework/abp-samples/tree/master/UrlBasedLocalization) +- [Request Localization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization/select-language-culture) diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/cover.png b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/cover.png new file mode 100644 index 0000000000..fcea11e4d6 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/cover.png differ diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/blazor-server-zh-hans.png b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/blazor-server-zh-hans.png new file mode 100644 index 0000000000..7db95b4b16 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/blazor-server-zh-hans.png differ diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/blazor-webapp-tr.png b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/blazor-webapp-tr.png new file mode 100644 index 0000000000..91008fd572 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/blazor-webapp-tr.png differ diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/module-identity-users.png b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/module-identity-users.png new file mode 100644 index 0000000000..bd4b996df4 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/module-identity-users.png differ diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/mvc-home-en.png b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/mvc-home-en.png new file mode 100644 index 0000000000..e9bc6a9cbd Binary files /dev/null and b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/mvc-home-en.png differ diff --git a/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/mvc-home-tr.png b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/mvc-home-tr.png new file mode 100644 index 0000000000..01bee42b31 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-29-Url-Based-Localization/images/mvc-home-tr.png differ diff --git a/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/Post.md b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/Post.md new file mode 100644 index 0000000000..fe4bc61636 --- /dev/null +++ b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/Post.md @@ -0,0 +1,254 @@ +## Introduction + +AI coding tools went from “cool autocomplete” to “basically your junior dev (who never sleeps)” in just a couple of years. + +In 2026, the landscape is **crowded, competitive, and honestly a bit confusing**. Every model claims to be the best at coding—but depending on what you actually *do* (APIs, frontend, DevOps, debugging), the “best” can change fast. + +So instead of hype, let’s break down the **top AI coding models in 2026**, ranked by: + +* Real-world dev usefulness +* Code quality & correctness +* Context handling +* Tooling ecosystem + +We'll check the AI models against these topics: + +![](pic1.jpg) + +--- + +## 🏆 1. GPT-5.4 (OpenAI) — The All-Round Beast + +Let’s not dance around it—**GPT-5.4 is still the most versatile coding model right now.** + +### Why it’s #1 + +* Extremely strong across **all languages** +* Handles **large codebases** without losing context +* Excellent at: + + * Refactoring + * Architecture suggestions + * Debugging complex issues + +### Where it shines + +* Full-stack development +* API design +* Writing clean, production-ready code + +### Where it struggles + +* Occasionally over-engineers solutions +* Can be slower than lightweight models + +### As a result; + +If you want a **default “just works” coding AI**, this is it. + +--- + +## 🥈 2. Claude 4.7 (Anthropic) — The Clean Code Specialist + +Claude 4.7 has built a reputation for writing code that feels like it came from a senior engineer who drinks too much coffee but cares deeply about readability. + +### Strengths + +* Beautiful, readable code +* Strong reasoning for: + + * Refactoring + * Code reviews + * Documentation + +### Killer feature + +* Massive context window → great for: + + * Large repositories + * Long discussions + * System design + +### Weak spots + +* Slightly less aggressive in solving edge-case bugs +* Sometimes too “safe” in decisions + +### As a result; + +Perfect if you care about **maintainability over raw speed**. + +--- + +## 🥉 3. Gemini 3.1 (Google) — The Multimodal Powerhouse + +Gemini 3.1 is where things get interesting. + +This isn’t just a coding model—it’s a **multi-input problem solver**. + +### What makes it different + +* Understands: + + * Code + * Screenshots + * Diagrams + * Logs + +### Where it dominates + +* Debugging UI issues from screenshots +* DevOps + cloud workflows +* Cross-referencing documentation + +### Downsides + +* Code style can be inconsistent +* Sometimes less deterministic than GPT-5 + +### As a result; + +If your workflow includes **visual debugging or cloud-heavy systems**, this is insanely useful. + +--- + +## ⚡ 4. Mistral Code (Open Models) — The Speed King + +Mistral AI’s coding models are gaining serious attention. + +### Why devs love it + +* Fast +* Cheap (or free if self-hosted) +* Great for: + + * Autocomplete + * Small functions + * Local development + +### Trade-offs + +* Not as strong in deep reasoning +* Limited compared to closed models + +### As a result; + +Best choice for: + +* Privacy-sensitive environments +* Offline/local setups +* Lightweight coding tasks + +--- + +## 🧠 5. Code Llama 4 — The Open-Source Veteran + +Code Llama 4 is still very relevant, especially in enterprise setups. + +### Strengths + +* Fully open-source +* Customizable & fine-tunable +* Good baseline performance + +### Weaknesses + +* Behind top-tier models in reasoning +* Needs tuning for best results + +### As a result; + +If your company says “no cloud AI,” this is your friend. + +--- + +## 📊 Comparison Table Between AI Models + +| Model | Best For | Weakness | +| ------------ | ------------------------ | --------------------- | +| GPT-5.4 | Everything | Slightly slower | +| Claude 4.7 | Clean, maintainable code | Less aggressive fixes | +| Gemini 3.1 | Multimodal workflows | Inconsistent style | +| Mistral Code | Speed & local usage | Shallow reasoning | +| Code Llama 4 | Open-source flexibility | Needs tuning | + +Image Prompt: +A sleek table-style infographic comparing AI models with icons, performance bars, and labels like “Best for speed”, “Best for reasoning”. + +--- + +## 🤔 When to Use What (Real Scenarios) + +### Use GPT-5.4 if: + +* You’re building a full product +* You need architecture + implementation +* You want fewer “AI mistakes” + +--- + +### Use Claude 4.7 if: + +* You’re reviewing code +* You care about readability +* You’re working in a team + +--- + +### Use Gemini 3.1 if: + +* You debug using screenshots/logs +* You work with cloud infrastructure +* You want multimodal workflows + +--- + +### Use Mistral / Code Llama if: + +* You need local/private AI +* You want low cost +* You’re okay trading power for control + +--- + +## 🔌 Where ABP Framework Fits In + +If you're working with **ASP.NET Core and the ABP Framework**, these models can seriously boost productivity: + +* GPT-5.4 → Generate **application services, DTOs, and modules** +* Claude → Clean up **domain layer logic** +* Gemini → Help debug **UI + backend integration issues** + +The sweet spot? + +👉 Use AI to scaffold ABP layers, then refine manually. +That keeps your architecture clean while still saving hours. + +--- + +## 🚨 Reality Check + +AI coding models in 2026 are powerful—but: + +* They still hallucinate edge cases +* They don’t fully understand your business logic +* They can fix somewhere, break another +* They can not fix a bug even after you write 10 different prompts + +So yeah—**don’t ship blind**. + +Treat them like: + +> A fast junior dev… who needs code review. + +--- + +## TL;DR + +👉 There’s no single “winner”—just the best tool for your workflow. + +![](pic2.png) + +--- + +If you're experimenting with these models in real projects (especially with ABP), it's worth trying **multiple models side-by-side**. The differences become obvious *fast*. diff --git a/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/cover.png b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/cover.png new file mode 100644 index 0000000000..e21f08e5bc Binary files /dev/null and b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/cover.png differ diff --git a/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/pic1.jpg b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/pic1.jpg new file mode 100644 index 0000000000..9fc5d913ad Binary files /dev/null and b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/pic1.jpg differ diff --git a/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/pic2.png b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/pic2.png new file mode 100644 index 0000000000..95c4c84b6c Binary files /dev/null and b/docs/en/Community-Articles/2026-04-17-Top-AI-Coding-Models-2026-Rankings/pic2.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/cover.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/cover.png new file mode 100644 index 0000000000..90401eacef Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/cover.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png new file mode 100644 index 0000000000..06416698a7 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-identity-users.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-identity-users.png new file mode 100644 index 0000000000..17ab8684ea Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-identity-users.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-dashboard.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-dashboard.png new file mode 100644 index 0000000000..586cfaaed7 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-dashboard.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png new file mode 100644 index 0000000000..5e77d998a0 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-permission-management.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-permission-management.png new file mode 100644 index 0000000000..b1842a6727 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-permission-management.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-saas-tenants.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-saas-tenants.png new file mode 100644 index 0000000000..87bae5b787 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-saas-tenants.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png new file mode 100644 index 0000000000..c03c045682 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-first-run.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-first-run.png new file mode 100644 index 0000000000..02b0e8e264 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-first-run.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png new file mode 100644 index 0000000000..3864805c0d Binary files /dev/null and b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png differ diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md new file mode 100644 index 0000000000..ca3098f8a1 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md @@ -0,0 +1,213 @@ +# ABP 10.4.2 Expands Blazor UI Options with MudBlazor Support + +With ABP 10.4.2, new Blazor projects can now use **MudBlazor** (Material Design) as an alternative to the long-standing default, **Blazorise** (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.4.2 packages are live on nuget.org. + +## Why add another Blazor UI library? + +Blazorise has been ABP's default Blazor UI library for years and **remains the default and is fully supported** — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.4 does not change anything for them. + +We added MudBlazor because one Blazor UI choice cannot fit every team: + +- **Design language** — Bootstrap and Material Design serve different audiences, and forcing a single choice does not fit every team +- **Open-source preference** — MudBlazor is MIT-licensed, which works well for teams that want an open-source frontend component stack without extra component-library licensing or compliance overhead +- **Ecosystem fit** — Material Design third-party components (charts, rich text editors, data visualization, and so on) tend to integrate more naturally with a MudBlazor project + +For new projects you can start with MudBlazor right away. Existing Blazorise projects do not need to be rewritten just to switch UI libraries. + +### Who should consider MudBlazor? + +- Teams that want the frontend component stack **fully open source** with no licensing to manage (individual developers, open-source community projects, education / learning settings) +- Organizations with internal **third-party dependency or supply-chain compliance** requirements that prefer MIT-licensed components +- New Blazor projects that want to start with **Material Design** +- Teams already comfortable with the **MudBlazor ecosystem** (charts, rich text, rich UI components) + +## What the MudBlazor option covers + +### Framework core + +`Volo.Abp.MudBlazorUI` provides the MudBlazor implementation of ABP's UI service abstractions, so code written against `IUiMessageService` / `IUiNotificationService` / `IUiPageProgressService` runs unchanged in a MudBlazor project. Key building blocks: + +- `MudBlazorUiMessageService` — `Info` / `Success` / `Warn` / `Error` / `Confirm` rendered through `MudDialog` +- `MudBlazorUiNotificationService` — toast notifications via `MudSnackbar` +- `MudBlazorUiPageProgressService` — top progress bar via `MudProgressLinear` +- `AbpMudCrudPageBase<...>` — the MudBlazor counterpart to Blazorise's `AbpCrudPageBase` +- `AbpMudExtensibleDataGrid` — a `MudDataGrid` wrapper integrated with Object Extension and time-zone conversion +- `UiMessageAlert` / `UiNotificationAlert` / `PageAlert` — page-level alert and notification containers + +Theming is split across three hosts — Blazor Server, WebAssembly, and MauiBlazor — each shipped with matching bundling contributors and modules that wire MudBlazor's JS and CSS into the ABP bundle system. + +### Three themes + +- **LeptonX MudBlazor** +- **LeptonX Lite MudBlazor** +- **Basic Theme MudBlazor** + +Each theme's layout adopts MudBlazor components such as `MudAppBar`, `MudDrawer`, `MudNavLink`, and `MudMenu`, while keeping the theme's original color palette, dim / light / system modes, and RTL support. + +![LeptonX MudBlazor Dashboard](mud-leptonx-dashboard.png) +*LeptonX rendered with MudBlazor* + +![LeptonX Lite MudBlazor Dashboard](mud-leptonx-lite-dashboard.png) +*LeptonX Lite rendered with MudBlazor* + +![Basic Theme MudBlazor Dashboard](mud-basic-theme-dashboard.png) +*Basic Theme rendered with MudBlazor* + +The LeptonX themes reuse the same `lpx-*` CSS classes across both UI libraries, so the overall information architecture, page layout, and theme experience stay consistent with the Blazorise version. Individual controls follow each UI library's own conventions. + +![Blazorise vs MudBlazor on the same LeptonX theme](mud-vs-blazorise-leptonx.png) +*The same LeptonX theme — MudBlazor on the left, Blazorise on the right* + +### Module coverage + +Open-source modules in `abpframework/abp` that ship with a MudBlazor implementation: + +- **Account** +- **Identity** — Users / Roles / OUs / ClaimTypes +- **Permission Management** — parent/child permissions with `MudTreeView` and tri-state `MudCheckBox` +- **Setting Management** — grouped settings with `MudTabs` (including theme switching) +- **Tenant Management** +- **Feature Management** + +Additional MudBlazor implementations available on the Pro side, for example: + +- **Identity Pro** — extra management around Sessions, SecurityLogs, and more +- **OpenIddict Pro** — Application / Scope management +- **Saas** — Tenant / Edition management with a connection-string dialog +- **Audit Logging** — `MudDataGrid` with a detail `MudDialog` +- **Language Management** / **Text Template Management** +- **File Management** / **Chat** / **CMS Kit Pro** +- **AI Management** / **GDPR** / **Payment**, and more + +![Identity user management with MudDataGrid](mud-identity-users.png) +*Identity user management built on `AbpMudExtensibleDataGrid`* + +![Permission management modal](mud-permission-management.png) +*Permission Management uses `MudTreeView` and tri-state `MudCheckBox` for parent/child permissions* + +![Saas tenants list](mud-saas-tenants.png) +*Saas module: tenant list with a "New tenant" dialog that includes connection-string editing* + +### Component mapping at a glance + +If you already know Blazorise, here are the most common mappings: + +| Blazorise | MudBlazor | +|-----------|-----------| +| `TextEdit @bind-Text` | `MudTextField @bind-Value` | +| `Select / SelectItem` | `MudSelect / MudSelectItem` | +| `DataGrid` | `MudDataGrid` (wrapped by ABP as `AbpMudExtensibleDataGrid`) | +| `Modal Show()/Hide()` | `MudDialog ShowAsync()/CloseAsync()` | +| `Validations` | `MudForm` + built-in validation | +| `Row / Column ColumnSize.Is6` | `MudGrid / MudItem xs="12" sm="6"` | +| Bootstrap Icons `bi-*` | `Icons.Material.Filled.*` | + +A full mapping table with razor examples lives in the [ABP Blazor UI documentation](https://abp.io/docs/latest/framework/ui/blazor). + +### Supported Blazor project types + +ABP's MudBlazor support covers the Blazor project types you can create and run directly: + +- **Blazor Server** (`-u blazor-server`) +- **Blazor WebAssembly** (`-u blazor`) +- **Blazor WebApp** (`-u blazor-webapp`, including InteractiveAuto) + +### ABP Suite + +ABP Suite detects the solution's UI library and generates the matching CRUD page automatically: + +```csharp +public partial class Books : AbpMudCrudPageBase +{ + private MudDialog _createDialog; + private MudForm _createFormRef; +} +``` + +The razor templates also split by UI library — Blazorise uses `` + `` + ``, MudBlazor uses `` + `` + ``. + +## Choosing between Blazorise and MudBlazor + +Both UI libraries are production-ready and neither is strictly better. Common factors: + +- **Familiarity** — teams comfortable with Bootstrap tend to stay on Blazorise; teams comfortable with Material Design pick MudBlazor +- **Design system** — Bootstrap-style products lean toward Blazorise, Material Design products lean toward MudBlazor +- **Ecosystem** — existing Bootstrap component libraries or design assets fit Blazorise; Material Design third-party components fit MudBlazor more naturally +- **Existing projects** — keep maintaining live Blazorise projects as they are; if you want to try MudBlazor, start a new project with it +- **Licensing** — the two UI libraries have different license terms, so check each library's official license page before making a choice ([Blazorise](https://blazorise.com/license) / [MudBlazor](https://github.com/MudBlazor/MudBlazor/blob/dev/LICENSE)) + +Do not mix the two libraries within a single project — the choice is per solution, not per file. + +## Creating a MudBlazor project in ABP Studio + +### ABP Studio (recommended) + +Open ABP Studio → **New Solution** → pick a template → in the UI configuration step, select **Blazor UI library = MudBlazor**. Everything else works the same as a Blazorise project. After Build & Run you land on a MudBlazor-styled application. + +![ABP Studio New Solution wizard with MudBlazor selected](mud-studio-blazor-ui-library-dropdown.png) +*New Solution wizard: pick MudBlazor for the Blazor UI library* + +![First run after creation](mud-studio-first-run.png) +*Studio Build & Run brings up a MudBlazor + LeptonX dashboard in the embedded browser* + +### CLI + +```bash +# Blazorise (default; --blazor-ui-library can be omitted) +abp new MyApp -u blazor + +# MudBlazor +abp new MyApp -u blazor --blazor-ui-library mudblazor + +# Tiered + WebApp + LeptonX + MudBlazor +abp new MyApp -t app --tiered -u blazor-webapp --blazor-ui-library mudblazor --theme leptonx + +# Microservice + MudBlazor + Blazor Server +abp new MyApp -t microservice -u blazor-server --blazor-ui-library mudblazor + +# Reusable Module + MudBlazor +abp new My.Module -t module -u blazor --blazor-ui-library mudblazor +``` + +Run `abp new --help` for the full option list. + +Suite-generated MudBlazor CRUD pages are covered in the **ABP Suite** section above. + +--- + +## Try it out + +```bash +abp new MyMudApp -u blazor-server --blazor-ui-library mudblazor --theme leptonx +``` + +Documentation: + +- [Forms & Validation (MudBlazor)](https://abp.io/docs/latest/framework/ui/blazor/forms-validation?BlazorUI=MudBlazor) +- [LeptonX with MudBlazor](https://abp.io/docs/latest/ui-themes/lepton-x/blazor) +- [Basic Theme MudBlazor variant](https://abp.io/docs/latest/framework/ui/blazor/basic-theme) +- [Page Header (MudBlazor)](https://abp.io/docs/latest/framework/ui/blazor/page-header) + +## FAQ + +**I'm already using Blazorise — will upgrading to 10.4 / 10.4.2 break my project?** +No. Blazorise stays the default, and package paths, type names, and namespaces are fully compatible. Follow the standard ABP upgrade flow. + +**Can I use Blazorise and MudBlazor in the same project?** +We don't recommend it. The UI library is a project-level choice — themes, bundling, and module dependencies all switch with it. Mixing both within a single solution leads to bundle conflicts, duplicated layouts, and similar issues. + +**What about my custom razor pages?** +Your custom Razor pages are tied to the UI library they were built with, so switching libraries means rewriting those pages using the component mapping above. Template-generated pages and module-provided pages don't need to be touched. + +## Wrapping up + +MudBlazor is now a first-class Blazor UI library in ABP. With 10.4.2 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single `abp new` command. + +If you hit a bug, have a suggestion, or want a particular module's MudBlazor UX prioritized, let us know via [GitHub Issues](https://github.com/abpframework/abp/issues) or [abp.io support](https://abp.io/support). + +## References + +- [MudBlazor official site](https://mudblazor.com) +- [ABP Blazor UI documentation](https://abp.io/docs/latest/framework/ui/blazor) +- [ABP LeptonX theme](https://abp.io/themes/leptonx) +- [ABP Studio download](https://abp.io/studio) diff --git a/docs/en/cli/differences-between-old-and-new-cli.md b/docs/en/cli/differences-between-old-and-new-cli.md index 440b785797..2d1cef6f3b 100644 --- a/docs/en/cli/differences-between-old-and-new-cli.md +++ b/docs/en/cli/differences-between-old-and-new-cli.md @@ -7,9 +7,9 @@ # Old ABP CLI vs New ABP CLI -ABP CLI (Command Line Interface) is a command line tool to perform some common operations for ABP based solutions or ABP Studio features. With **v8.2+**, the old/legacy ABP CLI has been replaced with a new [CLI](index.md) system to align with the new templating system and [ABP Studio](../studio/index.md). Also, some superior features/commands have been introduced with the new CLI, such as `kube-connect` and `kube-intercept` commands. +ABP CLI (Command Line Interface) is a command line tool to perform some common operations for ABP based solutions or ABP Studio features. With **v8.2+**, the old/classic ABP CLI has been replaced with a new [CLI](index.md) system to align with the new templating system and [ABP Studio](../studio/index.md). Also, some superior features/commands have been introduced with the new CLI, such as `kube-connect` and `kube-intercept` commands. -In this guide, you will learn the motivation behind this change, some questions that you may have, how to use the old/legacy CLI, its features, and more... +In this guide, you will learn the motivation behind this change, some questions that you may have, how to use the old/classic CLI, its features, and more... ## Reason For The Change diff --git a/docs/en/cli/index.md b/docs/en/cli/index.md index 69740c19a0..8196ba993e 100644 --- a/docs/en/cli/index.md +++ b/docs/en/cli/index.md @@ -9,74 +9,75 @@ ABP CLI (Command Line Interface) is a command line tool to perform some common operations for ABP based solutions or [ABP Studio](../studio/index.md) features. +This document describes `Volo.Abp.Studio.Cli`, the ABP CLI package that works with the ABP Studio template system. Modern templates, including React UI support, are available through this package. If you need to run the classic `Volo.Abp.Cli`, pass `--old` at the end of the command. + ## Installation ABP CLI is a [dotnet global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools). Install it using a command line window: -````bash +```bash dotnet tool install -g Volo.Abp.Studio.Cli -```` +``` To update an existing installation: -````bash +```bash dotnet tool update -g Volo.Abp.Studio.Cli -```` +``` ## Global Options While each command may have a set of options, there are some global options that can be used with any command: -* `--skip-cli-version-check` or `-scvc`: Skips checking the latest version of the ABP CLI. If you don't specify, it will check the latest version and shows a warning message if there is a newer version of the ABP CLI. +- `--skip-cli-version-check` or `-scvc`: Skips checking the latest version of the ABP CLI. If you don't specify, it will check the latest version and shows a warning message if there is a newer version of the ABP CLI. - `--skip-extension-version-check` or `-sevc`: Skips checking the latest version of the ABP CLI extensions. If you don't specify, it will check the latest version and download the latest version if there is a newer version of the ABP CLI extensions. -* `--old`: ABP CLI has two variations: `Volo.Abp.Studio.Cli` and `Volo.Abp.Cli`. New features/templates are added to the `Volo.Abp.Studio.Cli`. But if you want to use the old version, you can use this option **at the end of your commands**. For example, `abp new Acme.BookStore --old`. -* `--help` or `-h`: Shows help for the specified command. +- `--old`: ABP CLI has two variations: `Volo.Abp.Studio.Cli` and `Volo.Abp.Cli`. New features and templates are added to `Volo.Abp.Studio.Cli`. If you want to use the old version, use this option **at the end of your commands**. For example, `abp new Acme.BookStore --old`. +- `--help` or `-h`: Shows help for the specified command. ## Commands Here is the list of all available commands before explaining their details: -* **[`help`](../cli#help)**: Shows help on the usage of the ABP CLI. -* **[`cli`](../cli#cli)**: Update or remove ABP CLI. -* **[`new`](../cli#new)**: Generates a new solution based on the ABP [startup templates](../solution-templates/index.md). -* **[`new-module`](../cli#new-module)**: Generates a new module based on the given template. -* **[`new-package`](../cli#new-package)**: Generates a new package based on the given template. -* **[`update`](../cli#update)**: Automatically updates all ABP related NuGet and NPM packages in a solution. -* **[`clean`](../cli#clean)**: Deletes all `BIN` and `OBJ` folders in the current folder. -* **[`clean-logs`](../cli#clean-logs)**: Delete all `*logs.txt` files in the current folder and its subfolders. -* **[`add-package`](../cli#add-package)**: Adds an ABP package to a project. -* **[`add-package-ref`](../cli#add-package-ref)**: Adds package to given project. -* **[`install-module`](../cli#install-module)**: Adds a [multi-package application module](../modules/index.md) to a given module. -* **[`install-local-module`](../cli#install-local-module)**: Installs a local module to given module. -* **[`list-modules`](../cli#list-modules)**: Lists names of application modules. -* **[`list-templates`](../cli#list-templates)**: Lists the names of available templates to create a solution. -* **[`get-source`](../cli#get-source)**: Downloads the source code of a module. -* **[`add-source-code`](../cli#add-source-code)**: Downloads the source code and replaces package references with project references. -* **[`init-solution`](../cli#init-solution)**: Creates ABP Studio configuration files for a given solution. -* **[`kube-connect`](../cli#kube-connect)**: Connects to kubernetes environment. (*Available for* ***Business*** *or higher licenses*) -* **[`kube-intercept`](../cli#kube-intercept)**: Intercepts a service running in Kubernetes environment. (*Available for* ***Business*** *or higher licenses*) -* **[`list-module-sources`](../cli#list-module-sources)**: Lists the remote module sources. -* **[`add-module-source`](../cli#add-module-source)**: Adds a remote module source. -* **[`delete-module-source`](../cli#delete-module-source)**: Deletes a remote module source. -* **[`generate-proxy`](../cli#generate-proxy)**: Generates client side proxies to use HTTP API endpoints. -* **[`remove-proxy`](../cli#remove-proxy)**: Removes previously generated client side proxies. -* **[`switch-to-preview`](../cli#switch-to-preview)**: Switches to the latest preview version of the ABP. -* **[`switch-to-nightly`](../cli#switch-to-nightly)**: Switches to the latest [nightly builds](../release-info/nightly-builds.md) of the ABP related packages on a solution. -* **[`switch-to-stable`](../cli#switch-to-stable)**: Switches to the latest stable versions of the ABP related packages on a solution. -* **[`switch-to-local`](../cli#switch-to-local)**: Changes NuGet package references on a solution to local project references. -* **[`upgrade`](../cli#upgrade)**: It converts the application to use pro modules. -* **[`translate`](../cli#translate)**: Simplifies to translate localization files when you have multiple JSON [localization](../framework/fundamentals/localization.md) files in a source control repository. -* **[`login`](../cli#login)**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. -* **[`login-info`](../cli#login-info)**: Shows the current user's login information. -* **[`logout`](../cli#logout)**: Logouts from your computer if you've authenticated before. -* **[`bundle`](../cli#bundle)**: Generates script and style references for ABP Blazor and MAUI Blazor project. -* **[`install-libs`](../cli#install-libs)**: Install NPM Packages for MVC / Razor Pages and Blazor Server UI types. -* **[`clear-download-cache`](../cli#clear-download-cache)**: Clears the templates download cache. -* **[`check-extensions`](../cli#check-extensions)**: Checks the latest version of the ABP CLI extensions. -* **[`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](../cli#help): Shows help on the usage of the ABP CLI. +- [cli](../cli#cli): Update or remove ABP CLI. +- [new](../cli#new): Generates a new solution based on the ABP [startup templates](../solution-templates/index.md). Use `--modern` to create solutions with the modern template system and React UI. +- [new-module](../cli#new-module): Generates a new module based on the given template. Use `--modern` to use modern module templates. +- [new-package](../cli#new-package): Generates a new package based on the given template. +- [update](../cli#update): Automatically updates all ABP related NuGet and NPM packages in a solution. +- [clean](../cli#clean): Deletes all `BIN` and `OBJ` folders in the current folder. +- [clean-logs](../cli#clean-logs): Delete all `*logs.txt` files in the current folder and its subfolders. +- [add-package](../cli#add-package): Adds an ABP package to a project. +- [add-package-ref](../cli#add-package-ref): Adds package to given project. +- [install-module](../cli#install-module): Adds a [multi-package application module](../modules/index.md) to a given module. +- [install-local-module](../cli#install-local-module): Installs a local module to given module. +- [list-modules](../cli#list-modules): Lists names of application modules. +- [list-templates](../cli#list-templates): Lists the names of available templates to create a solution. +- [get-source](../cli#get-source): Downloads the source code of a module. +- [add-source-code](../cli#add-source-code): Downloads the source code and replaces package references with project references. +- [init-solution](../cli#init-solution): Creates ABP Studio configuration files for a given solution. +- [kube-connect](../cli#kube-connect): Connects to Kubernetes environment. (*Available for* ***Business*** *or higher licenses*) +- [kube-intercept](../cli#kube-intercept): Intercepts a service running in Kubernetes environment. (*Available for* ***Business*** *or higher licenses*) +- [list-module-sources](../cli#list-module-sources): Lists the remote module sources. +- [add-module-source](../cli#add-module-source): Adds a remote module source. +- [delete-module-source](../cli#delete-module-source): Deletes a remote module source. +- [generate-proxy](../cli#generate-proxy): Generates client side proxies to use HTTP API endpoints. +- [remove-proxy](../cli#remove-proxy): Removes previously generated client side proxies. +- [switch-to-preview](../cli#switch-to-preview): Switches to the latest preview version of the ABP. +- [switch-to-nightly](../cli#switch-to-nightly): Switches to the latest [nightly builds](../release-info/nightly-builds.md) of the ABP related packages on a solution. +- [switch-to-stable](../cli#switch-to-stable): Switches to the latest stable versions of the ABP related packages on a solution. +- [switch-to-local](../cli#switch-to-local): Changes NuGet package references on a solution to local project references. +- [upgrade](../cli#upgrade): It converts the application to use pro modules. +- [translate](../cli#translate): Simplifies to translate localization files when you have multiple JSON [localization](../framework/fundamentals/localization.md) files in a source control repository. +- [login](../cli#login): Authenticates on your computer with your [abp.io](https://abp.io/) username and password. +- [login-info](../cli#login-info): Shows the current user's login information. +- [logout](../cli#logout): Logouts from your computer if you've authenticated before. +- [bundle](../cli#bundle): Generates script and style references for ABP Blazor and MAUI Blazor project. +- [install-libs](../cli#install-libs): Install NPM Packages for MVC / Razor Pages and Blazor Server UI types. +- [clear-download-cache](../cli#clear-download-cache): Clears the templates download cache. +- [check-extensions](../cli#check-extensions): Checks the latest version of the ABP CLI extensions. +- [install-old-cli](../cli#install-old-cli): Installs old ABP CLI. +- [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 @@ -84,16 +85,16 @@ Shows basic usages of the ABP CLI. Usage: -````bash +```bash abp help [command-name] -```` +``` Examples: -````bash +```bash abp help # Shows a general help. abp help new # Shows help about the "new" command. -```` +``` ### cli @@ -101,239 +102,344 @@ Update or remove ABP CLI. Usage: -````bash +```bash abp cli [command-name] -```` +``` Examples: -````bash +```bash abp cli update abp cli update --preview abp cli update --version 1.0.0 abp cli remove abp cli check-version abp cli clear-cache -```` +``` ### new -Generates a new solution based on the ABP [startup templates](../solution-templates). See [new solution create sample commands](new-command-samples.md) +Generates a new solution based on the ABP [startup templates](../solution-templates). See [new solution create sample commands](new-command-samples.md). + +The `new` command uses the ABP Studio template system by default. Add `--modern` to create a solution from the modern template system. Modern templates are React-first and are not available through the classic CLI (`--old`). Usage: -````bash +```bash abp new [options] -```` +``` Examples: -````bash -abp new Acme.BookStore -```` +```bash +abp new Acme.BookStore --template app +``` -* `Acme.BookStore` is the solution name here. -* Common convention is to name a solution is like *YourCompany.YourProject*. However, you can use different naming like *YourProject* (single level namespacing) or *YourCompany.YourProduct.YourModule* (three levels namespacing). +- `Acme.BookStore` is the solution name here. +- Common convention is to name a solution is like *YourCompany.YourProject*. However, you can use different naming like *YourProject* (single level namespacing) or *YourCompany.YourProduct.YourModule* (three levels namespacing). For more samples, go to [ABP CLI Create Solution Samples](new-command-samples.md) #### Options -* `--template` or `-t`: Specifies the template name. Default template name is `app`, which generates a application solution. Available templates: - * **`empty`**: Empty solution template. - * **`app`**: Application template. Additional options: - * `--ui-framework` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: - * `mvc`: ASP.NET Core MVC. There are some additional options for this template: - * `--tiered`: Creates a tiered solution where Web and Http API layers are physically separated. If not specified, it creates a layered solution which is less complex and suitable for most scenarios. (*Available for* ***Team*** *or higher licenses*) - * `angular`: Angular UI. There are some additional options for this template: - * `--tiered`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. (*Available for* ***Team*** *or higher licenses*) - * `--progressive-web-app` or `-pwa`: Specifies the project as Progressive Web Application. - * `blazor-webapp`: Blazor Web App UI. There are some additional options for this template: - * `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. - * `--progressive-web-app` or `-pwa`: Specifies the project as Progressive Web Application. - * `blazor`: Blazor UI. There are some additional options for this template: - * `--tiered`The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. (*Available for* ***Team*** *or higher licenses*) - * `--progressive-web-app` or `-pwa`: Specifies the project as Progressive Web Application. - * `blazor-server`: Blazor Server UI. There are some additional options for this template: - * `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. (*Available for* ***Team*** *or higher licenses*) - * `maui-blazor`: Blazor Maui UI (*Available for* ***Team*** *or higher licenses*). There are some additional options for this template: - * `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. - * `no-ui`: Without UI. No front-end layer will be created. There are some additional options for this template: - * `--tiered`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. (*Available for* ***Team*** *or higher licenses*) - * `--mobile` or `-m`: Specifies the mobile application framework. Default value is `none`. Available frameworks: - * `none`: Without any mobile application. - * `react-native`: React Native. This mobile option is only available for active **license owners**. - * `maui`: MAUI. This mobile option is only available for ABP. (*Available for* ***Team*** *or higher licenses*) - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](../framework/data/entity-framework-core/other-dbms.md) (after creating the solution). **Note:** When specifying the connection string, make sure to enclose it in double quotes, for example: `--connection-string "Server=localhost;Database=MyProjectName;Trusted_Connection=True"`. - * `--skip-migrations` or `-sm`: Skips the creating initial database migration step. - * `--skip-migrator` or `-smr`: Skips the run database migrator step. - * `--public-website`: Public Website is a front-facing website for describing your project, listing your products and doing SEO for marketing purposes. Users can login and register on your website with this website. This option is only included in PRO templates. - * `--without-cms-kit`: When you add a public website to your solution, it automatically includes the [CmsKit](./../modules/cms-kit-pro/index.md) module. If you don't want to include *CmsKit*, you can use this parameter. - * `--separate-tenant-schema`: Creates a different DbContext for tenant schema. If not specified, the tenant schema is shared with the host schema. This option is only included in PRO templates. - * `--sample-crud-page` or `-scp`: It adds the [BookStore](./../tutorials/book-store/index.md) sample to your solution. - * `--theme` or `-th`: Specifes the theme. Default theme is `leptonx`. Available themes: - * `leptonx`: LeptonX Theme. (*Available for* ***Team*** *or higher licenses*) - * `leptonx-lite`: LeptonX-Lite Theme. - * `basic`: Basic Theme. - * `--use-open-source-template`or `-uost`: Uses the open-source template. (*Available for* ***Team*** *or higher licenses*) - * **`app-nolayers`**: Single-layer application template. Additional options: - * `--ui-framework` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: - * `mvc`: ASP.NET Core MVC. There are some additional options for this template: - * `angular`: Angular UI. There are some additional options for this template: - * `blazor`: Blazor UI. There are some additional options for this template: - * `blazor-server`: Blazor Server UI. There are some additional options for this template: - * `no-ui`: Without UI. No front-end layer will be created. There are some additional options for this template: - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](../framework/data/entity-framework-core/other-dbms.md) (after creating the solution). **Note:** When specifying the connection string, make sure to enclose it in double quotes, for example: `--connection-string "Server=localhost;Database=MyProjectName;Trusted_Connection=True"`. - * `--skip-migrations` or `-sm`: Skips the creating initial database migration step. - * `--skip-migrator` or `-smr`: Skips the run database migrator step. - * `--sample-crud-page` or `-scp`: It adds the [BookStore](./../tutorials/book-store/index.md) sample to your solution. - * `--theme`: Specifes the theme. Default theme is `leptonx`. Available themes: - * `leptonx`: LeptonX Theme. (*Available for* ***Team*** *or higher licenses*) - * `leptonx-lite`: LeptonX-Lite Theme. - * `basic`: Basic Theme. - * `--use-open-source-template`or `-uost`: Uses the open-source template. (*Available for* ***Team*** *or higher licenses*) - * **`microservice`**: Microservice solution template (*Available for* ***Business*** *or higher licenses*). Additional options: - * `--ui-framework` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: - * `mvc`: ASP.NET Core MVC. There are some additional options for this template: - * `angular`: Angular UI. There are some additional options for this template: - * `blazor`: Blazor UI. There are some additional options for this template: - * `blazor-server`: Blazor Server UI. There are some additional options for this template: - * `maui-blazor`: Blazor Maui UI. There are some additional options for this template: - * `no-ui`: Without UI. No front-end layer will be created. There are some additional options for this template: - * `--mobile` or `-m`: Specifies the mobile application framework. Default value is `none`. Available frameworks: - * `none`: Without any mobile application. - * `react-native`: React Native. - * `maui`: MAUI. - * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. - * `--theme`: Specifes the theme. Default theme is `leptonx`. Available themes: - * `leptonx`: LeptonX Theme. - * `basic`: Basic Theme. - * `--public-website`: Public Website is a front-facing website for describing your project, listing your products and doing SEO for marketing purposes. Users can login and register on your website with this website. This option is only included in PRO templates. - * `--no-grafana-dashboard` or `-ngd`: Does not add example Grafana Dashboard to the solution. -* `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. -* `--local-framework-ref` or `-lfr`: Uses local projects references to the ABP framework instead of using the NuGet packages. It tries to find the paths from `ide-state.json`. The file is located at `%UserProfile%\.abp\studio\ui\ide-state.json` (for Windows) and `~/.abp/studio/ui/ide-state.json` (for MAC). -* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. -* `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's: - * `SqlServer` - * `MySQL` - * `PostgreSQL` - * `SQLite` (`app` & `app-nolayers`) - * `Oracle` (`app` & `app-nolayers`) - * `Oracle-Devart` (`app` & `app-nolayers`) -* `--dont-run-install-libs`: Skip installing client side packages. -* `--dont-run-bundling`: Skip bundling for Blazor packages. -* `--no-kubernetes-configuration` or `-nkc`: Skips the Kubernetes configuration files. -* `--no-social-logins` or `-nsl`: Skipts the social login configuration. -* `--no-tests` or `-ntp`: Does not add test projects. -* *Module Options*: You can skip some modules if you don't want to add them to your solution, or include if you want them (*Available for* ***Team*** *or higher licenses*). Available commands: - * `-no-saas`: Skips the Saas module. - * `-no-gdpr`: Skips the GDPR module. - * `-no-openiddict-admin-ui`: Skips the OpenIddict Admin UI module. - * `-no-audit-logging`: Skips the Audit Logging module. - * `-no-language-management`: Skips the Language Management module. - * `-no-text-template-management`: Skips the Text Template Management module. - * `-file-management`: Includes the File Management module. - * `-chat`: Includes the Chat module. - * `-ai-management`: Includes the [AI Management](./../modules/ai-management/index.md) module. - * `-ai-providers `: AI providers (comma-separated). Valid values: `Ollama`, `OpenAI`. (Requires `-ai-management`) -* `--legacy`: Generates a legacy solution. - * `trust-version`: Trusts the user's version and does not check if the version exists or not. If the template with the given version is found in the cache, it will be used, otherwise throws an exception. +- `--template` or `-t`: Specifies the template name. Default template name is `app`, which generates an application solution. Available templates: + - **`empty`**: Empty solution template. + - **`app`**: Application template. Additional options: + - `--ui-framework` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: + - `mvc`: ASP.NET Core MVC. There are some additional options for this template: + - `--tiered`: Creates a tiered solution where Web and Http API layers are physically separated. If not specified, it creates a layered solution which is less complex and suitable for most scenarios. (*Available for* ***Team*** *or higher licenses*) + - `angular`: Angular UI. There are some additional options for this template: + - `--tiered`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. (*Available for* ***Team*** *or higher licenses*) + - `--progressive-web-app` or `-pwa`: Specifies the project as Progressive Web Application. + - `blazor-webapp`: Blazor Web App UI. There are some additional options for this template: + - `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. + - `--progressive-web-app` or `-pwa`: Specifies the project as Progressive Web Application. + - `blazor`: Blazor UI. There are some additional options for this template: + - `--tiered`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. (*Available for* ***Team*** *or higher licenses*) + - `--progressive-web-app` or `-pwa`: Specifies the project as Progressive Web Application. + - `blazor-server`: Blazor Server UI. There are some additional options for this template: + - `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. (*Available for* ***Team*** *or higher licenses*) + - `maui-blazor`: Blazor Maui UI (*Available for* ***Team*** *or higher licenses*). There are some additional options for this template: + - `--tiered`: The Auth Server and the API Host project comes as separate projects and run at different endpoints. It has 3 startup projects: *HttpApi.Host*, *AuthServer* and *Blazor* and and each runs on different endpoints. If not specified, you will have a single endpoint for your web project. + - `react`: React SPA UI. Only available when `--modern` flag is used. See [Modern Templates](#modern-templates) below. + - `no-ui`: Without UI. No front-end layer will be created. There are some additional options for this template: + - `--tiered`: The Auth Server project comes as a separate project and runs at a different endpoint. It separates the Auth Server from the API Host application. If not specified, you will have a single endpoint in the server side. (*Available for* ***Team*** *or higher licenses*) + - `--mobile` or `-m`: Specifies the mobile application framework. Default value is `none`. Available frameworks: + - `none`: Without any mobile application. + - `react-native`: React Native. This mobile option is only available for active **license owners**. + - `maui`: MAUI. This mobile option is only available for ABP. (*Available for* ***Team*** *or higher licenses*). Not supported with `--modern`. + - `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: + - `ef`: Entity Framework Core. + - `mongodb`: MongoDB. + - `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](../framework/data/entity-framework-core/other-dbms.md) (after creating the solution). **Note:** When specifying the connection string, make sure to enclose it in double quotes, for example: `--connection-string "Server=localhost;Database=MyProjectName;Trusted_Connection=True"`. + - `--skip-migrations` or `-sm`: Skips the creating initial database migration step. + - `--skip-migrator` or `-smr`: Skips the run database migrator step. + - `--public-website`: Public Website is a front-facing website for describing your project, listing your products and doing SEO for marketing purposes. Users can login and register on your website with this website. This option is only included in PRO templates. + - `--without-cms-kit`: When you add a public website to your solution, it automatically includes the [CmsKit](./../modules/cms-kit-pro/index.md) module. If you don't want to include *CmsKit*, you can use this parameter. + - `--separate-tenant-schema`: Creates a different DbContext for tenant schema. If not specified, the tenant schema is shared with the host schema. This option is only included in PRO templates. + - `--sample-crud-page` or `-scp`: It adds the [BookStore](./../tutorials/book-store/index.md) sample to your solution. + - `--theme` or `-th`: Specifies the theme. Default theme is `leptonx`. Available themes: + - `leptonx`: LeptonX Theme. (*Available for* ***Team*** *or higher licenses*) + - `leptonx-lite`: LeptonX-Lite Theme. + - `basic`: Basic Theme. + - `--use-open-source-template` or `-uost`: Uses the open-source template. (*Available for* ***Team*** *or higher licenses*) + - **`app-nolayers`**: Single-layer application template. Additional options: + - `--ui-framework` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: + - `mvc`: ASP.NET Core MVC. There are some additional options for this template: + - `angular`: Angular UI. There are some additional options for this template: + - `blazor`: Blazor UI. There are some additional options for this template: + - `blazor-server`: Blazor Server UI. There are some additional options for this template: + - `react`: React SPA UI. Only available when `--modern` flag is used. See [Modern Templates](#modern-templates) below. + - `no-ui`: Without UI. No front-end layer will be created. There are some additional options for this template: + - `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: + - `ef`: Entity Framework Core. + - `mongodb`: MongoDB. + - `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](../framework/data/entity-framework-core/other-dbms.md) (after creating the solution). **Note:** When specifying the connection string, make sure to enclose it in double quotes, for example: `--connection-string "Server=localhost;Database=MyProjectName;Trusted_Connection=True"`. + - `--skip-migrations` or `-sm`: Skips the creating initial database migration step. + - `--skip-migrator` or `-smr`: Skips the run database migrator step. + - `--sample-crud-page` or `-scp`: It adds the [BookStore](./../tutorials/book-store/index.md) sample to your solution. + - `--theme`: Specifies the theme. Default theme is `leptonx`. Available themes: + - `leptonx`: LeptonX Theme. (*Available for* ***Team*** *or higher licenses*) + - `leptonx-lite`: LeptonX-Lite Theme. + - `basic`: Basic Theme. + - `--use-open-source-template` or `-uost`: Uses the open-source template. (*Available for* ***Team*** *or higher licenses*) + - **`microservice`**: Microservice solution template (*Available for* ***Business*** *or higher licenses*). Additional options: + - `--ui-framework` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: + - `mvc`: ASP.NET Core MVC. There are some additional options for this template: + - `angular`: Angular UI. There are some additional options for this template: + - `blazor`: Blazor UI. There are some additional options for this template: + - `blazor-server`: Blazor Server UI. There are some additional options for this template: + - `maui-blazor`: Blazor Maui UI. There are some additional options for this template: + - `react`: React SPA + React Admin Console. Only available when `--modern` flag is used. See [Modern Templates](#modern-templates) below. + - `no-ui`: Without UI. No front-end layer will be created. There are some additional options for this template: + - `--mobile` or `-m`: Specifies the mobile application framework. Default value is `none`. Available frameworks: + - `none`: Without any mobile application. + - `react-native`: React Native. + - `maui`: MAUI. Not supported with `--modern`. + - `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: + - `ef`: Entity Framework Core. + - `mongodb`: MongoDB. + - `--theme`: Specifies the theme. Default theme is `leptonx`. Available themes: + - `leptonx`: LeptonX Theme. + - `basic`: Basic Theme. + - `--public-website`: Public Website is a front-facing website for describing your project, listing your products and doing SEO for marketing purposes. Users can login and register on your website with this website. This option is only included in PRO templates. + - `--no-grafana-dashboard` or `-ngd`: Does not add example Grafana Dashboard to the solution. +- `--modern`: Uses the modern variant of the selected template. Modern templates are React-first and use a different template source shipped with ABP Studio, instead of NuGet extension packages. See [Modern Templates](#modern-templates) below. +- `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. +- `--local-framework-ref` or `-lfr`: Uses local projects references to the ABP framework instead of using the NuGet packages. It tries to find the paths from `ide-state.json`. The file is located at `%UserProfile%\.abp\studio\ui\ide-state.json` (for Windows) and `~/.abp/studio/ui/ide-state.json` (for MAC). +- `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. +- `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's: + - `SqlServer` + - `MySQL` + - `PostgreSQL` + - `SQLite` (`app` & `app-nolayers`) + - `Oracle` (`app` & `app-nolayers`) + - `Oracle-Devart` (`app` & `app-nolayers`) +- `--dont-run-install-libs`: Skip installing client side packages. +- `--dont-run-bundling`: Skip bundling for Blazor packages. +- `--no-kubernetes-configuration` or `-nkc`: Skips the Kubernetes configuration files. +- `--no-social-logins` or `-nsl`: Skips the social login configuration. +- `--no-multi-tenancy`: Disables multi-tenancy support in the generated solution. +- `--no-tests` or `-ntp`: Does not add test projects. +- *Module Options*: You can skip some modules if you don't want to add them to your solution, or include if you want them (*Available for* ***Team*** *or higher licenses*). Available commands: + - `-no-saas`: Skips the Saas module. + - `-no-gdpr`: Skips the GDPR module. + - `-no-openiddict-admin-ui`: Skips the OpenIddict Admin UI module. + - `-no-audit-logging`: Skips the Audit Logging module. + - `-no-language-management`: Skips the Language Management module. + - `-no-text-template-management`: Skips the Text Template Management module. + - `-file-management`: Includes the File Management module. + - `-chat`: Includes the Chat module. + - `--ai-management`: Includes the AI Management module. + - `--ai-providers`: Specifies AI providers (comma-separated). Available values: `Ollama`, `OpenAI`. Requires `--ai-management`. +- `--legacy`: Generates a classic solution. + - `trust-version`: Trusts the user's version and does not check if the version exists or not. If the template with the given version is found in the cache, it will be used, otherwise throws an exception. + +##### Modern Template Options + +The following options apply only when `--modern` is used: + +| Option | Description | Templates | +| --- | --- | --- | +| `--shadcn-theme ` | Sets the shadcn/ui color theme for the generated React apps. See [Shadcn Theme Values](#shadcn-theme-values). | all `--modern` templates | +| `--admin-password ` | Sets the initial admin user password. | all `--modern` templates | +| `--modular` | Generates a modular monolith variant. | `app-nolayers --modern` | +| `--services ` | Adds extra microservice names as a comma-separated list (for example, `Ordering,Shipping`). | `microservice --modern` | + +#### Modern Templates + +Add `--modern` to a supported template to use its modern variant. Modern templates use a different template source, shipped with ABP Studio, compared to classic templates that use NuGet extension packages. They are **React-first** and have a narrower set of supported options. + +```bash +abp new Acme.BookStore --template app --modern +abp new Acme.BookStore --template app-nolayers --modern +abp new Acme.BookStore --template microservice --modern +``` + +| Template + `--modern` | UI Framework | Mobile | +| ----------------------- | ---------------------------------------------------------- | ------------------------ | +| `app --modern` | `react` (default) or `no-ui` | `none` or `react-native` | +| `app-nolayers --modern` | `react` (default) or `no-ui` | `none` or `react-native` | +| `microservice --modern` | `react` (default, includes React Admin Console) or `no-ui` | `none` or `react-native` | +> Blazor, Angular, MVC, and MAUI Blazor UI frameworks are **not** supported with `--modern`. The `maui` mobile option is also not supported with `--modern`. +> +> Options that are not supported by a modern template are ignored with a warning in the CLI output. +> +> `--modern` can also be used with `--ready-config-path` (`-rcp`) and `--solution-history-id` (`-shi`). In these cases, the template in the JSON configuration or solution history record is mapped to its modern variant. See [Using Existing Configuration](new-command-samples.md#using-existing-configuration) for configuration-file and solution-history examples. + +For `app --modern` and `app-nolayers --modern`, the generated solution includes a `react/` folder for your application. The ABP Admin Console is hosted by the backend through the `Volo.Abp.AdminConsole` package and is served from `/admin-console/`; there is no separate `apps/react-admin-console/` folder. + +When using `--template microservice --modern`, the generated solution includes: + +- `apps/react/` — React SPA (main user-facing application) +- `apps/react-admin-console/` — React Admin Console (administration interface) +- `apps/auth-server/` — OpenIddict authentication server +- `gateways/web/` — YARP reverse proxy for the React apps +- `gateways/mobile/` — Gateway for mobile apps (only when `--mobile` is set) + +Two OpenIddict clients are automatically seeded: `MyProjectName_App` (React SPA) and `MyProjectName_AdminConsole` (React Admin Console). + +Examples: + +```bash +# Modern layered app with React UI +abp new Acme.BookStore --template app --modern + +# Modern single-layer app with React UI +abp new Acme.BookStore --template app-nolayers --modern + +# Modern microservice (React + React Admin Console) +abp new Acme.BookStore --template microservice --modern + +# Modern microservice with no UI +abp new Acme.BookStore --template microservice --modern --ui-framework no-ui + +# Modern single-layer modular monolith +abp new Acme.BookStore --template app-nolayers --modern --modular + +# Modern microservice with PostgreSQL +abp new Acme.BookStore --template microservice --modern --database-management-system postgresql + +# Modern microservice with additional services +abp new Acme.BookStore --template microservice --modern --services Ordering,Shipping + +# Modern microservice with React Native mobile +abp new Acme.BookStore --template microservice --modern --mobile react-native +``` + +##### Shadcn Theme Values + +Use `--shadcn-theme ` with `--modern` templates: + +- `slate` (default) +- `pink` +- `blue` +- `turquoise` +- `orange` +- `purple` ### new-module Generates a new module. -````bash +```bash abp new-module [options] -```` +``` Examples: -````bash +```bash abp new-module Acme.BookStore -t module:ddd -```` +``` #### options -* `--template` or `-t`: Specifies the template name. Default template name is `module:ddd`, which generates a DDD module. Module templates are provided by the main template, see their own startup template documentation for available modules. `empty:empty` and `module:ddd` template is available for all solution structure. -* `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. -* `--target-solution` or `-ts`: If set, the new module will be added to the given solution. Otherwise the new module will added to the closest solution in the file system. If no solution found, it will throw an error. -* `--solution-folder` or `-sf`: Specifies the target folder in the [Solution Explorer](../studio/solution-explorer.md#folder) virtual folder system. -* `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. This option is only available if the module template supports it. You can add multiple values separated by commas, such as `ef,mongodb` if the module template supports it. Available providers: - * `ef`: Entity Framework Core. - * `mongodb`: MongoDB. -* `--ui-framework` or `-u`: Specifies the UI framework. This option is only available if the module template supports it. You can add multiple values separated by commas, such as `mvc,angular` if the module template supports it. Available frameworks: - * `mvc`: ASP.NET Core MVC. - * `angular`: Angular UI. - * `blazor`: Blazor UI. - * `blazor-server`: Blazor Server UI. +- `--template` or `-t`: Specifies the template name. Default template name is `module:ddd`, which generates a DDD module. Module templates are provided by the main template, see their own startup template documentation for available modules. `empty:empty` and `module:ddd` template is available for all solution structure. +- `--modern`: Uses the modern variant of the selected module template. +- `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory. +- `--target-solution` or `-ts`: If set, the new module will be added to the given solution. Otherwise the new module will added to the closest solution in the file system. If no solution found, it will throw an error. +- `--solution-folder` or `-sf`: Specifies the target folder in the [Solution Explorer](../studio/solution-explorer.md#folder) virtual folder system. +- `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. This option is only available if the module template supports it. You can add multiple values separated by commas, such as `ef,mongodb` if the module template supports it. Available providers: + - `ef`: Entity Framework Core. + - `mongodb`: MongoDB. +- `--ui-framework` or `-u`: Specifies the UI framework. This option is only available if the module template supports it. You can add multiple values separated by commas, such as `mvc,angular` if the module template supports it. Available frameworks: + - `mvc`: ASP.NET Core MVC. + - `angular`: Angular UI. + - `blazor`: Blazor UI. + - `blazor-server`: Blazor Server UI. + +#### Modern Module Templates + +Add `--modern` to use the modern module variant: + +```bash +abp new-module Acme.BookStore.Orders --modern +abp new-module Acme.BookStore.Orders --modern -t module:ddd +abp new-module Acme.BookStore.Orders --modern -t module:standard +``` + +When the target solution itself is modern, the modern module variant is selected automatically even if `--modern` is not passed. + +Options that are not supported by a modern module template are ignored with a warning in the CLI output. ### new-package Generates a new package. -````bash +```bash abp new-package [options] -```` +``` Examples: -````bash +```bash abp new-package --name Acme.BookStore.Domain --template lib.domain -```` +``` #### options -* `--template` or `-t`: Specifies the template name. This parameter doesn't have a default value and must be set. Available templates and their sub-options: - * `lib.class-library` - * `lib.domain-shared` - * `--add-localization`: Includes default localization configuration & language files. - * `lib.domain` - * `--add-settings`: Includes default settings configuration. - * `--add-db-properties`: Includes the default Database Properties class. - * `--add-domain-shared`: Includes an additional Domain Shared package. - * `lib.application-contracts` - * `lib.application` - * `--add-mapperly`: Adds Mapperly configuration. - * `--add-application-contracts`: Includes an additional contracts package. - * `lib.ef` - * `--include-migrations`: Allows migration operations on this package. - * `--connection-string-name`: Default value is the last part of the package's namespace (or package name simply). - * `--connection-string`: Connection string value. The default value is null. You can set it later. **Note:** When specifying the connection string, make sure to enclose it in double quotes, for example: `--connection-string "Server=localhost;Database=MyProjectName;Trusted_Connection=True"`. - * `lib.mongodb` - * `lib.http-api` - * `lib.http-api-client` - * `lib.mvc` - * `--add-mapperly`: Adds Mapperly configuration. - * `lib.blazor` - * `--add-mapperly`: Adds Mapperly configuration. - * `--add-menu-contributors`: Includes default menu contributors. - * `lib.blazor-wasm` - * `lib.blazor-server` - * `host.http-api` - * `--add-serilog`: Includes Serilog configuration. - * `--add-swagger`: Includes Swagger configuration. - * `host.mvc` - * `--add-serilog`: Includes Serilog configuration. - * `--add-swagger`: Includes Swagger configuration. - * `host.blazor-wasm` - * `--backend`: Name of the backend project in the module (not path). - * `host.blazor-server` - * `abp.console` - * `csharp.console` - * `csharp.library` -* `--module-file` or `-m`: If set, the new package will be added to the given module. Otherwise the new package will added to the closest module in the file system. If no module found, it will throw an error. -* `--name` or `-n`: Specifies the name of the package. If not set, a name based on the template type and module name will be generated. -* `--folder` or `-f`: Specifies the target folder in the target module's virtual folder system. +- `--template` or `-t`: Specifies the template name. This parameter doesn't have a default value and must be set. Available templates and their sub-options: + - `lib.class-library` + - `lib.domain-shared` + - `--add-localization`: Includes default localization configuration & language files. + - `lib.domain` + - `--add-settings`: Includes default settings configuration. + - `--add-db-properties`: Includes the default Database Properties class. + - `--add-domain-shared`: Includes an additional Domain Shared package. + - `lib.application-contracts` + - `lib.application` + - `--add-mapperly`: Adds Mapperly configuration. + - `--add-application-contracts`: Includes an additional contracts package. + - `lib.ef` + - `--include-migrations`: Allows migration operations on this package. + - `--connection-string-name`: Default value is the last part of the package's namespace (or package name simply). + - `--connection-string`: Connection string value. The default value is null. You can set it later. **Note:** When specifying the connection string, make sure to enclose it in double quotes, for example: `--connection-string "Server=localhost;Database=MyProjectName;Trusted_Connection=True"`. + - `lib.mongodb` + - `lib.http-api` + - `lib.http-api-client` + - `lib.mvc` + - `--add-mapperly`: Adds Mapperly configuration. + - `lib.blazor` + - `--add-mapperly`: Adds Mapperly configuration. + - `--add-menu-contributors`: Includes default menu contributors. + - `lib.blazor-wasm` + - `lib.blazor-server` + - `host.http-api` + - `--add-serilog`: Includes Serilog configuration. + - `--add-swagger`: Includes Swagger configuration. + - `host.mvc` + - `--add-serilog`: Includes Serilog configuration. + - `--add-swagger`: Includes Swagger configuration. + - `host.blazor-wasm` + - `--backend`: Name of the backend project in the module (not path). + - `host.blazor-server` + - `abp.console` + - `csharp.console` + - `csharp.library` +- `--module-file` or `-m`: If set, the new package will be added to the given module. Otherwise the new package will added to the closest module in the file system. If no module found, it will throw an error. +- `--name` or `-n`: Specifies the name of the package. If not set, a name based on the template type and module name will be generated. +- `--folder` or `-f`: Specifies the target folder in the target module's virtual folder system. ### update @@ -341,25 +447,25 @@ Updating all ABP related packages can be tedious since there are many packages o Usage: -````bash +```bash abp update [options] -```` +``` -* If you run in a directory with a .csproj file, it updates all ABP related packages of the project to the latest versions. -* If you run in a directory with a .sln file, it updates all ABP related packages of the all projects of the solution to the latest versions. -* If you run in a directory that contains multiple solutions in sub-folders, it can update all the solutions, including Angular projects. +- If you run in a directory with a .csproj file, it updates all ABP related packages of the project to the latest versions. +- If you run in a directory with a .sln file, it updates all ABP related packages of the all projects of the solution to the latest versions. +- If you run in a directory that contains multiple solutions in sub-folders, it can update all the solutions, including Angular projects. Note that this command can upgrade your solution from a previous version, and also can upgrade it from a preview release to the stable release of the same version. #### Options -* `--npm`: Only updates NPM packages. -* `--nuget`: Only updates NuGet packages. -* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default -* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default. -* `--check-all`: Check the new version of each package separately. Default is `false`. -* `--version` or `-v`: Specifies the version to use for update. If not specified, latest version is used. -* `--leptonx-version` or `-lv`: Specifies the LeptonX version to use for update. If not specified, latest version or the version that is compatible with `--version` argument is used. +- `--npm`: Only updates NPM packages. +- `--nuget`: Only updates NuGet packages. +- `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default +- `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default. +- `--check-all`: Check the new version of each package separately. Default is `false`. +- `--version` or `-v`: Specifies the version to use for update. If not specified, latest version is used. +- `--leptonx-version` or `-lv`: Specifies the LeptonX version to use for update. If not specified, latest version or the version that is compatible with `--version` argument is used. ### clean @@ -367,50 +473,51 @@ Deletes all `BIN` and `OBJ` folders in the current folder. Usage: -````bash +```bash abp clean -```` +``` + ### clean-logs Delete all `*logs.txt` files in the current folder and its subfolders. Usage: -````bash +```bash abp clean-logs -```` - +``` ### add-package Adds an ABP package to a project by, -* Adding related nuget package as a dependency to the project. -* Adding `[DependsOn(...)]` attribute to the module class in the project (see the [module development document](../framework/architecture/modularity/basics.md)). +- Adding related nuget package as a dependency to the project. +- Adding `[DependsOn(...)]` attribute to the module class in the project (see the [module development document](../framework/architecture/modularity/basics.md)). > Notice that the added module may require additional configuration which is generally indicated in the documentation of the related package. Basic usage: -````bash +```bash abp add-package [options] -```` +``` Examples: -````bash +```bash abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic -```` +``` -* This example adds the `Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic` package to the project. +- This example adds the `Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic` package to the project. #### Options -* `--project` or `-p`: Specifies the project (.csproj) file path. If not specified, CLI tries to find a .csproj file in the current directory. -* `--with-source-code`: Downloads the source code of the package to your solution folder and uses local project references instead of NuGet/NPM packages. -* `--add-to-solution-file`: Adds the downloaded package to your solution file, so you will also see the package when you open the solution on a IDE. (only available when `--with-source-code` is True) +- `--project` or `-p`: Specifies the project (.csproj) file path. If not specified, CLI tries to find a .csproj file in the current directory. +- `--with-source-code`: Downloads the source code of the package to your solution folder and uses local project references instead of NuGet/NPM packages. +- `--add-to-solution-file`: Adds the downloaded package to your solution file, so you will also see the package when you open the solution on a IDE. (only available when `--with-source-code` is True) > Currently only the source code of the basic theme packages([MVC](../framework/ui/mvc-razor-pages/basic-theme.md) and [Blazor](../framework/ui/blazor/basic-theme.md)) can be downloaded. +> > - Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic > - Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme > - Volo.Abp.AspNetCore.Components.Web.BasicTheme @@ -420,61 +527,61 @@ abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic Adds one or more package reference to target project, also adds ABP module dependency. Both reference and target projects must belong to same module. -````bash +```bash abp add-package-ref [options] -```` +``` Examples: -````bash +```bash abp add-package-ref Acme.BookStore.Domain abp add-package-ref "Acme.BookStore.Domain Acme.BookStore.Domain.Shared" -t Acme.BookStore.Web -```` +``` #### Options -* `--target-project` or `-t`: Name of the project that reference will be added. If not set, project in the current directory will be used. +- `--target-project` or `-t`: Name of the project that reference will be added. If not set, project in the current directory will be used. ### install-module Installs a module, that is published as nuget packages, to a local module. Project relations are created according the types of the projects. For Examples: a `lib.domain-shared` project is added to `lib.domain-shared` project -````bash +```bash abp install-module [options] -```` +``` Examples: -````bash +```bash abp install-module Volo.Blogging abp install-module Volo.Blogging -t "modules/crm/Acme.Crm.abpmdl" -```` +``` #### Options -* `--target-module` or `-t`: Path (or folder path) of the target module that the other module will be installed to. If not set, the closest module to the current directory will be used. -* `--version` or `-v`: Nuget version of the module to be installed. +- `--target-module` or `-t`: Path (or folder path) of the target module that the other module will be installed to. If not set, the closest module to the current directory will be used. +- `--version` or `-v`: Nuget version of the module to be installed. ### install-local-module Installs one module to another. Project relations are created according the types of the projects. For Examples: a `lib.domain-shared` project is added to `lib.domain-shared` project -````bash +```bash abp install-local-module [options] -```` +``` Examples: -````bash +```bash abp install-local-module Acme.OrderManagement abp install-local-module Acme.OrderManagement -t "modules/crm/Acme.Crm.abpmdl" -```` +``` #### Options -* `--target-module` or `-t`: Path (or folder path) of the target module that the other module will be installed to. If not set, the closest module to the current directory will be used. +- `--target-module` or `-t`: Path (or folder path) of the target module that the other module will be installed to. If not set, the closest module to the current directory will be used. ### list-modules @@ -482,9 +589,9 @@ Lists names of open-source application modules. Usage: -````bash +```bash abp list-modules [options] -```` +``` Examples: @@ -508,9 +615,9 @@ Downloads the source code of a module to your computer. Usage: -````bash +```bash abp get-source [options] -```` +``` Examples: @@ -522,118 +629,118 @@ abp get-source Volo.Blogging --local-framework-ref --abp-path D:\GitHub\abp #### Options -* `--output-folder` or `-o`: Specifies the directory that source code will be downloaded in. If not specified, current directory is used. -* `--version` or `-v`: Specifies the version of the source code that will be downloaded. If not specified, latest version is used. -* `--preview`: If no version option is specified, this option specifies if latest [preview version](../release-info/previews.md) will be used instead of latest stable version. -* `--local-framework-ref --abp-path`: Path of [ABP GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. +- `--output-folder` or `-o`: Specifies the directory that source code will be downloaded in. If not specified, current directory is used. +- `--version` or `-v`: Specifies the version of the source code that will be downloaded. If not specified, latest version is used. +- `--preview`: If no version option is specified, this option specifies if latest [preview version](../release-info/previews.md) will be used instead of latest stable version. +- `--local-framework-ref --abp-path`: Path of [ABP GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. ### add-source-code Downloads the source code of a module and replaces package references with project references. This command only works if your ABP Commercial License has source-code access, or if source-code of the target module is free to all type of ABP Commercial Licenses. -````bash +```bash abp add-source-code [options] -```` +``` Examples: -````bash +```bash abp add-source-code Volo.Chat --add-to-solution-file -```` +``` #### Options -* `--target-module` or `-t`: The module that will refer the downloaded source code. If not set, the module in the current directory will be used. -* `--add-to-solution-file`: Adds the downloaded source code to C# solution file and ABP Studio solution file. +- `--target-module` or `-t`: The module that will refer the downloaded source code. If not set, the module in the current directory will be used. +- `--add-to-solution-file`: Adds the downloaded source code to C# solution file and ABP Studio solution file. ### init-solution Creates necessary files for a solution to be readable by ABP Studio. If the solution is generated via ABP Studio, you don't need this command. But it is not generated by ABP Studio, you need this command to make it work with ABP Studio. -````bash +```bash abp init-solution [options] -```` +``` Examples: -````bash +```bash abp init-solution --name Acme.BookStore -```` +``` #### Options -* `--name` or `-n`: Name for the solution. If not set, it will be the same as the name of closest c# solution in the file system. +- `--name` or `-n`: Name for the solution. If not set, it will be the same as the name of closest c# solution in the file system. ### kube-connect Connects to Kubernetes cluster (*Available for* ***Business*** *or higher licenses*). Press `ctrl+c` to disconnect. -````bash +```bash abp kube-connect [options] -```` +``` Examples: -````bash +```bash abp kube-connect abp kube-connect -p Default.abpk8s.json abp kube-connect -c docker-desktop -ns mycrm-local -```` +``` #### Options -* `--profile` or `-p`: Kubernetes Profile path or name to be used. Path can be relative (to current directory) or full path, or you can simply give the name of profile if you run this command in same directory with the solution or profile. This parameter is not needed if you use `--namespace` and `--context` parameters. -* `--namespace` or `-ns`: The namespace that services running on. -* `--context` or `-c`: The context that services running in. -* `--wireguard-password` or `-wp`: Wireguard password for the profile. This is not needed if you already set it on the ABP Studio user interface. -* `--solution-path` or `-sp`: Path of the solution. If not set, the closest solution in file system will be used. +- `--profile` or `-p`: Kubernetes Profile path or name to be used. Path can be relative (to current directory) or full path, or you can simply give the name of profile if you run this command in same directory with the solution or profile. This parameter is not needed if you use `--namespace` and `--context` parameters. +- `--namespace` or `-ns`: The namespace that services running on. +- `--context` or `-c`: The context that services running in. +- `--wireguard-password` or `-wp`: Wireguard password for the profile. This is not needed if you already set it on the ABP Studio user interface. +- `--solution-path` or `-sp`: Path of the solution. If not set, the closest solution in file system will be used. ### kube-intercept Intercepts a service running in Kubernetes environment (*Available for* ***Business*** *or higher licenses*). Press `ctrl+c` to stop interception. -````bash +```bash abp kube-intercept [options] -```` +``` Examples: -````bash +```bash abp kube-intercept mycrm-product-service -ns mycrm-local abp kube-intercept mycrm-product-service -ns mycrm-local -a MyCrm.ProductService.HttpApi.Host.csproj abp kube-intercept mycrm-product-service -ns mycrm-local -a MyCrm.ProductService.HttpApi.Host.csproj -pm 8080:80,8081:443 -```` +``` #### Options -* `--application` or `-a`: Relative or full path of the project that will intercept the service. If not set, the project in the current directory will be used. -* `--namespace` or `-ns`: The namespace that service running on. -* `--context` or `-sc`: The context that service running in. Default value is `docker-desktop`. -* `--port-mappings` or `-pm`: Port mappings for the service. +- `--application` or `-a`: Relative or full path of the project that will intercept the service. If not set, the project in the current directory will be used. +- `--namespace` or `-ns`: The namespace that service running on. +- `--context` or `-sc`: The context that service running in. Default value is `docker-desktop`. +- `--port-mappings` or `-pm`: Port mappings for the service. ### list-module-sources With this command, you can see the list of remote module sources that you can use to install modules. It is similar to the NuGet feed list in Visual Studio. -````bash +```bash abp list-module-sources -```` +``` ### add-module-source Adds a remote module source to the list of sources that you can use to install modules. -````bash +```bash abp add-module-source [options] -```` +``` You can create your own module source and add it to the list. It accepts a name and a url or a path as parameter. If you provide a path, it should be a local path that contains the modules json file. If you provide a url, it should be a url that contains the modules json file. The json file should be in the following format: -````json +```json { "name": "ABP Open Source Modules", "modules" : { @@ -643,40 +750,40 @@ You can create your own module source and add it to the list. It accepts a name ... } } -```` +``` When you add a module source, you can install modules from that source using the `install-module` command. It attempts to find the package from NuGet, such as `Volo.Abp.Account.Installer`. You can configure a private NuGet feed and publish your modules to that feed. Each module has an installer package that is utilized to install the module into a solution. When you publish your module to a private feed, you should also publish the installer package to the same feed. Examples: -````bash +```bash abp add-module-source -n "Custom Source" -p "D:\packages\abp\modules.json" abp add-module-source -n "Custom Http Source" -p "https://raw.githubusercontent.com/x/abp-module-store/main/abp-module-store.json" -```` +``` #### Options -* `--name` or `-n`: The name of the module source. -* `--path` or `-p`: The path of the module source. It can be a local path or a url. +- `--name` or `-n`: The name of the module source. +- `--path` or `-p`: The path of the module source. It can be a local path or a url. ### delete-module-source Deletes a remote module source from the list of sources that you can use to install modules. -````bash +```bash abp delete-module-source [options] -```` +``` Examples: -````bash +```bash abp delete-module-source -n "Custom Source" -```` +``` #### Options -* `--name` or `-n`: The name of the module source. +- `--name` or `-n`: The name of the module source. ### generate-proxy @@ -684,39 +791,38 @@ Generates Angular, C# or JavaScript service proxies for your HTTP APIs to make e Usage: -````bash +```bash abp generate-proxy -t [options] -```` +``` Examples: -````bash +```bash abp generate-proxy -t ng -url https://localhost:44302/ abp generate-proxy -t js -url https://localhost:44302/ abp generate-proxy -t csharp -url https://localhost:44302/ -```` +``` #### Options -* `--type` or `-t`: The name of client type. Available clients: - * `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: - * `--without-contracts`: Avoid generating the application service interface, class, enum and dto types. - * `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. - * `ng`: Angular. There are some additional options for this client: - * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. - * `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. - * `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. - * `--module`: Backend module name. Default value: `app`. - * `--entry-point`: Targets the Angular project to place the generated code. - * `--url`: Specifies api definition url. Default value is API Name's url in environment file. - * `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). - - * `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: - * `--output` or `-o`: JavaScript file path or folder to place generated code in. -* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. -* `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. -* `--url` or `-u`: API definition URL from. -* `--service-type` or `-st`: Specifies the service type to generate. `application`, `integration` and `all`, Default value: `all` for C#, `application` for JavaScript / Angular. +- `--type` or `-t`: The name of client type. Available clients: + - `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: + - `--without-contracts`: Avoid generating the application service interface, class, enum and dto types. + - `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. + - `ng`: Angular. There are some additional options for this client: + - `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. + - `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. + - `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. + - `--module`: Backend module name. Default value: `app`. + - `--entry-point`: Targets the Angular project to place the generated code. + - `--url`: Specifies api definition url. Default value is API Name's url in environment file. + - `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). + - `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: + - `--output` or `-o`: JavaScript file path or folder to place generated code in. +- `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. +- `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. +- `--url` or `-u`: API definition URL from. +- `--service-type` or `-st`: Specifies the service type to generate. `application`, `integration` and `all`, Default value: `all` for C#, `application` for JavaScript / Angular. > See the [Angular Service Proxies document](../framework/ui/angular/service-proxies.md) for more. @@ -728,34 +834,34 @@ This can be especially useful when you generate proxies for multiple modules bef Usage: -````bash +```bash abp remove-proxy -t [options] -```` +``` Examples: -````bash +```bash abp remove-proxy -t ng abp remove-proxy -t js -m identity -o Pages/Identity/client-proxies.js abp remove-proxy -t csharp --folder MyProxies/InnerFolder -```` +``` #### Options -* `--type` or `-t`: The name of client type. Available clients: - * `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: - * `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. - * `ng`: Angular. There are some additional options for this client: - * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. - * `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. - * `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. - * `--url`: Specifies api definition url. Default value is API Name's url in environment file. - * `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). - * `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: - * `--output` or `-o`: JavaScript file path or folder to place generated code in. -* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. -* `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. -* `--url` or `-u`: API definition URL from. +- `--type` or `-t`: The name of client type. Available clients: + - `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: + - `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. + - `ng`: Angular. There are some additional options for this client: + - `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. + - `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. + - `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. + - `--url`: Specifies api definition url. Default value is API Name's url in environment file. + - `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). + - `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: + - `--output` or `-o`: JavaScript file path or folder to place generated code in. +- `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. +- `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. +- `--url` or `-u`: API definition URL from. > See the [Angular Service Proxies document](../framework/ui/angular/service-proxies.md) for more. @@ -765,14 +871,13 @@ You can use this command to switch your solution or project to latest preview ve Usage: -````bash +```bash abp switch-to-preview [options] -```` +``` #### Options -* `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. - +- `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. ### switch-to-nightly @@ -780,13 +885,13 @@ You can use this command to switch your solution or project to latest [nightly]( Usage: -````bash +```bash abp switch-to-nightly [options] -```` +``` #### Options -* `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. +- `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. ### switch-to-stable @@ -794,12 +899,13 @@ If you're using the ABP preview packages (including nightly previews), you can s Usage: -````bash +```bash abp switch-to-stable [options] -```` +``` + #### Options -* `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. +- `--directory` or `-d`: Specifies the directory. The solution or project should be in that directory or in any of its sub directories. If not specified, default is the current directory. ### switch-to-local @@ -807,20 +913,20 @@ Changes all NuGet package references to local project references for all the .cs Usage: -````bash +```bash abp switch-to-local [options] -```` -#### Options +``` -* `--solution` or `-s`: Specifies the solution directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. +#### Options -* `--paths` or `-p`: Specifies the local paths that the projects are inside. You can use `|` character to separate the paths. +- `--solution` or `-s`: Specifies the solution directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory. +- `--paths` or `-p`: Specifies the local paths that the projects are inside. You can use `|` character to separate the paths. Examples: -````bash +```bash abp switch-to-local --paths "D:\Github\abp|D:\Github\my-repo" -```` +``` ### upgrade @@ -829,35 +935,35 @@ This command is specially designed for users who already started their developme Usage: -````bash +```bash abp upgrade [-t ] [options] -```` +``` Examples: -````bash +```bash abp upgrade -t app abp upgrade -t app --language-management --gdpr --audit-logging-ui --text-template-management --openiddict-pro abp upgrade -t app-nolayers --audit-logging-ui abp upgrade -t app-nolayers -p D:\MyProjects\MyProject -```` +``` #### Options -* `--path` or `-p`: Specifies the module path. The module should be in that directory. If not specified, the default is the current directory. -* `--gdpr`: Installs GDPR module too. -* `--language-management`: Installs Language Management module too. -* `--audit-logging-ui`: Installs Audit Logging Pro (UI) module too. -* `--text-template-management`: Installs Text Template Management module too. -* `--openiddict-pro`: Installs OpenIddict Pro (UI) module too. +- `--path` or `-p`: Specifies the module path. The module should be in that directory. If not specified, the default is the current directory. +- `--gdpr`: Installs GDPR module too. +- `--language-management`: Installs Language Management module too. +- `--audit-logging-ui`: Installs Audit Logging Pro (UI) module too. +- `--text-template-management`: Installs Text Template Management module too. +- `--openiddict-pro`: Installs OpenIddict Pro (UI) module too. ### translate Simplifies to translate [localization](../framework/fundamentals/localization.md) files when you have multiple JSON [localization](../framework/fundamentals/localization.md) files in a source control repository. -* This command will create a unified json file based on the reference culture. -* It searches all the localization `JSON` files in the current directory and all subdirectories (recursively). Then creates a single file (named `abp-translation.json` by default) that includes all the entries need to be translated. -* Once you translate the entries in this file, you can then apply your changes to the original localization files using the `--apply` command. +- This command will create a unified json file based on the reference culture. +- It searches all the localization `JSON` files in the current directory and all subdirectories (recursively). Then creates a single file (named `abp-translation.json` by default) that includes all the entries need to be translated. +- Once you translate the entries in this file, you can then apply your changes to the original localization files using the `--apply` command. > The main purpose of this command is to translate ABP localization files (since the [abp repository](https://github.com/abpframework/abp) has tens of localization files to be translated in different directories). @@ -865,38 +971,38 @@ Simplifies to translate [localization](../framework/fundamentals/localization.md First step is to create the unified translation file: -````bash +```bash abp translate -c [options] -```` +``` Examples: -````bash +```bash abp translate -c de -```` +``` This command created the unified translation file for the `de` (German) culture. ##### Additional Options -* `--reference-culture` or `-r`: Default `en`. Specifies the reference culture. -* `--output` or `-o`: Output file name. Default `abp-translation.json`. -* `--all-values` or `-all`: Include all keys to translate. By default, the unified translation file only includes the missing texts for the target culture. Specify this parameter if you may need to revise the values already translated before. +- `--reference-culture` or `-r`: Default `en`. Specifies the reference culture. +- `--output` or `-o`: Output file name. Default `abp-translation.json`. +- `--all-values` or `-all`: Include all keys to translate. By default, the unified translation file only includes the missing texts for the target culture. Specify this parameter if you may need to revise the values already translated before. #### Applying Changes Once you translate the entries in the unified translation file, you can apply your changes to the original localization files using the `--apply` parameter: -````bash +```bash abp translate --apply # apply all changes abp translate -a # shortcut for --apply -```` +``` Then review changes on your source control system to be sure that it has changed the proper files and send a Pull Request if you've translated ABP resources. Thank you in advance for your contribution. ##### Additional Options -* `--file` or `-f`: Default: `abp-translation.json`. The translation file (use only if you've used the `--output` option before). +- `--file` or `-f`: Default: `abp-translation.json`. The translation file (use only if you've used the `--output` option before). #### Online DeepL translate @@ -904,9 +1010,9 @@ The `translate` command also supports online translation. You need to provide yo It will search all the `en.json(reference-culture)` files in the directory and sub-directory and then translate and generate the corresponding `zh-Hans.json(culture)` files. -````bash +```bash abp translate -c zh-Hans --online --deepl-auth-key -```` +``` ### login @@ -938,42 +1044,42 @@ abp logout ### bundle -This command generates script and style references for ABP Blazor WebAssembly and MAUI Blazor project and updates the **index.html** file. It helps developers to manage dependencies required by ABP modules easily. In order for ```bundle``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain a Blazor or MAUI Blazor project file(*.csproj). +This command generates script and style references for ABP Blazor WebAssembly and MAUI Blazor project and updates the **index.html** file. It helps developers to manage dependencies required by ABP modules easily. In order for `bundle` command to work, its **executing directory** or passed `--working-directory` parameter's directory must contain a Blazor or MAUI Blazor project file(*.csproj). Usage: -````bash +```bash abp bundle [options] -```` +``` > This command is no longer needed if you are using Global Assets feature. See [Managing Global Scripts & Styles](../framework/ui/blazor/global-scripts-styles.md) for more information. #### Options -* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a Blazor project file. -* ```--force``` or ```-f```: Forces to build project before generating references. -* ```--project-type``` or ```-t```: Specifies the project type. Default type is `webassembly`. Available types: - * `webassembly` - * `maui-blazor` -* `--version` or `-v`: Specifies the ABP Framework version that the project is using. This is helpful for those who use central package management. +- `--working-directory` or `-wd`: Specifies the working directory. This option is useful when executing directory doesn't contain a Blazor project file. +- `--force` or `-f`: Forces to build project before generating references. +- `--project-type` or `-t`: Specifies the project type. Default type is `webassembly`. Available types: + - `webassembly` + - `maui-blazor` +- `--version` or `-v`: Specifies the ABP Framework version that the project is using. This is helpful for those who use central package management. `bundle` command reads the `appsettings.json` file inside the Blazor and MAUI Blazor project for bundling options. For more details about managing style and script references in Blazor or MAUI Blazor apps, see [Managing Global Scripts & Styles](../framework/ui/blazor/global-scripts-styles.md) ### install-libs -This command install NPM Packages for MVC / Razor Pages and Blazor Server UI types. Its **executing directory** or passed ```--working-directory``` parameter's directory must contain a project file(*.csproj). +This command install NPM Packages for MVC / Razor Pages and Blazor Server UI types. Its **executing directory** or passed `--working-directory` parameter's directory must contain a project file(*.csproj). `install-libs` command reads the `abp.resourcemapping.js` file to manage package. For more details see [Client Side Package Management](../framework/ui/mvc-razor-pages/client-side-package-management.md). Usage: -````bash +```bash abp install-libs [options] -```` +``` #### Options -* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a project file. +- `--working-directory` or `-wd`: Specifies the working directory. This option is useful when executing directory doesn't contain a project file. ### check-extensions @@ -981,9 +1087,9 @@ This command checks the installed ABP CLI extensions and updates them if necessa Usage: -````bash +```bash abp check-extensions -```` +``` ### install-old-cli @@ -995,35 +1101,6 @@ Usage: abp install-old-cli [options] ``` -### mcp-studio - -Starts an MCP stdio bridge for AI tools (Cursor, Claude Desktop, VS Code, etc.) that connects to the local ABP Studio instance. ABP Studio must be running for this command to work. - -> You do not need to run this command manually. It is invoked automatically by your AI tool once you add the MCP configuration to your IDE. See the [Configuration](#configuration) examples below. - -> This command connects to the **local ABP Studio** instance. It is separate from the `abp mcp` command, which connects to the ABP.IO cloud MCP service and requires an active license. - -Usage: - -```bash -abp mcp-studio [options] -``` - -Options: - -* `--endpoint` or `-e`: Overrides ABP Studio MCP endpoint. Default value is `http://localhost:38280/mcp/`. - -Example: - -```bash -abp mcp-studio -abp mcp-studio --endpoint http://localhost:38280/mcp/ -``` - -For detailed configuration examples (Cursor, Claude Desktop, VS Code) and the full list of available MCP tools, see the [Model Context Protocol (MCP)](../studio/model-context-protocol.md) documentation. - -> You can also run `abp help mcp-studio` to see available options and example IDE configuration snippets directly in your terminal. - ### generate-razor-page `generate-razor-page` command to generate a page class and then use it in the ASP NET Core pipeline to return an HTML page. @@ -1104,7 +1181,7 @@ body { console.log('MyPage.js loaded!'); ``` -5. Finally, run the `generate-razor-page` command under the `Views` folder: +1. Finally, run the `generate-razor-page` command under the `Views` folder: ```bash > abp generate-razor-page @@ -1134,11 +1211,11 @@ app.Use(async (httpContext, next) => }); ``` -![Razor Page](./../images/abp-generate-razor-page.png) +Razor Page #### Options -* ```--version``` or ```-v```: Specifies the version for ABP CLI to be installed. +- `--version` or `-v`: Specifies the version for ABP CLI to be installed. ### generate-jwks @@ -1146,10 +1223,12 @@ Generates an RSA key pair for use with OpenIddict `private_key_jwt` client authe 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. | + +| 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. @@ -1161,11 +1240,11 @@ 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`. +- `--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 @@ -1183,9 +1262,7 @@ 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 @@ -1235,5 +1312,6 @@ var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync( ## See Also -* [Examples for the new command](./new-command-samples.md) -* [Video tutorial](https://abp.io/video-courses/essentials/abp-cli) +- [Examples for the new command](./new-command-samples.md) +- [Video tutorial](https://abp.io/video-courses/essentials/abp-cli) + diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index b0a76124b4..4366360f30 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -340,10 +340,6 @@ "text": "Monitoring Applications", "path": "studio/monitoring-applications.md" }, - { - "text": "Model Context Protocol (MCP)", - "path": "studio/model-context-protocol.md" - }, { "text": "Working with Kubernetes", "path": "studio/kubernetes.md" @@ -358,6 +354,36 @@ } ] }, + { + "text": "AI Agent", + "items": [ + { + "text": "Overview", + "path": "studio/ai-agent.md", + "isIndex": true + }, + { + "text": "Configuration", + "path": "studio/ai-agent-configuration.md" + }, + { + "text": "Workflows", + "path": "studio/ai-agent-workflows.md" + }, + { + "text": "Built-in Capabilities", + "path": "studio/ai-agent-built-in-capabilities.md" + }, + { + "text": "Git Integration", + "path": "studio/ai-agent-git-integration.md" + }, + { + "text": "Coding with AI Agent", + "path": "studio/coding-with-ai-agent.md" + } + ] + }, { "text": "Concepts", "path": "studio/concepts.md" @@ -586,6 +612,10 @@ } ] }, + { + "text": "Application URLs", + "path": "framework/infrastructure/app-urls.md" + }, { "text": "Background Jobs", "items": [ @@ -1040,7 +1070,7 @@ ] } ] - }, + } ] }, { @@ -1866,6 +1896,68 @@ } ] }, + { + "text": "React", + "items": [ + { + "text": "Overview", + "path": "framework/ui/react/index.md", + "isIndex": true + }, + { + "text": "Configuration and Development", + "items": [ + { + "text": "Environment Variables", + "path": "framework/ui/react/environment-variables.md" + }, + { + "text": "Unit Testing", + "path": "framework/ui/react/unit-testing.md" + } + ] + }, + { + "text": "Core Features", + "items": [ + { + "text": "Authorization", + "path": "framework/ui/react/authorization.md" + }, + { + "text": "Localization", + "path": "framework/ui/react/localization.md" + }, + { + "text": "Permission Management", + "path": "framework/ui/react/permission-management.md" + }, + { + "text": "HTTP Requests", + "path": "framework/ui/react/http-requests.md" + } + ] + }, + { + "text": "Customization and Components", + "items": [ + { + "text": "Customization", + "path": "framework/ui/react/customization.md" + }, + { + "text": "Components", + "path": "framework/ui/react/components/index.md", + "isIndex": true + } + ] + }, + { + "text": "Admin Console", + "path": "framework/ui/react/admin-console.md" + } + ] + }, { "text": "React Native", "items": [ @@ -2198,6 +2290,10 @@ } ] }, + { + "text": "Modular Monolith", + "path": "solution-templates/modular-monolith" + }, { "text": "Microservice Solution", "isLazyExpandable": true, diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json index a5665a1215..cd78818f8c 100644 --- a/docs/en/docs-params.json +++ b/docs/en/docs-params.json @@ -12,6 +12,17 @@ "NG": "Angular" } }, + { + "name": "BlazorUI", + "displayName": "Blazor UI Library", + "values": { + "Blazorise": "Blazorise", + "MudBlazor": "MudBlazor" + }, + "dependsOn": { + "UI": ["Blazor", "BlazorServer", "BlazorWebApp", "MAUIBlazor"] + } + }, { "name": "DB", "displayName": "Database", diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 11ac3f7af1..23c8316ee2 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -357,6 +357,8 @@ context.Services ``` +The configuration above resolves the current tenant from the incoming request (inbound). To make the **outbound** URLs your application generates — such as the password reset link inside an Account email — point to the tenant's subdomain as well, configure `AppUrlOptions`. See [Application URLs](../../infrastructure/app-urls.md#multi-tenant-aware-urls). + ##### Custom Tenant Resolvers You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below: diff --git a/docs/en/framework/fundamentals/index.md b/docs/en/framework/fundamentals/index.md index 7ffa99fd89..5becda669b 100644 --- a/docs/en/framework/fundamentals/index.md +++ b/docs/en/framework/fundamentals/index.md @@ -17,6 +17,7 @@ The following documents explains the fundamental building blocks to create ABP s * [Dependency Injection](./dependency-injection.md) * [Exception Handling](./exception-handling.md) * [Localization](./localization.md) +* [URL-Based Localization](./url-based-localization.md) * [Logging](./logging.md) * [Object Extensions](./object-extensions.md) * [Options](./options.md) diff --git a/docs/en/framework/fundamentals/localization.md b/docs/en/framework/fundamentals/localization.md index 7a4fcf24ac..5fd243eeef 100644 --- a/docs/en/framework/fundamentals/localization.md +++ b/docs/en/framework/fundamentals/localization.md @@ -126,6 +126,43 @@ var str2 = L["Hi__0"]; // Bye World! var str3 = L["Hi__1"]; // Hello World! ```` +You can have more than one localization file with the same culture: files will be merged. This is useful for large modules where splitting translations by feature keeps each file manageable. + +**Example file structure:** + +``` +Localization/ +└── MyResource/ + ├── en.json ← base / shared strings + ├── en_Authors.json ← Author feature strings + ├── en_Books.json ← Book feature strings + └── en_Users.json ← User feature strings +``` + +Files are sorted by name (ordinal order) before merging, so the effective merge order is `en.json` → `en_Authors.json` → `en_Books.json` → `en_Users.json`. + +``` +en.json en_Authors.json en_Books.json +┌─────────────────┐ ┌───────────────┐ ┌──────────────────┐ +│ DisplayName=Name│ │ Author.Id=Id │ │ Book.Id=ISBN │ +│ SaveButton=Save │ │ Author.Bio=.. │ │ Book.Title=Title │ +└─────────────────┘ └───────────────┘ └──────────────────┘ + │ │ │ + └──────────────────┴──────────────────┘ + │ merge (later file wins on duplicate keys) + ▼ + ┌────────────────────┐ + │ DisplayName = Name │ + │ SaveButton = Save │ + │ Author.Id = Id │ + │ Author.Bio = ... │ + │ Book.Id = ISBN │ + │ Book.Title = Title│ + └────────────────────┘ +``` + +> Note: If the same key is defined in multiple files, the value from the last file (in sort order) wins. + ### Default Resource `AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified: @@ -294,6 +331,10 @@ Configure(options => }); ``` +## URL-Based Localization + +ABP supports embedding the culture code directly in the URL path (e.g. `/en/products`, `/zh-Hans/about`), which is useful for SEO-friendly and shareable localized URLs. See the [URL-Based Localization](./url-based-localization.md) document for details. + ## The Client Side See the following documents to learn how to reuse the same localization texts in the JavaScript side; diff --git a/docs/en/framework/fundamentals/url-based-localization.md b/docs/en/framework/fundamentals/url-based-localization.md new file mode 100644 index 0000000000..55d29b50a1 --- /dev/null +++ b/docs/en/framework/fundamentals/url-based-localization.md @@ -0,0 +1,173 @@ +````json +//[doc-seo] +{ + "Description": "Learn how to use ABP's URL-based localization to embed culture in the URL path, enabling SEO-friendly and shareable localized URLs." +} +```` + +# URL-Based Localization + +ABP supports embedding the current culture directly in the URL path, for example `/tr/products` or `/en/about`. This approach is widely used by documentation sites, e-commerce platforms, and any site that needs SEO-friendly, shareable localized URLs. + +By default, ABP detects language from QueryString (`?culture=tr`), Cookie, and `Accept-Language` header. URL path detection is **opt-in** and fully backward-compatible. + +## Enabling URL-Based Localization + +Configure the `AbpRequestLocalizationOptions` in your [module class](../architecture/modularity/basics.md): + +````csharp +Configure(options => +{ + options.UseRouteBasedCulture = true; +}); +```` + +That's all you need. The framework automatically handles the rest. + +## What Happens Automatically + +When you set `UseRouteBasedCulture` to `true`, ABP automatically registers the following: + +* **`RouteDataRequestCultureProvider`** — A built-in ASP.NET Core provider that reads `{culture}` from route data. ABP inserts it after `QueryStringRequestCultureProvider` and before `CookieRequestCultureProvider`. +* **`{culture}/{controller}/{action}` route** — A conventional route for MVC controllers. The `{culture}` parameter uses a custom route constraint (`AbpCultureRouteConstraint`) that only matches culture values configured in `AbpLocalizationOptions.Languages`, so URLs like `/enterprise/products` are not mistaken for culture-prefixed routes. +* **`AbpCultureRoutePagesConvention`** — An `IPageRouteModelConvention` that adds `{culture}/...` route selectors to all Razor Pages. +* **`AbpCultureRouteUrlHelperFactory`** — Replaces the default `IUrlHelperFactory` to auto-inject culture into `Url.Page()` and `Url.Action()` calls. +* **`AbpCultureMenuItemUrlProvider`** — Prepends the culture prefix to navigation menu item URLs (MVC / Blazor Server). +* **`AbpWasmCultureMenuItemUrlProvider`** — Prepends the culture prefix to menu item URLs in Blazor WebAssembly (reads the `UseRouteBasedCulture` flag from `/api/abp/application-configuration`). + +You do not need to configure these individually. + +## URL Generation + +When a request has a `{culture}` route value, all URL generation methods automatically include the culture prefix: + +````csharp +// In a Razor Page — culture is auto-injected, no manual parameter needed +@Url.Page("/About") // Generates: /zh-Hans/About +@Url.Action("About", "Home") // Generates: /zh-Hans/Home/About +```` + +Menu items registered via `IMenuContributor` also automatically get the culture prefix. No changes are needed in your menu contributors or theme. + +## Language Switching + +ABP's built-in language switcher (the `/Abp/Languages/Switch` action) automatically replaces the culture segment in the `returnUrl`. The controller reads the culture from the request cookie to identify the current page culture and replaces it with the new one: + +| Before switching | After switching to English | +|---|---| +| `/tr/products` | `/en/products` | +| `/tenant-a/zh-Hans/about` | `/tenant-a/en/about` | +| `/home?culture=tr&ui-culture=tr` | `/home?culture=en&ui-culture=en` | +| `/about` (no prefix) | `/about` (unchanged) | + +No changes are needed in any theme or language switcher component. + +## MVC / Razor Pages + +MVC and Razor Pages have the most complete support. Everything works automatically when `UseRouteBasedCulture = true` — route registration, URL generation, menu links, and language switching. **No code changes are needed in your pages or controllers.** + +## Blazor Server + +Blazor Server uses SignalR (WebSocket) for the interactive circuit. The HTTP middleware pipeline only runs on the **initial page load** — subsequent interactions happen over the WebSocket connection. ABP handles this by persisting the detected URL culture to a **Cookie** on the first request, so the entire Blazor circuit uses the correct language. + +Culture detection, cookie persistence, menu URLs, and language switching all work automatically. No additional configuration is needed beyond the `UseRouteBasedCulture` option. + +### What requires manual changes + +**Blazor component routes**: ASP.NET Core does not provide an `IPageRouteModelConvention` equivalent for Blazor components. You must manually add the `{culture}` route to each page: + +````razor +@page "/" +@page "/{culture}" + +@code { + [Parameter] + public string? Culture { get; set; } +} +```` + +````razor +@page "/About" +@page "/{culture}/About" + +@code { + [Parameter] + public string? Culture { get; set; } +} +```` + +> This applies to your own application pages. ABP built-in module pages (Identity, Tenant Management, Settings, Account, etc.) already include `@page "/{culture}/..."` routes out of the box — you do not need to add them manually. + +## Blazor WebAssembly (WebApp) + +Blazor WebAssembly (WASM) runs in the browser. On the **first page load**, the server renders the page via SSR, and the culture is detected from the URL. After WASM downloads, subsequent renders run in the browser. The WASM app fetches `/api/abp/application-configuration` from the server to get the current culture, so the culture stays consistent. + +Culture detection, cookie persistence, menu URLs, and language switching all work automatically. The WASM client reads the `UseRouteBasedCulture` flag from the server via `/api/abp/application-configuration`, so no client-side configuration is needed. + +### What requires manual changes + +Same as Blazor Server — you must manually add `@page "/{culture}/..."` routes to your Blazor pages. + +## Angular + +The [ABP Angular UI](../ui/angular/quick-start.md) runs in the browser. The server still applies `UseRouteBasedCulture`; the client reads **`localization.useRouteBasedCulture`** from `/api/abp/application-configuration` (same payload as other UI types). There is no separate Angular setting. + +### Routing + +Angular does not add a culture segment to your route config automatically. Use **`withOptionalRouteCulturePrefix`** from **`@abp/ng.core`** so one route tree matches both **`/identity/users`** and **`/en/identity/users`** (the first path segment is matched only when it looks like a culture code, e.g. `en`, `tr`, `zh-Hans`). + +````typescript +import { Routes } from '@angular/router'; +import { withOptionalRouteCulturePrefix } from '@abp/ng.core'; + +const appRoutesCore: Routes = [ + // ... your routes (path: '', 'account', 'identity', lazy children, etc.) +]; + +export const appRoutes = withOptionalRouteCulturePrefix(appRoutesCore); +```` + +![Angular: routes wrapped with optional culture prefix](../../images/url-based-localization-angular-routes.png) + +### URL → session language + +When **`useRouteBasedCulture`** is **true**, **`RouteBasedCultureService`** (from `@abp/ng.core`) keeps the session language aligned with the first URL segment after navigation. This runs during application bootstrap and on each **`NavigationEnd`**. + +### Menu links, breadcrumbs, and `routerLink` + +Menu paths from **`RoutesService`** are usually **without** a culture prefix (`/identity/users`). Use the **`abpRouteCultureUrl`** pipe on **`routerLink`** (or **`RouteBasedCultureUrlService.prefixPathWithCulture`**) so links navigate to **`/en/identity/users`** when route-based culture is enabled. The **Basic** theme navigation and **Theme Shared** breadcrumb links follow this pattern. + +![Angular: culture-prefixed menu or URL bar](../../images/url-based-localization-angular-menu-url.png) + +### Language switcher (toolbar) + +If the user selects a language in the UI, call **`RouteBasedCultureUrlService.applyLanguageSelection(cultureName)`** (or **`navigateToUrlWithCulture`**) instead of only updating the session language. That rewrites the current URL’s culture segment (or prepends it) so the address bar and session stay consistent; **`RouteBasedCultureService`** then picks up the culture from the URL after navigation. + +### Active menu, breadcrumbs, and route matching + +The browser URL may be **`/en/identity/users`** while menu items and **`RoutesService`** paths stay **`/identity/users`**. For comparisons (active state, **`findRoute`**, permission guard, dynamic layout), normalize the current URL with **`RouteBasedCultureUrlService.normalizeForMenuMatch`** (or **`stripCulturePrefixIfEnabled`**) or use **`getRoutePathForMatching`** where **`getRoutePath`** was used. + +### Configuration refresh + +**`RouteBasedCultureUrlService`** refreshes its cached **`useRouteBasedCulture`** and **languages** when application configuration is updated (for example after **`refreshAppState`**), so hot paths do not query configuration on every change detection cycle. + +## Multi-Tenancy Compatibility + +URL-based localization is fully compatible with [multi-tenancy URL routing](../architecture/multi-tenancy/index.md). The culture route is registered as a conventional route `{culture}/{controller}/{action}`. If your application uses tenant routing (e.g., `/{tenant}/...`), the tenant middleware strips the tenant segment before routing, and the culture segment is handled separately. + +Language switching also supports tenant-prefixed URLs. For example, `/tenant-a/zh-Hans/About` correctly switches to `/tenant-a/en/About`. + +## API Routes + +Routes like `/api/products` have no `{culture}` segment, so `RouteDataRequestCultureProvider` returns `null` and falls through to the next provider (Cookie → `Accept-Language` → default). API routes are completely unaffected. + +## Culture Detection Priority + +ASP.NET Core has a built-in [`RouteDataRequestCultureProvider`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.localization.routing.routedatarequestcultureprovider) (in `Microsoft.AspNetCore.Localization.Routing`) that reads culture from route data, but it is not included in the default provider list. When `UseRouteBasedCulture` is enabled, ABP inserts it after `QueryStringRequestCultureProvider` and before `CookieRequestCultureProvider`. The resulting provider order is: + +1. `QueryStringRequestCultureProvider` (ASP.NET Core default — useful for debugging and testing) +2. `RouteDataRequestCultureProvider` (URL path — inserted by ABP when enabled) +3. `CookieRequestCultureProvider` (ASP.NET Core default) +4. `AcceptLanguageHeaderRequestCultureProvider` (ASP.NET Core default) + +If a URL contains an invalid culture code (e.g. `/xyz1234/page`), `RequestLocalizationMiddleware` ignores it and falls through to the next provider. No error is thrown. diff --git a/docs/en/framework/infrastructure/app-urls.md b/docs/en/framework/infrastructure/app-urls.md new file mode 100644 index 0000000000..cc7d634800 --- /dev/null +++ b/docs/en/framework/infrastructure/app-urls.md @@ -0,0 +1,164 @@ +```json +//[doc-seo] +{ + "Description": "Configure cross-application URLs in ABP with AppUrlOptions and IAppUrlProvider, including multi-tenant subdomain templates and redirect URL validation." +} +``` + +# Application URLs + +ABP provides the `AppUrlOptions` options class and the `IAppUrlProvider` service to centrally configure and resolve URLs that point to **other applications** in your solution (for example, an MVC/Razor Pages UI, an Auth Server, an HTTP API host, etc.). They are typically used when code in one application needs to build a link that targets another — like the Account module putting a **password reset link** into an email. + +* Defines `AppUrlOptions` to register the **root URL** and named relative URLs of each application. +* Provides `IAppUrlProvider` to **resolve** those URLs at runtime, with optional **tenant-aware** placeholder substitution. +* Supports **subdomain-style templates** (e.g. `https://{0}.example.com`) that produce per-tenant URLs without extra code. +* Maintains a `RedirectAllowedUrls` list used by `IAppUrlProvider.IsRedirectAllowedUrlAsync` to validate redirect targets. + +> `AppUrlOptions` is defined in the `Volo.Abp.UI.Navigation` package, which comes pre-installed with the [application startup template](../../solution-templates/layered-web-application). + +## Configuring Application URLs + +`AppUrlOptions` exposes a dictionary of **applications**, each with a `RootUrl` and a set of named `Urls`. + +**Example: Set the root URL and a named URL for the MVC application** + +```csharp +Configure(options => +{ + options.Applications["MVC"].RootUrl = "https://my-app.com"; + options.Applications["MVC"].Urls["MyPage"] = "my-page"; +}); +``` + +* `"MVC"` is the **application key**. Some modules (such as Account) register their URLs under a known key — `"MVC"` is the default for the **server-side UI**. You can use any key you want for your own applications. +* `RootUrl` is the **base URL** of that application. +* `Urls[urlName]` is a **relative path** appended to `RootUrl`. The final URL is built as `RootUrl.EnsureEndsWith('/') + Urls[urlName]`, so the relative path should **not** start with a `/`. When `RootUrl` is `null`, the value of `Urls[urlName]` is returned as-is. + +The Account module, for example, **pre-registers** its URLs in its application module: + +**Example: How the Account module registers the password reset URL** + +```csharp +Configure(options => +{ + options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword"; +}); +``` + +> So configuring `Applications["MVC"].RootUrl` in your own module is usually enough to make password reset and similar Account email links point to the right host. + +### Defaults in the application startup template + +The ABP **application startup template** wires `Applications["MVC"].RootUrl` to the `App:SelfUrl` setting and seeds `RedirectAllowedUrls` from `App:RedirectAllowedUrls`: + +```csharp +Configure(options => +{ + options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; + options.RedirectAllowedUrls.AddRange( + configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty()); +}); +``` + +> This is why Account email links point to your **host URL** out of the box: they reuse `App:SelfUrl`. If that default isn't what you want — for example, in a subdomain-based **multi-tenant** setup — override `Applications["MVC"].RootUrl` with the template you need (see [Multi-Tenant Aware URLs](#multi-tenant-aware-urls)). + +## Using `IAppUrlProvider` + +[Inject](../fundamentals/dependency-injection.md) the `IAppUrlProvider` service into any class that needs to build a cross-application URL. + +**Example: Resolve a root URL and a named URL of the MVC application** + +```csharp +public class MyNotificationSender : ITransientDependency +{ + private readonly IAppUrlProvider _appUrlProvider; + + public MyNotificationSender(IAppUrlProvider appUrlProvider) + { + _appUrlProvider = appUrlProvider; + } + + public async Task SendAsync() + { + var rootUrl = await _appUrlProvider.GetUrlAsync("MVC"); + var pageUrl = await _appUrlProvider.GetUrlAsync("MVC", "MyPage"); + } +} +``` + +* `GetUrlAsync(appName)` returns the configured `RootUrl` for the given application. +* `GetUrlAsync(appName, urlName)` returns the **combined URL** described above. +* `GetUrlAsync(...)` throws an `AbpException` when the resolved URL is `null` or empty (e.g. both `RootUrl` and `Urls[urlName]` are unset). Use `GetUrlOrNullAsync(...)` if you'd rather get `null` and decide what to do yourself. +* `NormalizeUrlAsync(url)` applies tenant placeholder substitution to a URL string that you already have. Useful when the URL doesn't come from `AppUrlOptions`. + +## Multi-Tenant Aware URLs + +If your solution uses **subdomain-based** multi-tenancy (see the [Domain/Subdomain Tenant Resolver](../architecture/multi-tenancy/index.md#domainsubdomain-tenant-resolver)), you'll usually want the **outbound URLs** you generate (email links, redirects) to also be tenant-aware — otherwise the link in a password reset email won't point to the tenant's subdomain. + +`AppUrlOptions` supports the following **placeholders** in any URL value. They are substituted by `IAppUrlProvider` based on the **current tenant**: + +| Placeholder | Replaced with | +| --- | --- | +| `{0}` | Current tenant **name** | +| `{%{{{ {{tenantName}} }}}%}` | Current tenant **name** | +| `{%{{{ {{tenantId}} }}}%}` | Current tenant **id** | + +The `{0}` placeholder uses the **same convention** as `AddDomainTenantResolver("{0}.example.com")`, so a typical subdomain-tenant setup looks like this: + +**Example: Tenant-aware Account email links via a subdomain template** + +```csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.example.com"); +}); + +Configure(options => +{ + options.Applications["MVC"].RootUrl = "https://{0}.example.com"; +}); +``` + +With this configuration, password reset emails sent to a tenant whose name is `acme` will contain a link starting with `https://acme.example.com/`, matching the tenant's subdomain. + +### Host (no tenant) Fallback + +When there is **no current tenant** (host-side request), the placeholder **and the dot following it** are removed together: + +| Template | Tenant `acme` | Host (no tenant) | +| --- | --- | --- | +| `https://{0}.example.com` | `https://acme.example.com` | `https://example.com` | +| `https://{%{{{ {{tenantId}} }}}%}.example.com` | `https://3a21....example.com` | `https://example.com` | + +A single subdomain-style template like the ones above therefore works for **both** tenant and host scenarios without extra configuration. + +> If your subdomain is based on the tenant **id** rather than the name, use `https://{%{{{ {{tenantId}} }}}%}.example.com`. The resolver's `{0}` placeholder accepts both name and id when finding a tenant, but `AppUrlOptions` substitutes `{0}` with the tenant **name**; if those two don't match, switch to the explicit `{%{{{ {{tenantId}} }}}%}` form on the `AppUrlOptions` side. + +## Redirect Allowed URLs + +`AppUrlOptions.RedirectAllowedUrls` is a list of URL entries used by `IAppUrlProvider.IsRedirectAllowedUrlAsync(url)` to decide whether a redirect target is allowed. A URL is allowed when it satisfies **either** of: + +* **Prefix match**: the URL string **starts with** a configured entry (case-insensitive). +* **Subdomain match**: the URL and the entry have the **same scheme** and **port**, and the URL's host **ends with** `.{entry-host}`. + +**Example: Register allowed redirect URLs (including a wildcard)** + +```csharp +Configure(options => +{ + options.RedirectAllowedUrls.Add("https://my-app.com"); + options.RedirectAllowedUrls.Add("https://admin.my-app.com"); + + options.RedirectAllowedUrls.Add("https://*.my-app.com"); +}); +``` + +* A **plain entry** like `https://my-app.com` allows any URL that starts with that prefix, plus any subdomain of `my-app.com`. +* A **wildcard entry** like `https://*.my-app.com` allows any subdomain of `my-app.com`; the `*.` is stripped before the subdomain check. +* Entries also go through **tenant placeholder substitution**, so `https://{0}.my-app.com` is resolved to the current tenant's URL first (e.g. `https://acme.my-app.com`) and then compared. Use the wildcard form when you need to allow *any* tenant subdomain regardless of the current tenant. + +## See Also + +* [Multi-Tenancy](../architecture/multi-tenancy/index.md) +* [Account Module](../../modules/account.md) +* [Emailing](emailing.md) diff --git a/docs/en/framework/infrastructure/emailing.md b/docs/en/framework/infrastructure/emailing.md index a9304b2e57..ff7ab152df 100644 --- a/docs/en/framework/infrastructure/emailing.md +++ b/docs/en/framework/infrastructure/emailing.md @@ -265,3 +265,4 @@ So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent ## See Also * [MailKit integration for sending emails](./mail-kit.md) +* [Application URLs](./app-urls.md) — for building cross-application links inside email content (e.g. password reset links). diff --git a/docs/en/framework/infrastructure/image-manipulation.md b/docs/en/framework/infrastructure/image-manipulation.md index 49a2e74c46..587fab07c7 100644 --- a/docs/en/framework/infrastructure/image-manipulation.md +++ b/docs/en/framework/infrastructure/image-manipulation.md @@ -1,12 +1,12 @@ ```json //[doc-seo] { - "Description": "Learn how to efficiently compress and resize images in your applications using ABP Framework's extensible services powered by ImageSharp and Magick.NET." + "Description": "Learn how to efficiently compress and resize images in your applications using ABP Framework's extensible services powered by ImageSharp, Magick.NET and SkiaSharp." } ``` # Image Manipulation -ABP provides services to compress and resize images and implements these services with popular [ImageSharp](https://sixlabors.com/products/imagesharp/) and [Magick.NET](https://github.com/dlemstra/Magick.NET) libraries. You can use these services in your reusable modules, libraries and applications, so you don't depend on a specific imaging library. +ABP provides services to compress and resize images and implements these services with popular [ImageSharp](https://sixlabors.com/products/imagesharp/), [Magick.NET](https://github.com/dlemstra/Magick.NET) and [SkiaSharp](https://github.com/mono/SkiaSharp) libraries. You can use these services in your reusable modules, libraries and applications, so you don't depend on a specific imaging library. > The image resizer/compressor system is designed to be extensible. You can implement your own image resizer/compressor contributor and use it in your application. @@ -46,10 +46,11 @@ public class YourModule : AbpModule ## Providers -ABP provides two image resizer/compressor implementations out of the box: +ABP provides three image resizer/compressor implementations out of the box: * [Magick.NET](#magick-net-provider) * [ImageSharp](#imagesharp-provider) +* [SkiaSharp](#skiasharp-provider) You should install one of these provides to make it actually working. @@ -334,6 +335,67 @@ Configure(options => }); ``` +## SkiaSharp Provider + +`Volo.Abp.Imaging.SkiaSharp` NuGet package implements the image operations using the [SkiaSharp](https://github.com/mono/SkiaSharp) library. + +## Installation + +You can add this package to your application by either using the [ABP CLI](../../cli) or manually installing it. Using the [ABP CLI](../../cli) is the recommended approach. + +### Using the ABP CLI + +Open a command line terminal in the folder of your project (.csproj file) and type the following command: + +```bash +abp add-package Volo.Abp.Imaging.SkiaSharp +``` + +### Manual Installation + +If you want to manually install; + +1. Add the [Volo.Abp.Imaging.SkiaSharp](https://www.nuget.org/packages/Volo.Abp.Imaging.SkiaSharp) NuGet package to your project: + +``` +dotnet add package Volo.Abp.Imaging.SkiaSharp +``` + +2. Add `AbpImagingSkiaSharpModule` to your [module](../architecture/modularity/basics.md)'s dependency list: + +```csharp +[DependsOn(typeof(AbpImagingSkiaSharpModule))] +public class MyModule : AbpModule +{ + //... +} +``` + +### Configuration + +`SkiaSharpResizerOptions` is an [options object](../fundamentals/options.md) that is used to configure the SkiaSharp image resize system. It has the following properties: + +* `SKSamplingOptions`: The sampling options used by SkiaSharp when resizing. (Default: `SKSamplingOptions.Default`) +* `Quality`: The quality of the encoded image (0-100). (Default: `75`) + +`SkiaSharpCompressOptions` is an [options object](../fundamentals/options.md) that is used to configure the SkiaSharp image compression system. It has the following properties: + +* `Quality`: The quality of the encoded image (0-100). (Default: `75`) + +**Example usage:** + +```csharp +Configure(options => +{ + options.Quality = 80; +}); + +Configure(options => +{ + options.Quality = 60; +}); +``` + ## ASP.NET Core Integration `Volo.Abp.Imaging.AspNetCore` NuGet package defines attributes for controller actions that can automatically compress and/or resize uploaded files. diff --git a/docs/en/framework/infrastructure/text-templating/razor.md b/docs/en/framework/infrastructure/text-templating/razor.md index d2f690c72c..a8bc1c55a6 100644 --- a/docs/en/framework/infrastructure/text-templating/razor.md +++ b/docs/en/framework/infrastructure/text-templating/razor.md @@ -10,6 +10,8 @@ The Razor template is a standard C# class, so you can freely use the functions of C#, such as `dependency injection`, using `LINQ`, custom methods, and even using `Repository`. +> The Razor engine compiles template content into a fully-trusted .NET assembly via Roslyn and executes it in the host process, so editing a Razor template at runtime is functionally equivalent to executing arbitrary server-side code. `RazorTemplateRenderingEngine.IsSandboxed` is therefore `false`, and the [Text Template Management](../../../modules/text-template-management.md) module requires the `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` permission (in addition to `EditContents`) before allowing such templates to be edited via its UI. Grant the related permission only to fully trusted developers/operators. If you need a sandboxed engine for content editors, consider [Scriban](scriban.md), which is configured to honor Scriban's [safe runtime boundaries](https://github.com/scriban/scriban/blob/master/site/docs/runtime/safe-runtime.md) by default. + ## Installation diff --git a/docs/en/framework/infrastructure/text-templating/scriban.md b/docs/en/framework/infrastructure/text-templating/scriban.md index bb72624294..d2bb8e4a26 100644 --- a/docs/en/framework/infrastructure/text-templating/scriban.md +++ b/docs/en/framework/infrastructure/text-templating/scriban.md @@ -7,6 +7,31 @@ # Scriban Integration +## Safe Runtime (Sandbox) + +Scriban's [safe runtime](https://github.com/scriban/scriban/blob/master/site/docs/runtime/safe-runtime.md) builds the practical sandbox out of four boundaries: which globals you expose through `ScriptObject`, which .NET members you allow through the member filter, whether you configure `TemplateContext.TemplateLoader` for `include`, and which `TemplateContext` execution limits you enable. ABP's `ScribanTemplateRenderingEngine` is configured to honor these boundaries by default: + +| Boundary | ABP default | +|----------|-------------| +| Globals exposed | Only the `globalContext` (`Dictionary`) entries, the `model` you pass to `RenderAsync`, and the `L` localization helper. | +| .NET member access | `TemplateContext.MemberFilter` is set to `IsMemberAllowed`, an allowlist that exposes public properties only. Methods, fields, events, and `object`-level members (`GetType`, `ToString`, ...) are not reachable, which closes reflection-based escape paths such as `{%{{{ model.GetType.Assembly.GetType "..." }}}%}`. | +| `TemplateLoader` | Not configured. `include` directives have no template loader and cannot read templates from disk or other sources unless you explicitly wire one up. | +| Execution limits | Scriban's defaults (`LoopLimit = 1000`, `RecursiveLimit = 100`, `LimitToString = 1 MB`, `RegexTimeOut = 10s`). Override `CreateScribanTemplateContext` to tighten these for your own scenarios. | + +The recommended way to expose data to a Scriban template is via `ScriptObject` or `IDictionary` — the keys you put there are exactly what the template can see. When you pass a .NET object as `model`, the `MemberFilter` ensures only properties are exposed, but the safest pattern is to pre-build a dictionary or `ScriptObject` so the surface is fully under your control: + +````csharp +await _templateRenderer.RenderAsync( + "MyTemplate", + model: new Dictionary + { + { "name", user.Name }, + { "email", user.Email } + }); +```` + +If you must pass a .NET object whose methods/fields the template needs to read, override `ScribanTemplateRenderingEngine.IsMemberAllowed` to relax the filter. Only do so when the model objects are trusted and do not carry secrets, since methods and reflection entry points become reachable to whoever can edit the template content. + ## Installation It is suggested to use the [ABP CLI](../../../cli) to install this package. diff --git a/docs/en/framework/ui/angular/authorization.md b/docs/en/framework/ui/angular/authorization.md index 365333224a..bec5478d3a 100644 --- a/docs/en/framework/ui/angular/authorization.md +++ b/docs/en/framework/ui/angular/authorization.md @@ -105,7 +105,7 @@ function configureAuthFilter() { } ``` -- `AuthErrorFilter:` is a model for filter object and it have 3 properties +- `AuthErrorFilter:` is a model for filter object and it has 3 properties - `id:` a unique key in the list for the filter object - `executable:` a status for the filter object. If it's false then it won't work, yet it'll stay in the list - `execute:` a function that stores the skip logic diff --git a/docs/en/framework/ui/angular/checkbox-component.md b/docs/en/framework/ui/angular/checkbox-component.md index fa3824027d..c44952d545 100644 --- a/docs/en/framework/ui/angular/checkbox-component.md +++ b/docs/en/framework/ui/angular/checkbox-component.md @@ -14,7 +14,6 @@ The ABP Checkbox Component is a reusable form input component for the checkbox t - `label` - `labelClass (default form-check-label)` - `checkboxId` -- `checkboxReadonly` - `checkboxReadonly (default form-check-input)` - `checkboxStyle` diff --git a/docs/en/framework/ui/angular/data-table-column-extensions.md b/docs/en/framework/ui/angular/data-table-column-extensions.md index 81312cdd75..ed473b937f 100644 --- a/docs/en/framework/ui/angular/data-table-column-extensions.md +++ b/docs/en/framework/ui/angular/data-table-column-extensions.md @@ -15,6 +15,8 @@ Entity prop extension system allows you to add a new column to the data table fo You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells. +> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z). + ## How to Set Up In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../../modules/identity.md). @@ -64,7 +66,7 @@ Import `identityEntityPropContributors` in your routing configuration and pass i ```js // src/app/app.routes.ts -// other imports +import { Routes } from '@angular/router'; import { identityEntityPropContributors } from './entity-prop-contributors'; export const APP_ROUTES: Routes = [ @@ -84,6 +86,20 @@ export const APP_ROUTES: Routes = [ ]; ``` +#### Legacy NgModule projects + +```js +{ + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + entityPropContributors: identityEntityPropContributors, + }), + ), +}, +``` + That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `identity` package. ## How to Render Custom HTML in Cells diff --git a/docs/en/framework/ui/angular/dynamic-form-extensions.md b/docs/en/framework/ui/angular/dynamic-form-extensions.md index a92a06d609..5a9a157e10 100644 --- a/docs/en/framework/ui/angular/dynamic-form-extensions.md +++ b/docs/en/framework/ui/angular/dynamic-form-extensions.md @@ -16,6 +16,8 @@ Form prop extension system allows you to add a new field to the create and/or ed You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contributor for an edit form. +> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z). + ## How to Set Up In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../../modules/identity.md) and validate it. @@ -69,7 +71,7 @@ Import `identityCreateFormPropContributors` and `identityEditFormPropContributor ```js // src/app/app.routes.ts -// other imports +import { Routes } from '@angular/router'; import { identityCreateFormPropContributors, identityEditFormPropContributors, @@ -93,6 +95,21 @@ export const APP_ROUTES: Routes = [ ]; ``` +#### Legacy NgModule projects + +```js +{ + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + createFormPropContributors: identityCreateFormPropContributors, + editFormPropContributors: identityEditFormPropContributors, + }), + ), +}, +``` + That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `identity` package. ## Object Extensions diff --git a/docs/en/framework/ui/angular/entity-action-extensions.md b/docs/en/framework/ui/angular/entity-action-extensions.md index 5a8e74a2f9..5bdded94c0 100644 --- a/docs/en/framework/ui/angular/entity-action-extensions.md +++ b/docs/en/framework/ui/angular/entity-action-extensions.md @@ -15,6 +15,8 @@ Entity action extension system allows you to add a new action to the action menu You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access the current entity in your code. +> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z). + ## How to Set Up In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../../modules/identity.md). @@ -56,12 +58,12 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li ### Step 2. Import and Use Entity Action Contributors -Import `identityEntityActionContributors` in your routing configuration and pass it to the static `configureRoutes` method for `identity` routes as seen below: +Import `identityEntityActionContributors` in your routing configuration and pass it to the static `createRoutes` method for `identity` routes as seen below: ```js // src/app/app.routes.ts -// other imports +import { Routes } from '@angular/router'; import { identityEntityActionContributors } from './entity-action-contributors'; export const APP_ROUTES: Routes = [ @@ -81,6 +83,20 @@ export const APP_ROUTES: Routes = [ ]; ``` +#### Legacy NgModule projects + +```js +{ + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + entityActionContributors: identityEntityActionContributors, + }), + ), +}, +``` + That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `identity` package. ## How to Place a Custom Modal and Trigger It by Entity Actions diff --git a/docs/en/framework/ui/angular/extensions-overall.md b/docs/en/framework/ui/angular/extensions-overall.md index 03971a46c4..73eae88816 100644 --- a/docs/en/framework/ui/angular/extensions-overall.md +++ b/docs/en/framework/ui/angular/extensions-overall.md @@ -16,6 +16,8 @@ See the documents below for the details: * [Page Toolbar Extension](page-toolbar-extensions.md) * [Dynamic Form (or Form Prop) Extensions](dynamic-form-extensions.md) +> **Standalone-first:** Current ABP templates use standalone APIs (`app.config.ts`, `app.routes.ts`, and `createRoutes()`). Extension examples register contributors through route-level lazy loading — `loadChildren` returns routes from `createRoutes({ ... })`, not NgModules. Legacy NgModule projects can pass the same contributor options to `SomeModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z) for the `forLazy()` → `createRoutes()` migration. + ## Extensible Table Component Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible table. diff --git a/docs/en/framework/ui/angular/feature-libraries.md b/docs/en/framework/ui/angular/feature-libraries.md index 48ef6de504..b4c9325505 100644 --- a/docs/en/framework/ui/angular/feature-libraries.md +++ b/docs/en/framework/ui/angular/feature-libraries.md @@ -9,6 +9,8 @@ ABP has an ever-growing number of feature modules and [introducing a new one](../../architecture/modularity/basics.md) is always possible. When the UI is Angular, these features have modular Angular libraries accompanying them. +> **Standalone-first:** Current templates use standalone APIs. Configuration providers such as `provideIdentityConfig()` belong in `app.config.ts`, and features are lazy-loaded via `createRoutes()` in `app.routes.ts`. The `loadChildren` pattern below loads **route definitions**, not NgModules. In legacy NgModule projects, replace `createRoutes()` with `IdentityModule.forLazy()` (or the equivalent `forLazy()` method on the feature module). See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z). + ## Feature Library Content Each library has at least two key elements: @@ -85,6 +87,20 @@ When you load the identity feature like this, the "Users" page, for example, wil Depending on the library, the `.createRoutes` static method may also receive some options that configure how the feature works. +#### Legacy NgModule projects + +If your application still uses NgModules, lazy-load the feature with `forLazy()` instead: + +```js +{ + path: "identity", + loadChildren: () => + import("@abp/ng.identity").then((m) => m.IdentityModule.forLazy()), +}, +``` + +Pass the same options object to `forLazy({ ... })` that you would pass to `createRoutes({ ... })` when configuring extensions or other feature options. + --- 1 _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./modifying-the-menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ [↩](#a-modify-route) diff --git a/docs/en/framework/ui/angular/features.md b/docs/en/framework/ui/angular/features.md index eece10bfb2..e190519f98 100644 --- a/docs/en/framework/ui/angular/features.md +++ b/docs/en/framework/ui/angular/features.md @@ -7,7 +7,7 @@ # Features -You can get the value of a feature on the client-side using the [config state service](./config-state.md) if it is allowed by the feature definition on the server-side. +You can get the value of a feature on the client-side using the [config state service](./config-state-service.md) if it is allowed by the feature definition on the server-side. > This document explains how to get feature values in an Angular application. See the [Features document](../../infrastructure/features.md) to learn the feature system. diff --git a/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md b/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md index 6db3c95fda..b9f8fa14f5 100644 --- a/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md +++ b/docs/en/framework/ui/angular/how-replaceable-components-work-with-extensions.md @@ -9,6 +9,8 @@ Additional UI extensibility points ([Entity action extensions](../angular/entity-action-extensions.md), [data table column extensions](../angular/data-table-column-extensions.md), [page toolbar extensions](../angular/page-toolbar-extensions.md) and others) are used in ABP pages to allow to control entity actions, table columns and page toolbar of a page. If you replace a page, you need to apply some configurations to be able to work extension components in your component. Let's see how to do this by replacing the roles page. +> **Standalone-first:** The example below uses standalone components with an `imports` array and `inject()`. Current ABP templates follow this pattern. Module-based examples in older docs remain valid for legacy projects — see [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z). + Create a new component called `MyRolesComponent`: ```bash diff --git a/docs/en/framework/ui/angular/internet-connection-service.md b/docs/en/framework/ui/angular/internet-connection-service.md index 945234f8bd..c2d067f4be 100644 --- a/docs/en/framework/ui/angular/internet-connection-service.md +++ b/docs/en/framework/ui/angular/internet-connection-service.md @@ -17,7 +17,7 @@ When you inject the InternetConnectionService you can get the current internet s # How To Use -İt's easy, just inject the service and get the network status. +It's easy, just inject the service and get the network status. **You can get via signal** ```ts diff --git a/docs/en/framework/ui/angular/oauth-module.md b/docs/en/framework/ui/angular/oauth-module.md index d8ecab52fc..9e6d4a8e1e 100644 --- a/docs/en/framework/ui/angular/oauth-module.md +++ b/docs/en/framework/ui/angular/oauth-module.md @@ -7,7 +7,7 @@ # ABP OAuth Package -The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0. +The authentication functionality has been moved from @abp/ng.core to @abp/ng.oauth since v7.0. If your app is version 8.3 or higher, you should include "provideAbpOAuth()" after "provideAbpCore()" in the `appConfig` array of your `app.config.ts`. diff --git a/docs/en/framework/ui/angular/page-toolbar-extensions.md b/docs/en/framework/ui/angular/page-toolbar-extensions.md index 30c5c9813d..1348bdc265 100644 --- a/docs/en/framework/ui/angular/page-toolbar-extensions.md +++ b/docs/en/framework/ui/angular/page-toolbar-extensions.md @@ -15,6 +15,8 @@ Page toolbar extension system allows you to add a new action to the toolbar of a You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button. +> **Standalone-first:** Current ABP templates use standalone APIs. The `loadChildren` examples below lazy-load routes from `createRoutes({ ... })` — they do not require NgModules. Legacy NgModule projects can pass the same options to `IdentityModule.forLazy({ ... })` instead. See [ABP Now Supports Angular Standalone Applications](https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z). + ## How to Add an Action to Page Toolbar In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../../modules/identity.md) to the console. @@ -65,7 +67,7 @@ Import `identityToolbarActionContributors` in your routing configuration and pas ```js // src/app/app.routes.ts -// other imports +import { Routes } from '@angular/router'; import { identityToolbarActionContributors } from './toolbar-action-contributors'; export const APP_ROUTES: Routes = [ @@ -85,6 +87,20 @@ export const APP_ROUTES: Routes = [ ]; ``` +#### Legacy NgModule projects + +```js +{ + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + toolbarActionContributors: identityToolbarActionContributors, + }), + ), +}, +``` + That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package. ## How to Add a Custom Component to Page Toolbar @@ -100,7 +116,7 @@ We need to have a component before we can pass it to the toolbar action contribu ```js // src/app/click-me-button.component.ts -import { Component, Inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensible'; @@ -109,10 +125,7 @@ import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensibl template: ``, }) export class ClickMeButtonComponent { - constructor( - @Inject(EXTENSIONS_ACTION_DATA) - private data: ActionData - ) {} + private data = inject>(EXTENSIONS_ACTION_DATA); handleClick() { this.data.record.forEach(user => console.log(user.userName)); @@ -168,7 +181,7 @@ Import `identityToolbarActionContributors` in your routing configuration and pas ```js // src/app/app.routes.ts -// other imports +import { Routes } from '@angular/router'; import { identityToolbarActionContributors } from './toolbar-action-contributors'; export const APP_ROUTES: Routes = [ @@ -188,6 +201,20 @@ export const APP_ROUTES: Routes = [ ]; ``` +#### Legacy NgModule projects + +```js +{ + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + toolbarActionContributors: identityToolbarActionContributors, + }), + ), +}, +``` + That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components. ## How to Place a Custom Modal and Trigger It by Toolbar Actions diff --git a/docs/en/framework/ui/angular/quick-start.md b/docs/en/framework/ui/angular/quick-start.md index 2417b187b1..8a9f6cb7b4 100644 --- a/docs/en/framework/ui/angular/quick-start.md +++ b/docs/en/framework/ui/angular/quick-start.md @@ -7,7 +7,7 @@ # ABP Angular Quick Start -**In this version ABP uses Angular [21.0.x](https://github.com/angular/angular/tree/21.0.x) version. You don't have to install Angular CLI globally** +**In this version ABP uses Angular [21.2.x](https://github.com/angular/angular/tree/21.2.x) version. You don't have to install Angular CLI globally** ## How to Prepare Development Environment @@ -22,7 +22,6 @@ Please follow the steps below to prepare your development environment for Angula - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=visualstudioexptteam.vscodeintellicode) - [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) - [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - - [Angular 10 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout](https://marketplace.visualstudio.com/items?itemName=Mikael.Angular-BeastCode) - [JavaScript (ES6) code snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets) - [JavaScript Debugger](https://marketplace.visualstudio.com/items?itemName=ms-vscode.js-debug) (built-in, usually pre-installed) - [Git History](https://marketplace.visualstudio.com/items?itemName=donjayamanne.githistory) @@ -167,7 +166,7 @@ When you run the development server, variables defined in _environment.ts_ take 2. Run `yarn` or `npm install` if you have not installed dependencies already. 3. Run `yarn build:prod` or `npm run build:prod`. -Angular compiler optimizing the build using Terser +Browser blocking access to backend API due to self-signed certificate error Depending on project size, the compilation may take a few minutes. When it is finished, the compiled output will be placed inside the _/dist_ folder. Voila! You have deployment-ready build artifacts. diff --git a/docs/en/framework/ui/angular/settings.md b/docs/en/framework/ui/angular/settings.md index dc352e96a1..28689ce5f4 100644 --- a/docs/en/framework/ui/angular/settings.md +++ b/docs/en/framework/ui/angular/settings.md @@ -7,7 +7,7 @@ # Settings -You can get settings on the client-side using the [config state service](./config-state.md) if they are allowed by their setting definition on the server-side. +You can get settings on the client-side using the [config state service](./config-state-service.md) if they are allowed by their setting definition on the server-side. > This document only explains how settings work in the Angular UI projects. See the [settings document](../../infrastructure/settings.md) to understand the ABP setting system. diff --git a/docs/en/framework/ui/angular/testing.md b/docs/en/framework/ui/angular/testing.md index ab215f7eba..4ec5577f73 100644 --- a/docs/en/framework/ui/angular/testing.md +++ b/docs/en/framework/ui/angular/testing.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Learn how to unit test your ABP Angular UI applications with preconfigured Karma and Jasmine, plus ABP-specific testing topics." + "Description": "Learn how to unit test your ABP Angular UI applications with preconfigured Vitest and TestBed, plus ABP-specific testing topics." } ``` @@ -9,89 +9,105 @@ ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.dev/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**. -## Setup +## Test Stack -In Angular, unit tests use [Karma](https://karma-runner.github.io/) and [Jasmine](https://jasmine.github.io) by default. Although we like Jest more, we chose not to deviate from these defaults, so **the application template you download will have Karma and Jasmine preconfigured**. You can find the Karma configuration inside the _karma.conf.js_ file in the root folder. You don't have to do anything. Adding a spec file and running `npm test` will work. +The application template you download is preconfigured for unit testing. You can add a `*.spec.ts` file and run `yarn test` without adding extra test infrastructure. -## Basics +| Package / API | Purpose | +| --- | --- | +| [Vitest](https://vitest.dev/) | Test runner and assertion library. | +| [jsdom](https://github.com/jsdom/jsdom) | Browser-like DOM environment for component tests. | +| `@angular/core/testing` (`TestBed`) | The standard testing utilities of Angular for components, services, and pipes. | +| `@abp/ng.core/testing` | ABP testing module and helpers that replace real ABP services with mocks. | +| `@abp/ng.theme.shared/testing` | Testing module for shared theme features such as validation. | -An over-simplified spec file looks like this: +ABP Angular packages in the [framework repository](https://github.com/abpframework/abp/tree/dev/npm/ng-packs) use the same Vitest setup. Library tests there also use [`@ngneat/spectator/vitest`](https://github.com/ngneat/spectator) for HTTP and component tests, but the application template uses `TestBed` directly. -```js -import { CoreTestingModule } from "@abp/ng.core/testing"; -import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; -import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { NgxValidateCoreModule } from "@ngx-validate/core"; -import { MyComponent } from "./my.component"; +## Configuration -describe("MyComponent", () => { - let fixture: ComponentFixture; +The test target in _angular.json_ uses Angular's built-in Vitest builder: - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [MyComponent], - imports: [ - CoreTestingModule.withConfig(), - ThemeSharedTestingModule.withConfig(), - ThemeBasicTestingModule.withConfig(), - NgxValidateCoreModule, - ], - providers: [ - /* mock providers here */ - ], - }).compileComponents(); - }) - ); +```json +// angular.json - beforeEach(() => { - fixture = TestBed.createComponent(MyComponent); - fixture.detectChanges(); - }); +"test": { + "builder": "@angular/build:unit-test" +} +``` - it("should be initiated", () => { - expect(fixture.componentInstance).toBeTruthy(); - }); -}); +Spec files are compiled with _tsconfig.spec.json_, which enables Vitest globals: + +```json +// tsconfig.spec.json + +{ + "compilerOptions": { + "types": ["vitest/globals"] + }, + "include": ["src/**/*.spec.ts"] +} ``` -If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**. +You do not need a _karma.conf.js_ file. Angular CLI wires Vitest and jsdom for you. -## Tips +## Running Tests -### Angular Testing Library +Run tests in watch mode: + +```bash +yarn test +``` -Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. +Run tests once, which is useful for CI: -The simple example above can be written with Angular Testing Library as follows: +```bash +ng test --watch=false +``` + +Vitest exits with a non-zero status code when a test fails, so the command above works in pipelines. + +## Basics -```js +An over-simplified spec file looks like this: + +```ts import { CoreTestingModule } from "@abp/ng.core/testing"; -import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; -import { ComponentFixture } from "@angular/core/testing"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NgxValidateCoreModule } from "@ngx-validate/core"; -import { render } from "@testing-library/angular"; +import { AuthService } from "@abp/ng.core"; +import { vi } from "vitest"; import { MyComponent } from "./my.component"; describe("MyComponent", () => { let fixture: ComponentFixture; + let mockAuthService: { isAuthenticated: boolean; navigateToLogin: ReturnType }; beforeEach(async () => { - const result = await render(MyComponent, { + mockAuthService = { + isAuthenticated: false, + navigateToLogin: vi.fn(), + }; + + await TestBed.configureTestingModule({ imports: [ CoreTestingModule.withConfig(), ThemeSharedTestingModule.withConfig(), - ThemeBasicTestingModule.withConfig(), NgxValidateCoreModule, + MyComponent, ], providers: [ - /* mock providers here */ + { + provide: AuthService, + useValue: mockAuthService, + }, ], - }); + }).compileComponents(); + }); - fixture = result.fixture; + beforeEach(() => { + fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); }); it("should be initiated", () => { @@ -100,45 +116,36 @@ describe("MyComponent", () => { }); ``` -Very similar, as you can see. The real difference kicks in when we use queries and fire events. +If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**. -```js -// other imports -import { getByLabelText, screen } from "@testing-library/angular"; -import userEvent from "@testing-library/user-event"; +Current templates use standalone components, so put the component under test in the `imports` array instead of `declarations`. -describe("MyComponent", () => { - beforeEach(/* removed for sake of brevity */); +If your application uses `@abp/ng.theme.basic`, also import `ThemeBasicTestingModule.withConfig()` from `@abp/ng.theme.basic/testing`. - it("should display advanced filters", () => { - const filters = screen.getByTestId("author-filters"); - const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; - expect(nameInput.offsetWidth).toBe(0); +### Mocking Dependencies - const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); - userEvent.click(advancedFiltersBtn); +Use Vitest mocks instead of Jasmine spies: - expect(nameInput.offsetWidth).toBeGreaterThan(0); +```ts +import { vi } from "vitest"; - userEvent.type(nameInput, "fooo{backspace}"); - expect(nameInput.value).toBe("foo"); - }); -}); +const deleteSpy = vi.fn().mockReturnValue(of(null)); +fixture.componentInstance.service.delete = deleteSpy; + +expect(deleteSpy).toHaveBeenCalledWith("some-id"); ``` -The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below: +The template's `home.component.spec.ts` is a good reference for mocking ABP services and asserting DOM behavior with `TestBed`. -- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) -- [User Event](https://testing-library.com/docs/ecosystem-user-event) -- [Examples](https://github.com/testing-library/angular-testing-library/tree/main/apps/example-app/src/app/examples) +## Tips ### Clearing DOM After Each Spec -One thing to remember is that Karma runs tests in real browser instances. That means, you will be able to see the result of your test code, but also have problems with components attached to the document body which may not get cleared after each test, even when you configure Karma to do so. +Tests run in jsdom, not a real browser. Components attached to `document.body` — such as modals, confirmation dialogs, and toasts — may not be removed automatically between specs. -We have prepared a simple function with which you can clear any leftover DOM elements after each test. +We have prepared a simple function with which you can clear leftover DOM elements after each test: -```js +```ts // other imports import { clearPage } from "@abp/ng.core/testing"; @@ -147,239 +154,189 @@ describe("MyComponent", () => { afterEach(() => clearPage(fixture)); - beforeEach(async () => { - const result = await render(MyComponent, { - /* removed for sake of brevity */ - }); - fixture = result.fixture; - }); - // specs here }); ``` -Please make sure you use it because Karma will fail to remove dialogs otherwise and you will have multiple copies of modals, confirmation boxes, and alike. +Please use it when you test features that render into the document body. Otherwise you may end up with multiple copies of modals, confirmation boxes, and similar elements. ### Waiting -Some components, modals, in particular, work off-detection-cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them. +Some components, modals in particular, work off the change-detection cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them. -For this purpose, we have prepared a `wait` function. +For this purpose, we have prepared a `wait` function: -```js +```ts // other imports import { wait } from "@abp/ng.core/testing"; describe("MyComponent", () => { - beforeEach(/* removed for sake of brevity */); + let fixture: ComponentFixture; it("should open a modal", async () => { - const openModalBtn = screen.getByRole("button", { name: "Open Modal" }); - userEvent.click(openModalBtn); + const openModalBtn = fixture.nativeElement.querySelector('[role="button"]'); + openModalBtn.click(); await wait(fixture); - const modal = screen.getByRole("dialog"); - + const modal = fixture.nativeElement.ownerDocument.querySelector('[role="dialog"]'); expect(modal).toBeTruthy(); - - /* wait again after closing the modal */ }); }); ``` The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right. -## Testing Example +### Angular Testing Library + +Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. It is not included in the application template by default, but you can add `@testing-library/angular` and `@testing-library/user-event` if you prefer that style. -Here is an example test suite. It doesn't cover all, but gives quite a good idea about what the testing experience will be like. +The ABP testing modules work the same way with Testing Library: -```js -import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing"; -import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +```ts +import { CoreTestingModule } from "@abp/ng.core/testing"; import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; import { ComponentFixture } from "@angular/core/testing"; -import { - NgbCollapseModule, - NgbDatepickerModule, - NgbDropdownModule, -} from "@ng-bootstrap/ng-bootstrap"; import { NgxValidateCoreModule } from "@ngx-validate/core"; -import { CountryService } from "@proxy/countries"; -import { - findByText, - getByLabelText, - getByRole, - getByText, - queryByRole, - render, - screen, -} from "@testing-library/angular"; -import userEvent from "@testing-library/user-event"; -import { BehaviorSubject, of } from "rxjs"; -import { CountryComponent } from "./country.component"; - -const list$ = new BehaviorSubject({ - items: [{ id: "ID_US", name: "United States of America" }], - totalCount: 1, -}); - -describe("Country", () => { - let fixture: ComponentFixture; +import { render, screen } from "@testing-library/angular"; +import { MyComponent } from "./my.component"; - afterEach(() => clearPage(fixture)); +describe("MyComponent", () => { + let fixture: ComponentFixture; beforeEach(async () => { - const result = await render(CountryComponent, { + const result = await render(MyComponent, { imports: [ CoreTestingModule.withConfig(), ThemeSharedTestingModule.withConfig(), - ThemeBasicTestingModule.withConfig(), NgxValidateCoreModule, - NgbCollapseModule, - NgbDatepickerModule, - NgbDropdownModule, ], providers: [ - { - provide: CountryService, - useValue: { - getList: () => list$, - }, - }, + /* mock providers here */ ], }); fixture = result.fixture; }); - it("should display advanced filters", () => { - const filters = screen.getByTestId("country-filters"); - const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; - expect(nameInput.offsetWidth).toBe(0); - - const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); - userEvent.click(advancedFiltersBtn); - - expect(nameInput.offsetWidth).toBeGreaterThan(0); - - userEvent.type(nameInput, "fooo{backspace}"); - expect(nameInput.value).toBe("foo"); - - userEvent.click(advancedFiltersBtn); - expect(nameInput.offsetWidth).toBe(0); - }); - - it("should have a heading", () => { - const heading = screen.getByRole("heading", { name: "Countries" }); - expect(heading).toBeTruthy(); + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); }); +}); +``` - it("should render list in table", async () => { - const table = await screen.findByTestId("country-table"); +The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below: - const name = getByText(table, "United States of America"); - expect(name).toBeTruthy(); - }); +- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) +- [User Event](https://testing-library.com/docs/ecosystem-user-event) +- [Examples](https://github.com/testing-library/angular-testing-library/tree/main/apps/example-app/src/app/examples) - it("should display edit modal", async () => { - const actionsBtn = screen.queryByRole("button", { name: /actions/i }); - userEvent.click(actionsBtn); +When you use Testing Library with modals or confirmation dialogs, combine it with `clearPage` and `wait` from `@abp/ng.core/testing` as shown above. - const editBtn = screen.getByRole("button", { name: /edit/i }); - userEvent.click(editBtn); +## Testing Example - await wait(fixture); +Here is an example based on the application template's `home.component.spec.ts`. It shows how to mock an ABP service and assert component state and DOM output: - const modal = screen.getByRole("dialog"); - const modalHeading = queryByRole(modal, "heading", { name: /edit/i }); - expect(modalHeading).toBeTruthy(); +```ts +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { AuthService } from "@abp/ng.core"; +import { vi } from "vitest"; +import { HomeComponent } from "./home.component"; - const closeBtn = getByText(modal, "×"); - userEvent.click(closeBtn); +describe("HomeComponent", () => { + let fixture: ComponentFixture; + let mockAuthService: { isAuthenticated: boolean; navigateToLogin: ReturnType }; - await wait(fixture); + beforeEach(async () => { + mockAuthService = { + isAuthenticated: false, + navigateToLogin: vi.fn(), + }; - expect(screen.queryByRole("dialog")).toBeFalsy(); + await TestBed.configureTestingModule({ + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + NgxValidateCoreModule, + HomeComponent, + ], + providers: [ + { + provide: AuthService, + useValue: mockAuthService, + }, + ], + }).compileComponents(); }); - it("should display create modal", async () => { - const newBtn = screen.getByRole("button", { name: /new/i }); - userEvent.click(newBtn); - - await wait(fixture); - - const modal = screen.getByRole("dialog"); - const modalHeading = queryByRole(modal, "heading", { name: /new/i }); - - expect(modalHeading).toBeTruthy(); + it("should be initiated", () => { + fixture = TestBed.createComponent(HomeComponent); + fixture.detectChanges(); + expect(fixture.componentInstance).toBeTruthy(); }); - it("should validate required name field", async () => { - const newBtn = screen.getByRole("button", { name: /new/i }); - userEvent.click(newBtn); + describe("when login state is false", () => { + beforeEach(() => { + mockAuthService.isAuthenticated = false; + fixture = TestBed.createComponent(HomeComponent); + fixture.detectChanges(); + }); - await wait(fixture); + it("hasLoggedIn should be false", () => { + expect(fixture.componentInstance.hasLoggedIn).toBe(false); + }); - const modal = screen.getByRole("dialog"); - const nameInput = getByRole(modal, "textbox", { - name: /^name/i, - }) as HTMLInputElement; + it("button should exist", () => { + const button = fixture.nativeElement.querySelector('[role="button"]'); + expect(button).toBeDefined(); + }); - userEvent.type(nameInput, "x"); - userEvent.type(nameInput, "{backspace}"); + describe("when button clicked", () => { + beforeEach(() => { + const button = fixture.nativeElement.querySelector('[role="button"]'); + button.click(); + }); - const nameError = await findByText(modal, /required/i); - expect(nameError).toBeTruthy(); + it("navigateToLogin should have been called", () => { + expect(mockAuthService.navigateToLogin).toHaveBeenCalled(); + }); + }); }); +}); +``` - it("should delete a country", () => { - const getSpy = spyOn(fixture.componentInstance.list, "get"); - const deleteSpy = jasmine.createSpy().and.returnValue(of(null)); - fixture.componentInstance.service.delete = deleteSpy; - - const actionsBtn = screen.queryByRole("button", { name: /actions/i }); - userEvent.click(actionsBtn); - - const deleteBtn = screen.getByRole("button", { name: /delete/i }); - userEvent.click(deleteBtn); +For list pages with modals, confirmations, and service proxies, keep using the ABP testing modules, mock your generated proxy services with `vi.fn()`, and use `clearPage` / `wait` when body-level UI is involved. - const confirmText = screen.getByText("AreYouSure"); - expect(confirmText).toBeTruthy(); +## CI Configuration - const confirmBtn = screen.getByRole("button", { name: "Yes" }); - userEvent.click(confirmBtn); +Run unit tests once in CI with: - expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id); - expect(getSpy).toHaveBeenCalledTimes(1); - }); -}); +```sh +ng test --watch=false ``` -## CI Configuration - -You would need a different configuration for your CI environment. To set up a new configuration for your unit tests, find the test project in _angular.json_ file and add one as seen below: +If you need a dedicated CI configuration, add one under the `test` target in _angular.json_: ```json // angular.json "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { /* several options here */ }, + "builder": "@angular/build:unit-test", "configurations": { - "production": { - "karmaConfig": "karma.conf.prod.js" + "ci": { + "watch": false } } } ``` -Now you can copy the _karma.conf.js_ as _karma.conf.prod.js_ and use any configuration you like in it. Please check [Karma configuration file document](http://karma-runner.github.io/5.2/config/configuration-file.html) for config options. - -Finally, don't forget to run your CI tests with the following command: +Then run: ```sh -npm test -- --prod +ng test --configuration=ci ``` ## See Also diff --git a/docs/en/framework/ui/blazor/basic-theme.md b/docs/en/framework/ui/blazor/basic-theme.md index 33c0cb7175..f8cd0c7023 100644 --- a/docs/en/framework/ui/blazor/basic-theme.md +++ b/docs/en/framework/ui/blazor/basic-theme.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -20,6 +21,18 @@ The Basic Theme is a theme implementation for the Blazor UI. It is a minimalist > See the [Theming document](theming.md) to learn about themes. +{{if BlazorUI == "MudBlazor"}} + +> **MudBlazor Variant** — When the `--blazor-ui-library mudblazor` option is used, the Basic Theme ships as a MudBlazor variant. Replace `BasicTheme` with `MudBlazorBasicTheme` everywhere in this document (package names, module type names and namespaces). The MudBlazor variant is **not** based on Bootstrap — it uses MudBlazor's Material Design layout components. +> +> Concrete package names you will see when using the MudBlazor variant: +> +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorBasicTheme` +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorBasicTheme.Bundling` +> * Module types: `Abp{...}MudBlazorBasicThemeModule`, `Abp{...}MudBlazorBasicThemeBundlingModule` + +{{end}} + ## Installation If you need to manually this theme, follow the steps below: diff --git a/docs/en/framework/ui/blazor/components/submit-button.md b/docs/en/framework/ui/blazor/components/submit-button.md index 832554c9b1..82dbeeeee9 100644 --- a/docs/en/framework/ui/blazor/components/submit-button.md +++ b/docs/en/framework/ui/blazor/components/submit-button.md @@ -1,12 +1,21 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { - "Description": "Explore the `SubmitButton` component in Blazor UI, designed for easy form submissions with localization support and loading indicators." + "Description": "Explore the submit button component in Blazor UI, designed for easy form submissions with localization support and loading indicators." } ``` # Blazor UI: SubmitButton Component +{{if BlazorUI == "Blazorise"}} + `SubmitButton` is a simple wrapper around `Button` component. It is used to be placed inside of page Form or Modal dialogs where it can response to user actions and to be activated as a default button by pressing an ENTER key. Once clicked it will go into the `disabled` state and also it will show a small loading indicator until clicked event is finished. ## Quick Example @@ -29,4 +38,54 @@ Notice that we didn't specify any text, like `Save Changes`. This is because `Su @L["Save"] -``` \ No newline at end of file +``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +The MudBlazor variant of ABP UI does not ship a dedicated `SubmitButton` wrapper. Use the standard `MudButton` together with the typical `Processing`/`Disabled` pattern to disable the button and show a progress indicator while the click handler is running. + +## Quick Example + +```razor + + @if (_processing) + { + + } + @L["Save"] + + +@code { + private bool _processing; + + private async Task SaveAsync() + { + _processing = true; + try + { + // ... your save operation + } + finally + { + _processing = false; + } + } +} +``` + +## Submit on Enter + +When the button is placed inside a `` or a ``, pressing ENTER inside an input control submits the form. To run validation before saving, call `_form.Validate()` first. See the [Forms & Validation](../forms-validation.md) page for details. + +## Use Inside `AbpMudCrudPageBase` + +The MudBlazor CRUD page base (`AbpMudCrudPageBase`) already wires up the standard create/update buttons inside its dialogs and shows a progress indicator while the application service call is running. In most cases you don't need to author a save button by hand; override `OnCreatingEntityAsync` / `OnUpdatingEntityAsync` instead. + +> Check the [MudBlazor button documentation](https://mudblazor.com/components/button) for all available options. + +{{end}} \ No newline at end of file diff --git a/docs/en/framework/ui/blazor/customization-overriding-components.md b/docs/en/framework/ui/blazor/customization-overriding-components.md index 8855ff89bd..c919ed084e 100644 --- a/docs/en/framework/ui/blazor/customization-overriding-components.md +++ b/docs/en/framework/ui/blazor/customization-overriding-components.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -41,6 +42,8 @@ The next step is to create a razor component, like `MyBranding.razor`, in your a The content of the `MyBranding.razor` is shown below: +{{if BlazorUI == "Blazorise"}} + ````html @using Volo.Abp.DependencyInjection {{if UI == "BlazorServer"}} @@ -58,6 +61,33 @@ The content of the `MyBranding.razor` is shown below: ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +The MudBlazor variant uses the LeptonX-based MudBlazor theme by default. The component to override is the `Branding` component shipped by the active MudBlazor theme: + +````html +@using Volo.Abp.DependencyInjection +{{if UI == "BlazorServer"}} +@using Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} +{{if UI == "Blazor"}} +@using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} + +@inherits Branding +@attribute [ExposeServices(typeof(Branding))] +@attribute [Dependency(ReplaceServices = true)] + + + +```` + +> If you are using the MudBlazor BasicTheme or a different MudBlazor theme, replace the namespace with the namespace of that theme's `Themes/` folder. + +{{end}} + Let's explain the code: * `@inherits Branding` line inherits the Branding component defined by the [Basic Theme](basic-theme.md) (in the {{if UI == "BlazorServer"}}`Volo.Abp.AspNetCore.Components.Server.BasicTheme.Themes.Basic`{{end}} {{if UI == "Blazor"}}`Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic`{{end}} namespace). @@ -75,6 +105,8 @@ Now, you can run the application to see the result: If you prefer to use code-behind file for the C# code of your component, you can use the attributes in the C# side. +{{if BlazorUI == "Blazorise"}} + **MyBlazor.razor** ````html @@ -113,6 +145,50 @@ namespace MyProject.Blazor.Components } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +**MyBlazor.razor** + +````html +{{if UI == "BlazorServer"}} +@using Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} +{{if UI == "Blazor"}} +@using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX +{{end}} +@inherits Branding + + + +```` + +**MyBlazor.razor.cs** + +````csharp +{{if UI == "BlazorServer"}} +using Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX; +{{end}} +{{if UI == "Blazor"}} +using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme.Themes.MudBlazorLeptonX; +{{end}} + +using Volo.Abp.DependencyInjection; + +namespace MyProject.Blazor.Components +{ + [ExposeServices(typeof(Branding))] + [Dependency(ReplaceServices = true)] + public partial class MyBranding + { + + } +} +```` + +{{end}} + ## Theming The [Theming](theming.md) system allows you to build your own theme. You can create your theme from scratch or get the [Basic Theme](basic-theme.md) and change however you like. diff --git a/docs/en/framework/ui/blazor/data-table-column-extensions.md b/docs/en/framework/ui/blazor/data-table-column-extensions.md index b89b825201..bb6bc6eeb0 100644 --- a/docs/en/framework/ui/blazor/data-table-column-extensions.md +++ b/docs/en/framework/ui/blazor/data-table-column-extensions.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -102,6 +109,8 @@ public class CustomTableColumn Navigate to the razor file and paste the following code. +{{if BlazorUI == "Blazorise"}} + ```csharp @using System @using Volo.Abp.Identity @@ -116,6 +125,27 @@ else } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@using Volo.Abp.Identity + +@if (Data.As().EmailConfirmed) +{ + +} +else +{ + +} +``` + +> When using MudBlazor, the standard data grid in module pages is `AbpMudExtensibleDataGrid`. You can replace `Component = typeof(CustomTableColumn)` exactly the same way as in Blazorise; the column system is shared across both UI libraries. + +{{end}} + Navigate back to the `CustomizedUserManagement` class, and use `Component` property to specify the custom blazor component. ```csharp diff --git a/docs/en/framework/ui/blazor/entity-action-extensions.md b/docs/en/framework/ui/blazor/entity-action-extensions.md index ab3d4b3cba..78d16f7327 100644 --- a/docs/en/framework/ui/blazor/entity-action-extensions.md +++ b/docs/en/framework/ui/blazor/entity-action-extensions.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -95,6 +102,8 @@ Here, the list of the properties that you use in the `EntityAction`. #### Example +{{if BlazorUI == "Blazorise"}} + ```csharp var clickMeAction = new EntityAction() { @@ -115,3 +124,33 @@ var clickMeAction = new EntityAction() } }; ``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +var clickMeAction = new EntityAction() +{ + Text = "Click Me!", + Clicked = (data) => + { + //TODO: Write your custom code + + return Task.CompletedTask; + }, + Color = MudBlazor.Color.Error, + Icon = MudBlazor.Icons.Material.Filled.PanTool, + ConfirmationMessage = (data) => "Are you sure you want to click to the action?", + Visible = (data) => + { + //TODO: Write your custom visibility action + //var selectedUser = data.As(); + return true; + } +}; +``` + +> The MudBlazor variant uses `MudBlazor.Color` enum values (e.g. `Color.Primary`, `Color.Error`, `Color.Success`) for `Color`, and Material Icon constants (e.g. `Icons.Material.Filled.Edit`) for `Icon`. The `EntityAction` model itself is shared with Blazorise; only the values you put inside it change. + +{{end}} diff --git a/docs/en/framework/ui/blazor/error-handling.md b/docs/en/framework/ui/blazor/error-handling.md index 2fae4ff38d..211c56f25e 100644 --- a/docs/en/framework/ui/blazor/error-handling.md +++ b/docs/en/framework/ui/blazor/error-handling.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -36,7 +37,9 @@ There are different type of `Exception` classes handled differently by the ABP. **Example** -````csharp +{{if BlazorUI == "Blazorise"}} + +````razor @page "/" @using Volo.Abp @@ -60,11 +63,41 @@ There are different type of `Exception` classes handled differently by the ABP. {{end}} +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/" +@using Volo.Abp + +Throw test exception + +@code +{ + private async Task TestException() + { + try + { + throw new UserFriendlyException("A user friendly error message!"); + } + catch(UserFriendlyException ex) + { + await HandleErrorAsync(ex); + } + } +} +```` + +{{end}} + +{{end}} + {{if UI == "Blazor"}} **Example** -````csharp +{{if BlazorUI == "Blazorise"}} + +````razor @page "/" @using Volo.Abp @@ -78,6 +111,28 @@ There are different type of `Exception` classes handled differently by the ABP. } } ```` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/" +@using Volo.Abp + +Throw test exception + +@code +{ + private void TestException() + { + throw new UserFriendlyException("A user friendly error message!"); + } +} +```` + +{{end}} + {{end}} ABP automatically handle the exception and show an error message to the user: diff --git a/docs/en/framework/ui/blazor/forms-validation.md b/docs/en/framework/ui/blazor/forms-validation.md index 7fb1423e77..769fc71dbb 100644 --- a/docs/en/framework/ui/blazor/forms-validation.md +++ b/docs/en/framework/ui/blazor/forms-validation.md @@ -1,12 +1,21 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { - "Description": "Learn how to implement form validation in ABP Blazor UI using Blazorise's validation infrastructure with practical examples." + "Description": "Learn how to implement form validation in ABP Blazor UI using Blazorise or MudBlazor with practical examples." } ``` # Blazor UI: Forms & Validation +{{if BlazorUI == "Blazorise"}} + ABP Blazor UI is based on the [Blazorise](https://blazorise.com/docs) and does not have a built-in form validation infrastructure. However, you can use the [Blazorise validation infrastructure](https://blazorise.com/docs/components/validation) to validate your forms. ## Sample @@ -44,4 +53,118 @@ _The example is provided by official Blazorise documentation._ } ``` -> Check the [Blazorise documentation](https://blazorise.com/docs/components/validation) for more information and examples. \ No newline at end of file +> Check the [Blazorise documentation](https://blazorise.com/docs/components/validation) for more information and examples. + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +ABP Blazor UI built on top of [MudBlazor](https://mudblazor.com) uses MudBlazor's built-in form components and validation infrastructure. MudBlazor accepts a `ValidationAttribute` (e.g. `[Required]`, `[EmailAddress]` from ASP.NET Core's `DataAnnotations`) on the input's `Validation` parameter, plus custom `Func` / `Func>` delegates. FluentValidation can be plugged in the same way. + +## Sample + +The most common pattern is wrapping inputs in a `` and binding the form's validation state through `IsValid`: + +> Standard MudBlazor and ABP usings (`@using MudBlazor`, `@using Volo.Abp.MudBlazorUI`, etc.) come from the project's `_Imports.razor`. The example below only adds the additional usings needed for validation. + +```razor +@using System.ComponentModel.DataAnnotations + + + + + + + + + Submit + + + + +@code { + private MudForm _form; + private bool _isValid; + private SampleModel _model = new(); + + private async Task SubmitAsync() + { + await _form.Validate(); + if (_isValid) + { + // ... + } + } + + public class SampleModel + { + [Required] + public string Name { get; set; } + + [Required, EmailAddress] + public string Email { get; set; } + } +} +``` + +### Inputs Used in CRUD Pages + +ABP's MudBlazor CRUD pages (see `AbpMudCrudPageBase`) use a `` containing a `` and standard MudBlazor inputs: + +* `` / `` for text and multi-line text +* `` / `` for dropdowns +* `` for booleans +* `` / `` for date and time +* `` for numbers + +`AbpMudCrudPageBase.CreateEntityAsync` and `UpdateEntityAsync` validate the form for you (`CreateFormRef.Validate()` / `EditFormRef.Validate()`) and only call the corresponding hook when the form is valid. To inject custom logic before the application service call, override `OnCreatingEntityAsync` / `OnUpdatingEntityAsync` (do **not** re-validate inside the override): + +```csharp +protected override Task OnCreatingEntityAsync() +{ + // mutate NewEntity here if needed + return base.OnCreatingEntityAsync(); +} +``` + +> Check the [MudBlazor documentation](https://mudblazor.com/components/form) for the full list of validation modes and the [MudBlazor inputs reference](https://mudblazor.com/components/textfield). + +### Modal Focus Behavior + +By default a `MudFocusTrap` inside `` focuses the first tabbable child element after the dialog opens. When the dialog contains a `` as the first child, that "first tabbable element" is the tab button — not the first input on the active tab — so the user has to click into the field manually. + +For dialogs that contain a ``, use this three-part setup to focus the intended input automatically: + +```razor + + + + + + + ... + + ... +``` + +- `DefaultFocus="DefaultFocus.None"` on the `` disables the focus trap's automatic focus so it doesn't grab the tab button. +- `KeepPanelsAlive="true"` on the `` mounts every tab panel up front, so the first input's `firstRender` happens at dialog-open time (otherwise inactive panels are mounted later, and `AutoFocus` runs after the dialog is already visible). +- `AutoFocus="true"` on the first input asks MudBlazor to focus that field on its first render. + +`DialogOptions.DefaultFocus` is ignored for inline dialogs (` + ShowAsync()`), so always set `DefaultFocus` directly on the `` element. + +For a dialog without `` (first child is the input), `DefaultFocus="DefaultFocus.FirstChild"` (the MudBlazor default) is enough and you don't need `AutoFocus`. + +{{end}} \ No newline at end of file diff --git a/docs/en/framework/ui/blazor/overall.md b/docs/en/framework/ui/blazor/overall.md index 23077e307e..f5d5cf7b78 100644 --- a/docs/en/framework/ui/blazor/overall.md +++ b/docs/en/framework/ui/blazor/overall.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -95,6 +102,8 @@ Currently, three themes are **officially provided**: There are a set of standard libraries that comes pre-installed and supported by all the themes: +{{if BlazorUI == "Blazorise"}} + * [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. * [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree. * [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. @@ -106,6 +115,22 @@ These libraries are selected as the base libraries and available to the applicat > Beginning from June, 2021, the Blazorise library has dual licenses; open source & commercial. Based on your yearly revenue, you may need to buy a commercial license. See [this post](https://blazorise.com/news/announcing-2022-blazorise-plans-and-pricing-updates) to learn more. The Blazorise license is bundled with ABP and commercial customers doesn't need to buy an extra Blazorise license. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +* [MudBlazor](https://mudblazor.com/) as the component library, providing a complete set of Material Design components built natively for Blazor (form controls, data grid, dialogs, snackbars, dates, etc.). +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [Flag Icon](https://github.com/lipis/flag-icons) as a library to show flags of countries. + +These libraries are selected as the base libraries and available to the applications and modules. + +The MudBlazor variant ships its own theming, dialog, snackbar and popover providers (see [Theming](theming.md)). The MudBlazor library is MIT-licensed and is bundled with ABP at no extra cost. + +> Bootstrap is **not** required when using MudBlazor; MudBlazor brings its own layout and component styles. + +{{end}} + ### The Layout The themes provide the layout. So, you have a responsive layout with the standard features already implemented. The screenshot below has taken from the layout of the [Basic Theme](basic-theme.md): diff --git a/docs/en/framework/ui/blazor/page-header.md b/docs/en/framework/ui/blazor/page-header.md index ed2e16903d..eb4ea8ed30 100644 --- a/docs/en/framework/ui/blazor/page-header.md +++ b/docs/en/framework/ui/blazor/page-header.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -7,7 +14,23 @@ # Blazor UI: Page Header -You can use the`PageHeader` component to set the page title, the breadcrumb items and the toolbar items for a page. Before using the `PageHeader` component, you need to add a using statement for the `Volo.Abp.AspNetCore.Components.Web.Theming.Layout` namespace. +You can use the `PageHeader` component to set the page title, the breadcrumb items and the toolbar items for a page. Before using the `PageHeader` component, you need to add a using statement for the namespace: + +{{if BlazorUI == "Blazorise"}} + +```razor +@using Volo.Abp.AspNetCore.Components.Web.Theming.Layout +``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +``` + +{{end}} Once you add the `PageHeader` component to your page, you can control the related values using the parameters. @@ -30,6 +53,8 @@ Breadcrumbs can be added using the `BreadcrumbItems` property. **Example: Add Language Management to the breadcrumb items.** +{{if BlazorUI == "Blazorise"}} + Create a collection of `Volo.Abp.BlazoriseUI.BreadcrumbItem` objects and set the collection to the `BreadcrumbItems` parameter. ```csharp @@ -44,6 +69,31 @@ public partial class Index } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Create a collection of `MudBlazor.BreadcrumbItem` objects and set the collection to the `BreadcrumbItems` parameter. The MudBlazor `BreadcrumbItem` constructor takes `(string text, string href, bool disabled = false, string icon = null)`. + +```csharp +using MudBlazor; + +public partial class Index +{ + protected List BreadcrumbItems { get; } = new(); + + protected override void OnInitialized() + { + BreadcrumbItems.Add(new BreadcrumbItem( + text: "Language Management", + href: null, + disabled: true)); + } +} +``` + +{{end}} + Navigate back to the razor page. ```csharp @@ -57,19 +107,38 @@ The theme then renders the breadcrumb. An example render result can be: * The Home icon is rendered by default. Set `BreadcrumbShowHome` to `false` to hide it. * Breadcrumb items will be activated based on current navigation. Set `BreadcrumbShowCurrent` to `false` to disable it. -You can add as many items as you need. `BreadcrumbItem` constructor gets three parameters: +You can add as many items as you need. + +{{if BlazorUI == "Blazorise"}} + +The `Volo.Abp.BlazoriseUI.BreadcrumbItem` constructor gets three parameters: * `text`: The text to show for the breadcrumb item. * `url` (optional): A URL to navigate to, if the user clicks to the breadcrumb item. * `icon` (optional): An icon class (like `fas fa-user-tie` for Font-Awesome) to show with the `text`. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +The `MudBlazor.BreadcrumbItem` constructor takes: + +* `text`: The text to show for the breadcrumb item. +* `href`: A URL to navigate to (use `null` for the current page). +* `disabled` (optional): When `true`, the item is rendered as the current/non-clickable item. +* `icon` (optional): A Material icon (e.g. `Icons.Material.Filled.Language`). + +{{end}} + ## Page Toolbar Page toolbar can be set using the `Toolbar` property. **Example: Add a "New Item" toolbar item to the page toolbar.** -Create a `PageToolbar` object and define toolbar items using the `AddButton` extension method. +Create a `PageToolbar` object and define toolbar items using the `AddButton` extension method. + +{{if BlazorUI == "Blazorise"}} ```csharp public partial class Index @@ -87,6 +156,28 @@ public partial class Index } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +public partial class Index +{ + protected PageToolbar Toolbar { get; } = new(); + + protected override void OnInitialized() + { + Toolbar.AddButton("New Item", () => + { + //Write your click action here + return Task.CompletedTask; + }, icon: MudBlazor.Icons.Material.Filled.Add); + } +} +``` + +{{end}} + Navigate back to the razor page and set the `Toolbar` parameter. ```csharp diff --git a/docs/en/framework/ui/blazor/page-layout.md b/docs/en/framework/ui/blazor/page-layout.md index 75666fd17a..abd32745d0 100644 --- a/docs/en/framework/ui/blazor/page-layout.md +++ b/docs/en/framework/ui/blazor/page-layout.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -36,6 +43,8 @@ Indicates current selected menu item name. Menu item name should match a unique Menu item name can be set on runtime too. +{{if BlazorUI == "Blazorise"}} + ```html @inject PageLayout PageLayout @@ -49,6 +58,25 @@ Menu item name can be set on runtime too. } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@inject PageLayout PageLayout + +Change Menu + +@code{ + protected void SetCategoriesMenuAsSelected() + { + PageLayout.MenuItemName = "MyProjectName.Categories"; + } +} +``` + +{{end}} + ![leptonx selected menu item](../../../images/leptonx-selected-menu-item-example.gif) @@ -57,6 +85,9 @@ Menu item name can be set on runtime too. ## BreadCrumbs BreadCrumbItems are used to render breadcrumbs in the PageHeader. + +{{if BlazorUI == "Blazorise"}} + ```csharp @inject PageLayout PageLayout @@ -65,6 +96,21 @@ BreadCrumbItems are used to render breadcrumbs in the PageHeader. } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@using MudBlazor +@inject PageLayout PageLayout + +@{ + PageLayout.BreadcrumbItems.Add(new BreadcrumbItem("My Page", "/my-page")); +} +``` + +{{end}} + ## Toolbar ToolbarItems are used to render action toolbar items in the PageHeader. diff --git a/docs/en/framework/ui/blazor/page-toolbar-extensions.md b/docs/en/framework/ui/blazor/page-toolbar-extensions.md index 9c96bf90f7..96c48b000d 100644 --- a/docs/en/framework/ui/blazor/page-toolbar-extensions.md +++ b/docs/en/framework/ui/blazor/page-toolbar-extensions.md @@ -1,3 +1,10 @@ +```json +//[doc-params] +{ + "BlazorUI": ["Blazorise", "MudBlazor"] +} +``` + ```json //[doc-seo] { @@ -27,6 +34,8 @@ We will use the [component override system](customization-overriding-components. Here, the content of the overridden `SetToolbarItemsAsync` method. +{{if BlazorUI == "Blazorise"}} + ```csharp protected override async ValueTask SetToolbarItemsAsync() { @@ -38,10 +47,31 @@ protected override async ValueTask SetToolbarItemsAsync() }, "file-import", Blazorise.Color.Secondary); } ``` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +protected override async ValueTask SetToolbarItemsAsync() +{ + await base.SetToolbarItemsAsync(); + Toolbar.AddButton("Import users from excel", () => + { + //TODO: Write your custom code + return Task.CompletedTask; + }, MudBlazor.Icons.Material.Filled.Upload, MudBlazor.Color.Secondary); +} +``` + +{{end}} + > In order to use the `AddButton` extension method, you need to add a using statement for the `Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars` namespace. Here, the entire content of the file. +{{if BlazorUI == "Blazorise"}} + ```csharp using System.Threading.Tasks; using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars; @@ -67,6 +97,37 @@ namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity } ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```csharp +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity.Blazor.Pages.Identity; + +namespace MyCompanyName.MyProjectName.Blazor.Pages.Identity +{ + [ExposeServices(typeof(UserManagement))] + [Dependency(ReplaceServices = true)] + public class CustomizedUserManagement : UserManagement + { + protected override async ValueTask SetToolbarItemsAsync() + { + await base.SetToolbarItemsAsync(); + Toolbar.AddButton("Import users from excel", () => + { + //TODO: Write your custom code + return Task.CompletedTask; + }, MudBlazor.Icons.Material.Filled.Upload, MudBlazor.Color.Secondary); + } + } +} +``` + +{{end}} + When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `Order` to set the order of the button component relative to the other components). ## Advanced Use Cases @@ -83,9 +144,21 @@ For this example, we've created a `MyToolbarComponent` component under the `/Pag `MyToolbarComponent.razor` content: -````csharp +{{if BlazorUI == "Blazorise"}} + +````razor ```` + +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +CLICK ME +```` + +{{end}} We will leave the `MyToolbarComponent.razor.cs` file empty. Then you can add the `MyToolbarComponent` to the user management page toolbar: diff --git a/docs/en/framework/ui/blazor/theming.md b/docs/en/framework/ui/blazor/theming.md index 7ded33608f..1fff0a057f 100644 --- a/docs/en/framework/ui/blazor/theming.md +++ b/docs/en/framework/ui/blazor/theming.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -52,6 +53,8 @@ All the themes must depend on the [Volo.Abp.AspNetCore.Components.Server.Theming {{end}} +{{if BlazorUI == "Blazorise"}} + * [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. * [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree. * [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. @@ -61,6 +64,31 @@ These libraries are selected as the base libraries and available to the applicat > Bootstrap's JavaScript part is not used since the Blazorise library already provides the necessary functionalities to the Bootstrap components in a native way. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +* [MudBlazor](https://mudblazor.com/) as the component library, providing a Material Design component set built natively for Blazor (form controls, data grid, dialogs, snackbars, dates, etc.). +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [Flag Icon](https://github.com/lipis/flag-icons) as a library to show flags of countries. + +These libraries are selected as the base libraries and available to the applications and modules. + +A theme using the MudBlazor variant must place the four MudBlazor providers in the layout root so dialogs, snackbars and popovers work everywhere: + +```razor + + + + + +@Body +``` + +The provided themes (`Volo.Abp.AspNetCore.Components.Server.MudBlazorLeptonXTheme`, `Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorLeptonXTheme`, etc.) ship these providers as part of their layout templates. + +{{end}} + ### The Layout All themes must define a layout for the application. The following image shows the user management page in the [Basic Theme](basic-theme.md) application layout: @@ -90,6 +118,8 @@ A theme is simply a Razor Class Library. The easiest way of creating a new theme is adding [Basic Theme Source Code](https://github.com/abpframework/abp/tree/dev/modules/basic-theme) module with source codes and customizing it. +{{if BlazorUI == "Blazorise"}} + {{if UI == "Blazor"}} ```bash abp add-package Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme --with-source-code --add-to-solution-file @@ -102,6 +132,24 @@ abp add-package Volo.Abp.AspNetCore.Components.Server.BasicTheme --with-source-c ``` {{end}} +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +{{if UI == "Blazor"}} +```bash +abp add-package Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme --with-source-code --add-to-solution-file +``` +{{end}} + +{{if UI == "BlazorServer"}} +```bash +abp add-package Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme --with-source-code --add-to-solution-file +``` +{{end}} + +{{end}} + ### Global Styles / Scripts A theme generally needs to add a global style to the page. ABP provides a system to manage the [Global Styles and Scripts](global-scripts-styles.md). A theme can implement the `IBundleContributor` to add global style or script files to the page. diff --git a/docs/en/framework/ui/index.md b/docs/en/framework/ui/index.md index d892c63c69..b1b4bcecd0 100644 --- a/docs/en/framework/ui/index.md +++ b/docs/en/framework/ui/index.md @@ -1,16 +1,17 @@ ```json //[doc-seo] { - "Description": "Explore ABP's UI options, including MVC, Blazor, Angular, React Native, and MAUI, to build dynamic applications effortlessly." + "Description": "Explore ABP's UI options, including React, MVC, Blazor, Angular, React Native, and MAUI, to build dynamic applications effortlessly." } ``` # ABP UI Options -ABP provides several options for building the user interface (UI) in your applications. Here are some of the officially supported UI options you can use with ABP: +ABP provides several options for building the user interface (UI) in your applications. React is part of the **modern template system**. Here are some of the officially supported UI options you can use with ABP: +* [React](./react/index.md) *(modern template system only)* * [MVC / Razor Pages](./mvc-razor-pages/overall.md) * [Blazor](./blazor/overall.md) * [Angular](./angular/quick-start.md) * [React Native](./react-native/index.md) -* [MAUI](./maui/index.md) \ No newline at end of file +* [MAUI](./maui/index.md) diff --git a/docs/en/framework/ui/mvc-razor-pages/overall.md b/docs/en/framework/ui/mvc-razor-pages/overall.md index fc5b0b0ac0..425c94d2d9 100644 --- a/docs/en/framework/ui/mvc-razor-pages/overall.md +++ b/docs/en/framework/ui/mvc-razor-pages/overall.md @@ -17,8 +17,8 @@ ABP provides a convenient and comfortable way of creating web applications using ASP.NET Core provides two models for UI development: -* **[MVC (Model-View-Controller)](https://docs.microsoft.com/en-us/aspnet/core/mvc/)** is the classic way that exists from the version 1.0. This model can be used to create UI pages/components and HTTP APIs. -* **[Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/)** was introduced with the ASP.NET Core 2.0 as a new way to create web pages. +- **[MVC (Model-View-Controller)](https://docs.microsoft.com/en-us/aspnet/core/mvc/)** is the classic way that exists from the version 1.0. This model can be used to create UI pages/components and HTTP APIs. +- **[Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/)** was introduced with the ASP.NET Core 2.0 as a new way to create web pages. **ABP supports both** of the MVC and the Razor Pages models. However, it is suggested to create the **UI pages with Razor Pages** approach and use the **MVC model to build HTTP APIs**. So, all the pre-build modules, samples and the documentation is based on the Razor Pages for the UI development, while you can always apply the MVC pattern to create your own pages. @@ -32,18 +32,18 @@ The [application startup template](../../../solution-templates/application-modul ABP provides a complete [Theming](theming.md) system with the following goals: -* Reusable [application modules](../../../modules) are developed **theme-independent**, so they can work with any UI theme. -* UI theme is **decided by the final application**. -* The theme is distributed via NuGet/NPM packages, so it is **easily upgradable**. -* The final application can **customize** the selected theme. +- Reusable [application modules](../../../modules) are developed **theme-independent**, so they can work with any UI theme. +- UI theme is **decided by the final application**. +- The theme is distributed via NuGet/NPM packages, so it is **easily upgradable**. +- The final application can **customize** the selected theme. ### Current Themes Currently, three themes are **officially provided**: -* The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. -* The [Lepton Theme](https://abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP](https://abp.io/) license. -* The [LeptonX Theme](https://x.leptontheme.com/) is a theme that has both [commercial](../../../ui-themes/lepton-x/mvc.md) and [lite](../../../ui-themes/lepton-x-lite/asp-net-core.md) choices. +- The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. +- The [Lepton Theme](https://abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP](https://abp.io/) license. +- The [LeptonX Theme](https://x.leptontheme.com/) is a theme that has both [commercial](../../../ui-themes/lepton-x/mvc.md) and [lite](../../../ui-themes/lepton-x-lite/asp-net-core.md) choices. There are also some community-driven themes for the ABP (you can search on the web). @@ -72,7 +72,7 @@ You can use these libraries directly in your applications, without needing to ma The themes provide the standard layouts. So, you have responsive layouts with the standard features already implemented. The screenshot below has taken from the Application Layout of the [Basic Theme](basic-theme.md): -![basic-theme-application-layout](../../../images/basic-theme-account-layout.png) +basic-theme-application-layout See the [Theming](theming.md) document for more layout options and other details. @@ -90,13 +90,13 @@ Dynamic JavaScript API Client Proxy system allows you to consume your server sid **Example: Get a list of authors from the server** -````js +```js acme.bookStore.authors.author.getList({ maxResultCount: 10 }).then(function(result){ console.log(result.items); }); -```` +``` `acme.bookStore.authors.author.getList` is an auto-generated function that internally makes an AJAX call to the server. @@ -108,7 +108,7 @@ ABP makes it easier & type safe to write Bootstrap HTML. **Example: Render a Bootstrap modal** -````html +```html @@ -116,7 +116,7 @@ ABP makes it easier & type safe to write Bootstrap HTML. -```` +``` See the [Tag Helpers](tag-helpers) document for more. @@ -126,9 +126,9 @@ ABP provides `abp-dynamic-form` and `abp-input` tag helpers to dramatically simp **Example: Use `abp-dynamic-form` to create a complete form based on a model** -````html +```html -```` +``` See the [Forms & Validation](forms-validation.md) document for details. @@ -136,14 +136,14 @@ See the [Forms & Validation](forms-validation.md) document for details. ABP provides a flexible and modular Bundling & Minification system to create bundles and minify style/script files on runtime. -````html +```html -```` +``` Also, Client Side Package Management system offers a modular and consistent way of managing 3rd-party library dependencies. @@ -157,11 +157,11 @@ See the [Bundling & Minification](bundling-minification.md) and [Client Side Pac ABP provides a lot of built-in solutions to common application requirements; -* [Widget System](widgets.md) can be used to create reusable widgets & create dashboards. -* [Page Alerts](page-alerts.md) makes it easy to show alerts to the user. -* [Modal Manager](modals.md) provides a simple way to build and use modals. -* [Data Tables](data-tables.md) integration makes straightforward to create data grids. +- [Widget System](widgets.md) can be used to create reusable widgets & create dashboards. +- [Page Alerts](page-alerts.md) makes it easy to show alerts to the user. +- [Modal Manager](modals.md) provides a simple way to build and use modals. +- [Data Tables](data-tables.md) integration makes straightforward to create data grids. ## Customization -There are a lot of ways to customize the theme and the UIs of the pre-built modules. You can override components, pages, static resources, bundles and more. See the [User Interface Customization Guide](customization-user-interface.md). +There are a lot of ways to customize the theme and the UIs of the pre-built modules. You can override components, pages, static resources, bundles and more. See the [User Interface Customization Guide](customization-user-interface.md). \ No newline at end of file diff --git a/docs/en/framework/ui/react-native/index.md b/docs/en/framework/ui/react-native/index.md index 862d836ac4..92b7c0815d 100644 --- a/docs/en/framework/ui/react-native/index.md +++ b/docs/en/framework/ui/react-native/index.md @@ -5,12 +5,12 @@ } ``` -````json +```json //[doc-params] { "Architecture": ["Monolith", "Tiered", "Microservice"] } -```` +``` # Getting Started with React Native @@ -18,7 +18,7 @@ The ABP platform provides a basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated with your ABP-based backends**. -![React Native gif](../../../images/react-native-introduction.gif) +React Native gif ## How to Prepare Development Environment @@ -28,10 +28,9 @@ Please follow the steps below to prepare your development environment for React 2. **[Optional] Install Yarn:** You can install Yarn v1 (not v2) by following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 provides a better developer experience compared to npm v6 and below. You can skip this step and use npm, which is built into Node.js. 3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE that works seamlessly with TypeScript. While you can use any IDE, including Visual Studio or Rider, VS Code typically provides the best developer experience for React Native projects. 4. **[Optional] Install an Emulator/Simulator:** If you want to test on Android emulators or iOS simulators (instead of using the Web View method), you'll need to install one of the following: - - **Android Studio & Emulator:** Install [Android Studio](https://developer.android.com/studio) and set up an Android Virtual Device (AVD) through the AVD Manager. You can follow the [Android Studio Emulator guide](https://docs.expo.dev/workflow/android-studio-emulator/) on expo.io documentation. - - **Xcode & iOS Simulator:** On macOS, install [Xcode](https://developer.apple.com/xcode/) from the App Store, which includes the iOS Simulator. You can follow the [iOS Simulator guide](https://docs.expo.dev/workflow/ios-simulator/) on expo.io documentation. - - > **Note:** The Web View method (recommended for quick testing) doesn't require an emulator or simulator. If you prefer a CLI-based approach for Android, you can check the [setting up android emulator without android studio](setting-up-android-emulator.md) guide as an alternative. + - **Android Studio & Emulator:** Install [Android Studio](https://developer.android.com/studio) and set up an Android Virtual Device (AVD) through the AVD Manager. You can follow the [Android Studio Emulator guide](https://docs.expo.dev/workflow/android-studio-emulator/) on expo.io documentation. + - **Xcode & iOS Simulator:** On macOS, install [Xcode](https://developer.apple.com/xcode/) from the App Store, which includes the iOS Simulator. You can follow the [iOS Simulator guide](https://docs.expo.dev/workflow/ios-simulator/) on expo.io documentation. + > **Note:** The Web View method (recommended for quick testing) doesn't require an emulator or simulator. If you prefer a CLI-based approach for Android, you can check the [setting up android emulator without android studio](setting-up-android-emulator.md) guide as an alternative. ## How to Start a New React Native Project @@ -41,7 +40,7 @@ You have multiple options to initiate a new React Native project that works with ABP Studio is the most convenient and flexible way to create a React Native application based on the ABP framework. Follow the [tool documentation](../../../studio) and select the option below: -![React Native option](../../../images/react-native-option.png) +React Native option ### 2. Using ABP CLI @@ -61,7 +60,6 @@ This command creates a solution containing an **Angular** or **MVC** project (de Before running the React Native application, install the dependencies by running `yarn install` or `npm install` in the `react-native` directory. - ### Web View (Recommended - Quickest Method) The quickest way to test the application is by using the web view. While testing on a physical device is also supported, we recommend using [local HTTPS development](https://docs.expo.dev/guides/local-https-development/) as it requires fewer backend modifications. @@ -69,28 +67,21 @@ The quickest way to test the application is by using the web view. While testing Follow these steps to set up the web view: 1. Navigate to the `react-native` directory and start the application by running: - ```bash + ```bash yarn web - ``` - + ``` 2. Generate SSL certificates by running the following command in a separate directory: - ```bash + ```bash mkcert localhost - ``` - + ``` 3. Set up the local proxy by running: - ```bash + ```bash yarn create:local-proxy - ``` + ``` The default port is `443`. To use a different port, specify the `SOURCE_PORT` environment variable: - ```bash - SOURCE_PORT=8443 yarn create:local-proxy - ``` - 4. If you changed the port in the previous step, update the `apiUrl` in `Environment.ts` accordingly. - 5. Update the mobile application settings in the database and re-run the migrations. If you specified a custom port, ensure the port is updated in the configuration as well: - ```json + ```json "OpenIddict": { "Applications": { "MyApplication_Mobile": { @@ -99,7 +90,7 @@ Follow these steps to set up the web view: } } } - ``` + ``` ### Running on Emulator/Simulator @@ -111,17 +102,17 @@ If you prefer to test on an Android emulator or iOS simulator, you'll need to co {{ if Architecture == "Monolith" }} -![react native monolith environment local IP](../../../images/react-native-monolith-environment-local-ip.png) +react native monolith environment local IP {{ else if Architecture == "Tiered" }} -![react native tiered environment local IP](../../../images/react-native-tiered-environment-local-ip.png) +react native tiered environment local IP > Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` project. {{ else }} -![react native microservice environment local IP](../../../images/react-native-environment-local-ip.png) +react native microservice environment local IP > Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.AuthServer` project. @@ -131,22 +122,22 @@ If you prefer to test on an Android emulator or iOS simulator, you'll need to co > The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app, and it includes many features. -![expo-cli-options](../../../images/rn-options.png) +expo-cli-options In the image above, you can start the application on an Android emulator, an iOS simulator, or a physical phone by scanning the QR code with the [Expo Client](https://expo.io/tools#client) or by choosing the corresponding option. ### Expo -![React Native login screen on iPhone 16](../../../images/rn-login-iphone.png) +React Native login screen on iPhone 16 ### Android Studio 1. Start the emulator in **Android Studio** before running the `yarn start` or `npm start` command. 2. Press **a** to open in Android Studio. -![React Native login screen on Android Device](../../../images/rn-login-android-studio.png) +React Native login screen on Android Device -Enter **admin** as the username and **1q2w3E\*** as the password to log in to the application. +Enter **admin** as the username and **1q2w3E** as the password to log in to the application. The application is up and running. You can continue to develop your application based on this startup template. @@ -172,11 +163,10 @@ A React Native application running on an Android emulator or a physical phone ** {{ if Architecture == "Monolith" }} -![React Native monolith host project configuration](../../../images/react-native-monolith-be-config.png) +React Native monolith host project configuration - Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address in the `RootUrl` property with your local IP address. Then, run the database migrator. - Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. - ```json { "Kestrel": { @@ -191,11 +181,10 @@ A React Native application running on an Android emulator or a physical phone ** {{ else if Architecture == "Tiered" }} -![React Native tiered project configuration](../../../images/react-native-tiered-be-config.png) +React Native tiered project configuration - Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address in the `RootUrl` property with your local IP address. Then, run the database migrator. - Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. - ```json { "Kestrel": { @@ -207,9 +196,7 @@ A React Native application running on an Android emulator or a physical phone ** } } ``` - - Open the `appsettings.Development.json` file in the `.HttpApi.Host` folder. Add this configuration to accept global requests. Additionally, you need to configure the authentication server as mentioned above. - ```json { "Kestrel": { @@ -230,10 +217,9 @@ A React Native application running on an Android emulator or a physical phone ** {{ else if Architecture == "Microservice" }} -![React Native microservice project configuration](../../../images/react-native-microservice-be-config.png) +React Native microservice project configuration - Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. - ```json { "App": { @@ -248,9 +234,7 @@ A React Native application running on an Android emulator or a physical phone ** } } ``` - - Open the `appsettings.Development.json` file in the `.AdministrationService` folder. Add this configuration to accept global requests for testing the React Native application in the development environment. You should also provide the authentication server configuration. Additionally, you need to apply the same process for all services you will use in the React Native application. - ```json { "App": { @@ -271,9 +255,7 @@ A React Native application running on an Android emulator or a physical phone ** } } ``` - - Update the `appsettings.json` file in the `.IdentityService` folder. Replace the `localhost` configuration with your local IP address for the React Native application. - ```json { //... @@ -292,9 +274,7 @@ A React Native application running on an Android emulator or a physical phone ** } } ``` - - Finally, update the mobile gateway configurations as follows: - ```json //gateways/mobile/MyMicroserviceProject.MobileGateway/Properties/launchSettings.json { @@ -319,7 +299,6 @@ A React Native application running on an Android emulator or a physical phone ** } } ``` - ```json //gateways/mobile/MyMicroserviceProject.MobileGateway/appsettings.json { @@ -367,7 +346,6 @@ A React Native application running on an Android emulator or a physical phone ** } } ``` - {{ end }} -Run the backend application(s) as described in the [getting started document](../../../get-started). +Run the backend application(s) as described in the [getting started document](../../../get-started). \ No newline at end of file diff --git a/docs/en/framework/ui/react/admin-console.md b/docs/en/framework/ui/react/admin-console.md new file mode 100644 index 0000000000..47bc355b76 --- /dev/null +++ b/docs/en/framework/ui/react/admin-console.md @@ -0,0 +1,191 @@ +```json +//[doc-seo] +{ + "Description": "Learn how the ABP Admin Console works with React UI applications and how it is hosted under /admin-console." +} +``` + +# Admin Console + +The **ABP Admin Console** is the React-based administration UI for ABP applications. It provides management pages for ABP modules and is available in React UI solutions created with ABP Studio v3.0+ or `abp new --modern --ui-framework react`. + +The Admin Console is delivered as the `Volo.Abp.AdminConsole` NuGet package for layered and single-layer solutions. In microservice solutions, the template also includes a standalone `apps/react-admin-console/` React app. + +## What It Provides + +The Admin Console contains administration pages for the ABP modules included in the host application. Module pages are activated based on the backend services available in the host, so a solution only shows pages for modules it actually has. + +The built-in module areas include: + +| Module | Notes | +| --- | --- | +| Identity Pro | User, role, claim, and organization unit management when Identity services are available. | +| Account Pro | Account management pages and account-related flows. | +| OpenIddict | Application and scope management when OpenIddict services are available. | +| Audit Logging UI | Optional. Visible when Audit Logging services are available. | +| AI Management | Optional. Visible when AI Management services are available. | +| Text Template Management | Optional. Visible when Text Template Management services are available. | + +Other module pages, such as Setting Management, SaaS, GDPR, or customization pages, can also be available depending on the solution and installed modules. + +## Hosting Model + +The Admin Console is served under: + +```text +/admin-console/* +``` + +API endpoints used by the Admin Console are served under: + +```text +/admin-console/api/* +``` + +The `Volo.Abp.AdminConsole` package embeds the built React SPA under `wwwroot/admin-console/` and registers it with ABP's Virtual File System. `AdminConsoleSpaMiddleware` then serves static assets and falls back to `index.html` for client-side routes. + +The middleware deliberately lets `/admin-console/api/*` requests pass through to MVC controllers. + +## Layered and Single-Layer Templates + +For layered and single-layer modern templates: + +- The developer-owned React app is in the `react/` folder. +- The Admin Console UI is embedded in the backend through the `Volo.Abp.AdminConsole` NuGet package. +- There is no separate `react-admin-console/` source folder in the generated solution. +- The backend host serves Admin Console pages under `/admin-console/*`. + +Example URL: + +```text +https://localhost:44300/admin-console/ +``` + +The main React app links to the Admin Console through `getAdminConsoleUrl()`. + +## Microservice Template + +For the microservice modern template: + +- The main React app is in `apps/react/`. +- The Admin Console app is in `apps/react-admin-console/`. +- Both are served through the Web Gateway. +- The Admin Console has its own OpenIddict client, normally `_AdminConsole`. + +The main React app uses `adminConsoleUrl` from `dynamic-env.json` to open the Admin Console origin and `/admin-console` base path. + +## Module Discovery + +The Admin Console calls: + +```text +GET /admin-console/api/modules +``` + +The backend checks for module application service contracts and returns which module areas are available. The discovery keys include: + +| Key | Backend service check | +| --- | --- | +| `identity` | `Volo.Abp.Identity.IIdentityUserAppService` | +| `saas` | `Volo.Saas.Host.ITenantAppService` | +| `auditLogging` | `Volo.Abp.AuditLogging.IAuditLogsAppService` | +| `gdpr` | `Volo.Abp.Gdpr.IGdprRequestAppService` | +| `openIddict` | `Volo.Abp.OpenIddict.Applications.IApplicationAppService` | +| `textTemplateManagement` | `Volo.Abp.TextTemplateManagement.TextTemplates.ITemplateDefinitionAppService` | +| `aiManagement` | AI Management service contracts, with a legacy AI engine fallback. | + +`settingManagement` is always returned as available by the discovery endpoint, while access to pages is still controlled by permissions. + +## Configuration Endpoint + +The Admin Console also uses: + +```text +GET /admin-console/api/config +``` + +This endpoint provides Admin Console runtime settings such as authority, client ID, scopes, application name, customization options, and localization language configuration. + +Host applications can configure Admin Console options from the `AdminConsole` configuration section or by configuring `AbpAdminConsoleOptions`. + +## Configuring the Admin Console + +In layered and single-layer modern React templates, the embedded Admin Console is configured from the backend host application's `appsettings.json` file. The generated template includes an `AdminConsole` section similar to the following: + +```json +{ + "AdminConsole": { + "IsEnabled": true, + "RedirectRootToAdminConsole": true, + "Authority": "https://localhost:44300", + "ClientId": "Acme_BookStore_AdminConsole", + "Scope": "openid profile email offline_access Acme_BookStore", + "LocalizationLanguages": [ "en", "tr" ], + "ThemeOverrideCssPath": "/theme-override.css", + "InitialTheme": "system", + "CustomizationPermissionName": "AdminConsole.Customization" + } +} +``` + +You can also configure the same values in the module class with `AbpAdminConsoleOptions`: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.IsEnabled = true; + options.RedirectRootToAdminConsole = true; + options.Authority = "https://localhost:44300"; + options.ClientId = "Acme_BookStore_AdminConsole"; + options.Scope = "openid profile email offline_access Acme_BookStore"; + options.LocalizationLanguages = new[] { "en", "tr" }; + options.ThemeOverrideCssPath = "/theme-override.css"; + options.InitialTheme = "system"; + options.CustomizationPermissionName = "AdminConsole.Customization"; + }); +} +``` + +The most commonly changed options are: + +| Option | Description | +| --- | --- | +| `IsEnabled` | Enables or disables the embedded Admin Console SPA middleware. | +| `RedirectRootToAdminConsole` | Redirects the backend root path (`/`) to `/admin-console`. | +| `Authority` | OpenID Connect authority URL. If it is `null`, the host origin is used. | +| `ClientId` | OpenIddict client ID used by the Admin Console SPA. | +| `Scope` | Space-separated OAuth scopes requested by the Admin Console. | +| `LocalizationLanguages` | UI language codes exposed to the Admin Console. If empty, the frontend falls back to `en`. | +| `ThemeOverrideCssPath` | Optional CSS path or absolute URL injected into the Admin Console HTML. | +| `InitialTheme` | Initial theme behavior: `light`, `dark`, `system`, or `both`. | +| `CustomizationPermissionName` | Permission required to show and use the Admin Console customization page. If not set, customization is disabled. | + +The `ApplicationName`, `LogoUrl`, `InitialTheme`, and `ThemeOverrideCssPath` values can also be changed from the Admin Console customization UI when `CustomizationPermissionName` is configured and the current user has that permission. Values saved from the customization UI are stored as settings and override the defaults from configuration. + +In microservice solutions, the Admin Console is a separate React app under `apps/react-admin-console/`. It still uses its own OpenIddict client (`_AdminConsole`) and runtime configuration, while the backend exposes the same `/admin-console/api/config` and `/admin-console/api/modules` endpoints. + +## Permissions + +Admin Console routes still require permissions. For example: + +- Identity pages use `AbpIdentity.*` permissions. +- OpenIddict pages use `OpenIddictPro.Application` and `OpenIddictPro.Scope`. +- Audit Logging uses `AuditLogging.AuditLogs`. +- Text Template Management uses `TextTemplateManagement.*`. +- AI Management uses `AIManagement.*`. + +The main React app's Admin Console menu item only requires authentication. The Admin Console performs detailed permission checks for its own pages. + +## Customization + +The developer-owned React app is intended for application-specific pages. The Admin Console is an ABP-managed administration surface and should normally be updated by updating ABP packages. + +For layered and single-layer hosts, the package supports host-side options such as application name, localization languages, and theme override CSS path. For larger UI changes, prefer building your own pages in the main React app or extending the backend modules through supported ABP extension points. + +## See Also + +- [React UI](./index.md) +- [Environment Variables](./environment-variables.md) +- [Permission Management](./permission-management.md) diff --git a/docs/en/framework/ui/react/authorization.md b/docs/en/framework/ui/react/authorization.md new file mode 100644 index 0000000000..4f6d673d87 --- /dev/null +++ b/docs/en/framework/ui/react/authorization.md @@ -0,0 +1,183 @@ +```json +//[doc-seo] +{ + "Description": "Learn how authentication and authorization are configured in ABP React UI applications." +} +``` + +# Authorization in React UI + +OAuth is preconfigured in ABP React UI templates. When you create a React solution with ABP Studio v3.0+ or `abp new --modern --ui-framework react`, the template includes OpenID Connect settings, an OpenIddict client, route guards, and authentication hooks. + +The React app authenticates against the ABP Auth Server using the **Authorization Code flow with PKCE**, which is the recommended flow for browser-based applications. + +## Packages + +The template uses these packages for authentication: + +| Package | Purpose | +| --- | --- | +| `@volo/abp-oidc-auth` | Framework-agnostic OIDC client helpers for ABP/OpenIddict backends. | +| `@volo/abp-react-oidc-auth` | React adapter for the ABP OIDC client. | +| `oidc-client-ts` | Underlying OIDC protocol implementation. | + +The package list also includes `@volo/abp-app-config` and `@volo/abp-react-app-config`, which are used to fetch application configuration and permissions after authentication. + +## OAuth Configuration + +The OIDC settings are resolved from runtime configuration first and fall back to `src/env.ts`. + +```ts +export function getOAuthConfig(): { + issuer: string + redirectUri: string + clientId: string + scope: string + responseType: 'code' +} { + return { + issuer: loadedConfig?.oAuthConfig?.issuer ?? env.oauth.issuer, + redirectUri: loadedConfig?.oAuthConfig?.redirectUri ?? env.oauth.redirectUri, + clientId: loadedConfig?.oAuthConfig?.clientId ?? env.oauth.clientId, + scope: loadedConfig?.oAuthConfig?.scope ?? env.oauth.scope, + responseType: 'code', + } +} +``` + +The important configuration values are: + +- `oAuthConfig.issuer`: Auth Server / OpenIddict authority URL. +- `oAuthConfig.redirectUri`: URL where the Auth Server redirects after login. +- `oAuthConfig.clientId`: OpenIddict client ID, normally `_App`. +- `oAuthConfig.scope`: Scopes requested by the React app. + +See [Environment Variables](./environment-variables.md) for the full runtime configuration model. + +## Initializing Authentication + +The app loads runtime configuration before initializing OIDC: + +```tsx +async function bootstrap() { + await loadRuntimeConfig() + initUserManager() + createRoot(document.getElementById('root')!).render( + + + + ) +} +``` + +`initUserManager()` creates the ABP React OIDC client: + +```ts +client = createAbpReactOidcAuth({ + authority: config.issuer, + clientId: config.clientId, + redirectUri: config.redirectUri, + postLogoutRedirectUri: config.redirectUri, + scope: config.scope, + responseType: config.responseType, + automaticSilentRenew: true, + userStoreType: 'localStorage', + userStorePrefix: `oidc.${config.clientId}`, + silentRedirectUri: `${window.location.origin}/silent-renew.html`, +}) +``` + +The template stores the OIDC user in local storage and enables silent renewal with `public/silent-renew.html`. + +## Auth Provider and Hook + +`AuthProvider` wraps the app and handles the OIDC callback: + +```tsx +export function AuthProvider({ children }: { children: ReactNode }) { + const authClient = getAuthClient() + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + if (!params.has('code') || !params.has('state')) return + void authClient.handleSigninCallback().then(() => + window.history.replaceState({}, document.title, window.location.pathname) + ) + }, []) + + return {children} +} +``` + +Use `useAuth()` in components: + +```tsx +import { useAuth } from '@/lib/auth/AuthContext' + +export function LoginButton() { + const { isAuthenticated, isLoading, login, logout, user } = useAuth() + + if (isLoading) return null + + return isAuthenticated ? ( + + ) : ( + + ) +} +``` + +## Route Protection + +The React template uses TanStack Router. Protected routes use `beforeLoad` guards. + +```ts +const identityLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/identity', + component: IdentityLayout, + beforeLoad: authGuard, +}) +``` + +`authGuard` checks the current OIDC user and redirects unauthenticated users to the Auth Server: + +```ts +export async function authGuard({ location }: GuardContext) { + const user = await userManager.getUser() + if (!user || user.expired) { + await userManager.signinRedirect({ + state: { returnUrl: location.href }, + }) + throw new Error('Redirecting to login') + } +} +``` + +Routes that also require a permission use `createPermissionGuard`: + +```ts +const usersRoute = createRoute({ + getParentRoute: () => identityLayoutRoute, + path: 'users', + component: UsersPage, + beforeLoad: createPermissionGuard('AbpIdentity.Users'), +}) +``` + +Permission checks are explained in [Permission Management](./permission-management.md). + +## OpenIddict Clients + +The generated OpenIddict clients depend on the template: + +- Layered and single-layer modern templates use the main React client, normally `_App`. +- Microservice modern templates also include an Admin Console client, normally `_AdminConsole`, because the Admin Console is a separate React app. + +If you change URLs after generation, update both the runtime configuration and the corresponding OpenIddict client redirect URLs. + +## See Also + +- [Environment Variables](./environment-variables.md) +- [Permission Management](./permission-management.md) +- [Authorization](../../../framework/fundamentals/authorization/index.md) diff --git a/docs/en/framework/ui/react/components/index.md b/docs/en/framework/ui/react/components/index.md new file mode 100644 index 0000000000..856759acd5 --- /dev/null +++ b/docs/en/framework/ui/react/components/index.md @@ -0,0 +1,184 @@ +```json +//[doc-seo] +{ + "Description": "Learn about the component architecture and UI libraries used by ABP React UI applications." +} +``` + +# Components + +ABP React UI templates use a source-owned component architecture. The generated app includes shadcn/ui-style primitives, layout components, feature components, route pages, and shared infrastructure under `src/lib/`. + +The goal is to give you a working React application that you can customize without replacing framework-owned black boxes. + +## Component Structure + +The main React app is organized like this: + +```text +src/ +├── components/ +│ ├── layout/ +│ ├── ui/ +│ └── identity/ +├── lib/ +│ ├── api/ +│ ├── auth/ +│ ├── i18n/ +│ ├── routing/ +│ └── theme/ +├── locales/ +├── pages/ +└── routes/ +``` + +The exact folders can vary by selected template options and modules. + +## UI Stack + +The React template uses: + +| Library | Purpose | +| --- | --- | +| React | UI rendering. | +| Vite | Build tool and development server. | +| TanStack Router | Client-side routing. | +| TanStack Query | Server state, queries, mutations, and cache invalidation. | +| shadcn/ui-style components | Source-owned UI primitives built on Radix UI and Tailwind CSS. | +| Radix UI | Accessible low-level UI primitives. | +| Tailwind CSS | Utility-first styling and design tokens. | +| React Hook Form | Form state management. | +| Zod | Form and DTO validation schemas. | +| Axios | HTTP client. | +| i18next / react-i18next | Localization. | +| Zustand | Lightweight client state when needed. | +| Sonner | Toast notifications. | +| Lucide React | Icons. | + +## `components/ui` + +`src/components/ui/` contains reusable UI primitives. These components are copied into your project and can be edited directly. + +Common components include: + +- `Button` +- `Input` +- `Label` +- `Table` +- `Dialog` +- `DropdownMenu` +- `Select` +- `Card` +- `Tabs` +- `Badge` +- `DatePicker` +- `ConfirmDialog` + +Use these primitives to build application pages and feature components. + +```tsx +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export function ReportCard() { + return ( + + + Reports + + + + + + ) +} +``` + +## Layout Components + +Layout components are under `src/components/layout/`. + +Important components include: + +- `RootLayout`: root shell used by TanStack Router. +- `Header`: top bar, login button, theme toggle, and user menu. +- `Sidebar`: route-config-driven navigation menu. +- `UserMenu`: account-related dropdown menu. + +The sidebar reads `src/lib/routing/route-config.ts`, checks authentication and permissions, and renders internal or external links. + +## Feature Components + +Feature-specific components should live near the feature that owns them. For example, Identity-specific layout components live under `src/components/identity/`, while Books-specific UI is implemented in `src/pages/books/BooksPage.tsx` in the sample template. + +As a rule: + +- Put generic, reusable primitives in `components/ui`. +- Put application layout in `components/layout`. +- Put feature-specific components under `components/` or next to the page when they are only used by one page. + +## Pages + +Route pages live under `src/pages/`. A page usually combines: + +- UI primitives from `components/ui`. +- API functions from `src/lib/api`. +- Server state from TanStack Query. +- Form state from React Hook Form. +- Validation schemas from Zod. +- Permissions from `usePermissions()`. +- Localized strings from `useTranslation()`. + +The Books page is the best full CRUD reference when the sample CRUD option is selected. + +## Forms + +Forms use React Hook Form and Zod: + +```tsx +const productSchema = z.object({ + name: z.string().min(1, 'Required'), + price: z.number().min(0), +}) + +type ProductFormData = z.infer + +const form = useForm({ + resolver: zodResolver(productSchema), + defaultValues: { + name: '', + price: 0, + }, +}) +``` + +This keeps runtime validation and TypeScript types close to each other. + +## Routing Components + +Routes are configured in `src/routes/router.tsx` with TanStack Router. Use: + +- `authGuard` for authenticated pages. +- `createPermissionGuard('Permission.Name')` for permission-protected pages. +- `RootLayout` and nested layouts for shared page structure. + +Menu entries are configured separately in `src/lib/routing/route-config.ts`, so route registration and navigation display can evolve independently. + +## API Components and Hooks + +API functions live under `src/lib/api/` and use the shared `api` Axios instance. Components normally consume these functions through TanStack Query: + +```tsx +const usersQuery = useQuery({ + queryKey: ['app', 'users', queryParams], + queryFn: () => getAppUsers(queryParams), +}) +``` + +This keeps HTTP details out of rendering components and gives you caching, loading states, refetching, and mutation invalidation. + +## See Also + +- [Customization](../customization.md) +- [HTTP Requests](../http-requests.md) +- [Unit Testing](../unit-testing.md) diff --git a/docs/en/framework/ui/react/customization.md b/docs/en/framework/ui/react/customization.md new file mode 100644 index 0000000000..3e64018dab --- /dev/null +++ b/docs/en/framework/ui/react/customization.md @@ -0,0 +1,208 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to customize ABP React UI applications, including pages, themes, sidebar navigation, and the user menu." +} +``` + +# Customization + +The React app generated by ABP is fully owned by your solution. All source code is available, so you can change pages, components, routes, themes, menus, API calls, and layout behavior just like in any other React application. + +This page focuses on the main developer-owned React app. The same general approach applies to the public-web React app if your solution includes one. The Admin Console is an ABP-managed administration surface; see [Admin Console](./admin-console.md) for details. + +## General Customization + +Application pages live under `src/pages/`. The template includes practical references: + +- **Users page**: a simple page that lists users and links to the Admin Console for full user and role management. +- **Books page**: a full CRUD sample when the sample CRUD option is selected during solution creation. It demonstrates TanStack Query, forms, Zod validation, dialogs, tables, permissions, and toast notifications. + +Shared UI and infrastructure live under: + +```text +src/ +├── components/ +│ ├── layout/ +│ └── ui/ +├── lib/ +│ ├── api/ +│ ├── auth/ +│ ├── i18n/ +│ ├── routing/ +│ └── theme/ +└── pages/ +``` + +## Adding a Page + +Create a page under `src/pages/`: + +```tsx +export function ReportsPage() { + return ( +
+

Reports

+
+ ) +} +``` + +Register it with TanStack Router in `src/routes/router.tsx`: + +```tsx +const reportsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/reports', + component: ReportsPage, + beforeLoad: createPermissionGuard('MyProjectName.Reports'), +}) + +const routeTree = rootRoute.addChildren([ + indexRoute, + reportsRoute, +]) +``` + +Use `authGuard` for pages that only require authentication and `createPermissionGuard` for pages that require a permission. + +## Theming + +The React template uses **shadcn/ui**-style components, Radix UI primitives, Tailwind CSS, and CSS variables. + +Theme tokens are defined in `src/styles/globals.css`: + +```css +:root { + --background: oklch(0.978 0.003 264); + --foreground: oklch(0.205 0.008 264); + --primary: oklch(0.48 0.10 278); + --radius: 0.5rem; +} + +.dark { + --background: oklch(0.16 0.004 264); + --foreground: oklch(0.92 0.005 264); + --primary: oklch(0.62 0.12 278); +} +``` + +ABP Studio's modern wizard can generate different shadcn theme color presets and light/dark/system theme behavior. + +## Changing Theme Colors + +To make a quick theme change, edit the CSS variables in `src/styles/globals.css`: + +```css +:root { + --primary: oklch(0.623 0.188 259.6); + --primary-foreground: oklch(1 0 0); +} +``` + +Because the generated shadcn/ui components consume these variables through Tailwind tokens, the change applies across buttons, links, active sidebar entries, focus rings, and other components that use the primary color. + +## Theme Mode Switcher + +Theme mode is handled by `src/lib/theme/ThemeProvider.tsx`. It supports: + +- `light` +- `dark` +- `system` + +The header cycles through the allowed modes: + +```tsx +const THEME_CYCLE: Theme[] = ['light', 'dark', 'system'] + +function ThemeToggle() { + const { theme, resolvedTheme, setTheme } = useTheme() + + function cycleTheme() { + const currentIndex = THEME_CYCLE.indexOf(theme) + const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % THEME_CYCLE.length + setTheme(THEME_CYCLE[nextIndex]) + } + + return +} +``` + +To remove the switcher or replace it with a dropdown, edit `src/components/layout/Header.tsx`. + +## Modifying the Sidebar Menu + +Sidebar navigation is defined in `src/lib/routing/route-config.ts`. + +Add a menu item: + +```ts +import { BarChart3 } from 'lucide-react' + +export const routeConfig: RouteConfigItem[] = [ + { + path: '/reports', + nameKey: 'Menu:Reports', + icon: BarChart3, + order: 10, + requiredPolicy: 'MyProjectName.Reports', + }, +] +``` + +Then add the localization key to `src/locales/en.json`: + +```json +{ + "Menu:Reports": "Reports" +} +``` + +Use these properties depending on the menu item: + +| Property | Use | +| --- | --- | +| `path` | Internal route path or logical path for an external item. | +| `nameKey` | Localization key shown in the sidebar. | +| `icon` | Optional Lucide icon. | +| `order` | Sorting order. | +| `requiredPolicy` | Hide the item unless the permission is granted. | +| `requiresAuth` | Hide the item unless the user is authenticated. | +| `externalHref` | Open an external URL or another app, such as the Admin Console. | +| `children` | Add nested sidebar items. | + +## Sidebar vs User Menu + +Use the **sidebar navigation** for application pages and module entry points. + +Use the **user menu** for account-specific actions, profile links, sessions, security logs, linked accounts, and logout. The user menu is implemented in `src/components/layout/UserMenu.tsx`. + +Example user menu item: + +```tsx + + + + {t('MyAccount::Preferences')} + + +``` + +## Customizing UI Components + +shadcn/ui components are copied into your project under `src/components/ui/`. They are not black-box components from a package. You can edit them directly. + +For example: + +- Change button variants in `src/components/ui/button.tsx`. +- Change dialog structure in `src/components/ui/dialog.tsx`. +- Add a new reusable component under `src/components/ui/`. +- Add feature-specific components under `src/components//`. + +Keep generic primitives in `components/ui` and business-specific components close to the feature or page that owns them. + +## See Also + +- [Components](./components/index.md) +- [Permission Management](./permission-management.md) +- [Admin Console](./admin-console.md) diff --git a/docs/en/framework/ui/react/environment-variables.md b/docs/en/framework/ui/react/environment-variables.md new file mode 100644 index 0000000000..2b84b0c4d0 --- /dev/null +++ b/docs/en/framework/ui/react/environment-variables.md @@ -0,0 +1,121 @@ +```json +//[doc-seo] +{ + "Description": "Learn how runtime configuration and environment variables work in ABP React UI applications." +} +``` + +# Environment Variables + +ABP React UI applications use a runtime configuration file and Vite environment variables together. The template is preconfigured by ABP Studio's modern wizard, available with ABP Studio **v3.0+**, so a newly created solution already contains working local values for the API, Auth Server, OpenIddict client, and Admin Console link. + +You usually change these values when moving the application to another environment such as staging or production. + +## Configuration Sources + +The React template reads configuration from these places: + +- `dynamic-env.json`: runtime configuration that can be changed without rebuilding the application. +- `public/dynamic-env.json`: the file served by the app. The Vite build copies the root `dynamic-env.json` into this location when it exists. +- `src/env.ts`: local fallback values used when runtime configuration is not loaded. +- `.env` files / shell variables: Vite variables such as `VITE_API_URL`, `VITE_AUTH_URL`, and `VITE_APP_URL`. + +For layered and single-layer modern templates, the React app is in the `react/` folder. For the microservice modern template, it is in `apps/react/`. + +## `dynamic-env.json` + +The runtime configuration file has the same purpose as Angular's dynamic environment configuration: it lets you deploy the same build artifact to different environments and change the API or authentication endpoints at runtime. + +```json +{ + "application": { + "baseUrl": "https://localhost:3000", + "name": "Acme.BookStore" + }, + "oAuthConfig": { + "issuer": "https://localhost:44301/", + "redirectUri": "https://localhost:3000", + "clientId": "Acme_BookStore_App", + "scope": "offline_access openid profile email phone AuthServer IdentityService AdministrationService" + }, + "apis": { + "default": { + "url": "https://localhost:44300", + "rootNamespace": "Acme.BookStore" + } + }, + "adminConsoleUrl": "https://localhost:44307" +} +``` + +The template loads `/dynamic-env.json` first and then tries `/getEnvConfig` for compatibility with environments that expose the file through that endpoint. + +## Available Values + +| Key | Description | +| --- | --- | +| `application.baseUrl` | Public URL of the React application. It is used as a fallback for OAuth redirect URLs. | +| `application.name` | Application name. | +| `application.logoUrl` | Optional logo URL for application branding. | +| `oAuthConfig.issuer` | Auth Server / OpenIddict authority URL. | +| `oAuthConfig.redirectUri` | Redirect URI registered for the React OpenIddict client. | +| `oAuthConfig.clientId` | OpenIddict client ID. The main React app uses `_App`. | +| `oAuthConfig.scope` | OAuth scopes requested by the SPA. | +| `apis.default.url` | Backend API base URL. In microservice solutions, this normally points to the Web Gateway. | +| `apis.default.rootNamespace` | Root namespace used by generated API code and module-specific clients. | +| `adminConsoleUrl` | Origin of the Admin Console app. The React template uses it to open `/admin-console`. | + +The `DynamicEnv` type also includes fields such as `production`, `oAuthConfig.requireHttps`, `oAuthConfig.responseType`, `oAuthConfig.strictDiscoveryDocumentValidation`, and `oAuthConfig.skipIssuerCheck`. The template's OIDC setup always uses the Authorization Code flow by setting `responseType` to `code`. + +## Vite Variables + +The React template uses Vite and reads environment variables with `loadEnv(mode, process.cwd(), '')`, so variables are not limited to the `VITE_` prefix inside `vite.config.ts`. + +The important variables for developers are: + +| Variable | Description | +| --- | --- | +| `VITE_API_URL` | Overrides the backend API or gateway URL used by the dev proxy and runtime fallback. | +| `VITE_AUTH_URL` | Overrides the Auth Server URL used by the dev proxy and runtime fallback. If omitted, the dev proxy can fall back to `VITE_API_URL`. | +| `VITE_APP_URL` | Overrides the React app URL used as the OAuth redirect URI fallback. | + +Example: + +```bash +VITE_API_URL=https://api.bookstore.example.com +VITE_AUTH_URL=https://auth.bookstore.example.com +VITE_APP_URL=https://bookstore.example.com +``` + +## What ABP Studio Preconfigures + +When a React solution is created with ABP Studio v3.0+ or `abp new --modern`, the template fills these values from the generated solution configuration: + +- Local launch ports for the React app, Web Gateway/API host, Auth Server, and Admin Console. +- The OpenIddict client ID, usually `_App`. +- OAuth scopes based on the selected modules, such as Identity, Administration, SaaS, Audit Logging, GDPR, File Management, AI Management, Language Management, or Chat. +- `adminConsoleUrl` when the template includes a separate Admin Console application. + +For local development, these generated values should work without manual changes. For production, update the API URL, Auth Server URL, redirect URI, client ID if you changed the seeded client, and any environment-specific scopes. + +## Development Proxy + +In development, `vite.config.ts` proxies these paths: + +- `/api` to `VITE_API_URL` or the generated API/gateway URL. +- `/connect` to `VITE_AUTH_URL`, `VITE_API_URL`, or the generated Auth Server URL. +- `/getEnvConfig` to `VITE_API_URL` or the generated API/gateway URL. + +This allows the React app to call same-origin paths during development while the backend services run on their own ports. + +## Deployment + +For deployment, prefer changing `dynamic-env.json` instead of rebuilding the React application for each environment. The file should be served with `application/json` content type and should not be rewritten to `index.html` by SPA fallback rules. + +If your server exposes `/getEnvConfig`, configure it to return the same JSON content as `dynamic-env.json`. + +## See Also + +- [React UI](./index.md) +- [Authorization](./authorization.md) +- [HTTP Requests](./http-requests.md) diff --git a/docs/en/framework/ui/react/http-requests.md b/docs/en/framework/ui/react/http-requests.md new file mode 100644 index 0000000000..2c01884635 --- /dev/null +++ b/docs/en/framework/ui/react/http-requests.md @@ -0,0 +1,213 @@ +```json +//[doc-seo] +{ + "Description": "Learn how HTTP requests are made in ABP React UI applications with Axios, runtime configuration, and ABP interceptors." +} +``` + +# HTTP Requests + +ABP React UI templates use [Axios](https://axios-http.com/) for HTTP requests. The generated app contains a shared Axios instance with ABP-specific request and response interceptors, plus typed API modules for backend endpoints. + +The shared client is defined in `src/lib/api/axios.ts` and exported as `api`. + +## Base URL + +The Axios base URL is resolved at request time from runtime configuration: + +```ts +export function getApiBaseUrl(): string { + const apiUrl = getApiUrl() + if (apiUrl.startsWith('http://') || apiUrl.startsWith('https://')) { + return apiUrl.replace(/\/$/, '') + '/api' + } + if (import.meta.env.DEV) { + return '/api' + } + return apiUrl.replace(/\/$/, '') + '/api' +} +``` + +The API URL comes from: + +1. `dynamic-env.json` -> `apis.default.url` +2. `VITE_API_URL` +3. `src/env.ts` generated fallback + +In microservice solutions, `apis.default.url` normally points to the Web Gateway. In layered and single-layer solutions, it normally points to the HTTP API host. + +## Shared Axios Instance + +The template creates one shared instance: + +```ts +export const api = axios.create({ + baseURL: '', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json', + }, +}) +``` + +Use this instance for application API modules instead of creating new Axios clients. It centralizes ABP headers, authentication, tenant handling, language handling, and redirects. + +## Request Interceptor + +Before each request, the template: + +- Sets `baseURL` from runtime configuration. +- Adds `Authorization: Bearer ` from the OIDC user. +- Adds `__tenant` when the user has selected a tenant. +- Adds `Accept-Language` from i18next. +- Keeps default AJAX headers such as `X-Requested-With`. + +```ts +api.interceptors.request.use(async (config) => { + config.baseURL = getApiBaseUrl() + + const user = await userManager.getUser() + if (user?.access_token) { + config.headers.Authorization = `Bearer ${user.access_token}` + } + + const tenantId = sessionStorage.getItem('abp_tenant_id') + if (tenantId && !config.headers.__tenant) { + config.headers.__tenant = tenantId + } + + if (i18n?.language) { + config.headers['Accept-Language'] = + config.headers['Accept-Language'] ?? i18n.language + } + + return config +}) +``` + +## Response Interceptor + +The response interceptor handles common authorization failures: + +- `401 Unauthorized`: redirects to login unless `skipAuthRedirect` is set. +- `403 Forbidden`: redirects to `/403` unless `skip403Redirect` is set. +- Other errors are rejected so the caller can handle them. + +```ts +api.interceptors.response.use( + (response) => response, + async (error) => { + const status = error.response?.status + + if (status === 401 && !error.config?.skipAuthRedirect) { + await userManager.signinRedirect() + return Promise.reject(new Error('Unauthorized - redirecting to login')) + } + + if (status === 403 && !error.config?.skip403Redirect) { + window.location.href = '/403' + return Promise.reject(new Error('Forbidden')) + } + + return Promise.reject(error) + } +) +``` + +Use `skipAuthRedirect` or `skip403Redirect` for calls where the component should handle the error itself. + +## Typed API Modules + +The template organizes backend calls under `src/lib/api/`. For example, the Books sample defines DTOs and functions in `books.ts`: + +```ts +import { api } from './axios' + +export interface PagedResultDto { + items: T[] + totalCount: number +} + +export interface BookDto { + id: string + name?: string + price: number +} + +export async function getBooks(): Promise> { + const { data } = await api.get>('/app/book', { + params: { + maxResultCount: 10, + skipCount: 0, + }, + }) + return data +} +``` + +Notice that the API module calls `/app/book`, not `/api/app/book`. The shared Axios base URL already includes the `/api` prefix when needed. + +## Using Requests from Components + +The template uses TanStack Query for server state: + +```tsx +const { data, isLoading } = useQuery({ + queryKey: ['books', skipCount], + queryFn: () => + getBooks({ + maxResultCount: 10, + skipCount, + sorting: 'creationTime desc', + }), +}) +``` + +Mutations use `useMutation` and invalidate related queries after success: + +```tsx +const createMutation = useMutation({ + mutationFn: createBook, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['books'] }) + toast.success(t('AbpUi::SavedSuccessfully')) + }, +}) +``` + +## Adding a New API Module + +Create a file under `src/lib/api/`: + +```ts +import { api } from './axios' + +export interface ProductDto { + id: string + name: string +} + +export async function getProducts(): Promise { + const { data } = await api.get('/app/product') + return data +} +``` + +Then consume it from a component with TanStack Query: + +```tsx +const productsQuery = useQuery({ + queryKey: ['products'], + queryFn: getProducts, +}) +``` + +## Development Proxy + +In development, Vite proxies `/api`, `/connect`, and `/getEnvConfig`. This lets the React app use same-origin paths while calls are forwarded to the backend, Auth Server, or gateway configured by `VITE_API_URL` and `VITE_AUTH_URL`. + +## See Also + +- [Environment Variables](./environment-variables.md) +- [Authorization](./authorization.md) +- [Permission Management](./permission-management.md) diff --git a/docs/en/framework/ui/react/index.md b/docs/en/framework/ui/react/index.md new file mode 100644 index 0000000000..e90a53c89c --- /dev/null +++ b/docs/en/framework/ui/react/index.md @@ -0,0 +1,142 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to build modern web applications with ABP React UI, including runtime configuration, authentication, Admin Console, shadcn/ui components, and testing." +} +``` + +# React UI + +ABP provides a **React UI** option for building modern, client-side web applications. React UI is part of the **modern template system** and is available with **ABP Studio v3.0+** through the Modern Wizard or with `abp new --modern` using [ABP CLI](../../../cli/index.md). + +React UI is not available in classic, non-modern templates. Use ABP Studio's modern template flow or `Volo.Abp.Studio.Cli` to create a React-based solution. + +## Technology Stack + +The React UI template is built with: + +| Technology | Purpose | +| --- | --- | +| [Vite](https://vite.dev/) | Build tool and dev server | +| [React](https://react.dev/) | UI framework | +| [TanStack Router](https://tanstack.com/router) | Client-side routing | +| [TanStack Query](https://tanstack.com/query) | Server state and API request orchestration | +| [shadcn/ui](https://ui.shadcn.com/) | Source-owned component library built on Radix UI and Tailwind CSS | +| [Zod](https://zod.dev/) | Schema validation | +| [React Hook Form](https://react-hook-form.com/) | Form state management | +| [Axios](https://axios-http.com/) | HTTP client | +| [Vitest](https://vitest.dev/) | Unit testing | +| [OpenID Connect / OIDC](https://openid.net/connect/) | Authentication against the ABP Auth Server | + +The template also includes ABP-specific NPM packages. These packages are maintained by ABP and published on npm; use the following links to view their package details: + +- [View `@volo/abp-app-config` on npm](https://www.npmjs.com/package/@volo/abp-app-config) +- [View `@volo/abp-oidc-auth` on npm](https://www.npmjs.com/package/@volo/abp-oidc-auth) +- [View `@volo/abp-react-app-config` on npm](https://www.npmjs.com/package/@volo/abp-react-app-config) +- [View `@volo/abp-react-oidc-auth` on npm](https://www.npmjs.com/package/@volo/abp-react-oidc-auth) + +## React App and Admin Console + +A modern React solution contains two UI surfaces: + +- **Your React application**: the developer-owned SPA where you build application-specific pages and features. +- **ABP Admin Console**: the React-based administration UI for ABP modules. + +The Admin Console is provided by the `Volo.Abp.AdminConsole` NuGet package in layered and single-layer templates. In microservice templates, it is also generated as a separate `apps/react-admin-console/` app and served through the Web Gateway. + +See [Admin Console](./admin-console.md) for hosting, module discovery, and permission details. + +## Solution Structure + +The React app location depends on the modern template type: + +- **Layered (`app --modern`) and single-layer (`app-nolayers --modern`)**: the React app lives in the `react/` folder at the solution root. +- **Microservice (`microservice --modern`)**: the React app lives at `apps/react/`. + +Typical structure: + +```text +react/ +├── dynamic-env.json +├── public/ +├── src/ +│ ├── components/ +│ ├── lib/ +│ ├── locales/ +│ ├── pages/ +│ ├── routes/ +│ └── main.tsx +├── package.json +├── vite.config.ts +└── vitest.config.ts +``` + +## Creating a Solution + +Install or update `Volo.Abp.Studio.Cli`, then create a modern solution: + +```bash +# Layered app with React UI +abp new Acme.BookStore --template app --modern --ui-framework react + +# Single-layer app with React UI +abp new Acme.BookStore --template app-nolayers --modern --ui-framework react + +# Microservice solution with React UI +abp new Acme.BookStore --template microservice --modern --ui-framework react +``` + +You can also use ABP Studio v3.0+ and select the modern template flow in the New Solution wizard. The wizard preconfigures local ports, runtime configuration, OIDC clients, theme options, and React/Admin Console wiring based on the selected template and modules. + +## Running the Application + +Start the backend from ABP Studio or by running the backend host projects, then start the React development server. + +For layered and single-layer templates: + +```bash +cd react +npm install +npm run dev +``` + +For microservice templates: + +```bash +cd apps/react +npm install +npm run dev +``` + +Run tests with: + +```bash +npm run test +``` + +Build for production with: + +```bash +npm run build +``` + +## Documentation Map + +Use these pages to learn each part of the React UI: + +- [Environment Variables](./environment-variables.md): runtime configuration, `dynamic-env.json`, Vite variables, and Studio-generated defaults. +- [Authorization](./authorization.md): OIDC, Authorization Code flow with PKCE, auth provider, hooks, and route guards. +- [Localization](./localization.md): i18next, local JSON resources, ABP localization keys, and request culture. +- [Permission Management](./permission-management.md): fetching granted policies, `usePermissions()`, route protection, and conditional UI. +- [HTTP Requests](./http-requests.md): Axios setup, interceptors, typed API modules, and TanStack Query usage. +- [Customization](./customization.md): changing pages, themes, sidebar items, user menu entries, and shadcn/ui components. +- [Components](./components/index.md): component architecture, UI primitives, layout components, forms, and routing. +- [Unit Testing](./unit-testing.md): Vitest, React Testing Library, examples, and test workflow. +- [Admin Console](./admin-console.md): the `Volo.Abp.AdminConsole` package, `/admin-console/*` hosting, module discovery, and optional modules. + +## See Also + +- [ABP Studio](../../../studio/index.md) +- [ABP CLI](../../../cli/index.md) +- [Authorization](../../../framework/fundamentals/authorization/index.md) +- [Localization](../../../framework/fundamentals/localization.md) diff --git a/docs/en/framework/ui/react/localization.md b/docs/en/framework/ui/react/localization.md new file mode 100644 index 0000000000..64298278f0 --- /dev/null +++ b/docs/en/framework/ui/react/localization.md @@ -0,0 +1,158 @@ +```json +//[doc-seo] +{ + "Description": "Learn how localization works in ABP React UI applications with i18next and ABP application configuration." +} +``` + +# Localization + +ABP React UI templates use [i18next](https://www.i18next.com/) with [react-i18next](https://react.i18next.com/). The generated app includes local JSON resources and integrates with ABP application configuration through the `@volo/abp-app-config` packages. + +## Localization Files + +The main React app stores client-side translations under `src/locales/`. + +```text +src/ +├── locales/ +│ └── en.json +└── lib/ + └── i18n/ + └── i18n.ts +``` + +The default `i18n.ts` imports the English resource and registers it: + +```ts +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import en from '@/locales/en.json' + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + }, + lng: 'en', + fallbackLng: 'en', + keySeparator: false, + nsSeparator: false, + interpolation: { + escapeValue: false, + }, +}) +``` + +`keySeparator` and `nsSeparator` are disabled so ABP-style keys such as `AbpIdentity::Users` and `Menu:Home` can be used directly. + +## Using Localized Text + +Use `useTranslation()` from `react-i18next` in components: + +```tsx +import { useTranslation } from 'react-i18next' + +export function BooksTitle() { + const { t } = useTranslation() + + return

{t('Menu:Books')}

+} +``` + +ABP localization keys commonly use the `ResourceName::Key` format: + +```tsx +{t('AbpIdentity::Users')} +{t('AbpAccount::Login')} +{t('AbpUi::SavedSuccessfully')} +``` + +Application-specific menu keys may use names like `Menu:Home` or `Menu:Books`. + +## Adding a Translation + +Add the key to `src/locales/en.json`: + +```json +{ + "Menu:Reports": "Reports", + "Reports": "Reports" +} +``` + +Then use it from a component: + +```tsx +const { t } = useTranslation() + +return

{t('Reports')}

+``` + +## Adding a Language + +Create a new JSON file, for example `src/locales/tr.json`: + +```json +{ + "Menu:Reports": "Raporlar", + "Reports": "Raporlar" +} +``` + +Register it in `src/lib/i18n/i18n.ts`: + +```ts +import en from '@/locales/en.json' +import tr from '@/locales/tr.json' + +i18n.use(initReactI18next).init({ + resources: { + en: { translation: en }, + tr: { translation: tr }, + }, + lng: 'en', + fallbackLng: 'en', +}) +``` + +If you add a language selector, call `i18n.changeLanguage('tr')` when the user chooses Turkish. + +## Server-Side ABP Localization + +ABP's backend localization system is still the source of truth for server-defined resources, validation messages, exception messages, and module texts. The React app uses ABP application configuration through `@volo/abp-app-config` / `@volo/abp-react-app-config` for auth and configuration data, and these packages can include localization resources when configured to do so. + +The main template currently creates the app configuration client with: + +```ts +export const appConfig = createAbpReactAppConfig({ + baseUrl: () => getApiUrl(), + includeLocalizationResources: false, +}) +``` + +Because `includeLocalizationResources` is disabled in the main React template, UI text is normally loaded from `src/locales/*.json`. If you enable server-provided localization resources, make sure your UI initialization merges them into i18next before rendering localized components. + +## Request Culture + +The shared Axios client sends the active i18next language with each request: + +```ts +if (i18n?.language) { + config.headers['Accept-Language'] = + config.headers['Accept-Language'] ?? i18n.language +} +``` + +This lets backend responses, validation messages, and exception messages use the selected culture when the server supports it. + +## Admin Console Localization + +The Admin Console has its own React app and localization setup. In layered and single-layer templates, it is served from the `Volo.Abp.AdminConsole` package. In microservice templates, it is generated as `apps/react-admin-console/`. + +The Admin Console host can expose available languages through `AdminConsole:LocalizationLanguages`, and `/admin-console/api/config` returns the normalized language list. + +## See Also + +- [React UI](./index.md) +- [HTTP Requests](./http-requests.md) +- [Localization](../../../framework/fundamentals/localization.md) diff --git a/docs/en/framework/ui/react/permission-management.md b/docs/en/framework/ui/react/permission-management.md new file mode 100644 index 0000000000..fc009ce5a2 --- /dev/null +++ b/docs/en/framework/ui/react/permission-management.md @@ -0,0 +1,171 @@ +```json +//[doc-seo] +{ + "Description": "Learn how permissions are fetched, stored, checked, and applied in ABP React UI applications." +} +``` + +# Permission Management + +ABP permissions are defined on the server side and are exposed to the React app through ABP application configuration. The React template uses those permissions to protect routes, hide sidebar items, and conditionally render UI actions. + +For the server-side permission system, see [Authorization](../../../framework/fundamentals/authorization/index.md). + +## Packages + +The React template uses: + +| Package | Purpose | +| --- | --- | +| `@volo/abp-app-config` | Framework-agnostic ABP application configuration client. | +| `@volo/abp-react-app-config` | React hooks and adapters for application configuration. | + +The template creates a shared app configuration client in `src/lib/auth/permissions.ts`: + +```ts +export const appConfig = createAbpReactAppConfig({ + baseUrl: () => getApiUrl(), + includeLocalizationResources: false, +}) +``` + +## Fetching Permissions + +After the user logs in, `AuthProvider` fetches application configuration with the current access token: + +```ts +const user = await authClient.getUserManager().getUser() +if (user && !user.expired) { + await fetchAppConfig(user.access_token ?? null) +} +``` + +`fetchAppConfig` also sends the current tenant ID when one is selected: + +```ts +export async function fetchAppConfig(token: string | null): Promise { + const headers: Record = {} + const tenantId = sessionStorage.getItem('abp_tenant_id') + if (tenantId) headers.__tenant = tenantId + await appConfig.fetchConfig(token, { headers }) +} +``` + +The response includes the current user's granted policies. These are stored by the app configuration client and exposed to React components. + +## Checking Permissions in Components + +Use `usePermissions()` from `src/lib/auth/permissions.ts`: + +```tsx +import { usePermissions } from '@/lib/auth/permissions' + +export function BookActions() { + const { isGranted } = usePermissions() + + return ( + <> + {isGranted('MyProjectName.Books.Edit') && } + {isGranted('MyProjectName.Books.Delete') && } + + ) +} +``` + +The Books page uses this pattern for edit and delete actions: + +```ts +const { isGranted } = usePermissions() +const canEdit = isGranted('MyProjectName.Books.Edit') +const canDelete = isGranted('MyProjectName.Books.Delete') +``` + +## Route Guards + +Routes can require a permission by using `createPermissionGuard`: + +```ts +const booksRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/books', + component: BooksPage, + beforeLoad: createPermissionGuard('MyProjectName.Books'), +}) +``` + +`createPermissionGuard` runs the authentication guard first, fetches app configuration if needed, and redirects to `/403` when the required policy is not granted. + +```ts +export function createPermissionGuard(requiredPolicy: string) { + return async (context: GuardContext) => { + await authGuard(context) + + if (!appConfig.getSnapshot()?.initialized) { + const user = await userManager.getUser() + await fetchAppConfig(user?.access_token ?? null) + } + + if (!isPolicyGranted(requiredPolicy)) throw redirect({ to: '/403' }) + } +} +``` + +## Sidebar Visibility + +The sidebar reads `routeConfig` and hides items that require missing permissions: + +```ts +export const routeConfig: RouteConfigItem[] = [ + { + path: '/identity/users', + nameKey: 'AbpIdentity::Users', + requiredPolicy: 'AbpIdentity.Users', + }, +] +``` + +The sidebar checks each item: + +```ts +if (item.requiresAuth && !isAuthenticated) return false +if (!item.requiredPolicy) return true +if (!isAuthenticated) return false +return isGranted(item.requiredPolicy) +``` + +Use `requiresAuth` for menu items that only require login. Use `requiredPolicy` when the item should only be visible to users with a specific permission. + +## Compound Policies + +The template's `isPolicyGranted` helper supports simple compound expressions: + +- `PermissionA || PermissionB` +- `PermissionA && PermissionB` + +This is useful for menu entries that should be visible when the user has one of several related module permissions. + +## Where Permissions Are Applied + +The generated React app uses permissions in these places: + +- **Users page**: the `/identity/users` route and sidebar entry require `AbpIdentity.Users`. The page links to the Admin Console for full user and role management. +- **Books page**: the route requires `MyProjectName.Books`; edit and delete actions check `MyProjectName.Books.Edit` and `MyProjectName.Books.Delete`. +- **Admin Console link**: the sidebar entry uses `requiresAuth` because the Admin Console performs its own module and route permission checks. + +The Admin Console applies module-specific permissions for pages such as: + +- Identity users and roles: `AbpIdentity.*`. +- OpenIddict applications and scopes: `OpenIddictPro.Application` and `OpenIddictPro.Scope`. +- Audit Logging UI: `AuditLogging.AuditLogs`. +- Text Template Management: `TextTemplateManagement.*`. +- AI Management: `AIManagement.*`. + +## Multi-Tenancy + +When a tenant is selected, the template stores the tenant ID in `sessionStorage` as `abp_tenant_id`. Permission and API requests send it with the `__tenant` header. This ensures the backend returns permissions and data for the selected tenant context. + +## See Also + +- [Authorization](./authorization.md) +- [HTTP Requests](./http-requests.md) +- [Authorization](../../../framework/fundamentals/authorization/index.md) diff --git a/docs/en/framework/ui/react/unit-testing.md b/docs/en/framework/ui/react/unit-testing.md new file mode 100644 index 0000000000..46a3089eeb --- /dev/null +++ b/docs/en/framework/ui/react/unit-testing.md @@ -0,0 +1,150 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to run and write unit tests in ABP React UI applications with Vitest and React Testing Library." +} +``` + +# Unit Testing React UI + +ABP React UI templates are preconfigured for unit testing. A solution created with ABP Studio v3.0+ or `abp new --modern --ui-framework react` includes Vitest, jsdom, React Testing Library, and jest-dom. + +You can add a test file and run the test command without adding extra test infrastructure. + +## Test Stack + +The React template uses: + +| Package | Purpose | +| --- | --- | +| `vitest` | Test runner and assertion library. | +| `jsdom` | Browser-like DOM environment for component tests. | +| `@testing-library/react` | Render React components and query the DOM like a user. | +| `@testing-library/jest-dom` | Extra DOM assertions such as `toBeInTheDocument`. | + +The template also includes `src/test/setup.ts`, which imports `@testing-library/jest-dom/vitest` and initializes the React i18n setup. + +## Configuration + +The test configuration is in `vitest.config.ts`: + +```ts +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + include: ['src/**/*.{test,spec}.{ts,tsx}'], + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}) +``` + +Tests can import application files with the same `@/` alias used by the app. + +## Running Tests + +Install dependencies once: + +```bash +npm install +``` + +Run tests in watch mode: + +```bash +npm run test +``` + +Run tests once, which is useful for CI: + +```bash +npm run test:run +``` + +The template's `package.json` maps these commands to `vitest` and `vitest run`. + +## Example Test + +The template includes example tests under `src/`. For example, `src/pages/home/HomePage.test.tsx` renders the home page and mocks the authentication hook: + +```tsx +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import { HomePage } from './HomePage' +import * as auth from '@/lib/auth/AuthContext' + +vi.mock('@/lib/auth/AuthContext', () => ({ + useAuth: vi.fn(), +})) + +describe('HomePage', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders login prompt when not authenticated', () => { + vi.mocked(auth.useAuth).mockReturnValue({ + isAuthenticated: false, + isLoading: false, + user: null, + login: vi.fn(), + logout: vi.fn(), + navigateToLogin: vi.fn(), + getAccessToken: vi.fn(), + } as unknown as ReturnType) + + render() + expect(screen.getByText('Welcome')).toBeInTheDocument() + expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument() + }) +}) +``` + +This style keeps the test focused on visible behavior. Dependencies that would require real authentication, network calls, or browser redirects are mocked. + +## Writing a Component Test + +Create a `*.test.tsx` file next to the component: + +```tsx +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { Button } from '@/components/ui/button' + +describe('Button', () => { + it('renders its content', () => { + render() + expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument() + }) +}) +``` + +Prefer queries such as `getByRole`, `getByLabelText`, and `getByText` because they describe what the user can see or do. + +## Writing a Service or Hook Test + +For non-component logic, use Vitest directly. The template includes tests for routing guards, permissions, authentication context, and Axios interceptors. + +When testing API code, mock the shared Axios instance or the lower-level dependency instead of calling a real backend. When testing permission behavior, mock the application configuration client or use the exported permission helpers. + +## Interpreting Output + +Vitest reports each test file, failed assertions, stack traces, and a summary of passed/failed tests. In watch mode, it reruns affected tests when files change. In `test:run` mode, Vitest exits with a non-zero status code if any test fails, which makes it suitable for CI pipelines. + +If a component test fails because an ABP service is not initialized, mock the hook or provider used by the component. For example, pages that call `useAuth()` or `usePermissions()` should provide a controlled mock for those hooks unless the test is specifically verifying the provider. + +## See Also + +- [Components](./components/index.md) +- [Authorization](./authorization.md) +- [Permission Management](./permission-management.md) diff --git a/docs/en/get-started/index.md b/docs/en/get-started/index.md index dcc7aa80c3..1522f3f955 100644 --- a/docs/en/get-started/index.md +++ b/docs/en/get-started/index.md @@ -20,6 +20,8 @@ Please select one of the following documents best fits for your application: - [WPF Application](wpf.md) - [Console Application](console.md) +If you seek a React-based web UI, use the **modern template system** with ABP Studio or `abp new --modern`. See [UI options](../framework/ui/index.md) for the full list of officially supported UI frameworks. + ## Which Startup Template is Suitable for Me? You can see the *[Solution Template Selection Guide](../solution-templates/guide.md)* if you are not sure which solution template is suitable for you. diff --git a/docs/en/get-started/layered-web-application.md b/docs/en/get-started/layered-web-application.md index f4a383ff0b..4cc0ce9ce9 100644 --- a/docs/en/get-started/layered-web-application.md +++ b/docs/en/get-started/layered-web-application.md @@ -18,6 +18,10 @@ In this quick start guide, you will learn how to create and run a layered (and potentially modular) web application using [ABP Studio](../studio/index.md). +> This page documents the **classic** layered flow. +> +> If you want **React UI**, use the **modern** template flow instead. Modern layered solutions create your application in the `react/` folder and host the ABP Admin Console from the backend at `/admin-console/`. See [React UI](../framework/ui/react/index.md) and the [ABP CLI modern templates](../cli/index.md#modern-templates) section for the correct path. + ## Setup your development environment First things first! Let's setup your development environment before creating the first project. The following tools should be installed on your development machine: @@ -150,7 +154,7 @@ If you uncheck the *Kubernetes Configuration* option, the solution will not incl On the next screen, you can configure the modularity options for your solution: -> If you select the *Setup as a modular solution* option, the solution is created more ready for [modular monolith development](../tutorials/modular-crm/index.md) and allows you to add sub-modules during the solution creation phase. +> If your goal is a new modular monolith, prefer the dedicated **Modular Monolith** architecture in ABP Studio. The modularity option on this screen is the classic-host path for keeping this layered solution modularity-ready and allowing sub-modules during solution creation. ![abp-studio-new-solution-modularity](images/abp-studio-new-solution-dialog-modularity_dark.png) @@ -281,4 +285,4 @@ You can start the following application(s): ## What's next? - [TODO Application Tutorial with Layered Solution](../tutorials/todo/layered/index.md) -- [Web Application Development Tutorial](../tutorials/book-store/index.md) \ No newline at end of file +- [Web Application Development Tutorial](../tutorials/book-store/index.md) diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index d30e3926fc..ec2d4eb513 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -11,6 +11,8 @@ In this quick start guide, you will learn how to create and run a microservice solution using [ABP Studio](../studio/index.md). +This guide follows the current **modern** microservice template in ABP Studio. When you select a web UI, ABP Studio creates the main application in `apps/react`, the administration UI in `apps/react-admin-console`, and optionally the public website in `apps/react-public-web`. + ## Setup your development environment First things first! Let's setup your development environment before creating the first project. The following tools should be installed on your development machine: @@ -18,7 +20,7 @@ First things first! Let's setup your development environment before creating the * [Visual Studio 2026](https://visualstudio.microsoft.com/vs/) or another IDE that supports .NET development * [.NET 10.0+](https://dotnet.microsoft.com/en-us/download/dotnet) * [Node v22.11+](https://nodejs.org/) -* [Yarn v1.22+ (not v2+)](https://classic.yarnpkg.com/en/docs/install) or npm v10+ (already installed with Node), **This is required for the Angular applications.** +* npm v10+ (already installed with Node) or [Yarn v1.22+ (not v2+)](https://classic.yarnpkg.com/en/docs/install) for the React applications * [Docker Desktop (with Kubernetes enabled)](https://www.docker.com/products/docker-desktop/) * [Helm](https://helm.sh/docs/intro/install/) * [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) @@ -62,11 +64,11 @@ On that screen, you can enable multi-tenancy for your solution. After selecting ![abp-studio-new-solution-dialog-ui-framework](images/abp-studio-new-solution-dialog-ui-framework-microservice.png) -Here, you see all the possible UI options supported by that startup solution template. You can pick your favorite one and click the *Next* button for the *Mobile Framework* selection screen: +For the modern microservice template, the web UI choices are `React` and `No UI`. Select `React` to create `MyCompanyName.MyProjectName.React` in `apps/react` together with the separate `MyCompanyName.MyProjectName.ReactAdminConsole` application in `apps/react-admin-console`. Then click the *Next* button for the *Mobile Framework* selection screen: ![abp-studio-new-solution-dialog-mobile-framework](images/abp-studio-new-solution-dialog-mobile-framework-microservice.png) -Here, you see all the mobile applications available in that startup solution template. These mobile applications are well-integrated into your solution and can use the same backend with your web application. They are simple (do not have pre-built features as much as the web application) but a very good starting point to build your mobile application. +For the modern microservice template, the mobile choices are `React Native` and `None`. If you select `React Native`, ABP Studio creates the mobile app under `apps/mobile/react-native` and adds the `MyCompanyName.MyProjectName.MobileGateway` application. > If you select a mobile application, an additional API Gateway is created that is only used by the mobile application. @@ -74,7 +76,7 @@ Pick the one best for you, or select the *None* if you don't want a mobile appli ![abp-studio-new-solution-dialog-public-web-site](images/abp-studio-new-solution-dialog-public-web-site.png) -You can select a public website to be created in your solution. The public website is a simple landing page that can be used to introduce your product, provide documentation, and so on. +If you enable the public website, ABP Studio creates the `MyCompanyName.MyProjectName.ReactPublicWeb` application under `apps/react-public-web` and the `MyCompanyName.MyProjectName.PublicGateway` application under `gateways/public`. ![abp-studio-new-solution-dialog-dynamic-localization](images/abp-studio-new-solution-dialog-dynamic-localization.png) @@ -136,7 +138,7 @@ Now, we are ready to allow ABP Studio to create our solution. Just click the *Cr ![abp-studio-created-new-microservice-solution](images/abp-studio-created-new-microservice-solution.png) -You can explore the solution, but you need to **wait for background tasks to be completed** before running any application in the solution (it can take up to a few minutes to set up all). +You can explore the solution, but you need to **wait for background tasks to be completed** before running any application in the solution. ABP Studio uses that time to install the required client-side dependencies and create the Auth Server signing certificate. > The solution structure can be different in your case based on the options you've selected. @@ -148,9 +150,13 @@ This **solution** consists of several **modules** shown in the *Solution Explore ![abp-studio-created-microservice-solution-explorer](images/abp-studio-created-microservice-solution-explorer.png) -Each leaf item (e.g. `Acme.CloudCrm.IdentityService` or `Acme.CloudCrm.Web`) in the tree above is an ABP Studio module. They are grouped into solution folders (`apps`, `gateways`, and `services`). +Each leaf item in the tree above is an ABP Studio module. They are grouped into solution folders (`apps`, `gateways`, and `services`): + +* `apps`: contains `Acme.CloudCrm.AuthServer`, the main React UI as `Acme.CloudCrm.React`, the separate administration UI as `Acme.CloudCrm.ReactAdminConsole`, and optionally `Acme.CloudCrm.ReactPublicWeb`. +* `gateways`: contains `Acme.CloudCrm.WebGateway` for the main web UI, and optionally `Acme.CloudCrm.PublicGateway` and `Acme.CloudCrm.MobileGateway`. +* `services`: contains backend microservices such as `Acme.CloudCrm.AdministrationService`, `Acme.CloudCrm.IdentityService`, and the optional services selected in the wizard. -Each module has a separate .NET Solution. You can open a module's (or .NET solution's) folder by right-clicking a module in the *Solution Explorer* tree, select *Open with* -> *Explorer* option as shown below: +The .NET applications in `apps/auth-server`, `gateways/*`, and `services/*` each have their own .NET solutions. The React applications are standard Node projects under `apps/react` and `apps/react-admin-console`, plus `apps/react-public-web` when the public website is enabled. You can open any module's folder by right-clicking it in the *Solution Explorer* tree and selecting *Open with* -> *Explorer* as shown below: ![abp-studio-open-module-folder](images/abp-studio-open-module-folder.png) @@ -158,15 +164,15 @@ If we open the `Acme.CloudCrm.IdentityService` module's path in the explorer, we ![abp-studio-microservice-example-identity-service-files](images/abp-studio-microservice-example-identity-service-files.png) -This microservice solution is designed to have separate .NET solutions for each service to make it possible to develop independently from the other services and applications. +This microservice solution is designed so each backend service, gateway, and the Auth Server can be developed independently, while the React applications stay in their own app folders. -You can open any module's .NET solution in your favorite IDE and make your development. The following figure is a screenshot from the *Identity* microservice opened in Visual Studio: +You can open any backend module's .NET solution in your favorite IDE and make your development. The following figure is a screenshot from the *Identity* microservice opened in Visual Studio: ![abp-studio-microservice-example-identity-service-in-visual-studio](images/abp-studio-microservice-example-identity-service-in-visual-studio.png) -If you explore that .NET solution, you will typically see some configuration code, and you won't see any business code. That's because the solution uses [pre-built application modules](../modules) as NuGet packages, and doesn't contain their source code. In this way, you can easily upgrade these application modules when a new version is available. +If you explore that .NET solution, you will typically see composition and configuration code, and you won't see the source code of the built-in modules. That's because the solution uses [pre-built application modules](../modules) as NuGet packages. In this way, you can easily upgrade these application modules when a new version is available. -You will typically add new microservices to the solution and perform your business logic inside these new services (however, you can always want to download the source code of any pre-built application module and include it into your solution to freely customize it). +You will typically add new microservices to the solution and perform your business logic inside these new services. You can also customize the React applications in `apps/react`, `apps/react-admin-console`, and, if enabled, `apps/react-public-web` when you need UI-specific changes. ## Running the Solution @@ -184,7 +190,7 @@ In the *Solution Runner* section (on the left side) you can see all the runnable > A leaf item in the *Solution Runner* is called as an *Application* as it is an executable application, excluding items under `Containers`. -As shown in the figure above, the executable applications are grouped into folders like `apps`, `gateways`, and `services`. You can start/stop them all, a group (folder) of them, or one by one. The `Containers` branch contains the needed docker containers for the applications. +As shown in the figure above, the executable applications are grouped into folders like `apps`, `gateways`, and `services`. You can start/stop them all, a group (folder) of them, or one by one. The `Containers` branch contains the needed docker containers for the applications. Before running the applications, you can run the all application by right-clicking the root item in the *Solution Runner* and select *Build* -> *Build All* action. However, you don't need to do that, because ABP Studio builds the applications before running them by default. @@ -192,6 +198,19 @@ Before running the applications, you can run the all application by right-clicki You can click the *Play* button on the root item in *Solution Runner* to start all the applications. +For the common React-based web flow, the main applications are: + +* `Acme.CloudCrm.AuthServer` +* `Acme.CloudCrm.WebGateway` +* `Acme.CloudCrm.AdministrationService` +* `Acme.CloudCrm.IdentityService` +* `Acme.CloudCrm.React` +* `Acme.CloudCrm.ReactAdminConsole` + +If you enabled optional features, also start the related applications such as `Acme.CloudCrm.ReactPublicWeb`, `Acme.CloudCrm.PublicGateway`, `Acme.CloudCrm.MobileGateway`, `Acme.CloudCrm.SaasService`, `Acme.CloudCrm.AuditLoggingService`, `Acme.CloudCrm.GdprService`, `Acme.CloudCrm.ChatService`, or `Acme.CloudCrm.FileManagementService`. + +> ABP Studio runs the React applications as CLI applications by executing `npm run dev` in their app folders. + > **About the Docker Containers** > > Docker will fetch the docker images before starting the containers in your first run (if they were not fetched before) and that process may take a few minutes depending on your internet connection speed. So, please wait for it to completely start. If the process takes more time than you expect, you can right-click on `Docker-Dependencies` and select the *Logs* command to see what's happening. @@ -200,29 +219,31 @@ You can click the *Play* button on the root item in *Solution Runner* to start a > > Some applications/services may fail on the first run. That may be because of service and database dependencies were not satisfied and an error occurs on the application startup. ABP Studio automatically restarts failing services until it is successfully started. Being completely ready for such a distributed solution may take a while, but it will be eventually started. -Once all the applications are ready, you can right-click the `Web` application and select the *Browse* command: +Once all the applications are ready, you can right-click the `Acme.CloudCrm.React` application and select the *Browse* command: ![abp-studio-microservice-solution-runner-browse](images/abp-studio-microservice-solution-runner-browse.png) -The *Browse* command opens the web application's UI in the built-in browser of ABP Studio: +The *Browse* command opens the main React application's UI in the built-in browser of ABP Studio: ![abp-studio-microservice-solution-runner-browse-microservice](images/abp-studio-microservice-solution-runner-browse-microservice.png) -You can browse your application in a full-featured web browser in ABP Studio. Click the *Login* button in the application UI, enter `admin` as username and `1q2w3E*` as password to login to the application. +You can browse your application in a full-featured web browser in ABP Studio. Click the *Login* button in the application UI, enter `admin` as username and `1q2w3E*` as password to log in to the application. + +Use the same *Browse* command on `Acme.CloudCrm.ReactAdminConsole` to open the administration UI. That application runs separately and uses `/admin-console/` as its base path. > You can also browse the other applications/services (that provides a UI) inside ABP Studio. In this way, you don't need to use an external browser or manually type the application's URL. ## Developing Services Using the Solution Runner -Solution Runner not only runs a multi-applications system easier, but is also useful while developing your services and applications. In a microservice solution, you typically focus on one or a few services and applications. Assume that you want to make a development in `IdentityService`. You can use the following development flow: +Solution Runner not only makes a multi-application system easier to run, but is also useful while developing your services and applications. In a microservice solution, you typically focus on one or a few services and applications. Assume that you want to make a development in `IdentityService`. You can use the following development flow: * Start all the applications/services in the solution and test if everything works as expected. * Stop the `IdentityService` in the Solution Runner. -* Open the `IdentityService`'s .NET solution in your favorite IDE (e.g. Visual Studio). As an easy way of opening it, you can use the *Solution Explorer*, find the `Acme.CloudCrm.IdentityService` module, right-click to it and select the *Open with* -> *Visual Studio* command. +* Open the `IdentityService`'s .NET solution in your favorite IDE (e.g. Visual Studio). As an easy way of opening it, you can use the *Solution Explorer*, find the `Acme.CloudCrm.IdentityService` module, right-click it and select the *Open with* -> *Visual Studio* command. * Make your development in the `IdentityService`. * Run (with or without debugging) your service in Visual Studio (or another IDE). -Once you run the `IdentityService` in Visual Studio, it will be completely integrated into the rest of the system since they all run in your local machine. In addition, the `IdentityService` application will automatically connect to ABP Studio and send runtime data to it as it works in ABP Studio. When you run an application out of ABP Studio, it is shown as *external* in the Solution Runner and you can't stop it in ABP Studio (you should stop where you've started): +Once you run the `IdentityService` in Visual Studio, it will be completely integrated into the rest of the system since they all run on your local machine. In addition, the `IdentityService` application will automatically connect to ABP Studio and send runtime data to it as it works in ABP Studio. When you run an application out of ABP Studio, it is shown as *external* in the Solution Runner and you can't stop it in ABP Studio (you should stop it where you've started it): ![abp-studio-microservice-solution-runner-external-service](images/abp-studio-microservice-solution-runner-external-service.png) @@ -236,6 +257,8 @@ To enable watching, right-click the application/service you want to watch, selec Now, you can make your development on the `IdentityService`. Whenever you save a code file, it is automatically rebuilt and restarted by ABP Studio, so any change will be effective on the running solution in a few seconds. +If you are working on the frontend instead, you can apply the same flow to `Acme.CloudCrm.React` or `Acme.CloudCrm.ReactAdminConsole`, and to `Acme.CloudCrm.ReactPublicWeb` when the public website is enabled, then continue from the related app folder with `npm run dev`. + When you enable watch for an application an *eye* icon is added near to the application: ![abp-studio-microservice-solution-runner-watch-enabled-icon](images/abp-studio-microservice-solution-runner-watch-enabled-icon.png) @@ -271,7 +294,7 @@ After building the Docker images, it is ready to install the Helm chart to Kuber > Installing chart should be fast. However, it may take time for being fully ready in Kubernetes. For example, if an image of a service (e.g. Redis, Rabbit) was not pulled before, it will need to pull image first. -Once the solution is ready in Kubernetes, you can open a browser and visit the following URL: https://cloudcrm-local-web It will open a web page as shown below: +Once the solution is ready in Kubernetes, you can open a browser and visit the following URL: `https://cloudcrm-local-web`. It opens the main web application as shown below: ![abp-studio-microservice-web-application-home-page](images/abp-studio-microservice-web-application-home-page.png) @@ -335,7 +358,7 @@ It will start the interception process, and finally you will see the *intercepti ![abp-studio-microservice-kubernetes-interception-enabled](images/abp-studio-microservice-kubernetes-interception-enabled.png) -From now on, all the traffic coming to the Audit Logging microservice is redirected to your local computer. If you open the Audit Logging page now (`https://cloudcrm-local-web/AuditLogs`), you get an error, because the request is redirected to your local machine but the Audit Logging service is not running on your local machine yet. +From now on, all the traffic coming to the Audit Logging microservice is redirected to your local computer. If you open the Audit Logging page now (`https://cloudcrm-local-web/admin-console/audit-logs`), you get an error, because the request is redirected to your local machine but the Audit Logging service is not running on your local machine yet. Open the `Acme.CloudCrm.AuditLoggingService` .NET solution in your IDE (e.g. Visual Studio), set the `Acme.CloudCrm.AuditLoggingService` as startup project and run it (using F5 for debug mode or CTRL+F5 to run it without debugging). @@ -355,7 +378,7 @@ A typical development flow can be as the following: * *Connect* to a Kubernetes cluster where the solution is already deployed (as explained in the *Kubernetes Integration: Connecting to the Cluster* section). You can do it yourself as explained in the *Kubernetes Integration: Working with Helm Charts* section. * *Intercept* a service you want to develop in your local machine. -* Develop, run, stop, fix, debug, re-run... your service easily in your local environment. You can test your service as integrated to others and visit the application UI in the Kubernetes (you can make it for the Web application as similar). +* Develop, run, stop, fix, debug, re-run... your service easily in your local environment. You can test your service as integrated to others and visit the application UI in Kubernetes. You can follow a similar flow for the main React application or the Admin Console. * Once your development is done, you can *Disable* the interception and re-deploy the service to the Kubernetes cluster. To re-deploy a service to Kubernetes, right-click the service and select *Commands* -> *Redeploy* command: diff --git a/docs/en/get-started/single-layer-web-application.md b/docs/en/get-started/single-layer-web-application.md index ef657722b0..ebd4465122 100644 --- a/docs/en/get-started/single-layer-web-application.md +++ b/docs/en/get-started/single-layer-web-application.md @@ -17,6 +17,10 @@ In this quick start guide, you will learn how to create and run a single layer web application using [ABP Studio](../studio/index.md). +> This page documents the **classic** single-layer flow. +> +> If you want **React UI**, use the **modern** template flow instead. Modern single-layer solutions create your application in the `react/` folder and host the ABP Admin Console from the backend at `/admin-console/`. See [React UI](../framework/ui/react/index.md) and the [ABP CLI modern templates](../cli/index.md#modern-templates) section for the correct path. + ## Setup your development environment First things first! Let's setup your development environment before creating the first project. The following tools should be installed on your development machine: @@ -116,7 +120,7 @@ You can change these settings later if needed. Then click the *Next* button for Configure any additional options as needed and click the *Next* button to continue. On the next screen, you can configure the modularity options for your solution: -> If you select the *Setup as a modular solution* option, the solution is created more ready for [modular monolith development](../tutorials/modular-crm/index.md) and allows you to add sub-modules during the solution creation phase. +> If your goal is a new modular monolith, prefer the dedicated **Modular Monolith** architecture in ABP Studio. The modularity option on this screen is the classic-host path for keeping this single-layer solution modularity-ready and allowing sub-modules during solution creation. ![abp-studio-no-layers-new-solution-modularity](images/abp-studio-no-layers-new-solution-modularity_dark.png) @@ -188,4 +192,4 @@ You can use `admin` as username and `1q2w3E*` as default password to login to th ## What's next? -- [TODO Application Tutorial with Single-Layer Solution](../tutorials/todo/single-layer/index.md) \ No newline at end of file +- [TODO Application Tutorial with Single-Layer Solution](../tutorials/todo/single-layer/index.md) diff --git a/docs/en/guides/ms-multi-tenant-domain-resolving.md b/docs/en/guides/ms-multi-tenant-domain-resolving.md index 7603491f56..c8fdaae84c 100644 --- a/docs/en/guides/ms-multi-tenant-domain-resolving.md +++ b/docs/en/guides/ms-multi-tenant-domain-resolving.md @@ -68,6 +68,8 @@ This configuration will allow subdomain tenant resolving. Ex, if you have a tena **For angular application**, you don't need to add anything since we will be overriding the angular environment via kubernetes values file. +> The configuration above resolves the current tenant from the incoming request. To make outbound URLs generated through `IAppUrlProvider` (Account email links, redirects, etc.) also tenant-aware, configure `AppUrlOptions` with the same `{0}` template. See [Application URLs](../framework/infrastructure/app-urls.md#multi-tenant-aware-urls). + ## Configuring AuthServer When the tenant try to login from an application (Ex `https://volosoft.angular.mystore.dev`) it will be redirected to AuthServer (`https://volosoft.authserver.mystore.dev`) and you will be seeing a **HTTP 400 error** related to `invalid redirect_uri`. If you check the authserver application logs (under Logs/logs.txt file or console logs), you will notice that the `https://volosoft.angular.mystore.dev` is not a valid redirect_uri since it is not been seeded by the OpenIddictDataSeeder. Only the **host** applications, gateways and microservice URLs are seeded (like `https://angular.mystore.dev`). diff --git a/docs/en/images/abp-studio-ai-agent.png b/docs/en/images/abp-studio-ai-agent.png new file mode 100644 index 0000000000..d74add4b2d Binary files /dev/null and b/docs/en/images/abp-studio-ai-agent.png differ diff --git a/docs/en/images/layered-project-dependencies-blazor-server.png b/docs/en/images/layered-project-dependencies-blazor-server.png index 747c5e4f0a..1bc41ab3ba 100644 Binary files a/docs/en/images/layered-project-dependencies-blazor-server.png and b/docs/en/images/layered-project-dependencies-blazor-server.png differ diff --git a/docs/en/images/layered-project-dependencies-blazor-wasm.png b/docs/en/images/layered-project-dependencies-blazor-wasm.png index 54678b3c3e..2356adcbde 100644 Binary files a/docs/en/images/layered-project-dependencies-blazor-wasm.png and b/docs/en/images/layered-project-dependencies-blazor-wasm.png differ diff --git a/docs/en/images/layered-project-dependencies-module.png b/docs/en/images/layered-project-dependencies-module.png index 68dc214ee9..4c96a54310 100644 Binary files a/docs/en/images/layered-project-dependencies-module.png and b/docs/en/images/layered-project-dependencies-module.png differ diff --git a/docs/en/images/layered-project-dependencies.png b/docs/en/images/layered-project-dependencies.png index 1d5c4f1195..30d4f5c419 100644 Binary files a/docs/en/images/layered-project-dependencies.png and b/docs/en/images/layered-project-dependencies.png differ diff --git a/docs/en/images/react-ui-and-admin-console.png b/docs/en/images/react-ui-and-admin-console.png new file mode 100644 index 0000000000..8b2dbea60b Binary files /dev/null and b/docs/en/images/react-ui-and-admin-console.png differ diff --git a/docs/en/images/ui-options.png b/docs/en/images/ui-options.png index 3c30cd16d7..49b40d2071 100644 Binary files a/docs/en/images/ui-options.png and b/docs/en/images/ui-options.png differ diff --git a/docs/en/images/url-based-localization-angular-menu-url.png b/docs/en/images/url-based-localization-angular-menu-url.png new file mode 100644 index 0000000000..f64ab92486 Binary files /dev/null and b/docs/en/images/url-based-localization-angular-menu-url.png differ diff --git a/docs/en/images/url-based-localization-angular-routes.png b/docs/en/images/url-based-localization-angular-routes.png new file mode 100644 index 0000000000..9ea19d386a Binary files /dev/null and b/docs/en/images/url-based-localization-angular-routes.png differ diff --git a/docs/en/index.md b/docs/en/index.md index 86b7104b06..3a23e08d8f 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -27,9 +27,9 @@ After getting started, you can read the following documents: ### UI Framework Options -ABP can work with any UI framework, while the following frameworks are supported and well-integrated out of the box: +ABP can work with any UI framework, while the following frameworks are supported and well-integrated out of the box. React is available with the modern template system. See the [UI options](./framework/ui/index.md) page for details. -ui options +ABP UI options including React ### Database Provider Options @@ -84,7 +84,7 @@ ABP Platform provides tooling to help you in your daily development. #### ABP CLI -[ABP CLI](cli.md) is a command-line tool to create new solutions and automate the things with your ABP based solutions. +[ABP CLI](./cli/index.md) is a command-line tool to create new solutions and automate the things with your ABP based solutions. ### Startup Templates diff --git a/docs/en/modules/account.md b/docs/en/modules/account.md index 3fc2c8e3aa..844e00e06f 100644 --- a/docs/en/modules/account.md +++ b/docs/en/modules/account.md @@ -43,6 +43,8 @@ Social/external login buttons becomes visible if you setup it. See the *Social/E ![account-module-forgot-password](../images/account-module-forgot-password.png) +> The host part of the password reset link is built from `AppUrlOptions.Applications["MVC"].RootUrl`. Configure it if the default `App:SelfUrl` isn't what you want users to see in emails — for example, when you use subdomain-based multi-tenancy and want the link to point to the tenant's subdomain. See [Application URLs](../framework/infrastructure/app-urls.md). + ### Account Management `/Account/Manage` page is used to change password and personal information of the user. diff --git a/docs/en/modules/account/shared-user-accounts.md b/docs/en/modules/account/shared-user-accounts.md index 6f028238c6..908b342de2 100644 --- a/docs/en/modules/account/shared-user-accounts.md +++ b/docs/en/modules/account/shared-user-accounts.md @@ -56,6 +56,8 @@ Users can leave a tenant. After leaving, the user is no longer a member of that > When a user leaves and later re-joins the same tenant, the `UserId` does not change and tenant-related data (roles, permissions, etc.) is preserved. +Tenant administrators with the `Identity.Users.Delete` permission can also remove a member from the tenant via the **Remove from tenant** action on the user list. This is a soft removal — the global account stays and the user can be re-invited later, exactly like the self-service Leave. + ## Inviting Users to a Tenant Tenant administrators can invite existing or not-yet-registered users to join a tenant. The invited user receives an email; clicking the link completes the join process. If the user doesn't have an account yet, they can register and join through the same flow. @@ -156,9 +158,12 @@ The following operations can only be performed by a host administrator when Shar - Enable or disable two-factor authentication - Change `LockoutEnabled` or `ShouldChangePasswordOnNextLogin` +> `Delete` here means deleting the **global user account**, not removing a user from a single tenant. Removing a member from one tenant is a tenant-level soft operation and is available to tenant administrators — see **Remove from tenant** below. + ### What tenant admins can do - Invite users to the tenant (see the Invitation flow above) +- Remove users from the tenant (soft removal; the global account stays) - Manage role and organization-unit assignments within the tenant - View audit / security logs scoped to the tenant @@ -172,4 +177,5 @@ If you plan to migrate an existing multi-tenant application from an isolated str 1. **Uniqueness check**: Before enabling Shared, ensure all existing usernames and emails are unique globally. ABP performs this check when you switch the strategy and reports conflicts. 2. **Tenants with separate databases**: If some tenants use separate databases, you must ensure the Host database contains matching user records in the `AbpUsers` table (and, if you use social login / passkeys, also sync `AbpUserLogins` and `AbpUserPasskeys`) so the Host-side records match the tenant-side data. After that, the framework can create/manage the user-to-tenant associations. + - **Important — each host-side shadow row must have a new primary key (`Id`) different from the tenant user's `Id`.** Generate a fresh `Guid` for every shadow row instead of reusing the tenant user's primary key. The framework relies on this to distinguish a separate-database tenant from a shared-database one; reusing the Id can mask "Leave Tenant" and external login / passkey synchronization on legacy data. The other identifying fields (`UserName`, `Email`, `PasswordHash`, `TenantId`, etc.) should still match the tenant-side row. diff --git a/docs/en/modules/elsa-pro.md b/docs/en/modules/elsa-pro.md index 6b93e42368..63e95bdb59 100644 --- a/docs/en/modules/elsa-pro.md +++ b/docs/en/modules/elsa-pro.md @@ -46,7 +46,7 @@ After adding the package references, open the module class of the project (e.g.: ### `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` -These two modules generally will be added to your authentication project. Please add `Volo.Elsa.Abp.AspNetCore` and `Volo.Elsa.Abp.Identity` packages to your project and add the `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` to the `DependsOn` attribute of your module class based on your project structure: +These two modules generally will be added to your authentication project. Please add `Volo.Abp.Elsa.AspNetCore` and `Volo.Abp.Elsa.Identity` packages to your project and add the `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` to the `DependsOn` attribute of your module class based on your project structure: ```xml @@ -65,20 +65,20 @@ These two modules generally will be added to your authentication project. Please The Elsa Workflows has its own database provider, and also has a Tenant/Role/User system. They are under active development, so the ABP Elsa module is not yet fully integrated. Below is the current status of each module in the ABP's Elsa Module: -- `AbpElsaAspNetCoreModule(Volo.Elsa.Abp.AspNetCore)` module is used to integrate Elsa authentication. -- `AbpElsaIdentityModule(Volo.Elsa.Abp.Identity)` module is used to integrate ABP Identity authentication. -- `AbpElsaApplicationModule(Volo.Elsa.Abp.Application)` and `AbpElsaApplicationContractsModule(Volo.Elsa.Abp.Application.Contracts)` modules are used to define the Elsa permissions. +- `AbpElsaAspNetCoreModule(Volo.Abp.Elsa.AspNetCore)` module is used to integrate Elsa authentication. +- `AbpElsaIdentityModule(Volo.Abp.Elsa.Identity)` module is used to integrate ABP Identity authentication. +- `AbpElsaApplicationModule(Volo.Abp.Elsa.Application)` and `AbpElsaApplicationContractsModule(Volo.Abp.Elsa.Application.Contracts)` modules are used to define the Elsa permissions. The rest of the projects/modules are basically empty and will be implemented in the future based on the Elsa features: -- `AbpElsaDomainModule(Volo.Elsa.Abp.Domain)` -- `AbpElsaEntityFrameworkCoreModule(Volo.Elsa.Abp.EntityFrameworkCore)` -- `AbpElsaHttpApiModule(Volo.Elsa.Abp.HttpApi)` -- `AbpElsaHttpApiClientModule(Volo.Elsa.Abp.HttpApi.Client)` -- `AbpElsaBlazorModule(Volo.Elsa.Abp.Blazor)` -- `AbpElsaBlazorServerModule(Volo.Elsa.Abp.Blazor.Server)` -- `AbpElsaBlazorWebAssemblyModule(Volo.Elsa.Abp.Blazor.WebAssembly)` -- `AbpElsaWebModule(Volo.Elsa.Abp.Web)` +- `AbpElsaDomainModule(Volo.Abp.Elsa.Domain)` +- `AbpElsaEntityFrameworkCoreModule(Volo.Abp.Elsa.EntityFrameworkCore)` +- `AbpElsaHttpApiModule(Volo.Abp.Elsa.HttpApi)` +- `AbpElsaHttpApiClientModule(Volo.Abp.Elsa.HttpApi.Client)` +- `AbpElsaBlazorModule(Volo.Abp.Elsa.Blazor)` +- `AbpElsaBlazorServerModule(Volo.Abp.Elsa.Blazor.Server)` +- `AbpElsaBlazorWebAssemblyModule(Volo.Abp.Elsa.Blazor.WebAssembly)` +- `AbpElsaWebModule(Volo.Abp.Elsa.Web)` ## Configure the Elsa Server @@ -129,7 +129,7 @@ The Elsa Workflow API endpoints check permissions. Also, it has a `*` wildcard p The ABP Elsa module defines all permissions that are used in the Elsa workflow. You can use ABP Permission Management module to manage the permissions. -`AbpElsaAspNetCoreModule(Volo.Elsa.Abp.AspNetCore)` module will check and add these permissions to the current user's claims: +`AbpElsaAspNetCoreModule(Volo.Abp.Elsa.AspNetCore)` module will check and add these permissions to the current user's claims: ![Elsa Permissions](../images/elsa-permissions.png) @@ -158,7 +158,7 @@ Elsa Studio requires authentication and there are two ways to authenticate Elsa ##### Elsa Studio - Password Flow Authentication -The `AbpElsaIdentityModule(Volo.Elsa.Abp.Identity)` module is used to integrate with [ABP Identity module](./identity-pro.md) to check Elsa Studio *username* and *password* against ABP Identity. +The `AbpElsaIdentityModule(Volo.Abp.Elsa.Identity)` module is used to integrate with [ABP Identity module](./identity-pro.md) to check Elsa Studio *username* and *password* against ABP Identity. You need to replace `UseIdentity` with `UseAbpIdentity` when configuring Elsa in your Elsa server project as follows: diff --git a/docs/en/modules/identity/two-factor-authentication.md b/docs/en/modules/identity/two-factor-authentication.md index 1ac4b5dd75..c94962ce99 100644 --- a/docs/en/modules/identity/two-factor-authentication.md +++ b/docs/en/modules/identity/two-factor-authentication.md @@ -121,3 +121,61 @@ Configure(IdentityConstants.TwoFactorRememberMeSche options.Cookie.Name = "MyRememberMeCookieName"; //override the cookie name }); ``` + +## How the Verification Code Is Generated + +The codes delivered by the **Email** and **SMS** verification providers are produced by ABP's built-in single-use token providers, registered in `AbpIdentityAspNetCoreModule`: + +- `AbpEmailTwoFactorTokenProvider` is registered under `TokenOptions.DefaultEmailProvider` and replaces ASP.NET Core Identity's TOTP-based `EmailTokenProvider`. +- `AbpPhoneNumberTwoFactorTokenProvider` is registered under `TokenOptions.DefaultPhoneProvider` and replaces ASP.NET Core Identity's TOTP-based `PhoneNumberTokenProvider`. + +Both derive from the abstract `AbpTwoFactorTokenProvider`. The `Authenticator` provider is unaffected: it is overridden by `AbpAuthenticatorTokenProvider`, which still relies on TOTP ([RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238)) because authenticator apps require it. + +On generation, the provider produces a cryptographically-random numeric code (default 6 digits), encrypts it together with an absolute UTC expiration via `IDataProtector`, and persists the resulting blob in the user tokens table. The plaintext code is sent to the user via email/SMS and is never stored. Validation reloads the persisted entry, verifies it has not expired, decrypts and compares constant-time against the submitted input, and — on success — removes the entry so it cannot be replayed. + +This persisted, single-use design has a few properties worth being explicit about: + +1. **A generated code is single-use.** Successful verification removes the stored entry. Re-submitting the same code from a concurrent session fails. +2. **Generating a new code invalidates the previous one.** `SetToken` overwrites the same `(provider, name)` row, so at most one code is valid at any time. Re-issuing a code (e.g. when the user requests a new one) replaces the stored entry and the previously delivered code stops working. +3. **The validity window is exactly the configured lifespan (3 minutes by default).** Expiration is captured as an absolute Unix-seconds value at generation time and is not extended at validation — in contrast to TOTP-based providers, which accept the previous timestep as well and effectively give a 3–6 minute window. +4. **Failed verification keeps the stored entry in place** so the user can retry until expiration. Rate-limiting incorrect attempts is delegated to ASP.NET Core Identity's lockout settings. +5. **Concurrent successful verification returns `false` instead of throwing.** Two requests racing to consume the same code go through the user row's `ConcurrencyStamp`; the loser surfaces as a normal validation failure rather than a 500. +6. **Expired or undecryptable entries are cleaned up on next access.** A stale entry encountered during validation is removed before returning `false`, so the next `GenerateAsync` starts from a clean slate. + +## Configuring the Default Providers + +Both built-in providers expose options classes (`AbpEmailTwoFactorTokenProviderOptions` and `AbpPhoneNumberTwoFactorTokenProviderOptions`) inheriting from `AbpTwoFactorTokenProviderOptions`: + +| Option | Default | Notes | +| --- | --- | --- | +| `TokenLifespan` | `TimeSpan.FromMinutes(3)` | Absolute lifetime of an issued code. | +| `CodeLength` | `6` | Number of digits in the generated code. Valid range: `1`–`9`. | + +Configure them in your module's `ConfigureServices`: + +```csharp +Configure(options => +{ + options.TokenLifespan = TimeSpan.FromMinutes(5); + options.CodeLength = 8; +}); + +Configure(options => +{ + options.TokenLifespan = TimeSpan.FromMinutes(2); +}); +``` + +## Replacing the Verification Code Provider + +If the built-in single-use behavior does not match your requirements (e.g. you need alphanumeric codes, a different storage backend or a custom delivery policy), you can replace either provider by registering your own `IUserTwoFactorTokenProvider` under the same key. `AddTokenProvider` with an existing key replaces the previous descriptor in `TokenOptions.ProviderMap`: + +```csharp +PreConfigure(builder => +{ + builder.AddTokenProvider(TokenOptions.DefaultEmailProvider); + builder.AddTokenProvider(TokenOptions.DefaultPhoneProvider); +}); +``` + +The most ergonomic starting point is to subclass `AbpTwoFactorTokenProvider` and override only the parts you want to change (for example `GenerateNumericCode`, `CreateProtector`, or the storage helpers). `AccountAppService.SendTwoFactorCodeAsync` and `SignInManager.TwoFactorSignInAsync` call through `UserManager.GenerateTwoFactorTokenAsync` and `UserManager.VerifyTwoFactorTokenAsync` respectively, so a registered replacement is invoked without any further wiring. diff --git a/docs/en/modules/text-template-management.md b/docs/en/modules/text-template-management.md index fa62ee7470..32b7cc31a9 100644 --- a/docs/en/modules/text-template-management.md +++ b/docs/en/modules/text-template-management.md @@ -164,6 +164,22 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do See the `TextTemplateManagementPermissions` class members for all permissions defined for this module. +The module exposes two edit-time permissions with different risk levels: + +| Permission | Required to edit | Default grant | +|------------|------------------|---------------| +| `TextTemplateManagement.TextTemplates.EditContents` | Templates rendered by a sandboxed engine (e.g. Scriban). Editing such templates is safe for content editors because the engine cannot execute arbitrary .NET code. | Granted to roles that need to edit template text. | +| `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` | Templates rendered by a **non-sandboxed** engine (e.g. Razor). Editing such templates is functionally equivalent to granting server-side code execution because the engine compiles the content into a .NET assembly that runs with the host process's privileges. | **Not granted to any role by default**, including `admin`. Must be granted explicitly. | + +Whether a template is sandboxed is determined by `ITemplateRenderingEngine.IsSandboxed` on the engine that renders it. Editing a non-sandboxed template requires **both** `EditContents` and `EditNonSandboxedContents`. + +The Text Template Management UI surfaces this distinction: + +- A warning banner is rendered above the editor for non-sandboxed templates. +- The save and restore buttons are disabled when the current user lacks `EditNonSandboxedContents` for a non-sandboxed template. + +> Treat `EditNonSandboxedContents` as equivalent to granting shell access to the application server. Only assign it to fully trusted developers or operators. + ### Angular UI diff --git a/docs/en/others/free-licenses-vs-pro-licenses.md b/docs/en/others/free-licenses-vs-pro-licenses.md index 96f06eecb1..c6383a6d20 100644 --- a/docs/en/others/free-licenses-vs-pro-licenses.md +++ b/docs/en/others/free-licenses-vs-pro-licenses.md @@ -29,6 +29,8 @@ The following table explains the major differences between the open-source ABP a | [PRO application modules](https://abp.io/modules) | | | | [PRO UI themes](https://abp.io/themes) | | | | [ABP Suite](https://abp.io/tools/suite) | | | +| [React UI](../framework/ui/react/index.md) | | | +| [ABP Studio AI Agent](https://abp.io/studio/ai-agent) | | | | [Premium support](https://abp.io/support) | | | | | [Download](https://abp.io/get-started) | [Pricing](https://abp.io/pricing) | @@ -62,21 +64,22 @@ ABP has many **additional modules** compared to the open-source ABP project. Als | Account | [Basic](https://docs.abp.io/en/abp/latest/Modules/Account) | [PRO](https://abp.io/modules/Volo.Account.Pro) | | Multi-Tenancy | [Basic](https://docs.abp.io/en/abp/latest/Modules/Tenant-Management) *(only tenant management)* | [PRO](https://abp.io/modules/Volo.Saas) (SaaS) | | CMS Kit | [Basic](https://docs.abp.io/en/abp/latest/Modules/Cms-Kit) | [PRO](https://abp.io/modules/Volo.CmsKit.Pro) | -| [Blogging](https://docs.abp.io/en/abp/latest/Modules/Blogging) | | | -| [Docs](https://docs.abp.io/en/abp/latest/Modules/Docs) | | | -| [Identity Server Integration](https://docs.abp.io/en/abp/latest/Modules/IdentityServer) | | | +| [Blogging](https://abp.io/modules/blogging) | | | +| [Docs](../modules/docs.md) | | | +| [Identity Server Integration](../modules/identity-server.md) | | | | [Identity Server Management UI](https://abp.io/modules/Volo.Identityserver.Ui) | | | -| [OpenIddict Integration](https://docs.abp.io/en/abp/latest/Modules/OpenIddict) | | | +| [OpenIddict Integration](../modules/openiddict.md) | | | | [OpenIddictManagement UI](https://abp.io/modules/Volo.OpenIddict.Pro) | | | -| [Audit Log Reporting UI](https://abp.io/modules/Volo.AuditLogging.Ui) | | | -| [Dynamic Language Management](https://abp.io/modules/Volo.LanguageManagement) | | | -| [Payment](https://abp.io/modules/Volo.Payment) | | | -| [Text Template Management](https://abp.io/modules/Volo.TextTemplateManagement) | | | -| [Chat](https://abp.io/modules/Volo.Chat) | | | -| [File Management](https://abp.io/modules/Volo.FileManagement) | | | -| [Forms](https://abp.io/modules/Volo.Forms) | | | -| [Twilio SMS](https://abp.io/modules/Volo.Abp.Sms.Twilio) | | | -| [GDPR](https://docs.abp.io/en/commercial/latest/modules/gdpr) | | | +| [Audit Log Reporting UI](../modules/audit-logging-pro.md) | | | +| [Dynamic Language Management](../modules/language-management.md) | | | +| [Payment](../modules/payment.md) | | | +| [Text Template Management](../modules/text-template-management.md) | | | +| [Chat](../modules/chat.md) | | | +| [File Management](../modules/file-management.md) | | | +| [Forms](../modules/forms.md) | | | +| [Twilio SMS](../modules/twilio-sms.md) | | | +| [GDPR](../modules/gdpr.md) | | | +| [AI Management](../modules/ai-management/index.md) | | | Some modules have "Basic" (open-source) and "PRO" (commercial) versions. The next sections show the differences between the basic and the PRO versions. @@ -153,6 +156,23 @@ A screenshot from the CRUD Page Generator: ABP Suite is a part of the ABP Platform that generates full source code from the backend to the client. ABP Suite is not a free tool for everyone. It is free for only the active ABP license holders. +## React UI and ABP Studio AI Agent + +ABP 10.4 introduces two important license-holder capabilities that are designed for modern, AI-assisted application development: + +| Feature | Open Source ABP Project | ABP | +| ------- | ----------------------- | --- | +| [React UI](../framework/ui/react/index.md) | | | +| [ABP Studio AI Agent](https://abp.io/studio/ai-agent) | | | + +- **[React UI](https://abp.io/community/announcements/react-ui-for-abp-framework-is-finally-here-7rfmgb2v) is available with the modern template system for ABP license holders. You can create a React-based solution with ABP Studio's Modern Wizard or with `abp new --modern --ui-framework react`. A modern React solution contains your own React application under `react/` or `apps/react/`, while ABP's standard administration pages are provided by the [ABP Admin Console](../framework/ui/react/admin-console.md). The Admin Console is delivered by the `Volo.Abp.AdminConsole` NuGet package in layered and single-layer templates, and as a separate `apps/react-admin-console/` application in microservice templates.** + +![React UI & Admin Console](../images/react-ui-and-admin-console.png) + +- **[ABP Studio AI Agent](https://abp.io/community/announcements/introducing-abp-studio-ai-agent-o1ni0toc) is also available for ABP license holders. It works inside ABP Studio to understand ABP solution structure, plan changes, edit code, build projects, run migrations, diagnose runtime errors and keep changes Git-safe. AI Agent usage is based on AI credits: existing customers have predefined AI credits, while new customers receive new AI credits according to the current commercial offering. See the [ABP Studio AI Agent](https://abp.io/studio/ai-agent) page for the latest feature and credit details** + +![ABP Studio AI Agent](../images/abp-studio-ai-agent.png) + ## LeptonX Lite (Basic Theme) vs LeptonX (PRO Theme) ABP provides a theming system that has the following goals: diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index fbf27ef2e0..a04f748244 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -7,6 +7,93 @@ # Package Version Changes +## 10.4.1 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| MudBlazor | 8.0.0 | 9.4.0 | #25393 | +| Scriban | 7.0.0 | 7.2.1 | #25493 | + +## 10.4.0-rc.2 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| Magick.NET-Q16-AnyCPU | 14.9.1 | 14.13.0 | #25427 | +| MongoDB.Driver | 3.8.0 | 3.8.1 | #25404 | + +## 10.4.0-rc.1 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Authentication.OpenIdConnect | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Authorization | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.Authorization | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.Web | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly.Authentication | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly.DevServer | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly.Server | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebView.Maui | 10.0.20 | 10.0.51 | #25313 | +| Microsoft.AspNetCore.DataProtection.StackExchangeRedis | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Mvc.NewtonsoftJson | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Mvc.Testing | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.TestHost | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.WebUtilities | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Bcl.AsyncInterfaces | 10.0.4 | 10.0.7 | #25313 | +| Microsoft.Data.Sqlite | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Design | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Proxies | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Relational | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.SqlServer | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Sqlite | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Tools | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Caching.Memory | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Caching.StackExchangeRedis | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.Binder | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.CommandLine | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.EnvironmentVariables | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.UserSecrets | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.DependencyInjection | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileProviders.Composite | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileProviders.Embedded | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileProviders.Physical | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileSystemGlobbing | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Hosting | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Hosting.Abstractions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Http | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Identity.Core | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Localization | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Logging | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Logging.Abstractions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Logging.Console | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Options | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Options.ConfigurationExtensions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Maui.Controls | 10.0.20 | 10.0.51 | #25313 | +| OpenIddict.Abstractions | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Core | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Server.AspNetCore | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Validation.AspNetCore | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Validation.ServerIntegration | 7.3.0 | 7.5.0 | #25306 | +| System.Collections.Immutable | 10.0.2 | 10.0.7 | #25313 | +| System.Management | 10.0.2 | 10.0.7 | #25313 | +| System.Security.Cryptography.Xml | 10.0.6 | 10.0.7 | #25313 | +| System.Security.Permissions | 10.0.2 | 10.0.7 | #25313 | +| System.Text.Encoding.CodePages | 10.0.2 | 10.0.7 | #25313 | +| System.Text.Encodings.Web | 10.0.2 | 10.0.7 | #25313 | +| System.Text.Json | 10.0.2 | 10.0.7 | #25313 | + +**Added:** + +| Package | Version | PR | +|---------|---------|-----| +| MudBlazor | 8.0.0 | #25235 | + ## 10.3.1 | Package | Old Version | New Version | PR | diff --git a/docs/en/release-info/migration-guides/abp-10-4.md b/docs/en/release-info/migration-guides/abp-10-4.md new file mode 100644 index 0000000000..c66f09c96a --- /dev/null +++ b/docs/en/release-info/migration-guides/abp-10-4.md @@ -0,0 +1,310 @@ +```json +//[doc-seo] +{ + "Description": "Upgrade your ABP solutions from v10.3 to v10.4 with this migration guide covering important behavior and integration changes." +} +``` + +# ABP Version 10.4 Migration Guide + +This document is a guide for upgrading ABP v10.3 solutions to ABP v10.4. There are no explicitly marked breaking changes in this release scope, but there are some important changes that may require action in specific application scenarios. + +> **Package Version Changes:** Before upgrading, review the [Package Version Changes](../../package-version-changes.md) document to see version changes on dependent NuGet packages and align your project with ABP's internal package versions. + +## Open-Source (Framework) + +This version contains the following changes on the open-source side: + +### URL-Based Localization + +**Who is affected** + +- Applications that enable the new URL-based localization option. +- Blazor applications with custom routable pages. +- Angular applications that need culture-prefixed URLs. + +**What changed** + +- ABP now supports embedding the culture in the URL path, such as `/en/products` or `/tr/identity/users`. +- The feature is opt-in and disabled by default. +- MVC and Razor Pages are handled automatically when enabled. +- ABP built-in Blazor module pages include culture-aware routes, but your own Blazor pages need manual route variants. +- Angular applications should wrap routes and culture-aware links with the new Angular helpers. + +**What to do** + +If you do not enable URL-based localization, no action is required. + +If you enable it, configure `AbpRequestLocalizationOptions`: + +```csharp +Configure(options => +{ + options.UseRouteBasedCulture = true; +}); +``` + +For custom Blazor pages, add culture route variants: + +```razor +@page "/Products" +@page "/{culture}/Products" +``` + +For Angular applications, wrap your route tree with `withOptionalRouteCulturePrefix` from `@abp/ng.core`, and use the route-culture URL helpers/pipes for menu links, breadcrumbs, and language switching. + +> See the [URL-Based Localization](../../framework/fundamentals/url-based-localization.md) document and [#25174](https://github.com/abpframework/abp/pull/25174) for details. + +### Localization JSON Files Can Be Split by Culture + +**Who is affected** + +- Applications/modules that want to split a large localization resource into multiple JSON files for the same culture. +- Applications that already have duplicate culture JSON files under the same localization resource path. + +**What changed** + +- ABP now allows multiple JSON files with the same `culture` value under the same localization resource. +- Files are merged in ordinal name order. +- If the same localization key exists in multiple files, the value from the last file wins. + +**What to do** + +No action is required for existing applications. + +If you split localization files, choose deterministic file names and avoid unintended duplicate keys: + +```text +Localization/ ++-- MyResource/ + +-- en.json + +-- en_Authors.json + +-- en_Books.json +``` + +> See [#25227](https://github.com/abpframework/abp/pull/25227) for details. + +### Email/SMS 2FA Codes Are Single-Use + +**Who is affected** + +- Applications using the Identity module's email or phone 2FA providers. +- Applications with custom flows around phone-number change tokens. +- Applications that replace or customize ASP.NET Core Identity token providers. + +**What changed** + +- ABP replaces the default email and phone 2FA providers with DataProtector-backed single-use providers. +- Generated codes are encrypted, persisted with an absolute expiration, and removed after successful validation. +- Generating a new code invalidates the previous one. +- `UserManager.GenerateChangePhoneNumberTokenAsync` / `VerifyChangePhoneNumberTokenAsync` also inherit the new stored-token semantics because ASP.NET Core Identity uses the default phone provider for phone-number changes. +- Phone-change tokens issued before upgrading may stop working after the upgrade. + +**What to do** + +- Re-test login, email 2FA, SMS 2FA, and phone-number change flows. +- If your application needs a different code lifetime or length, configure the new provider options: + +```csharp +Configure(options => +{ + options.TokenLifespan = TimeSpan.FromMinutes(5); + options.CodeLength = 8; +}); + +Configure(options => +{ + options.TokenLifespan = TimeSpan.FromMinutes(2); +}); +``` + +- If your application requires custom token behavior, replace the token provider by registering your own provider under `TokenOptions.DefaultEmailProvider` and/or `TokenOptions.DefaultPhoneProvider`. + +> See the [Two Factor Authentication](../../modules/identity/two-factor-authentication.md) document and [#25316](https://github.com/abpframework/abp/pull/25316) for details. + +### MudBlazor Support for Blazor UI + +**Who is affected** + +- Blazor applications that want to try the new MudBlazor-based ABP UI support. +- Module authors who build Blazor UI packages and want to support MudBlazor. + +**What changed** + +- ABP v10.4 introduces the initial MudBlazor integration packages, templates, bundling infrastructure, and module/theme support. +- Existing Blazorise-based applications are not automatically migrated to MudBlazor. + +**What to do** + +- No action is required for existing Blazorise-based applications. +- If you want to try MudBlazor support, create a new v10.4 RC solution/template or add the related MudBlazor packages according to the target UI type and test the UI carefully before production usage. + +> See [#25235](https://github.com/abpframework/abp/pull/25235) for details. + +### Text Template Rendering Engine — `IsSandboxed` Marker + +**Who is affected** + +- Custom rendering engines that implement `Volo.Abp.TextTemplating.ITemplateRenderingEngine` directly (not deriving from `TemplateRenderingEngineBase`). +- Modules and applications that surface template editing to non-developer users (e.g. the Text Template Management module). + +**What changed** + +- `ITemplateRenderingEngine` exposes a new required property: + + ```csharp + bool IsSandboxed { get; } + ``` + + Sandboxed engines (e.g. Scriban) interpret templates as a restricted DSL without .NET interop. Non-sandboxed engines (e.g. Razor) compile templates into fully-trusted .NET code that runs with the same privileges as the host process. +- `TemplateRenderingEngineBase` provides a virtual default of `false` (secure-by-default): engines that derive from the base class and don't override the property are treated as non-sandboxed. +- `RazorTemplateRenderingEngine` declares `IsSandboxed => false` (compiles to .NET assembly via Roslyn). +- `ScribanTemplateRenderingEngine` declares `IsSandboxed => true` and now sets Scriban's `TemplateContext.MemberFilter` so only public properties on imported objects are exposed; methods, fields, events and reflection entry points (`GetType`, `Assembly`, ...) are no longer reachable from Scriban templates. + +**What to do** + +- If your application registers a custom engine by implementing `ITemplateRenderingEngine` directly (without deriving from `TemplateRenderingEngineBase`), add the property: + + ```csharp + public bool IsSandboxed => false; // or true if your engine cannot execute host code + ``` + +- If your engine derives from `TemplateRenderingEngineBase`, no action is required for compilation; however, override `IsSandboxed => true` if your engine is genuinely sandboxed so callers (such as the Text Template Management module) treat its templates as safe to edit by non-developer users. +- Scriban templates that invoke methods on the model (e.g. `{%{{{ model.SomeMethod }}}%}`) or read fields stop working because the engine now whitelists public properties only. Templates that access only properties (the typical Scriban usage) are unaffected. To restore the previous behavior in custom hosts, derive from `ScribanTemplateRenderingEngine` and override `IsMemberAllowed` to allow methods or fields: + + ```csharp + protected override bool IsMemberAllowed(MemberInfo member) => true; + ``` + + Only do this when the model objects are trusted and do not carry secrets, since the previous behavior exposed reflection entry points (`GetType`, `Assembly`, ...) on imported .NET objects. + +> See [#25399](https://github.com/abpframework/abp/pull/25399) for details. + +### Text Template Management — `EditContents` Permission Split (security) + +**Who is affected** + +- Applications using the Text Template Management module that have granted the `TextTemplateManagement.TextTemplates.EditContents` permission to roles that are not fully trusted server administrators/developers. +- In particular, applications that use Razor templates (default for solutions referencing `Volo.Abp.TextTemplating.Razor`) and have any non-developer role with `EditContents`. + +**What changed** + +- A new permission `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` has been added. +- Editing a template whose rendering engine is **non-sandboxed** (i.e. `ITemplateRenderingEngine.IsSandboxed == false`, e.g. Razor) now requires **both** permissions: `EditContents` and `EditNonSandboxedContents`. +- The new permission is **not granted to any role by default**, including the `admin` role. The previously implicit assumption — that `EditContents` was enough to edit Razor templates — has been corrected: editing such templates is functionally equivalent to granting server-side code execution and is now gated by an explicit permission whose name communicates that risk. +- The Text Template Management UI (MVC, Blazor) renders a security warning banner when the current template's engine is non-sandboxed, and disables the save/restore buttons when the current user lacks the new permission. + +**What to do** + +After upgrading, audit which roles currently hold `EditContents` and decide which of them should also be granted `EditNonSandboxedContents`: + +1. Roles that should only edit sandboxed templates (e.g. Scriban, Liquid, plain HTML) need no further action — they keep editing those templates. +2. Roles that need to continue editing Razor templates must be granted `EditNonSandboxedContents` explicitly via the Permission Management page. +3. If — and only if — you have reviewed your role assignments and confirmed that every role currently holding `EditContents` is trusted to execute server-side code through Razor templates, you may seed the new permission for those roles via your `IDataSeedContributor`: + + ```csharp + var grants = await _permissionGrantRepository.GetListAsync( + TextTemplateManagementPermissions.TextTemplates.EditContents); + + foreach (var grant in grants) + { + await _permissionManager.SetAsync( + TextTemplateManagementPermissions.TextTemplates.EditNonSandboxedContents, + grant.ProviderName, + grant.ProviderKey, + isGranted: true); + } + ``` + +> Do not automate this seeding for arbitrary tenants/roles without first reviewing the current grants — auto-restoring the previously-implicit elevated trust would defeat the security improvement. The recommended path is to grant the new permission only to specific developer/operator roles via the Permission Management UI. + +If your application's data seeder grants all permissions in the `TextTemplateManagement` group to a role (e.g. the `admin` role), that role will automatically receive `EditNonSandboxedContents` on first run after upgrade. Audit your seeder if you want stricter defaults. + +> See the [Razor Integration](../../framework/infrastructure/text-templating/razor.md) document and [#25399](https://github.com/abpframework/abp/pull/25399) for details. + +### Dependency Updates + +**Who is affected** + +- Applications that pin ABP transitive dependencies directly. +- Applications that use OpenIddict, MongoDB, or Microsoft/System packages directly with fixed versions. + +**What changed** + +- OpenIddict was upgraded to **7.5.0**. +- MongoDB.Driver was upgraded to **3.8.0**. +- Several Microsoft/System packages were upgraded to **10.0.7** for CVE-2026-40372. +- `System.Security.Cryptography.Xml` was upgraded to **10.0.6**. + +**What to do** + +- Review your direct package references and align them with ABP's package versions where needed. +- Rebuild and run integration tests if you directly use upgraded packages. + +## Pro + +There are no explicitly marked breaking changes on the PRO side in this release scope. However, check the following if they apply to your application. + +### Account Pro Passwordless Email Login + +**Who is affected** + +- Applications using the Account Pro module that want to enable passwordless email login. + +**What changed** + +- Account Pro now supports email login by one-time link, one-time password (OTP), or both. +- The feature is disabled unless configured. +- Tokens are single-use, expire according to the configured lifetime, and respect tenant-scoped settings. + +**What to do** + +- No action is required if you do not enable passwordless email login. +- If you enable it, review account settings, email templates, token lifetime, rate limiting, and `PreventEmailEnumeration` behavior. +- Re-test login flows for host and tenant users. + +### QA Module AI Suggest + +**Who is affected** + +- Applications using the QA module and enabling AI-assisted answer suggestions. + +**What changed** + +- Authorized users can generate AI-suggested answers based on question context and existing answers. +- The feature is controlled by settings and permissions. + +**What to do** + +- No action is required unless you enable the feature. +- If enabled, configure the QA AI Suggest settings, prompt, character limits, and permissions. + +### AI Management MCP Server Enhancements + +**Who is affected** + +- Applications using the AI Management module's MCP server integration. + +**What changed** + +- MCP server configuration was enhanced for `stdio` transport scenarios and workspace relationships. + +**What to do** + +- Re-test MCP server connection, tool listing, and workspace chat flows after upgrading. + +### LeptonX URL-Based Localization Support + +**Who is affected** + +- Applications using LeptonX with URL-based localization. + +**What changed** + +- LeptonX has been updated for culture-aware navigation and language switching, including Angular support. +- Several menu, select, sidebar, and PathBase-related fixes were included. + +**What to do** + +- If you enable URL-based localization, re-test language switching, menu links, breadcrumbs, and PathBase deployments for your UI type. + diff --git a/docs/en/release-info/migration-guides/index.md b/docs/en/release-info/migration-guides/index.md index 580640bdb7..c64250ccc1 100644 --- a/docs/en/release-info/migration-guides/index.md +++ b/docs/en/release-info/migration-guides/index.md @@ -9,6 +9,7 @@ The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../upgrading.md). +- [10.3 to 10.4](abp-10-4.md) - [10.x to 10.3](abp-10-3.md) - [10.x to 10.2](abp-10-2.md) - [10.0 to 10.1](abp-10-1.md) @@ -34,3 +35,4 @@ The following documents explain how to migrate your existing ABP applications. W - [4.x to 4.2](abp-4-2.md) - [3.3.x to 4.0](abp-4-0.md) - [2.9.x to 3.0](../../framework/ui/angular/migration-guide-v3.md) + diff --git a/docs/en/release-info/release-notes.md b/docs/en/release-info/release-notes.md index 2f6c70094c..3a3c89012f 100644 --- a/docs/en/release-info/release-notes.md +++ b/docs/en/release-info/release-notes.md @@ -14,9 +14,22 @@ Also see the following notes about ABP releases: - [ABP Studio release notes](../studio/release-notes.md) - [Change logs for ABP pro packages](https://abp.io/pro-releases) +## 10.4 (2026-05-14) + +See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-4-stable-release-e0u81o2z)** for the v10.4 release. + +- URL-Based Localization +- Localization File Splitting +- Blazor UI: MudBlazor Support +- Identity: Single-Use Email/SMS 2FA Token Providers +- Account Pro: Passwordless Email Login +- AI Management: MCP Server Enhancements +- LeptonX: URL-Based Localization and Theme Improvements +- OpenIddict 7.5.0, MongoDB.Driver 3.8.0, and security-focused dependency updates + ## 10.3 (2026-04-15) -See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-3-release-candidate-hgnpr9jq)** for the v10.3 release. +See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-3-stable-release-aryi10am)** for the v10.3 release. - OpenIddict: `private_key_jwt` Client Authentication + `abp generate-jwks` - Event Bus: String-Based Event Publishing with Dynamic Payload diff --git a/docs/en/release-info/road-map.md b/docs/en/release-info/road-map.md index 9a19a3c4ae..47256bbb05 100644 --- a/docs/en/release-info/road-map.md +++ b/docs/en/release-info/road-map.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.4, planned for May 2026." + "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.5, planned for June 2026." } ``` @@ -11,36 +11,46 @@ This document provides a road map, release schedule, and planned features for th ## Next Versions -### v10.4 +### v10.5 -The next planned version will be 10.4, which is scheduled to be released as a stable version in May 2026. We will be mostly working on the following topics: +The next planned version will be 10.5, which is scheduled to be released as a stable version in mid-June 2026. We will be mostly working on the following topics: * Framework - * Blazor UI: Moving from Blazorise to MudBlazor - * Token Verification Improvements (Refresh Token Support) - * Upgrading 3rd-party Dependencies - * Enhancements in the Core Points + * Token verification improvements with refresh token support and distributed locking + * Angular UI fixes and proxy generation improvements + * Better handling for extra properties, object mapping and auditing edge cases + * Hybrid UI / page embedding infrastructure + * Upgrading 3rd-party dependencies and evaluating replacements where needed + * General bug fixing and improvements in core framework packages * ABP Suite - * Improvements on the generated codes for nullability - * Improvements on Master-Detail Page Design (making it more compact) + * Improvements on generated codes for nullability + * Improvements on master-detail page design (making it more compact) + * Low-Code system integration + * Better support for additional property types like `DateTimeOffset`, `TimeSpan` and numeric enum scenarios + * Improvements for generated file upload, navigation property and display-name experiences * ABP Studio - * Allow to Directly Create New Solutions with ABP's RC (Release Candidate) Versions - * Automate More Details on New Service Creation for a Microservice Solution - * Allow to Download ABP Samples from ABP Studio - * Support Multiple Concurrent Kubernetes Deployment/Integration Scenarios - * Improve the Module Installation Experience / Installation Guides + * AI Coding Agent and MCP integration + * Modern solution wizard improvements and low-code support + * Theme Builder: live preview, project integration and import/export + * Linux support and packaging improvements + * Better React / React Native / Thin UI template experience + * Better startup, diagnostics, module installation and solution runner experiences + * More automation for microservice solution creation and service/module addition + * Terminal, browser and built-in developer productivity enhancements * Application Modules - * AI Management: Chat History & Multi-Tenancy Features - * New Module: Chat with your data - * CMS Kit: CodeMirror v6 Compatibility Update - * Payment Module: Email Notification Improvements - * UI/UX Improvements on Existing Application Modules + * AI Management: chat history, multi-tenancy and tenant-scoped workspace capabilities + * New module: Chat with your data + * Low-Code designer and low-code platform integrations + * CMS Kit and public website improvements + * Payment module e-mail notification improvements + * UI/UX improvements on existing application modules -* Updating Existing Tutorials & Documents (with Other UI & DB Options) - * React Native template and implementation documents +* Updating existing tutorials & documents (with other UI & DB options) + * Documentation updates for modern React, Studio workflows and new CLI capabilities + * General refresh for roadmap, tutorials and feature comparison documents ## Backlog Items @@ -48,72 +58,78 @@ The *Next Versions* section above shows the main focus of the planned versions. ### Framework -The ABP framework is [open source](https://github.com/abpframework/abp) and free for everyone. You can see its [public backlog](https://github.com/abpframework/abp/milestone/2). Here, are some of the important features you can expect from next versions: - -* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure (while it is [already possible](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) to create or consume gRPC endpoints for your application, we plan to create endpoints for the [standard application modules](../modules/index.md)) +The ABP framework is [open source](https://github.com/abpframework/abp) and free for everyone. You can see its [public backlog](https://github.com/abpframework/abp/milestone/2). Here are some of the selected backlog items and longer-term topics: + +* [#23102](https://github.com/abpframework/abp/issues/23102) / ABP Hybrid UI System: Re-using module UIs in different technologies +* [#25123](https://github.com/abpframework/abp/issues/25123) / Upgrade jQuery to 4.x, or consider removing it as dependency +* [#24742](https://github.com/abpframework/abp/issues/24742) / Add Support for LiteDB as a Database Provider +* [#24442](https://github.com/abpframework/abp/issues/24442) / Add Couchbase EF Core Provider Integration +* [#24310](https://github.com/abpframework/abp/issues/24310) / Migrate Volo.Abp.AI Semantic Kernel to Microsoft Agent Framework +* [#23575](https://github.com/abpframework/abp/issues/23575) / Support list/enumerable of complex types for ABP dynamic/static C# proxies on GET requests +* [#23546](https://github.com/abpframework/abp/issues/23546) / Better ExtraProperties mapping for EF Core +* [#23935](https://github.com/abpframework/abp/issues/23935) / Hybrid Cache Support for EntityCache +* [#22931](https://github.com/abpframework/abp/issues/22931) / Angular - Support dynamic URLs for breadcrumbs +* [#25032](https://github.com/abpframework/abp/issues/25032) / Guidance and infrastructure considerations for gRPC-based scenarios +* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure * [#57](https://github.com/abpframework/abp/issues/57) / Built-in CQRS infrastructure * [#58](https://github.com/abpframework/abp/issues/58) / Content localization system (multilingual entities) * [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook system -* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB Integration for multitenancy +* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB integration for multitenancy * [#2296](https://github.com/abpframework/abp/issues/2296) / Feature toggling infrastructure * [#15932](https://github.com/abpframework/abp/issues/15932) / Introduce ABP Diagnostics Module * [#16744](https://github.com/abpframework/abp/issues/16744) / State Management API -* [#119](https://github.com/abpframework/abp/issues/119) / REST API Versioning Improvements -* [#2087](https://github.com/abpframework/abp/issues/2087) / RavenDB Database Support +* [#119](https://github.com/abpframework/abp/issues/119) / REST API versioning improvements +* [#2087](https://github.com/abpframework/abp/issues/2087) / RavenDB database support ### Application Modules / UI Themes -ABP Platform provides many (free and commercial) [pre-built application modules](../modules/index.md) and modern [UI themes](../ui-themes/index.md). In every release, many enhancements and bugfixes are already done on the existing modules. In addition, here some of the planned features for next versions: +ABP Platform provides many (free and commercial) [pre-built application modules](../modules/index.md) and modern [UI themes](../ui-themes/index.md). In every release, many enhancements and bugfixes are delivered for these modules and themes. Important backlog topics currently include: -* LeptonX theme: New layouts, styles and components -* CMS Kit module: Meta information for SEO, media gallery, RSS feed, content versioning, social media streams -* Payment module: Invoice system -* New module: User notification -* New module: Dynamic dashboard -* New module: User guiding -* New module: Keycloak integration +* AI Management module: chat history, multi-tenancy, tenant workspaces and operational hardening +* CMS Kit module: media gallery and richer public website capabilities +* Payment module: richer notifications and invoice-oriented scenarios +* Audit logging UI: filter redesign and UX improvements +* Identity Pro: richer filtering and organization-unit UX improvements +* LeptonX and existing UIs: new layouts, styles and usability refinements +* New module ideas: Chat with your data, AI Search, user notification and dynamic dashboard ### ABP Studio -[ABP Studio](../studio/index.md) is a cross-platform desktop application for ABP and .NET developers to simplify and automate daily tasks of developers. It has a community (free) edition as well as commercial editions. It is released and versioned independently from the ABP platform and frequently released. - -Here, are some of the important planned features for next ABP Studio versions: - -* Theme builder for the LeptonX theme -* Analyze user solutions to explore entities, domain services, application services, pages and other fundamental objects. -* Swagger authentication support for the built-in browser -* Show related requests/events (traces) together in the solution runner panel -* Built-in command terminal -* Automate all steps of new service creation for microservice solutions -* More options while creating new solutions -* Built-in ABP documentation experience -* Auto-execute terminal commands in markdown files -* Compare changes on the startup templates when a new ABP version is published -* Remove unused projects while downloading the source code of an existing module -* Testing/hosting applications for module templates -* Easily explore all module and package dependencies of a large solution -* Built-in deployment options -* Rapid application development features -* ABP support integration +[ABP Studio](../studio/index.md) is a cross-platform desktop application for ABP and .NET developers to simplify and automate daily tasks of developers. It has a community (free) edition as well as commercial capabilities. Here are some of the important planned features and active backlog topics for the next ABP Studio versions: + +* Theme builder for LeptonX, including live preview, management UI and project integration +* Analyze user solutions to explore entities, domain services, application services, pages and other fundamental objects +* AI agent/browser capabilities and developer-assistant experiences +* Built-in command terminal and richer embedded development tools +* Built-in or embedded VS Code experience +* Better Linux and cross-platform support +* Better startup templates, including React Native, Thin UI, Angular SSR and public website scenarios +* More options while creating new solutions, modules and services +* Better environment-variable, deployment and Kubernetes experiences +* Compare changes on startup templates when a new ABP version is published +* Rapid application development and low-code oriented features +* ABP support integration and better diagnostics/error experiences ### ABP Suite -[ABP Suite](../suite/index.md) is a GUI application that is mainly used to generate CRUD style pages in your application. You define your entity and it can generate all the code from the database layer to the UI layer. The generated code is clean and a perfect starting point to implement your custom requirements on top of it. - -Here, are some of the important planned features for the next ABP Suite versions: +[ABP Suite](../suite/index.md) is a GUI application that is mainly used to generate CRUD-style pages in your application. You define your entity and it can generate all the code from the database to the UI. Here are some of the important planned features for the next ABP Suite versions: -* Handle image properties for entities (in addition to file properties, which is already supported) -* Allow to define extra properties for DTOs those are not a part of the entity -* Allow to create pages instead of modals for CRUD page generation +* Better nullability support in generated code +* MudBlazor support +* Replacing the current templating system with a text engine while preserving backward compatibility +* Support for additional property types like `DateTimeOffset` and `TimeSpan` +* Handle image properties for entities (in addition to file properties, which are already supported) +* Allow to define display names and better ordering for properties and navigation properties +* Filter on inherited properties and improve relationship management experiences +* Allow to change generated file paths and support namespace-based foldering for generated UI code * View-only (detail view) modal/page for an entity -* Export child/detail entity records as a part of export operation for a main (master) entity -* Allow to add custom entity actions for an entity -* Allow to inherit from an existing entity class * Custom form layouts on CRUD page generation +* Allow to create pages instead of modals for CRUD page generation +* Export child/detail entity records as a part of export operations for a main (master) entity ## Feature Requests -Vote for your favorite feature on the related GitHub issues (and write your thoughts). You can create an issue on [the GitHub repository](https://github.com/abpframework/abp) for your feature requests, but please first search the existing issues. You can also contact [info@abp.io](mailto:info@abp.io) for your feature requests and other suggestions. +Vote for your favorite feature on the related GitHub issues (and write your thoughts). You can create an issue on [the GitHub repository](https://github.com/abpframework/abp) for your feature requests and suggestions. ## See Also diff --git a/docs/en/solution-templates/application-module/index.md b/docs/en/solution-templates/application-module/index.md index 19b2294a76..dec38a1cfa 100644 --- a/docs/en/solution-templates/application-module/index.md +++ b/docs/en/solution-templates/application-module/index.md @@ -9,6 +9,8 @@ This document explains how to create a **reusable [application module](../../modules)** based on the [module development best practices & conventions](../../framework/architecture/best-practices). +This page documents creating a **standalone module solution**. If you are using the modern [Modular Monolith solution template](../modular-monolith/index.md), ABP Studio can also scaffold additional modules during solution creation or later from *Solution Explorer*. The generated module structure still follows the same reusable module concepts explained here. + > Notice that the application module that is created in this tutorial is not an executable application. To see the module in action, you should install it into an executable application. > > It is advised to see the *[Modular Monolith Application Development Tutorial](../../tutorials/modular-crm/index.md)* to learn how to create application modules, install them into an executable web application, run and test the application. That tutorial uses the *Standard* module template, while this document explains the *DDD* module template. @@ -21,6 +23,8 @@ First, install the ABP Studio if you haven't installed before. You can follow th ### Creating a New Empty Solution +This empty-solution flow is for a standalone module repository. It is different from the modern modular monolith wizard, where modules are added to a main application solution. + Open the ABP Studio and click the `New solution` button in the welcome page or the `File > New Solution` top menu item. Click the `empty solution` link to select the empty solution template. ![New Solution](images/new-solution-v2.png) @@ -143,7 +147,9 @@ You can still create unit tests for your classes which will be harder to write ( ### Host Applications -The solution doesn't have a host application to run your module. However, you can create a [single-layer](../../get-started/single-layer-web-application.md) or [layered](../../get-started/layered-web-application.md) application and [import](../../studio/solution-explorer.md#imports) the created module into the host application. +The solution doesn't have a host application to run your module. For a new modern ABP Studio solution, the most direct host choice is the [Modular Monolith solution template](../modular-monolith/index.md). ABP Studio can add the module during solution creation or later from *Solution Explorer*. + +Classic single-layer and layered host applications are still valid options. You can create a [single-layer](../../get-started/single-layer-web-application.md) or [layered](../../get-started/layered-web-application.md) application and [import](../../studio/solution-explorer.md#imports) the created module into the host application. You can also see the *[Modular Monolith Application Development Tutorial](../../tutorials/modular-crm/index.md)* to learn how to create application modules, install them into an executable web application, run and test the application diff --git a/docs/en/solution-templates/guide.md b/docs/en/solution-templates/guide.md index 944e503c53..3d51d68fad 100644 --- a/docs/en/solution-templates/guide.md +++ b/docs/en/solution-templates/guide.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore ABP's guide to selecting the right startup template for your project, covering architectures like Microservices, N-Layered, and more." + "Description": "Explore ABP's guide to selecting the right classic or modern startup template for your project, covering Single-Layer, Layered, Modular Monolith, and Microservice architectures." } ``` @@ -9,11 +9,23 @@ ABP provides several [startup templates](index.md) to you. It is important to start with the right startup template that is suitable for your **project** and **team**. This guide aims to lead you to select the most proper startup template for your requirements. +ABP currently exposes two closely related template families: + +* **Classic templates**, documented with the `Single-Layer` and `Layered` names in this section. +* **Modern solution wizard architectures**, which use the names `Simple Monolith`, `Layered Monolith`, `Modular Monolith`, and `Microservice`. + +Throughout this guide: + +* **Single-Layer** corresponds to the modern **Simple Monolith** architecture. +* **N-Layered** corresponds to the modern **Layered Monolith** architecture. +* **Modular Monolith** has a dedicated modern solution path in ABP Studio. +* **Microservice** exists in both families, but the current microservice reference pages describe the modern React-based web structure. + The following **architectures** will be discussed based on ABP startup templates: -* **Single-Layer** (non-layered) application -* **N-Layered** application -* **Modular** application +* **Single-Layer / Simple Monolith** application +* **N-Layered / Layered Monolith** application +* **Modular Monolith** application * **Microservice** solution ## What is a Startup Template? @@ -24,7 +36,7 @@ In the following section, you will understand what a startup template is and wha A startup solution template is a **pre-architected** structure. For example, the [layered startup template](layered-web-application/index.md) is a great starting point if you want to build a layered application code-base based on [Domain-Driven Design](../framework/architecture/domain-driven-design/index.md) principles and patterns. -However, starting with any startup template **doesn't limit you** on adding or removing projects, layers, integration packages, and creating other applications/services. You can even start with a [single-layer application template]() and convert it to a microservice solution. However, if you want to build a microservice solution, starting with the [microservice startup template](microservice/index.md) is the best. +However, starting with any startup template **doesn't limit you** on adding or removing projects, layers, integration packages, and creating other applications/services. You can even start with a [single-layer application template](single-layer-web-application/index.md) and convert it to a microservice solution. However, if you want to build a microservice solution, starting with the [microservice startup template](microservice/index.md) is the best. So, it is **best to start with the most suitable startup template** for your purpose and then modify the solution to fit your custom requirements. @@ -52,7 +64,7 @@ Up to this point, it is explained what a startup template is and the features it ### Single-Layer Application Solution Template -The [single-layer solution template](single-layer-web-application/index.md) is the simplest. It provides a **minimal solution architecture** while starting a new project. Your .NET solution typically contains a **single, or a few .NET projects** depending on your UI and other preferences while creating your solution. +The [single-layer solution template](single-layer-web-application/index.md) is the simplest classic option. It provides a **minimal solution architecture** while starting a new project. In the modern solution wizard, the comparable architecture is **Simple Monolith**. Your .NET solution typically contains a **single, or a few .NET projects** depending on your UI and other preferences while creating your solution. The following figure shows a single-project web application that has [MVC (Razor Pages) UI](../framework/ui/mvc-razor-pages/overall.md) and [Entity Framework Core](../framework/data/entity-framework-core/index.md) database provider with the default configuration: @@ -85,7 +97,7 @@ These options are not implemented to keep the solution structure as simple as po ### Layered Solution Template -The [layered application startup template](layered-web-application/index.md) is a .NET solution that consists of several projects. +The [layered application startup template](layered-web-application/index.md) is the classic layered option. In the modern solution wizard, the comparable architecture is **Layered Monolith**. It is a .NET solution that consists of several projects. Each project represents a layer of the application or has a specific functionality for the solution. The exact project count in your solution depends on the options you have selected. @@ -117,9 +129,11 @@ In the following conditions, you may consider to use the layered solution templa ### Modular Monolith Applications -ABP does not provide a specific modular monolith application startup template. However, it is not needed. Let us explain why. +ABP Studio's modern solution wizard includes a dedicated [Modular Monolith solution template](modular-monolith/index.md). It creates a main application, enables modularity automatically, and lets you scaffold additional modules while creating the solution. This is the most direct path for new modular ABP Studio solutions. + +Classic ABP solutions can still build a modular monolith by combining a host application and reusable application modules. -The ABP Framework and [ABP Studio](../studio/index.md) are already designed to support modular application development from their beginning. ABP framework provides all the **necessary infrastructure** for [modularity](../framework/architecture/modularity/basics.md) and all other framework features are **compatible with modular solutions**. +The ABP Framework and [ABP Studio](../studio/index.md) are designed to support modular application development. ABP framework provides all the **necessary infrastructure** for [modularity](../framework/architecture/modularity/basics.md) and all other framework features are **compatible with modular solutions**. On the other hand, the main purpose of ABP Studio's [Solution Explorer panel](../studio/solution-explorer.md) is to **architect and build modular and complex software solutions**. You can easily create new modules, arrange dependencies between the modules and import/install these modules into a monolith application. While you can do all these manually yourself, ABP Studio makes it extremely easy to do and understand it. @@ -131,19 +145,20 @@ A **modular monolith** application consists of a **single host** application and In this example, `MyCrm.Host` is an almost-empty host application that has package references to other modules. Every module consists of two packages: implementation and contract packages. -You can follow the steps below to create such a modular solution with ABP Studio: +You can follow one of the paths below to create such a modular solution with ABP Studio: -* **Create a new application** using either [single-layer](single-layer-web-application/index.md) or [layered](layered-web-application/index.md) application startup template. That application will be the **host application** of your solution. -* **Create new modules** (right-click to the solution root, select the *Add* -> *New Module* -> ... command). -* **Import & Install** these **modules** to the host application. +* **Modern path**: Choose the [Modular Monolith solution template](modular-monolith/index.md), configure the main application, then add extra modules in the *Modularity* step or later from *Solution Explorer*. +* **Classic composition path**: Create a host application using either the [single-layer](single-layer-web-application/index.md) or [layered](layered-web-application/index.md) template, create new modules, then import and install these modules into the host application. > You can follow the **[Modular Monolith Application Development Tutorial](../tutorials/modular-crm/index.md)** to learn how to build a modular application step by step. #### Which Startup Template should be used for a Modular Application? -So, both [single-layer](single-layer-web-application/index.md) and [layered](layered-web-application/index.md) application startup templates are inherently modular. Just use one of them and start your modular solution. You may wonder which one to start: +For a new modern ABP Studio solution, use the dedicated [Modular Monolith solution template](modular-monolith/index.md). + +If you are composing a modular monolith by using the classic host + module approach, both [single-layer](single-layer-web-application/index.md) and [layered](layered-web-application/index.md) application startup templates are inherently modular. In that case, you may wonder which one to start: -* Use the **[single-layer startup template](single-layer-web-application/index.md)** for the host application of your modular monolith if you will leave the host application as empty. It will contain some configuration code of course, but it won't contain any actual application code. **This is the suggested approach.** +* Use the **[single-layer startup template](single-layer-web-application/index.md)** for the host application of your modular monolith if you will leave the host application as empty. It will contain some configuration code of course, but it won't contain any actual application code. **This is the suggested classic host approach.** * Use the **[layered application startup template](layered-web-application/index.md)** if you will write some application code into the hosting application. You may want to write some code that makes multiple module operations that are not easy to implement in a particular module. In that case, a layered hosting application will be a better way to organize your codebase. However, this approach can quickly move your solution away from a modular system. So, take your own risk. #### When Should You Start a Modular Monolith Application? @@ -164,7 +179,7 @@ Even if you are considering building a microservice architecture, it is usually ### Microservice Solution Template -ABP's [microservice startup template](microservice/index.md) includes multiple services, API gateways and applications that are well integrated into each other and ready to be a great **base solution for your microservice system**. +ABP's [microservice startup template](microservice/index.md) includes multiple services, API gateways and applications that are well integrated into each other and ready to be a great **base solution for your microservice system**. The current reference pages describe the modern React-based web structure. In the following picture, you can see an overall diagram that shows the main components of the solution (they vary based on the options while you are creating your solution): ![ms-overall-architecture](microservice/images/overall-architecture.png) diff --git a/docs/en/solution-templates/index.md b/docs/en/solution-templates/index.md index c833b8d508..56969fc6ef 100644 --- a/docs/en/solution-templates/index.md +++ b/docs/en/solution-templates/index.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore ABP's production-ready startup solution templates for Single-Layer, Layered, and Microservice architectures to kickstart your project!" + "Description": "Explore ABP's startup solution templates, from classic Single-Layer and Layered solutions to modern Modular Monolith and Microservice architectures." } ``` @@ -11,11 +11,14 @@ ABP provides pre-architected and production-ready templates to jump start a new > **You can see the [Solution Template Selection Guide](guide.md) if you are not sure which solution template is suitable for you.** +The reference pages below cover both classic and modern ABP Studio template families. The Single-Layer and Layered pages document the classic templates. The Modular Monolith and Microservice pages call out the current modern structure where it differs. + The following solution templates are provided out of the box: -* **[Single-Layer Solution](single-layer-web-application/index.md)**: A single-project solution. Recommended for building an application with a **simpler and easy to understand** architecture. -* **[Layered Solution](layered-web-application/index.md)**: A fully layered (multiple projects) solution based on [Domain Driven Design](../framework/architecture/domain-driven-design) practices. Recommended for long-term projects that need a **maintainable and extensible** codebase. -* **[Microservice Solution](microservice/index.md)**: A **distributed solution** to build **microservice systems**. It includes pre-built services, API gateways, web and mobile applications, Kubernetes and Helm configuration, and everything you need to start your large-scale microservice solution. +* **[Single-Layer Solution](single-layer-web-application/index.md)**: The classic single-project solution. In the modern solution wizard, the closest architecture is **Simple Monolith**. +* **[Layered Solution](layered-web-application/index.md)**: The classic fully layered (multiple projects) solution based on [Domain Driven Design](../framework/architecture/domain-driven-design) practices. In the modern solution wizard, the closest architecture is **Layered Monolith**. +* **[Modular Monolith](modular-monolith/index.md)**: The dedicated modern ABP Studio path for a main application plus reusable modules deployed as a single unit. +* **[Microservice Solution](microservice/index.md)**: A **distributed solution** to build **microservice systems**. The current reference pages describe the modern React-based web structure. * **[Application Module](application-module/index.md)**: A template that can be used to create a **reusable [application module](../modules/index.md)** based on the [module development best practices & conventions](../framework/architecture/best-practices/index.md). It is also suitable for creating **services** (with or without UI). * **Others** - [MAUI Application](../get-started/maui.md) @@ -25,4 +28,4 @@ The following solution templates are provided out of the box: ## See Also * [Solution Template Selection Guide](guide.md) -* [Get Started with ABP Platform](../get-started/index.md) \ No newline at end of file +* [Get Started with ABP Platform](../get-started/index.md) diff --git a/docs/en/solution-templates/microservice/authentication.md b/docs/en/solution-templates/microservice/authentication.md index f079334a24..05721dee24 100644 --- a/docs/en/solution-templates/microservice/authentication.md +++ b/docs/en/solution-templates/microservice/authentication.md @@ -45,4 +45,8 @@ The solution has an authentication server (auth-server) application to provide t ## Authentication Flows -The applications use several flows to authenticate users based on the application type. The MVC UI web application uses the [hybrid flow](https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth) (OpenID Connect Authentication) to authenticate users, while the SPA and Swagger applications use the [authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to authenticate users. After the user logs into the system and receives the token from the authentication server, the applications (microservices) use [JWT Bearer Authentication](https://jwt.io/introduction/) to authorize users. \ No newline at end of file +The current modern microservice template generates React-based web clients and, optionally, a React Native mobile client. The generated authentication flows are: + +* `react`, `react-admin-console`, and `react-public-web` (when enabled) use the [authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) against the `AuthServer`. +* `react-native` (when enabled) is configured with its own client id and scopes, uses the password grant plus refresh tokens against the `AuthServer`, and sends bearer tokens to backend APIs through the `MobileGateway`. +* Backend services, gateways, and other protected APIs use [JWT Bearer Authentication](https://jwt.io/introduction/) to validate the tokens issued by the authentication server. diff --git a/docs/en/solution-templates/microservice/health-check-configuration.md b/docs/en/solution-templates/microservice/health-check-configuration.md index 37cd515209..c966b25824 100644 --- a/docs/en/solution-templates/microservice/health-check-configuration.md +++ b/docs/en/solution-templates/microservice/health-check-configuration.md @@ -12,7 +12,7 @@ Health Check is a feature that allows applications to monitor their health and diagnose potential issues. The Microservice solution template comes with pre-configured Health Check system. -In the Microservice solution template, Health Check configuration is applied in all the services, gateways and UI applications (except Blazor Wasm & Blazor WebApp applications UI applications). +In the current modern microservice template, Health Check configuration is applied to the .NET applications in the solution, such as backend services, gateways, and `auth-server`. The generated React frontend applications (`react`, `react-admin-console`, and `react-public-web` when enabled) do not expose these ASP.NET Core health check endpoints. ### Configuration in `HealthChecksBuilderExtensions.cs` diff --git a/docs/en/solution-templates/microservice/index.md b/docs/en/solution-templates/microservice/index.md index 9b4ed6b3a3..9ea6c3e0f1 100644 --- a/docs/en/solution-templates/microservice/index.md +++ b/docs/en/solution-templates/microservice/index.md @@ -21,6 +21,8 @@ ABP Studio provides pre-architected and production-ready templates to jump start a new solution. One of them is the Microservice solution template. You can use it to build distributed systems with common microservice patterns. It includes multiple services, API gateways and applications that are well integrated to each other and ready to be a great base solution for your microservice system. +Unless stated otherwise, the pages in this section describe the current modern ABP Studio microservice template, where the web layer is React-based. + > **This document explains the Microservice solution template in every details. So, it is a reference document to fully understand the solution and refer when you have trouble.** > > **If you just want to quickly create a microservice solution, please refer to *[Quick Start: Creating a Microservice Solution with ABP Studio](../../get-started/microservice.md)* document.** @@ -61,4 +63,4 @@ ABP Studio provides pre-architected and production-ready templates to jump start * [Adding new API gateways](adding-new-api-gateways.md) * [Mono-repo vs multiple repository approaches](mono-repo-vs-multiple-repository-approaches.md) * [Authoring unit and integration tests](authoring-unit-and-integration-tests.md) - * [How to use with ABP Suite](how-to-use-with-abp-suite.md) \ No newline at end of file + * [How to use with ABP Suite](how-to-use-with-abp-suite.md) diff --git a/docs/en/solution-templates/microservice/localization-system.md b/docs/en/solution-templates/microservice/localization-system.md index ebc710f840..bc83151c2c 100644 --- a/docs/en/solution-templates/microservice/localization-system.md +++ b/docs/en/solution-templates/microservice/localization-system.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Learn how the ABP Framework manages localization in microservice solutions, enhancing resource management across applications seamlessly." + "Description": "Learn how localization works in ABP Studio's modern microservice solution template across backend services, React apps, and the optional React Native client." } ``` @@ -25,7 +25,7 @@ Like the other fundamental feature modules ([Permission Management](permission-m ## Language Management -> If the dynamic localization option is enabled, then the *Language Management Module** will be removed from the optional modules and a new microservice named `LanguageService` will be created. The `LanguageService` uses *Language Management Module* behind the scene. +> If the dynamic localization option is enabled, then the *Language Management Module* is removed from the optional modules and a new microservice named `LanguageService` is created. `LanguageService` uses the *Language Management Module* behind the scenes. The *Administration* microservice provides a set of APIs to manage localization. The localization resources are defined in each microservice, and when a microservice starts, it registers its localization resources to the related localization tables automatically. After that, you can see the localization resources from the [language texts](../../modules/language-management.md#language-texts) and manage them. @@ -39,106 +39,42 @@ When you create a new microservice solution, you can **enable dynamic localizati ![](./images/enable-dynamic-localization.png) -When you enable this option, a new microservice named **LanguageService** will be added (with the language management module integrated) and you can use its `LanguageServiceResource` class to use the localization entries in your UI application. It's already configured in your final host application, so you don't need to make any configuration related to that. To define a new localization entry you can either use the language files in the `LanguageService` or update the already defined localization entries in the UI (on the *Language Texts* page). - -After defining localization entries or updating them, you can inject the `IStringLocalizer<>` or `IHtmlLocalizer<>` services and use the localized values in your pages for MVC/Razor Pages UI, for instance: - -```html -@page -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L - -
-

@L["LongWelcomeMessage"]

-
-``` +When you enable this option, a new microservice named **LanguageService** is added with the language management module integrated. Define new shared localization entries in the `LanguageService` localization files, or update the registered texts from the *Language Texts* UI. The generated React applications can then consume those backend resources through the application configuration and `application-localization` endpoints. ## UI Localizations -In the microservice architecture, localizations also can be defined in the final host application, if they only need to be defined in the UI. When you create a new microservice solution template, independent from the UI, all configurations are made and you can directly define localization entries and use them in your final UI application. - -> **Note:** When you define localization entries in the host application, then you can't make dynamic localization with the language management module! (Because the language management module, would be unaware of the defined localization entries on the UI side) - -### MVC/Razor Pages & Blazor UIs +In the current modern microservice template, UI-only localizations live in the generated React or React Native applications. The template does not generate MVC, Blazor, or Angular hosts for the modern microservice solution structure. -For MVC & Blazor UIs, you can see the **Localization** directory in your final application, like in the following figure: +> **Note:** UI-only strings that you keep in frontend locale files are not managed by the Language Management module. Put shared or domain-level texts in backend localization resources if they should participate in dynamic localization. -![](./images/ui-localization-mvc.png) +### `react` -In this directory, you can see the language files, those are pre-defined for you to directly add localization entries. The related configurations are already made for you, in the module class (inside the `ConfigureLocalization` method) as below: +The main `react` application initializes `i18next` from `apps/react/src/locales/en.json`. It also: -```csharp - private void ConfigureLocalization(IWebHostEnvironment hostingEnvironment) - { - //code abbreviated for brevity... +* persists the selected culture in browser storage +* sets the document language +* loads available languages from the application configuration +* fetches additional cultures from `/api/abp/application-localization` and merges them into `i18next` - Configure(options => - { - options.Resources - .Add("en") - .AddVirtualJson("/Localization/CloudCrmWeb"); +Use the locale files under `apps/react/src/locales` for UI-only strings that belong only to this application. - options.DefaultResourceType = typeof(CloudCrmWebResource); - }); - } -``` +### `react-admin-console` -You can define new localization entries in the language files under the **Localization** folder and directly use them in your application by using the `IStringLocalizer<>` or `IHtmlLocalizer<>` services: +The `react-admin-console` application keeps its bundled translations under `apps/react-admin-console/src/locales/*.json` and initializes `i18next` from those files. -```html -@page -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L +Its supported language list is controlled by `AbpAdminConsoleOptions` using the `AdminConsole:LocalizationLanguages` configuration key, which is exposed to the SPA through `/admin-console/api/config`. If no languages are configured, the frontend falls back to `en`. -
-

@L["LongWelcomeMessage"]

-
-``` +Use the admin console locale files for UI-only texts that are specific to the administration surface. Keep shared module texts in backend localization resources. -### Angular UI - -Angular UI gets the localization resources from the [`application-localization`](../../framework/api-development/standard-apis/localization.md) API's response and merges these resources in the `ConfigStateService` for the localization entries/resources coming from the backend side. - -In addition, you may need to define some localization entries and only use them on the UI side. ABP already provides the related configuration for you, so you don't need to make any configurations related to that and instead you can directly define localization entries in the `app.config.ts` file of your angular application as follows: - -```ts -import { provideAbpCore, withOptions } from '@abp/ng.core'; - -export const appConfig: ApplicationConfig = { - providers: [ - // ... - provideAbpCore( - withOptions({ - environment, - registerLocaleFn: registerLocale(), - localizations: [ - { - culture: 'en', - resources: [ - { - resourceName: 'MyProjectName', - texts: { - "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information visit" - } - } - ] - } - ] - }), - ), - ], -}; -``` +### `react-native` -After defining the localization entries, it can be used as below: +When mobile is enabled, the generated React Native client stores its bundled translations under `apps/mobile/react-native/src/locales`. The `LocalizationService.ts` file: -{%{ -```html -
{{ 'MyProjectName::LongWelcomePage' | abpLocalization }}
-``` -}%} +* registers the available locale files +* exposes the language list used by the app +* maps the default resource name from `Environment.ts` -> For more information, please refer to [UI Localization section of the Angular Localization document](../../framework/ui/angular/localization.md). +Use these locale files for mobile-specific UI strings. ## Creating a New Localization Resource diff --git a/docs/en/solution-templates/microservice/mobile-applications.md b/docs/en/solution-templates/microservice/mobile-applications.md index c14895b7b9..224ff906e0 100644 --- a/docs/en/solution-templates/microservice/mobile-applications.md +++ b/docs/en/solution-templates/microservice/mobile-applications.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore how to integrate mobile applications with ABP's microservice solution, featuring options like MAUI and React Native for seamless development." + "Description": "Understand the optional React Native mobile application in ABP Studio's modern microservice solution template." } ``` @@ -19,162 +19,62 @@ > You must have an ABP Business or a higher license to be able to create a microservice solution. -The ABP Studio microservice solution template comes with an optional mobile application that is completely integrated to the solution. There are two options for the mobile application: +The current ABP Studio modern microservice solution template can optionally generate a mobile application under `apps/mobile/react-native`. -* MAUI -* React Native +The modern template does not generate a MAUI mobile application. Its mobile choices are **None** and **React Native**. -You can select the mobile application type while [creating your solution](../../get-started/microservice.md). +## The Mobile Gateway -## Fundamental Structures +If you enable the mobile application, an API gateway named `MobileGateway` is added under `gateways/mobile`. The React Native client calls backend APIs through this gateway. -The following sections explain the common structure of the mobile applications (valid for both of MAUI and React Native applications). +See *[API Gateways](api-gateways.md)* for the shared gateway structure. -### The Mobile Gateway +## Authentication -If you've selected to include the mobile application into your solution, an API Gateway, named `MobileGateway` is also added to the solution. It is located under the `gateways/mobile` in the solution folder. +The generated React Native app is configured in `Environment.ts` with: -You can refer to the *[API Gateways](api-gateways.md)* document to understand the structure of the mobile gateway. +* the `AuthServer` issuer URL +* the `MobileGateway` base URL +* the `ReactNative` client id and scopes -### Authentication +At runtime, the mobile client uses the password grant to exchange credentials for access and refresh tokens at the `AuthServer` `/connect/token` endpoint, then sends bearer tokens to backend APIs through the `MobileGateway`. Account-related operations such as registration, password reset, profile picture management, and logout use the generated API client under `src/api`. -Both of the MAUI and React Native applications are installed as native applications to the devices. So, they are using the [OpenID Connect](../../modules/openiddict.md) protocol to authenticate the users. The authentication is done by the `AuthServer` application. +## Built-in Capabilities -They don't run on a browser, so they can't use the [Cookie Authentication](../../modules/account.md#cookie-authentication) method. They are using the [JWT Bearer Authentication](../../modules/account.md#jwt-bearer-authentication) method. +The generated mobile app already includes screens and flows for: -Best way to communicate with the `AuthServer` application is using the browser. So, the mobile applications are opening a browser window to authenticate the user. Then browser redirects back to the mobile application with the authentication result. +* sign in, registration, forgot password, and reset password +* account and profile picture management +* user-facing settings such as language, theme, and logout -The following screenshot was taken from the *Login* page of the [Account](../../modules/account.md) module in the mobile application's UI: +Localization is handled by the bundled locale files under `src/locales` and `src/services/LocalizationService.ts`. Theme handling lives under `src/theme`. -![mobile-application-login-page](images/authserver-login-page-maui.png) +## Solution Structure +The React Native application is based on [React Native](https://reactnative.dev/) and [Expo](https://expo.dev/). The main files and folders in `apps/mobile/react-native` are: -### User Management +* `Environment.ts`: runtime URLs, client id, scopes, and default localization resource name. +* `src/api`: HTTP clients for token, account, and application configuration calls. +* `src/components`: reusable UI components. +* `src/contexts`: shared React contexts, including localization context. +* `src/hooks`: reusable React hooks. +* `src/interceptors`: request and response interception for the API client. +* `src/locales`: bundled UI translations. +* `src/navigators`: drawer, stack, and tab navigation definitions. +* `src/screens`: application pages such as login, home, settings, and account flows. +* `src/services`: cross-cutting services such as localization helpers. +* `src/store`: Redux actions, listeners, reducers, and selectors. +* `src/theme`, `src/types`, `src/utils`: shared theming, typings, and helper utilities. -User Management is implemented in the MAUI Application in the `Acme.CloudCrm.Maui` project with XAML and C# for MAUI and it is implemented in the React Native Application in the react-native project for React Native UI option. +## Running the Application -ABP MAUI IdentityUsersPage +The React Native app is not started by the ABP Studio solution runner. Run `AuthServer`, `MobileGateway`, and the required backend services first, then start the mobile app with the standard React Native / Expo toolchain. - -### Profile Management - -Profile management allows users to view and update their personal profile picture and their passwords. It provides a seamless experience for users to manage their profiles within the mobile application without navigating to the authserver web application. - -The following screenshot was taken from the *Profile* page in the MAUI application: - -ABP MAUI ProfilePage - -### Other Features - -#### Settings Page -The settings page allows users to change the language and theme of the application, manage their profiles, change their passwords, and also to logout from the application. - -ABP MAUI SettingsPage - -- **Language**: Applications implements ABP localization logic on the platforms. The language is automatically selected based on the device's language. Users can also change the language manually from the settings page. - -- **Dark/Light Theme**: ABP MAUI and React Native applications support both dark and light themes. The theme is automatically selected based on the device's theme. Users can also change the theme manually from the settings page. - -## Applications - -Following sections explain the structure of MAUI and React Native Applications. - -### The MAUI Application - -This is the mobile application that is built based on Microsoft's [MAUI framework](https://learn.microsoft.com/en-us/dotnet/maui). It will be in the solution only if you've selected the MAUI as your mobile application option. - -#### Project Structure -Entire MAUI application is built on the AppShell pattern of MAUI. You can find the AppShell class in the `Acme.CloudCrm.Maui` project. It is the entry point of the application. It is responsible for initializing the application and registering the services. You find all the pages and routing information in the `AppShell.xaml` file. - -- **Pages**: Pages are located in the `Pages` folder of the project. Each page has a XAML & C# file. XAML file is responsible for the UI and C# file is responsible for the initialization of the page. - -- **ViewModels**: ViewModels are located in the `ViewModels` folder of the project. Each ViewModel has a C# file. ViewModels are responsible for the business logic of the pages. - -- **Oidc**: Oidc folder contains the logic for the authentication of the application. It contains the `MauiAuthenticationBrowser` class which manages the authentication process of the application. - -- **Localization**: Localization folder contains the localization logic of the application. It contains regular ABP Localization logic and the `LocalizationResourceManager` class which is wrapper for the ABP localization logic on MAUI. - -- **Messages**: Messages folder contains the message data for the communication inside application. Messages are used to send data between pages and viewmodels. It's designed on the [MVVM Toolkit Messenger](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger) feature. - -- **Storage**: Storage folder contains the storage logic of the application. It contains the `IStorage` class which is wrapper for the [SecureStorage](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/storage/secure-storage) feature. It is used to store the authentication data of the user and preferences of the application. - -_Rest of the folders are MAUI default folders. You can check the [.NET MAUI single project documentatipon](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/single-project?view=net-maui-8.0) for more information._ - -#### Running the application -Before running the MAUI Application, rest of the applications in the solution must be running. Such as AuthServer, MobileGateway and the microservices. - -Make sure that you prepared devices for debugging. You can check the following documentation for each platform. - -- [Android](https://learn.microsoft.com/en-us/dotnet/maui/android/emulator/) -- [iOS](https://learn.microsoft.com/en-us/dotnet/maui/ios/pair-to-mac) -- [MacCatalyst](https://learn.microsoft.com/en-us/dotnet/maui/mac-catalyst/cli) -- [Windows](https://learn.microsoft.com/en-us/dotnet/maui/windows/setup) - -##### Network - -All the platforms including iOS, MacCataylst and Windows, runs the applications in the same network of the host. So, you can use the `localhost` address to connect to the applications. - -But in the **Android Emulator**, you need to use the `adb reverse` command to connect to the applications. You can use the following command to connect to the AuthServer application: +For Android emulators or devices, map the development ports before testing: ```bash -adb reverse tcp:44300 tcp:44300 +adb reverse tcp: tcp: +adb reverse tcp: tcp: ``` -> `44300` is an example port. You need to change it based on the port of the AuthServer & MobileGateway application. - -> You need to run the command for a running emulator. If you run the emulator after running the command, you need to run the command again. - - -##### Target Framework - -Since MAUI Applications have multiple target frameworks, you need to select the target framework before running the application. You can select the target framework from the context menu of the Solution Runner. - -![ABP Studio MAUI Target Framework](images/solutionrunner-maui-targetframework.png) - - -##### Running with ABP Studio -You can start the MAUI application with the solution runner. You can click the start button of the MAUI application in the solution runner tree. It will start the application on the selected target framework. Since they're not running on a process and they're running on a device, you can't see them as running state in the solution runner. After the application is deployed, it'll be opened on the device and it'll be shown as stopped in the solution runner. - ---- - -#### Development on MAUI Application - -You can follow [Mobile Application Development Tutorial - MAUI](../../tutorials/mobile/maui) to learn how to develop on MAUI Application. - -### The React Native Application - -This is the mobile application that is built based on Facebook's [React Native framework](https://reactnative.dev/) and [Expo](https://expo.dev/). It will be in the solution only if you've selected React Native as your mobile application option. - -#### Project Structure -- **Environment.ts**: file using for providing application level variables like `apiUrl`, `oAuthConfig` and etc. - -- **api**: The `api` folder contains HTTP request files that simplify API management in the React Native starter template - - `API.ts:` exports **axiosInstance**. It provides axios instance filled api url. - -- **components**: In the `components` folder, you can reach built in react native components that you can use in your app. These components **facilitates** your list, select and etc. operations. - -- **contexts**: `contexts` folder contains [react context](https://react.dev/reference/react/createContext). You can expots your contexts in this folder. `Localization context provided in here` - -- **hocs**: this folder is added to contain higher order components. The purpose is to wrap components with additional features or properties. It initially has a `PermissionHoc.tsx` that wraps a component to check the permission grant status. - -- **hooks**: covers the react native hooks where you can get a reference from [the official documentation](https://react.dev/reference/react/hooks). - -- **interceptors**: initializes a file called `APIInterceptor.ts` that has a function to manage the http operations in a better way. - -- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After creating a new *FeatureName*Navigator we need to provide in `DrawerNavigator.ts` file as `Drawer.Screen` - -- **screens**: folder has the content of navigated page. We will pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) - -- **store**: folder manages state-management operations. We will define `actions`, `listeners`, `reducers`, and `selectors` here. - -- **styles**: folder contains app styles. `system-style.ts` comes built in template we can also add new styles. - -- **utils**: folder contains helper functions that we can use in application - -#### Running the Application - -React Native applications can't be run with the solution runner. You need to run them with the React Native CLI. You can check the [React Native documentation](https://reactnative.dev/docs/environment-setup) to learn how to setup the environment for React Native development. - -Before running the React Native application, the rest of the applications in the solution must be running. Such as AuthServer, MobileGateway and the microservices. - -Then you can run the React Native application by following this documentation: [Getting Started with the React Native](../../framework/ui/react-native/index.md). \ No newline at end of file +Use the ports assigned to `MobileGateway` and `AuthServer` in your generated solution. diff --git a/docs/en/solution-templates/microservice/overview.md b/docs/en/solution-templates/microservice/overview.md index fc322565c3..5d1438f22c 100644 --- a/docs/en/solution-templates/microservice/overview.md +++ b/docs/en/solution-templates/microservice/overview.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore the Microservice solution template for ABP Framework, featuring pre-installed libraries and services for seamless development and production." + "Description": "Explore the modern ABP microservice solution template, featuring React-based web apps, API gateways, and pre-installed libraries and services for development and production." } ``` @@ -21,6 +21,8 @@ In this document, you will learn what the Microservice solution template offers to you. +This page describes the current modern microservice template. In this template family, the web layer supports `React` or `No UI`. + ## The Big Picture ![ms-overall-architecture](images/overall-architecture.png) @@ -45,9 +47,8 @@ All the following **libraries and services** are **pre-installed** and **configu The following features are built and pre-configured for you in the solution. * **Authentication** is fully configured based on best practices; - * **JWT Bearer Authentication** for microservices and applications. - * **OpenId Connect Authentication**, if you have selected the MVC UI. - * **Authorization code flow** is implemented, if you have selected a SPA UI (Angular or Blazor WASM). + * **JWT Bearer Authentication** for microservices and gateways. + * **OpenId Connect / authorization code flow** for the React web applications (`react`, `react-admin-console`, and `react-public-web` when enabled). * Other flows (resource owner password, client credentials...) are easy to use when you need them. * **[Permission](../../framework/fundamentals/authorization/index.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. * **[Background job system](../../framework/infrastructure/background-jobs/index.md)** with [RabbitMQ integrated](../../framework/infrastructure/background-jobs/rabbitmq.md). @@ -98,21 +99,22 @@ There are two database provider options are provided on a new microservice solut ### UI Frameworks -The solution comes with a main web application with the following UI Framework options: +The current modern microservice template supports the following web options: + +* **None**: Doesn't include the React web applications. +* **React**: Creates the web application set for the solution. + +When you select **React**, ABP Studio creates: -* **None** (doesn't include a web application to the solution) -* **Angular** -* **MVC / Razor Pages UI** -* **Blazor WebAssembly** -* **Blazor Server** -* **MAUI with Blazor (Hybrid)** +* `apps/react` as the main SPA behind the `web` gateway. +* `apps/react-admin-console` as the dedicated administration SPA. +* `apps/react-public-web` as an additional public site when the *Public Website* option is enabled. ### The Mobile Application -If you prefer, the solution includes a mobile application with its dedicated API Gateway. The mobile application is fully integrated to the system, implements authentication (login) and other ABP features, and includes a few screens that you can use and take as example. The following options are available: +If you prefer, the solution includes a mobile application with its dedicated API Gateway. The mobile application is fully integrated to the system, implements authentication (login) and other ABP features, and includes a few screens that you can use and take as example. In the current modern template, the available options are: * **None** (doesn't include a mobile application to the solution) -* **MAUI** * **React Native** ### Multi-Tenancy & SaaS Module diff --git a/docs/en/solution-templates/microservice/solution-structure.md b/docs/en/solution-templates/microservice/solution-structure.md index 90fd211136..efca1a2f44 100644 --- a/docs/en/solution-templates/microservice/solution-structure.md +++ b/docs/en/solution-templates/microservice/solution-structure.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore the folder structure of ABP Studio's microservice solution template, essential for effective project organization and development." + "Description": "Explore the folder structure of ABP Studio's modern microservice solution template, including its React apps, gateways, and services." } ``` @@ -23,15 +23,17 @@ This document explains the solution and folder structure of ABP Studio's [micros > This document assumes that you've created a new microservice solution by following the *[Quick Start: Creating a Microservice Solution with ABP Studio](../../get-started/microservice.md)* guide. +The current modern template uses React-based web applications. Older MVC, Angular, Blazor, and MAUI web app layouts are not part of this structure. + ## Understanding the ABP Solution Structure When you create a new microservice solution, you will see a tree structure similar to the one below in the *Solution Explorer* panel: ![microservice-solution-in-explorer](images/microservice-solution-in-explorer.png) -Each leaf item (e.g. `Acme.CloudCrm.IdentityService`, `Acme.CloudCrm.Web`, `Acme.CloudCrm.WebGateway`...) in the tree above is an **ABP Studio module**. An ABP Studio module can be a web application, an API gateway, a microservice, a console application or whatever .NET allows you to build. They are grouped into **folders** (`apps`, `gateways` and `services`) in that solution. +Each leaf item in the tree above is an **ABP Studio module**. They are grouped into **folders** (`apps`, `gateways` and `services`) in that solution. -**Each ABP Studio module has a separate .NET solution**; this allows your team to develop them individually, in keeping with the nature of the microservices architecture. +The .NET-based modules, such as `auth-server`, gateways, and backend services, keep their own .NET solution structure. Frontend applications such as `react`, `react-admin-console`, and `react-public-web` live in their own frontend folders. > Refer to the *[Concepts](../../studio/concepts.md)* document for a full definition of ABP Studio solution, module and package terms. @@ -48,13 +50,21 @@ The root folder of the solution will be similar to the following: The folder structure basically matches to the solution in ABP Studio's *Solution Explorer*: * `.abpstudio` folder contains your personal preferences for this solution and it is not added to your source control system (Git ignored). It is created and used by ABP Studio. -* `app` folder contains the applications that has a UI and typically used by the end users of your system. +* `apps` folder contains the applications of the solution: + * `auth-server` is the authentication server based on OpenIddict. + * `react` is the main React SPA when the web UI is enabled. + * `react-admin-console` is the dedicated administration SPA when the web UI is enabled. + * `react-public-web` is the optional public-facing site. + * `mobile/react-native` is the optional mobile application. * `etc` folder contains some additional files for the solution. It has the following sub-folders: * `abp-studio` folder contains settings that are managed by ABP Studio. This folder is added to your source control system and shared between developers. * `docker` folder contains docker-compose configuration to easily run infrastructure dependencies (e.g. RabbitMQ, Redis) of the solution on your local computer. * `helm` folder contains all the Helm charts and related scripts to deploy the solution to Kubernetes. - * `k8s` folder contains some additional files to setup *Kubernetes Dashboard* on your local machine. -* `gateways` folder contains one or more API Gateways (the count depends on if you've selected mobile application or other applications if available). This solution implements the [BFF](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends) (Backend for frontend pattern), that means it has a dedicated API Gateway for each different UI application. + * `scripts` folder contains helper scripts for initializing and running the solution. +* `gateways` folder contains one or more API Gateways. This solution implements the [BFF](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends) pattern, so it has a dedicated API Gateway for each different client type: + * `web` is the gateway for `react`. + * `public` is the gateway for `react-public-web`, when the public website is enabled. + * `mobile` is the gateway for the React Native application, when mobile is enabled. * `services` folder contains the microservices. The microservice count varies based on the options you've selected during the solution creation. However, the following microservices are always included: - * `administration` microservice is used to manage permissions, languages and other fundamental settings of the system. - * `identity` microservice is used to manage users, roles and their permissions. It basically serves to the [Identity](../../modules/identity.md) module's UI (and [OpenIddict](../../modules/openiddict.md) module's UI, if selected). \ No newline at end of file + * `administration` microservice manages permissions, settings, features, and other operational capabilities used by the solution. + * `identity` microservice manages users, roles, and related identity/OpenIddict endpoints used by the web applications. diff --git a/docs/en/solution-templates/microservice/web-applications.md b/docs/en/solution-templates/microservice/web-applications.md index 6c50a7801a..4e9e7eea0c 100644 --- a/docs/en/solution-templates/microservice/web-applications.md +++ b/docs/en/solution-templates/microservice/web-applications.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore the ABP Framework's microservice solution template, featuring integrated web applications and API gateways for seamless development." + "Description": "Explore the web applications in ABP Studio's modern microservice solution template, including react, react-admin-console, react-public-web, and AuthServer." } ``` @@ -19,15 +19,9 @@ > You must have an ABP Business or a higher license to be able to create a microservice solution. -The ABP Studio microservice solution template contains a few web applications. These applications are fully integrated to the solution, uses the [microservices](microservices.md) through the [API gateways](api-gateways.md). +The current ABP Studio microservice solution template uses React-based web applications. These applications are fully integrated to the solution and use the [microservices](microservices.md) through the [API gateways](api-gateways.md). -The following figure shows the application in the *[Solution Explorer](../../studio/solution-explorer.md)* pane of ABP Studio: - -![applications-in-microservice-solution](images/applications-in-microservice-solution.png) - - - -Count and types of the web applications depends on the options you've selected while [creating your solution](../../get-started/microservice.md). This document introduces and explains all the pre-built web applications included in the microservice solution template. +Count and type of the web applications depend on the options you've selected while [creating your solution](../../get-started/microservice.md). This document introduces the pre-built web applications included in the current modern microservice template. ## AuthServer @@ -35,7 +29,7 @@ Count and types of the web applications depends on the options you've selected w The `AuthServer` application is also used by microservices as Authority for JWT Bearer Authentication. -> You normally do not directly browse this application. It is used by the other applications to authenticate the users and applications.. +> You normally do not directly browse this application. It is used by the other applications to authenticate users and applications. The following screenshot was taken from the *Login* page of the [Account](../../modules/account.md) module in the application's UI: @@ -43,40 +37,42 @@ The following screenshot was taken from the *Login* page of the [Account](../../ That application is mainly based on the [OpenIddict](../../modules/openiddict.md), the [Identity](../../modules/identity.md), and the [Account](../../modules/account.md) modules. So, it basically has login, register, forgot password, two factor authentication and other authentication related pages. -## The Main Web Application (optional) - -This is the main web application of the solution. It uses the `Acme.CloudCrm.AuthServer` application as the [API gateway](api-gateways.md). It also uses the Authentication Server application to make users login. +## `react` -The following screenshot was taken from the *Role Management* page of the [Identity](../../modules/identity.md) module in the web application's UI: +`apps/react` is the main authenticated React SPA of the solution. It talks to backend services through the `web` [API gateway](api-gateways.md) and uses `AuthServer` for user login. -![web-application-ui](images/web-application-ui.png) +This application is the main user-facing web UI for the solution. In the generated route configuration, it can also deep-link users to the separate `react-admin-console` application for operational and administration tasks. -The following options are provided while [creating the solution](../../get-started/microservice.md): +If you choose `No UI` while creating the solution, ABP Studio doesn't generate `apps/react`. -* MVC / Razor Pages UI -* Angular -* Blazor WebAssembly -* Blazor Server -* MAUI Blazor (Hybrid) +## `react-admin-console` -The following sections explain each of these UI types. +`apps/react-admin-console` is the dedicated administration SPA. It uses the `/admin-console/` base path and focuses on back-office and operational capabilities such as: -### MVC / Razor Pages Web Application +* account management +* users, roles, claim types, and organization units +* tenants and editions +* OpenIddict applications and scopes +* settings, audit logs, and optional module UIs -`Acme.CloudCrm.Web` module is created if you've selected the MVC / Razor Pages UI while creating the solution. It has own .NET solution that is located under the `apps/web` folder of the solution root. +The route list is filtered by backend permissions and by which backend modules are actually available. -### Angular Web Application +## `Volo.Abp.AdminConsole` Backend Role -If you've selected the Angular UI while creating your solution, a folder named `angular` is included in the `apps` folder of the solution. That folder contains the main web application of the solution that is implemented using Angular. +`Volo.Abp.AdminConsole` is the backend-side companion for the admin console when you host it from an ASP.NET Core application. It is responsible for: -### Blazor WebAssembly Web Application +* serving the admin console under `/admin-console` +* exposing runtime configuration from `/admin-console/api/config` +* exposing module discovery from `/admin-console/api/modules` +* applying branding, theme, localization, and customization settings for the admin console +* optionally redirecting `/` to `/admin-console` -If you've selected the Blazor WebAssembly UI while creating your solution, `Acme.CloudCrm.Blazor` project is included in the `apps` folder of the solution. That folder contains the main web application of the solution that is implemented using Blazor WebAssembly. +The standalone `react-admin-console` app follows the same `/admin-console` route and OIDC conventions, so the frontend and backend pieces stay aligned. -### Blazor Server Web Application +## `react-public-web` -If you've selected the Blazor Server UI while creating your solution, `Acme.CloudCrm.Blazor` project is included in the `apps` folder of the solution. That folder contains the main web application of the solution that is implemented using Blazor Server. +When you enable the *Public Website* option, ABP Studio creates `apps/react-public-web`. This is a separate public-facing React application behind the `public` gateway. -### MAUI Blazor (Hybrid) Web Application +Use this application for anonymous or customer-facing traffic. Keep authenticated operational flows in `react` and `react-admin-console`. -If you've selected the MAUI Blazor (Hybrid) UI while creating your solution, `Acme.CloudCrm.MauiBlazor` project is included in the `apps` folder of the solution. That folder contains the main desktop application of the solution that is implemented using MAUI Blazor (Hybrid) that uses existing Blazor UI Implementation. +`react-public-web` can still participate in authentication flows when needed, but its role is different from the back-office applications: it is the public entry point, not the administration surface. diff --git a/docs/en/solution-templates/modular-monolith/index.md b/docs/en/solution-templates/modular-monolith/index.md new file mode 100644 index 0000000000..a49a268bc7 --- /dev/null +++ b/docs/en/solution-templates/modular-monolith/index.md @@ -0,0 +1,62 @@ +```json +//[doc-seo] +{ + "Description": "Learn how ABP Studio's modern Modular Monolith solution template organizes a main application and reusable modules in a single deployable solution." +} +``` + +# ABP Studio: Modular Monolith Solution Template + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Layered Solution", + "Path": "solution-templates/layered-web-application/index.md" + }, + "Next": { + "Name": "Microservice Solution", + "Path": "solution-templates/microservice/index.md" + } +} +```` + +ABP Studio's modern solution wizard includes a dedicated **Modular Monolith** architecture. Under the hood, it uses the modern no-layers application template for the main application, then enables modularity automatically. + +> **This page documents the modern modular monolith path. If you are working with the classic template family, see the [Solution Template Selection Guide](../guide.md) for the host + module composition approach.** + +## What ABP Studio Creates + +When you choose `Modular Monolith` in the modern solution wizard, ABP Studio: + +* creates the main application by using the modern no-layers host template. +* locks the solution into modular mode. +* creates a `main` folder in the solution model and moves the main application into it. +* creates a `modules` folder for reusable module solutions. +* lets you add additional modules during solution creation and optionally install them into the main application immediately. + +## Typical Layout + +In ABP Studio's *Solution Explorer*, the solution is organized around these areas: + +* `main`: the main application and its host-side assets. +* `modules`: one module solution per business capability. +* `etc`: shared infrastructure, run profiles, and deployment files. + +Additional modules are generated under the solution's `modules/` directory, while the main application remains the single deployable host. + +## How Modules Fit In + +The module solutions created for a modular monolith use the same reusable module concepts documented in the [Application Module Template](../application-module/index.md) page. + +Use this template when you want: + +* clear module boundaries without a distributed deployment model. +* separate module solutions for teams or business domains. +* a monolith today, with a cleaner path toward microservices later. + +## See Also + +* [Solution Template Selection Guide](../guide.md) +* [Application Module Template](../application-module/index.md) +* [Modular Monolith Application Development Tutorial](../../tutorials/modular-crm/index.md) diff --git a/docs/en/studio/ai-agent-built-in-capabilities.md b/docs/en/studio/ai-agent-built-in-capabilities.md new file mode 100644 index 0000000000..6ea69d9d8e --- /dev/null +++ b/docs/en/studio/ai-agent-built-in-capabilities.md @@ -0,0 +1,139 @@ +```json +//[doc-seo] +{ + "Description": "Capability matrix for ABP Studio AI Agent modes, built-in tools, Studio automation tools, connected MCP tools, and subagents." +} +``` + +# ABP Studio: AI Agent Built-in Capabilities + +````json +//[doc-nav] +{ + "Next": { + "Name": "AI Agent Git Integration", + "Path": "studio/ai-agent-git-integration" + } +} +```` + +ABP Studio exposes tools to the AI Agent based on the selected mode, run context, tool settings, permissions, and connected MCP server tools. The tool list is locked per running Agent-mode session to keep prompt caching and execution stable while other sessions run in parallel. + +## Mode Capability Matrix + +| Capability | Agent | Plan | Ask | +| --- | --- | --- | --- | +| Find files | Yes | Yes | Yes | +| Read files | Yes | Yes | Yes | +| Search code | Yes | Yes | Yes | +| ABP-aware outlines | Yes | Yes | Yes | +| Semantic code search | Yes | Yes | Yes | +| Ask user | Yes | Yes | Yes | +| Web search | Yes | Yes | Yes | +| Fetch URL | Yes | Yes | Yes | +| Fetch AI skills | Yes, when available | Yes, when available | Yes, when available | +| Write/edit/delete/rename files | Yes | No | No | +| Download files | Yes | No | No | +| Save learned lessons | Yes | No | No | +| Run shell command | Yes | No | No | +| Add migration | Yes | No | No | +| Create/read/update plan files | No | Yes | No | +| Update active plan steps | Yes | No | No | +| Search/read ABP docs directly | No | No | Yes | +| Studio automation tools | Yes | No | No | +| Connected MCP tools | Yes | No | No | +| Spawn subagents | Yes | Yes | Yes | + +ABP documentation tools are available to Ask mode and ABP documentation subagents. The main Agent-mode tool list does not include direct ABP docs search/read tools; Agent mode can use the ABP documentation subagent when documentation research is needed. + +## File Tools + +The file tools are scope-aware and `.abpignore`-aware. + +| Tool | Description | +| --- | --- | +| `find_files` | Finds files by filename or glob under accessible directories. Build output and dependency folders such as `bin`, `obj`, and `node_modules` are excluded. | +| `read_files` | Reads up to 10 files per call. Supports line ranges. Large files are automatically truncated. | +| `write_file` | Writes file content in Agent mode. | +| `edit_file_batch` | Applies structured edits in Agent mode. | +| `delete_path` | Deletes a file or directory in Agent mode. | +| `rename_path` | Renames or moves a file or directory in Agent mode. | + +Write operations use file change tracking. When another session modifies a file after it was read, the agent is instructed to re-read before overwriting. + +## Code Intelligence + +| Tool | Description | +| --- | --- | +| `search_code` | Searches exact or regex text across source and configuration files. | +| `get_outlines` | Returns ABP-aware outlines for code structures such as entities, DTOs, services, repositories, DbContexts, controllers, modules, and methods. | +| `semantic_code_search` | Searches indexed code by meaning rather than exact text. | + +These tools are available in all modes and are the primary way the agent grounds answers in the current solution. + +## Web And External Content + +| Tool | Description | +| --- | --- | +| `web_search` | Searches the web for current or external information. | +| `fetch_url` | Fetches a URL after domain permission is granted. Large content is cached under `.abpstudio/url-cache`. | +| `download_file` | Downloads a file in Agent mode after explicit permission. Downloads are limited to 10 MB. | + +URL fetch and download permissions are independent. Downloads always require explicit confirmation. + +## User Interaction + +The `ask_user` tool lets the agent request missing information during a run. Pending questions are tracked per session, so parallel sessions can wait for independent user input without sharing state. + +## Shell And Migration Tools + +Agent mode can run shell commands through `run_shell_command`. Shell execution is permission-gated. + +The `add_migration` tool adds Entity Framework Core migrations for configured migration projects. + +## Studio Automation Tools + +Agent mode can use enabled ABP Studio automation tools. + +| Tool | Description | +| --- | --- | +| `dotnet_build` | Builds the solution or targeted project context. | +| `build_solution` | Builds the whole solution when exposed by the run context. | +| `build_module` | Builds a selected module. | +| `build_package` | Builds a selected package. | +| `get_exceptions` | Reads monitored application exceptions. | +| `get_logs` | Reads monitored application logs. | +| `get_requests` | Reads monitored HTTP request data. | +| `get_events` | Reads monitored distributed event data. | +| `start_applications` | Starts applications from the active run profile. | +| `stop_applications` | Stops applications from the active run profile. | +| `start_containers` | Starts containers from the active run profile. | +| `stop_containers` | Stops containers from the active run profile. | +| `run_task` | Runs configured Studio tasks. | +| `install_libs` | Installs client-side library files. | +| `generate_csharp_proxies` | Generates C# client proxies. | +| `generate_angular_proxies` | Generates Angular client proxies. | + +## Subagents + +The `spawn_subagent` tool delegates bounded research work to read-only specialist subagents. + +| Subagent type | Purpose | +| --- | --- | +| `research` | Performs solution/code research using read-only tools. | +| `websearch` | Performs web-focused research. | +| `abpdocsearcher` | Searches and reads ABP documentation for framework-specific questions. | + +Subagents are read-only. They are intended for parallel research and return summarized findings to the main agent. + +## MCP Tools + +Configured MCP server tools can be added to Agent mode when: + +- The MCP server is configured and enabled. +- The server connection is successful. +- The individual MCP tool is not disabled in settings. + +MCP resources are visible in settings and can be opened for inspection, but the Agent-mode callable surface is composed from MCP tools. Plan and Ask modes do not receive MCP tools. + +This capability lets the AI Agent consume tools from external MCP servers. It does not make ABP Studio AI Agent an MCP server for external clients. diff --git a/docs/en/studio/ai-agent-configuration.md b/docs/en/studio/ai-agent-configuration.md new file mode 100644 index 0000000000..0a56c7dc1a --- /dev/null +++ b/docs/en/studio/ai-agent-configuration.md @@ -0,0 +1,157 @@ +```json +//[doc-seo] +{ + "Description": "Configuration reference for ABP Studio AI Agent models, reasoning, context limits, parallel sessions, permissions, MCP tool connections, AI rules, and learned lessons." +} +``` + +# ABP Studio: AI Agent Configuration + +````json +//[doc-nav] +{ + "Next": { + "Name": "AI Agent Workflows", + "Path": "studio/ai-agent-workflows" + } +} +```` + +AI Agent configuration controls model selection, execution limits, enabled tools, tool permissions, MCP tool connections, AI rules, and learned lessons. + +## Model Provider + +ABP Studio maintains a model list with metadata such as provider, context length, maximum completion tokens, image support, reasoning support, and tool support. + +The built-in default model set includes: + +| Model | Default role | Context length | +| --- | --- | --- | +| Claude Sonnet 4.6 | Main agent | 1,000,000 | +| Claude Haiku 4.5 | Research and text processing | 200,000 | +| Claude Opus 4.7 | Optional high-capability model | 1,000,000 | +| GPT-5.5 | Optional main/review model | 1,050,000 | +| GLM-5.1 | Optional text/code model without image support | 200,000 | + +The available model catalog can be refreshed and is cached locally for a limited period. + +![model-selector-menu](./images/ai-agent/model-selector-menu.png) + +## Model Roles + +ABP Studio separates model roles so each AI operation can use a model suited to its cost and capability profile. + +| Role | Used by | Default behavior | +| --- | --- | --- | +| Main Model | Normal Agent, Plan, and Ask conversations | Uses the active model selected for the agent. | +| Research Model | Research and ABP documentation subagents | Defaults to Claude Haiku 4.5. Can be set to same as main. | +| Text Processor Model | Lightweight text processing, error summarization, commit message generation, and lesson consolidation | Defaults to Claude Haiku 4.5. Can be set to same as main. | +| Git Review Model | AI Review in the Git panel | Defaults to "Ask me every time". Can be set to same as main or a fixed model. | + +The Git Review model picker does not change the main conversation model. When "Ask me every time" is selected, Studio asks for a model before each AI Review and passes that model only to the review run. + +![model-settings-agents](./images/ai-agent/model-settings-agents.png) + +![model-settings-git-review](./images/ai-agent/model-settings-git-review.png) + +## Reasoning Effort + +Reasoning effort controls the model's reasoning budget where the selected model/provider supports it. Supported values are `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. The default is `low`. + +When the context limit is set to `Max`, Studio uses the maximum context behavior for the model and increases reasoning effort for the run. + +## Context Limit + +The context limit controls the maximum context size used for agent runs. The default option is `300K`. The `Max` option delegates to the model's maximum context behavior. + +Large context settings can improve broad solution understanding, but they also increase latency and model cost. Narrow AI scopes and targeted attachments should be preferred when the task is limited to a module or package. + +## Parallel Sessions + +ABP Studio can run multiple agent sessions in parallel. The default maximum is 3 concurrent sessions, and the configured range is 1 to 5. Prompts beyond the limit are queued. + +Each running session locks its own model settings and tool snapshot so global settings changes do not mutate an already-running session. + +## Tool Enablement + +ABP Studio groups Studio automation tools in settings. Disabled tools are removed from the agent tool list before a run starts. + +Core Studio tools are available to Agent mode when enabled: + +- Build. +- Monitor exceptions, logs, HTTP requests, and distributed events. +- Start and stop applications. +- Start and stop containers. +- Run tasks. + +Extended Studio tools are loaded only when the run context enables them. These include specialized operations such as package/module builds, library installation, and proxy generation. + +Tools that exist for internal Studio use can be hidden from the AI agent. Hidden tools are not exposed to the model even when they are implemented in Studio. + +## Tool Permissions + +Some tools require explicit permission before execution. + +| Tool category | Permission behavior | +| --- | --- | +| Shell commands | Studio prompts before command execution unless the command has been allowed. | +| URL fetches | Studio prompts per domain unless the domain has been allowed. | +| File downloads | Studio always asks before downloading. | + +Permission choices include allow once, allow always, and skip. "Allow always" persists in the AI Agent settings and is reused by future sessions. + +## MCP Tool Connections + +ABP Studio can connect to user-configured Model Context Protocol (MCP) servers and expose their tools to Agent mode. This is an MCP client integration for the AI Agent. ABP Studio AI Agent does not expose itself as an MCP server for external AI clients. + +MCP server connections can be configured with: + +| Transport | Configuration | +| --- | --- | +| Stdio | Command, arguments, and environment variables. | +| HTTP | URL and headers. | + +Studio imports MCP server configuration from Cursor, Claude, VS Code, Windsurf, and bare MCP server JSON formats. Studio exports MCP server configuration in the standard `mcpServers` JSON shape. + +Connected MCP servers show their connection status, tool count, tools, and resources. Individual MCP tools can be disabled. Disabled MCP tools are omitted from Agent mode. MCP resources can be opened from settings for inspection. + +MCP tools are added only for connected and enabled servers. Plan and Ask modes do not receive MCP tools. + +![mcp-servers-settings](./images/ai-agent/mcp-servers-settings.png) + +## `.abpignore` + +The `.abpignore` file is placed in the solution root and uses `.gitignore` syntax. Files matched by `.abpignore` are inaccessible to the agent even when they are under the active AI scope. + +The default exclusions protect common secret and credential files, including: + +- `appsettings.secrets.json` +- `.env` files except `.env.example` +- certificate and key files such as `.pfx`, `.p12`, `.pem`, `.key`, and `.p8` +- local development Helm values +- tunnel and temporary key files + +## AI Rules + +AI rules are Markdown files with YAML frontmatter and the `.mdc` extension. Rules can be global or solution-specific. + +| Location | Scope | +| --- | --- | +| `%USERPROFILE%/.abp/studio/rules` | Global rules available on the machine. | +| `.abpstudio/ai-rules` | Solution rules shared with the solution when committed. | + +Rule frontmatter supports: + +```yaml +name: Rule Name +description: Short rule description +alwaysApply: true +``` + +Always-apply rules are injected into the agent's system prompt. Non-always-apply rules are exposed as available skills and can be loaded by the agent with the `fetch_ai_skills` tool. When a solution skill and a global skill have the same name, the solution skill takes precedence. + +## Learned Lessons + +The `save_lesson` tool records verified corrections into `.abpstudio/ai-rules/ai-learned-lessons.mdc`. Learned lessons are injected as high-priority context in future sessions. + +The agent is instructed to save a lesson only after a user correction is verified against source or tool output. When the learned lessons file becomes large, Studio can consolidate it with the text processor model. diff --git a/docs/en/studio/ai-agent-git-integration.md b/docs/en/studio/ai-agent-git-integration.md new file mode 100644 index 0000000000..a9bbec724d --- /dev/null +++ b/docs/en/studio/ai-agent-git-integration.md @@ -0,0 +1,148 @@ +```json +//[doc-seo] +{ + "Description": "Technical reference for ABP Studio AI Agent Git integration, AI Review, changed-file prompting, commit messages, merge conflicts, and GitHub issue or pull request handoff." +} +``` + +# ABP Studio: AI Agent Git Integration + +````json +//[doc-nav] +{ + "Next": { + "Name": "Coding with AI Agent", + "Path": "studio/coding-with-ai-agent" + } +} +```` + +ABP Studio integrates AI Agent features into the Git and GitHub workflow. The integration uses selected files, staged or working-tree diffs, review notes, GitHub issue/PR context, and image attachments to build prompts for dedicated AI review or coding sessions. + +![git-ai-review](./images/ai-agent/git-ai-review.png) + +## AI Review + +AI Review runs from the Git panel against selected changed files. It is a dedicated review flow, separate from the normal Agent chat. + +AI Review: + +- Uses only selected files. +- Skips binary files. +- Uses staged diffs for staged files and working-tree diffs for unstaged files. +- Annotates diff lines with actual line numbers. +- Streams suggestions back to the Git panel. +- Can inspect related source files with read/search tools. +- Can ask an ABP documentation subagent to verify ABP-specific patterns. +- Applies always-apply global and solution AI rules to the review. + +The review model is selected from the Git Review Model setting. When configured as "Ask me every time", Studio shows a model picker before starting the review. + +## Review Scope + +AI Review focuses on changed lines. Surrounding code can be used to understand context, but suggestions are attached to changed lines. + +The reviewer checks for: + +- Bugs and logic errors. +- Security issues. +- Performance problems. +- Code quality issues. +- ABP and DDD pattern violations. + +Generated files such as migrations can be skipped by the reviewer when they are recognized as generated output. + +## Suggestion Severity + +AI Review suggestions have one of the following severities: + +| Severity | Meaning | +| --- | --- | +| Error | A likely bug, crash, security issue, data loss risk, or other high-priority defect. | +| Warning | A potential production issue, bad practice, ABP convention violation, or maintainability problem. | +| Info | A lower-priority design or naming issue worth considering. | +| Confirmation | Positive confirmation for exceptional code. Limited to rare cases. | + +## Review Notes + +The Git panel can store user notes and AI suggestions against files and lines. Review content is used in two ways: + +- It can block commit actions while unresolved review content exists. +- It can be sent to the AI Agent as an implementation prompt. + +Blocking review content includes user notes and AI suggestions with severities other than `Info` and `Confirmation`. + +## Sending Review Findings To The Agent + +The Git panel can send review findings to AI Agent. Studio builds a prompt that includes: + +- A request to review and apply the notes for the current Git changes. +- File paths. +- Line numbers. +- Line type and line content. +- AI suggestions. +- User notes. + +Confirmation suggestions are excluded. Info suggestions are treated as optional context; warning and error suggestions are emphasized. + +The prompt instructs the agent to implement the requested changes, not only explain them. + +## Commit Message Generation + +Commit message generation uses selected file diffs and the Text Processor Model. It skips binary files and truncates large diffs before sending them to the model. + +The generator returns a concise past-tense commit message. It is intended to summarize the selected changes, not the whole repository state. + +## Merge Conflict Handoff + +When unresolved merge conflicts exist, the Git panel can send the conflict list to AI Agent. The generated prompt asks the agent to resolve conflicts and request clarification when the correct resolution is ambiguous. + +Conflict handoff can use the current session or create a new session. It starts with analysis skipped so the prompt is centered on the conflict files. + +## GitHub Issue Handoff + +GitHub issue details can be sent to AI Agent from Studio when GitHub integration is connected. + +The generated issue prompt includes: + +- Issue number and title. +- State. +- Author. +- Labels. +- Description. +- Included comments. +- Attached images referenced by the issue body or included comments. + +Comments can be excluded from the AI prompt. Images that belong to excluded comments are not attached. The prompt instructs the agent to solve the issue by making the necessary code changes. + +## GitHub Pull Request Handoff + +Pull request details can be sent to AI Agent when the current branch matches the PR branch. + +The generated PR prompt includes: + +- PR number and title. +- State. +- Author. +- Base and head branches. +- Labels. +- Included reviews. +- Included comments. +- Included requested changes. +- File path and line information for review comments when available. +- Diff hunks for review comments when available. +- Attached images from included comments and requested changes. + +Reviews, comments, and requested changes can be excluded from the AI prompt. Only included items are sent. + +## Image Attachments + +GitHub issue and PR handoff can attach downloaded images to the agent prompt. The prompt text annotates image references with the corresponding attached file name so the model can connect the image file to the original issue, comment, or review context. + +Image handling still depends on the selected model's image support. If the model does not support images, the normal attachment restrictions apply. + +## GitHub URL Fetching In Agent Chat + +Agent URL fetching can enrich GitHub URLs with issue or pull request information when GitHub integration is available. Pull request URL context can include review comments up to Studio's fetch limits. + +This is separate from the explicit GitHub issue/PR "Send to AI Agent" actions in the Git UI. diff --git a/docs/en/studio/ai-agent-workflows.md b/docs/en/studio/ai-agent-workflows.md new file mode 100644 index 0000000000..da2dc4fa57 --- /dev/null +++ b/docs/en/studio/ai-agent-workflows.md @@ -0,0 +1,110 @@ +```json +//[doc-seo] +{ + "Description": "Technical reference for ABP Studio AI Agent workflows, AI scopes, automatic pre/post actions, plan files, and workflow storage." +} +``` + +# ABP Studio: AI Agent Workflows + +````json +//[doc-nav] +{ + "Next": { + "Name": "AI Agent Built-in Capabilities", + "Path": "studio/ai-agent-built-in-capabilities" + } +} +```` + +AI Agent workflows define repeatable actions that Studio can run before or after an agent task. Workflows are associated with run profiles and are selected before a session starts. + +![workflow-settings](./images/ai-agent/workflow-settings.png) + +## Workflow Types + +| Type | Storage | Sharing behavior | +| --- | --- | --- | +| Personal workflow | `.abpstudio/workflows/*.json` | Local to the solution workspace and visible in all run profiles. | +| Shared workflow | The active `.abprun.json` run profile under the `workflows` section | Stored with the run profile and intended for source control. | + +The shared/personal setting is fixed when the workflow is created. + +Workflow configuration includes the workflow name, description, sharing mode, before steps, and after steps. + +![workflow-actions](./images/ai-agent/workflow-actions.png) + +## Supported Actions + +| Action | Description | +| --- | --- | +| Build | Builds the solution, selected modules, selected packages, or all configured targets. | +| Start Application | Starts selected applications, applications in selected folders, or all runnable applications. | +| Stop Application | Stops selected applications, applications in selected folders, or all targeted applications. | +| Restart Application | Runs the configured restart application workflow action for selected targets. | +| Start Containers | Starts selected containers or all containers. | +| Stop Containers | Stops selected containers or all containers. | +| Run Task | Runs selected Studio tasks. | +| Add Migration | Adds a database migration for the configured migration project when entity changes require it. | +| Install Libs | Installs client-side library files when the task requires it. | +| Generate C# Proxies | Generates C# client proxies with configured URL, module, service type, folder, target package, and contract options. | +| Generate Angular Proxies | Generates Angular proxies with configured URL, module, service type, and working directory. | + +## Before Tasks + +Before tasks run automatically only in Agent mode. Before the model receives control, Studio can run solution/package analysis and then execute the workflow's before tasks through the corresponding Studio tools. + +Plan and Ask modes are read-only and do not execute before tasks. + +## After Tasks + +After tasks are injected into the agent instructions as post-task guidance. The agent is expected to run relevant post-steps after completing the main work, but Studio treats them as contextual workflow guidance rather than unconditional execution. + +The agent may skip after tasks that are not relevant to the actual changes. For example, migration, build, library installation, and proxy generation actions are used only when the completed code changes require them. + +## Automatic Analysis And Build + +Agent mode performs ABP-aware analysis around coding tasks: + +- Before execution, Studio analyzes packages unless the run is explicitly started with analysis skipped. +- After execution, Studio checks changed packages. +- If the workflow already includes a build after task, Studio avoids duplicating that build and performs analysis of changed packages. +- If no build after task is present, Studio can build and analyze changed packages after the agent turn. + +This behavior applies to Agent mode. Read-only modes do not perform build or mutation steps. + +## Workflow Selection And Session Locking + +The selected workflow is stored on the session at execution time. A background session keeps using its original workflow even if the foreground run profile or workflow selection changes. + +Once a session has a cached system prompt, configuration that changes the prompt is locked for that session. New sessions receive the latest selected workflow. + +## AI Scopes In Workflows + +AI scopes and workflows are independent but complementary. The scope determines which directories agent file tools can access. The workflow determines which automation actions are available or recommended for the task. + +A workflow can target applications, containers, packages, modules, and tasks from the active run profile even when the AI scope is narrowed to a smaller area. File access remains governed by the scope. + +## Plan Files + +Plan mode creates and updates plan files under `.abpstudio/plans`. A plan file is a technical implementation document, not a chat transcript. + +Agent mode can attach to an active plan and update step state while implementing it. Plan step updates use stable step identifiers so progress can survive conversation turns. + +## Applying Plans + +When a plan is applied, Studio builds a prompt from the plan and sends it to the agent. The agent then works in Agent mode with the active plan context and updates plan steps as it completes work. + +## Workflow Design Constraints + +Workflows should contain deterministic project actions. They are not a replacement for prompts, AI rules, or review instructions. + +Recommended workflow contents: + +- Build steps required by the solution. +- Application/container start or stop steps required for verification. +- Proxy generation steps required after API contract changes. +- Migration steps for known database projects. +- Run tasks used by the team for validation. + +Avoid putting broad or ambiguous requirements into workflows. Use AI rules for coding standards and prompts for task-specific instructions. diff --git a/docs/en/studio/ai-agent.md b/docs/en/studio/ai-agent.md new file mode 100644 index 0000000000..b6e445b08d --- /dev/null +++ b/docs/en/studio/ai-agent.md @@ -0,0 +1,103 @@ +```json +//[doc-seo] +{ + "Description": "Technical reference for the ABP Studio AI Agent operating model, modes, sessions, scopes, prompts, attachments, and context handling." +} +``` + +# ABP Studio: AI Agent + +````json +//[doc-nav] +{ + "Next": { + "Name": "AI Agent Configuration", + "Path": "studio/ai-agent-configuration" + } +} +```` + +ABP Studio AI Agent is an integrated coding agent for ABP solutions. It combines solution metadata, run profile metadata, ABP-specific code analysis, controlled tool access, AI rules, and conversation state into agent sessions that can answer questions, create plans, and modify the solution depending on the selected mode. + +![ai-agent](./images/ai-agent/ai-agent-panel.png) + +The agent is solution-aware. Its system context includes the current solution, modules, packages, runnable applications, containers, tasks, run profile information, active AI scope, active workflow, enabled AI rules, and available tools for the selected mode. + +## Modes + +The selected mode determines the agent's effective permission boundary. + +### Agent + +Agent mode is used for coding and execution. It can read and write files, run shell commands, add migrations, run enabled Studio tools, call connected MCP tools, and update active plan steps. + +### Plan + +Plan mode is used for read-only implementation planning. It can inspect code and documentation, create or update plan files, and use read-only subagents. It cannot modify solution files or run shell/write tools. + +### Ask + +Ask mode is used for read-only question answering. It can inspect code, search/read ABP documentation, fetch relevant AI skills, and use read-only subagents. It cannot create plans or modify files. + +Mode selection is per session. A session keeps its selected mode for subsequent prompts unless the user changes it before sending the next message. + +## Sessions + +An AI Agent session stores the conversation, selected model, selected mode, AI scope, workflow, context notes, queued prompts, active plan state, attachments, and context usage. + +The first message of a session locks the configuration that affects the system prompt. The active AI scope and workflow are stored on the session so a background session continues with the configuration that was active when it started. Changing the foreground run profile, scope, or workflow does not rewrite the context of a running session. + +ABP Studio keeps multiple sessions per solution. Sessions can run in parallel up to the configured maximum. Additional prompts are queued until an execution slot is available. + +## Prompt Queue + +Prompts can be queued while a session is running. Queued prompts are attached to the same session and are sent after the current agent turn completes. The queue preserves the session configuration, including mode, scope, model, workflow, and active plan state. + +## Attachments + +The agent supports file and image attachments for prompts. Attachments are stored under the ABP Studio agent files area and become part of the prompt context for the target session. + +Attachment limits are enforced by ABP Studio: + +| Limit | Value | +| --- | --- | +| Maximum files per prompt | 10 | +| Maximum size per file | 20 MB | +| Supported file extensions | `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`, `.txt`, `.md`, `.cs`, `.js`, `.ts`, `.json` | + +Image attachments require a selected model that supports image input. If the selected model does not support images, the agent rejects image attachments for that run. + +## URL Context + +URLs attached to a prompt are fetched and included as contextual material when permitted. URL fetching uses explicit domain permission. Large fetched content is cached under `.abpstudio/url-cache` instead of being injected directly into the prompt. + +## AI Scopes + +An AI scope restricts which directories the agent can access. A scope can include the whole solution, selected modules, selected packages, and external folders. + +Scope resolution follows these rules: + +- The whole-solution scope includes the solution root and all module directories. +- Selected modules include their module directories. +- Selected packages include package directories. +- Existing external folders are included; missing external folders are ignored. +- Overlapping directories are collapsed to avoid duplicate indexing. +- The solution `.abpstudio` folder is accessible so the agent can read and write its own agent state, plans, rules, and workflow files where applicable. + +All file paths used by agent tools are validated against the resolved session scope. Rooted paths are accepted only when they are inside an accessible directory. Files excluded by `.abpignore` are blocked even if they are under an accessible directory. + +## Context Notes + +The agent maintains hidden context notes for long-running sessions. These notes preserve useful state across prompts without exposing implementation-only notes in the visible conversation. + +## Active Plans + +Plan files are stored under `.abpstudio/plans`. Plan mode can create and update plan files. Agent mode can execute an active plan and update step status through the plan-step tool. Active plan state is stored on the session and is cleared when all plan steps are completed. + +## Run Profile Context + +When a run profile is active, the system prompt includes information about runnable applications, containers, tasks, and workflow-related targets. This context allows the agent to call Studio tools by name without rediscovering the solution structure through file search. + +## Privacy Boundary + +The agent uses the files, prompts, Studio tool outputs, connected MCP tool outputs, attachments, and URLs supplied to a session. Sensitive files can be excluded with `.abpignore`; tool permissions restrict shell commands, domain fetches, and downloads. diff --git a/docs/en/studio/coding-with-ai-agent.md b/docs/en/studio/coding-with-ai-agent.md new file mode 100644 index 0000000000..77eb34350f --- /dev/null +++ b/docs/en/studio/coding-with-ai-agent.md @@ -0,0 +1,131 @@ +```json +//[doc-seo] +{ + "Description": "Technical guidance for coding with ABP Studio AI Agent, including mode selection, scopes, prompting, verification, Git review handoff, Studio tools, MCP tools, and parallel sessions." +} +``` + +# ABP Studio: Coding with AI Agent + +This document describes coding practices for ABP Studio AI Agent. It focuses on how to shape agent runs so the agent receives the correct context, uses the correct tools, and verifies changes with the same ABP Studio automation available to the developer. + +## Mode Selection + +Use the mode that matches the expected side effect. + +| Work type | Mode | +| --- | --- | +| Code changes, migrations, builds, proxy generation, app/container control, Studio tool execution, or MCP tool execution | Agent | +| Technical implementation design before coding | Plan | +| Explanation, source navigation, ABP documentation lookup, and codebase questions | Ask | + +Plan and Ask are read-only modes. They are appropriate when the desired output is a technical answer or implementation plan rather than modified files. + +## Scope Selection + +Set the AI scope before the first prompt in a session. The first message locks the scope for that session. + +Use a narrow scope when the task is isolated to one module or package. Use the whole-solution scope when the task depends on cross-module contracts, shared infrastructure, or multiple applications. + +External folders should be added only when their contents are required by the task. Files outside the resolved scope are not accessible to agent file tools. + +## Prompt Context + +Prompts should identify the target behavior, relevant packages, constraints, and expected verification. The agent already receives solution and run profile context, so prompts should focus on task-specific decisions. + +Useful prompt context includes: + +- The user-facing behavior to change. +- Affected module, package, or application names. +- Relevant file paths when known. +- API, DTO, database, or permission constraints. +- Required validation commands or Studio workflow. +- Whether migrations or proxy generation are expected. + +Avoid placing durable coding rules in every prompt. Use AI rules for project standards and prompts for task-specific requirements. + +## Changed-File Prompting + +When the task is based on current Git changes, use the Git panel selection or review handoff instead of manually describing all changes. Studio can build prompts from selected staged or working-tree diffs, AI suggestions, user review notes, line numbers, and line content. + +This keeps the prompt aligned with the actual Git state and avoids missing files that are selected in the commit/review workflow. + +## AI Review Handoff + +AI Review is a review workflow, not an implementation workflow. After suggestions are produced, send the review content to AI Agent when the intent is to apply fixes. + +The handoff prompt contains verified suggestions, user notes, file paths, and line references. The agent then runs in the normal coding workflow and can edit files, run builds, and update active plan steps depending on the session mode and tools. + +## Plans + +Use Plan mode for changes with broad design impact, uncertain architecture, or multiple implementation phases. Plan files are stored under `.abpstudio/plans` and can be applied later in Agent mode. + +An active plan gives Agent mode a durable implementation checklist. Agent mode updates plan step state as work completes. + +## Verification + +Agent mode has access to Studio build and automation tools when enabled. Verification should match the changed surface. + +| Change type | Typical verification | +| --- | --- | +| Application or domain code | Build affected package or solution. | +| Entity or DbContext changes | Add migration if required, then build. | +| HTTP API contract changes | Regenerate C# or Angular proxies when the consuming project requires them. | +| UI or client library changes | Install libs when new ABP module library assets are introduced. | +| Runtime behavior | Start required applications or containers and inspect logs, requests, exceptions, or events. | +| Review fixes | Re-run AI Review or build/test the affected packages. | + +Workflow after tasks can encode common verification steps, but the agent can skip workflow actions that do not apply to the completed changes. + +## Migrations + +Database migrations should be generated only when model changes require them. Workflows can configure the migration project so the agent can add a migration through the `add_migration` tool. + +Migration generation is serialized with build operations to avoid concurrent `dotnet` conflicts across sessions. + +## Proxy Generation + +Proxy generation is appropriate when API contract changes must be consumed by C# or Angular clients. + +Workflow actions can configure: + +- API base URL. +- Module name. +- Service type filter: `all`, `application`, or `integration`. +- Output folder or Angular working directory. +- C# target package. +- Whether C# contract classes are generated. + +## MCP Tools + +Use configured MCP tools when the agent must work with an external system that is not covered by built-in ABP Studio tools. MCP tools are available only in Agent mode, only when the MCP server is connected, and only when the individual tool is enabled. + +Because MCP tools can have side effects defined by the connected server, keep MCP connections limited to trusted servers and disable tools that should not be callable by the AI Agent. + +MCP tool configuration is for adding external tools to the AI Agent. It does not expose ABP Studio AI Agent as an MCP server for other AI clients. + +## AI Rules And Lessons + +Use always-apply AI rules for standards that should affect every run, such as architecture rules, naming conventions, security requirements, or team-specific ABP patterns. + +Use on-demand AI skills for specialized knowledge that should be loaded only when relevant. This keeps the system prompt smaller and avoids applying unrelated instructions. + +Learned lessons are for verified corrections. They should represent durable mistakes and corrections, not task notes. + +## Parallel Sessions + +Parallel sessions are useful for independent work, research, and review. Each session locks its model settings, scope, workflow, and tool snapshot when it starts. + +Avoid running multiple Agent-mode sessions that edit the same files. Studio detects some stale-file situations and serializes selected tool operations, but it does not replace normal coordination for overlapping edits. + +## Limits And Failure Modes + +Common boundaries: + +- Files outside the active AI scope are inaccessible. +- Files matched by `.abpignore` are inaccessible. +- Image attachments require a model with image support. +- Shell, URL fetch, and download operations can wait for permission. +- Disabled Studio tools and disabled MCP tools are omitted from the tool list. +- Plan and Ask modes cannot modify files. +- Hidden internal Studio tools are not exposed to the agent. diff --git a/docs/en/studio/images/ai-agent/ai-agent-panel.png b/docs/en/studio/images/ai-agent/ai-agent-panel.png new file mode 100644 index 0000000000..a519603402 Binary files /dev/null and b/docs/en/studio/images/ai-agent/ai-agent-panel.png differ diff --git a/docs/en/studio/images/ai-agent/git-ai-review.png b/docs/en/studio/images/ai-agent/git-ai-review.png new file mode 100644 index 0000000000..3ea9874504 Binary files /dev/null and b/docs/en/studio/images/ai-agent/git-ai-review.png differ diff --git a/docs/en/studio/images/ai-agent/mcp-servers-settings.png b/docs/en/studio/images/ai-agent/mcp-servers-settings.png new file mode 100644 index 0000000000..1cf4d4036b Binary files /dev/null and b/docs/en/studio/images/ai-agent/mcp-servers-settings.png differ diff --git a/docs/en/studio/images/ai-agent/model-selector-menu.png b/docs/en/studio/images/ai-agent/model-selector-menu.png new file mode 100644 index 0000000000..94fc464b88 Binary files /dev/null and b/docs/en/studio/images/ai-agent/model-selector-menu.png differ diff --git a/docs/en/studio/images/ai-agent/model-settings-agents.png b/docs/en/studio/images/ai-agent/model-settings-agents.png new file mode 100644 index 0000000000..1f130d4888 Binary files /dev/null and b/docs/en/studio/images/ai-agent/model-settings-agents.png differ diff --git a/docs/en/studio/images/ai-agent/model-settings-git-review.png b/docs/en/studio/images/ai-agent/model-settings-git-review.png new file mode 100644 index 0000000000..52fe97612b Binary files /dev/null and b/docs/en/studio/images/ai-agent/model-settings-git-review.png differ diff --git a/docs/en/studio/images/ai-agent/workflow-actions.png b/docs/en/studio/images/ai-agent/workflow-actions.png new file mode 100644 index 0000000000..d476281f7b Binary files /dev/null and b/docs/en/studio/images/ai-agent/workflow-actions.png differ diff --git a/docs/en/studio/images/ai-agent/workflow-settings.png b/docs/en/studio/images/ai-agent/workflow-settings.png new file mode 100644 index 0000000000..e3c39db3e5 Binary files /dev/null and b/docs/en/studio/images/ai-agent/workflow-settings.png differ diff --git a/docs/en/studio/model-context-protocol.md b/docs/en/studio/model-context-protocol.md deleted file mode 100644 index 7be6557df9..0000000000 --- a/docs/en/studio/model-context-protocol.md +++ /dev/null @@ -1,133 +0,0 @@ -```json -//[doc-seo] -{ - "Description": "Learn how to connect AI tools like Cursor, Claude Desktop, and VS Code to ABP Studio using the Model Context Protocol (MCP)." -} -``` - -# ABP Studio: Model Context Protocol (MCP) - -````json -//[doc-nav] -{ - "Next": { - "Name": "Working with Kubernetes", - "Path": "studio/kubernetes" - } -} -```` - -ABP Studio includes built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) support so AI tools can query runtime telemetry and control solution runner operations. - -## How It Works - -ABP Studio runs a local MCP server in the background. The `abp mcp-studio` CLI command acts as a stdio bridge that AI clients connect to. The bridge forwards requests to ABP Studio and returns responses. - -```text -MCP Client (Cursor / Claude Desktop / VS Code) - ──stdio──▶ abp mcp-studio ──HTTP──▶ ABP Studio -``` - -> ABP Studio must be running while MCP is used. If ABP Studio is not running (or its MCP endpoint is unavailable), `abp mcp-studio` returns an error to the AI client. - -## Configuration - -### Cursor (`.cursor/mcp.json`) - -```json -{ - "mcpServers": { - "abp-studio": { - "command": "abp", - "args": ["mcp-studio"] - } - } -} -``` - -### Claude Desktop (`claude_desktop_config.json`) - -```json -{ - "mcpServers": { - "abp-studio": { - "command": "abp", - "args": ["mcp-studio"] - } - } -} -``` - -Claude Desktop config file locations: - -- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` -- Windows: `%APPDATA%\Claude\claude_desktop_config.json` -- Linux: `~/.config/Claude/claude_desktop_config.json` - -### VS Code (`.vscode/mcp.json`) - -```json -{ - "servers": { - "abp-studio": { - "command": "abp", - "args": ["mcp-studio"] - } - } -} -``` - -### Quick Reference - -You can run `abp help mcp-studio` at any time to see the available options and example configuration snippets for each supported IDE directly in your terminal. - -### Generating Config Files from ABP Studio - -When creating a new solution, ABP Studio can generate MCP configuration files for Cursor and VS Code automatically. - -## Available Tools - -ABP Studio exposes the following tools to MCP clients. All tools operate on the currently open solution and selected run profile in ABP Studio. - -### Monitoring - -| Tool | Description | -|------|-------------| -| `list_applications` | Lists all running ABP applications connected to ABP Studio. | -| `get_exceptions` | Gets recent exceptions including stack traces and error messages. | -| `get_logs` | Gets log entries. Can be filtered by log level. | -| `get_requests` | Gets HTTP request information. Can be filtered by status code. | -| `get_events` | Gets distributed events for debugging inter-service communication. | -| `clear_monitor` | Clears collected monitor data. | - -### Application Control - -| Tool | Description | -|------|-------------| -| `list_runnable_applications` | Lists all applications in the current run profile with their state. | -| `start_application` | Starts a stopped application. | -| `stop_application` | Stops a running application. | -| `restart_application` | Restarts a running application. | -| `build_application` | Builds a .NET application using `dotnet build`. | - -### Container Control - -| Tool | Description | -|------|-------------| -| `list_containers` | Lists Docker containers in the current run profile with their state. | -| `start_containers` | Starts Docker containers (docker-compose up). | -| `stop_containers` | Stops Docker containers (docker-compose down). | - -### Solution Structure - -| Tool | Description | -|------|-------------| -| `get_solution_info` | Gets solution name, path, template, and run profile information. | -| `list_modules` | Lists all modules in the solution. | -| `list_packages` | Lists packages (projects) in the solution. Can be filtered by module. | -| `get_module_dependencies` | Gets module dependency/import information. | - -## Notes - -- Monitor data (exceptions, logs, requests, events) is kept in memory and is cleared when the solution is closed. -- The `abp mcp-studio` command connects to the local ABP Studio instance. This is separate from the `abp mcp` command, which connects to the ABP.IO cloud MCP service and requires an active license. diff --git a/docs/en/studio/monitoring-applications.md b/docs/en/studio/monitoring-applications.md index e06d387146..d994f6206c 100644 --- a/docs/en/studio/monitoring-applications.md +++ b/docs/en/studio/monitoring-applications.md @@ -11,8 +11,8 @@ //[doc-nav] { "Next": { - "Name": "Model Context Protocol (MCP)", - "Path": "studio/model-context-protocol" + "Name": "AI Agent", + "Path": "studio/ai-agent" } } ```` diff --git a/docs/en/studio/overview.md b/docs/en/studio/overview.md index f875e9ebf1..13e539ebcf 100644 --- a/docs/en/studio/overview.md +++ b/docs/en/studio/overview.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore ABP Studio's key features like Solution Explorer, Kubernetes Integration, and AI Assistant to optimize your ABP application development and management." + "Description": "Explore ABP Studio's key features like Solution Explorer, Kubernetes Integration, AI Agent, and monitoring to optimize your ABP application development and management." } ``` @@ -19,7 +19,7 @@ ## Introduction -ABP Studio, a comprehensive desktop application, offers a wide range of features and functionalities tailored to streamline the development and management of ABP-based applications. This article provides an overview of the key components of ABP Studio, including the Solution Explorer, Solution Runner, Kubernetes Integration, AI Assistant, Application Monitoring Area, Background Tasks, Notifications and Logs. Understanding these components is essential for efficiently utilizing ABP Studio to its full potential. +ABP Studio, a comprehensive desktop application, offers a wide range of features and functionalities tailored to streamline the development and management of ABP-based applications. This article provides an overview of the key components of ABP Studio, including the Solution Explorer, Solution Runner, Kubernetes Integration, AI Agent, Application Monitoring Area, Background Tasks, Notifications and Logs. Understanding these components is essential for efficiently utilizing ABP Studio to its full potential. ![overview-intro](./images/overview/overview-intro.png) @@ -69,7 +69,7 @@ The Welcome Screen is the initial user interface users encounter upon starting A ## Sidebar -Located on the left side of the interface, the Left Area is a panel that provides quick access to various functionalities like [Solution Explorer](./solution-explorer.md), [Solution Runner](./running-applications.md), Kubernetes Integration, and AI Assistant. Now let's examine each item. +Located on the left side of the interface, the Left Area is a panel that provides quick access to various functionalities like [Solution Explorer](./solution-explorer.md), [Solution Runner](./running-applications.md), Kubernetes Integration, and AI Agent. Now let's examine each item. ### Solution Explorer @@ -99,22 +99,22 @@ This pane is dedicated to managing Kubernetes services. It simplifies the proces ![kubernetes-integration-kubernetes-panel](./images/overview/kubernetes-integration-kubernetes.png) -### AI Assistant +### AI Agent -The AI Assistant is an integrated chat interface within ABP Studio that provides intelligent assistance for ABP-related questions. You can access it from the left sidebar by clicking the AI icon. +The AI Agent is an integrated coding agent in ABP Studio. It can answer ABP-related questions, create implementation plans, and work on the current solution with controlled access to files, Studio tools, workflows, Git context, and configured MCP tools. -For external AI tool integrations through MCP, see the [Model Context Protocol (MCP)](./model-context-protocol.md) documentation. +![ai-agent](./images/ai-agent/ai-agent-panel.png) -![ai-assistant](./images/overview/ai-assistant.png) +Key features of the AI Agent include: -Key features of the AI Assistant include: +- **Agent Modes**: Use Agent, Plan, or Ask mode depending on whether the session should modify files, create a plan, or answer questions. +- **Solution Context**: Work with the active solution, selected AI scope, run profile, workflow, and model settings. +- **Attachments**: Attach supported files or images to provide task-specific context. +- **Studio Integration**: Use enabled Studio tools for build, monitoring, applications, containers, tasks, proxies, migrations, and Git-related workflows. -- **Chat Interface**: Ask anything about ABP Framework, and get instant, context-aware responses to help with your development workflow. -- **File Attachments**: Attach files to your conversations to get more specific assistance with your code or configuration. -- **Recent Chats**: Access your conversation history to continue previous discussions or reference past solutions. -- **Credits System**: The AI Assistant uses a credits-based system, with usage tracked and displayed in the interface. +> **Note**: Review the Privacy Notice available in the AI Agent panel to understand how your data is handled. -> **Note**: Review the Privacy Notice available in the AI Assistant panel to understand how your data is handled. +For the technical AI Agent reference, see [ABP Studio: AI Agent](./ai-agent.md). ## Application Monitoring Area diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index 430bd1e892..f6c9e276f2 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -9,16 +9,35 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. -## 3.0.2 (2026-05-12) Latest - -* Modern template osx fix -* Update dark theme - BackgroundColorLighter -* Fix initial task warning handling -* Abp Studio ai agent cont -* Add Volo.Abp.Elsa to module list -* Register reCAPTCHA in HttpApi host templates for CmsKit contact endpoint -* Optimized system prompt in ABP Studio -* Register `MVC.RootUrl` in `HttpApi.Host` template +## 3.0.4 (2026-06-03) Latest + +* Blazor UI Selector: Added a built-in choice between MudBlazor and Blazorise for Blazor solution templates +* React Native Updates: Upgraded Expo to the latest version, improved keyboard handling, and ensured UI consistency +* Linux Support Enhancements: Improved system tool detection, terminal launching, and added full Linux publish/runtime support +* React Admin Security: Secured the React Admin Console sidebar menu by enforcing permission-based access gating +* Mobile Documentation: Added comprehensive setup guides and rules for React Native mobile templates +* Angular Microservice Fix: Resolved missing audit logging configurations and imports in microservice templates +* Modern Templates Licensing: Updated Modern architecture templates to be available exclusively for commercial/paid tiers +* Code & UI Stability: Fixed TypeScript compilation errors in React templates and resolved various minor system bugs + +## 3.0.3 (2026-05-20) + +* AI Agent Upgrades: Added browser automation tools and overall performance fixes +* React Language Fix: Fixed language and localization settings being ignored in React templates +* Admin Console Polish: Added icon support and visual enhancements to the React sidebar +* Admin Mode Drag & Drop: Fixed solution file drag-and-drop when running as Administrator on Windows +* Project Wizard Improvements: Added helpful guidance texts for modularity and options steps +* User & Security Fixes: Enhanced user management and fixed account-linking login permissions +* UI & System Tweaks: Polished modal window styles and resolved minor background template issues + +## 3.0.2 (2026-05-12) + +* AI Agent Enhancements: Optimized the core system prompt and continued overall agent improvements +* Elsa Workflow Integration: Added Volo.Abp.Elsa to the available module selection list +* Enhanced Security: Added automatic reCAPTCHA registration for CMS Kit contact forms in API templates +* macOS Template Fix: Resolved compatibility issues specifically affecting modern templates on macOS +* Dark Theme Polish: Updated the dark mode with a lighter, more balanced background color contrast +* Template Configuration Fixes: Automatically configured missing root URL settings and improved initial task warning handling ## 3.0.1 (2026-05-06) diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md index f16bb25f7a..d254004d4f 100644 --- a/docs/en/studio/version-mapping.md +++ b/docs/en/studio/version-mapping.md @@ -9,8 +9,10 @@ This document provides a general overview of the relationship between various versions of ABP Studio and the ABP version of the [ABP Solution Templates](../solution-templates/index.md) used when generating a new solution. Each version of ABP Studio is configured to create solutions with a specific ABP version. However, once a solution is created, you can easily update it to the latest version of ABP via ABP Studio or [ABP CLI](../cli/index.md#update). -| **ABP Studio Version** | **ABP Version of Startup Template** | +| **ABP Studio Version** | **ABP Version of Startup Template** | |------------------------|---------------------------| +| 3.0.4 | 10.4.1 | +| 3.0.3 | 10.4.0 | | 2.2.7 - 3.0.2 | 10.3.0 | | 2.2.5 - 2.2.6 | 10.2.0 | | 2.2.2 - 2.2.4 | 10.1.1 | diff --git a/docs/en/suite/create-solution.md b/docs/en/suite/create-solution.md index 39d3fb925e..1ec5a799cd 100644 --- a/docs/en/suite/create-solution.md +++ b/docs/en/suite/create-solution.md @@ -25,4 +25,6 @@ From ABP Suite v8.3, **create a new solution** option has been removed. Instead, it's suggessted to use [ABP Studio](../studio/index.md) or [ABP CLI](../cli/index.md) to create a new solution. After, creating an ABP Solution, then you can generate CRUD pages via ABP Suite as always. +> **Note:** ABP Suite does not generate React UI pages. If you create a modern React solution, use Suite where applicable for backend and application-layer generation, and develop the React UI in the source-owned [React UI](../framework/ui/react/index.md) application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. + > ABP Studio has a shortcut for running [ABP Suite](./index.md) to allow using it without starting it externally and using it on a browser, which means you can create, manage, deploy your applications in a single desktop application and also generate CRUD pages via Suite as a pre-integrated application to ABP Studio. \ No newline at end of file diff --git a/docs/en/suite/creating-many-to-many-relationship.md b/docs/en/suite/creating-many-to-many-relationship.md index f711c2a891..005cab9896 100644 --- a/docs/en/suite/creating-many-to-many-relationship.md +++ b/docs/en/suite/creating-many-to-many-relationship.md @@ -75,4 +75,6 @@ Click the **Navigations** tab. Then click the **Add navigation collection** butt The below image is the final page created by ABP Suite. The **new book** dialog has a **Categories** tab which lists all categories of the book and allows add/remove categories. +> **Note:** ABP Suite does not generate React UI for this page. The generated UI applies to Suite-supported UI stacks such as MVC, Blazor and Angular. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. + ![navigation-collection-books-page](../images/navigation-collection-books-page.8.1.png) diff --git a/docs/en/suite/creating-master-detail-relationship.md b/docs/en/suite/creating-master-detail-relationship.md index b1d8874d5b..94bda1b7fa 100644 --- a/docs/en/suite/creating-master-detail-relationship.md +++ b/docs/en/suite/creating-master-detail-relationship.md @@ -39,6 +39,8 @@ You need to specify the **Entity type** as _Master_ (default). Then, provide the Create a child entity, specify the **Entity type** as _Child_, and associate it with a master entity. In the figure above, you can see an example of, a child entity **OrderLine** with the associated master entity **Order**. When you specify the **Entity type** as _Child_, then the UI and tests will not be generated for the child entity and instead, all of the orchestration of the entity will be delegated to its master entity. +> **Note:** ABP Suite does not generate React UI for master-detail pages. The generated UI applies to Suite-supported UI stacks such as MVC, Blazor and Angular. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. + Application services will be generated for the child entity, so it's possible for you to consume its endpoints and create-update-list or delete the child entity specifically. Suite also respects your multi-tenancy selection and sets the child entity as a multi-tenant entity, if the master entity has multi-tenancy enabled. > **Note**: Child entities are not a good candidate to establish a many-to-many relationship and therefore that's disabled in the Suite UI. So, you can't create a many-to-many relationship for child entities, but you can create a one-to-many relationship. diff --git a/docs/en/suite/customizing-the-generated-code.md b/docs/en/suite/customizing-the-generated-code.md index e5061ca700..18f63d642c 100644 --- a/docs/en/suite/customizing-the-generated-code.md +++ b/docs/en/suite/customizing-the-generated-code.md @@ -58,6 +58,8 @@ You can see the example below, which demonstrates defining a new method in the ` For the UI side, ABP Suite provides convenient comment placeholders within pages for MVC, Blazor, and Angular UIs. These comment sections serve as hook points where you can add your custom code. By placing your custom logic with these hook points, you can enhance the UI to your needs without the fear of losing your changes during the next CRUD page generation. +> **Note:** ABP Suite does not generate React UI pages or React customization hook points. React UI is part of ABP's modern template system and is customized directly in the source-owned [React UI](../framework/ui/react/index.md) application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages and customize them more easily. + ### Customizations on MVC UI If you have created your application with MVC UI, you can see the extended classes for _the code-behind classes of your pages_, under the `Pages` folder: @@ -82,6 +84,8 @@ As can be seen in the figure above, you can search the related template file, cl ### Customizations on Blazor UI +> **Blazorise vs MudBlazor templates:** Suite ships two parallel template trees for Blazor — `Frontend.Blazor.*` (Blazorise) and `Frontend.Blazor.MudBlazor.*` (MudBlazor) — and picks the right one based on the [Blazor UI library](index.md#blazor-ui-library) detected in your solution. Customized templates are stored under `.suite/customized-templates/` keyed by the full resource name, so a customization made in a MudBlazor solution is never read by a Blazorise solution (or vice versa). If you migrate an existing solution from Blazorise to MudBlazor, you need to re-apply your customizations to the new resource names. + If you have created your application with Blazor UI, you can see the extended classes for _the code-behind classes of your pages_, under the `Pages` folder: ![](../images/custom-code-blazor-folder-structure.png) diff --git a/docs/en/suite/editing-templates.md b/docs/en/suite/editing-templates.md index 354adc8ead..3a6160a215 100644 --- a/docs/en/suite/editing-templates.md +++ b/docs/en/suite/editing-templates.md @@ -16,6 +16,8 @@ The are 2 template filters: 1. **UI**: You can switch between `Angular`, `MVC` and `Blazor` templates. 2. **Database provider:** You can switch between `EF Core` and `MongoDb` templates. +> **Note:** ABP Suite does not include React UI templates or generate React client-side code. React UI belongs to the modern template system and should be developed in the source-owned [React UI](../framework/ui/react/index.md) application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. + These filters are automatically selected based on your ABP solution. The selected solution is shown on the top right of the page. In the current screenshot it is "Acme.BookStore". ABP Suite templates are embedded resources which are stored in the `Volo.Abp.Commercial.SuiteTemplates` package. When you update your project, Suite templates are also being updated. To be able to generate the correct code for your project, the version of `Volo.Abp.Commercial.SuiteTemplates` must be the same as your Suite version. @@ -29,7 +31,10 @@ There's a naming convention for the template files. * If the template name has `Server` prefix, it's used for backend code like repositories, application services, localizations, controllers, permissions, mappings, unit tests. * If the template name has `Frontend.Angular` prefix, it's used for Angular code generation. The Angular code is being generated via [Angular Schematics](https://angular.dev/tools/cli/schematics). * If the template name has `Frontend.Mvc` prefix, it's used for razor pages, menus, JavaScript, CSS files. -* If the template name has `Frontend.Blazor` prefix, it's used for razor components. +* If the template name has `Frontend.Blazor` prefix (no `.MudBlazor` segment), it's used for Blazorise-based razor components. +* If the template name has `Frontend.Blazor.MudBlazor` prefix, it's used for [MudBlazor](https://mudblazor.com)-based razor components. + +> The template list only shows the variant that matches your solution's [Blazor UI library](index.md#blazor-ui-library) (Blazorise or MudBlazor), so you never see both trees side-by-side and the "outdated" check only flags the active set. ## How do I edit the templates? diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md index 8c605418e8..07f1e6ca91 100644 --- a/docs/en/suite/generating-crud-page.md +++ b/docs/en/suite/generating-crud-page.md @@ -108,7 +108,11 @@ To create a new entity, make sure the *-New entity-* is selected in the **Entity * **Update database:** When you add a new migration, ABP Suite can automatically execute update-database command so that the changes are being applied to the database. -* **Create user interface:** Creates pages, modals, components, `JavaScript`, `CSS ` files and adds the new page to the main menu. If you don't have a requirement to manage the entity via user interface, you can uncheck this option. +* **Create user interface:** Creates pages, modals, components, `JavaScript`, `CSS` files and adds the new page to the main menu. If you don't have a requirement to manage the entity via user interface, you can uncheck this option. + + > **Note:** This option does not generate React UI pages. ABP Suite's UI generation targets Suite-supported UI stacks such as MVC, Blazor and Angular. For React-based solutions, build the UI in the source-owned [React UI](../framework/ui/react/index.md) application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. + + > **Blazor UI library:** For Blazor solutions, the generated UI matches the underlying Blazor component library. Solutions that reference `Volo.Abp.MudBlazorUI` produce pages built around `MudDataGrid`, `MudDialog`, `MudForm` and the rest of the [MudBlazor](https://mudblazor.com) controls; Blazorise-based solutions keep emitting the original `DataGrid` / `Modal` / `Validations` markup. ABP Suite picks the variant automatically by scanning the Blazor project `.csproj` and shows the resolved value in the solution info tooltip — no extra switch is required. See the [Blazor UI library](index.md#blazor-ui-library) section for details. * **Excel export**: Creates a button that exports a list of all the data that were added to the entity to an Excel file. diff --git a/docs/en/suite/index.md b/docs/en/suite/index.md index b1c8d43fa7..dfeaca8cb4 100644 --- a/docs/en/suite/index.md +++ b/docs/en/suite/index.md @@ -21,6 +21,22 @@ ABP Suite is a complementary tool to the ABP Platform. ABP Suite allows you to build web pages in a matter of minutes. +> **Important:** ABP Suite does not generate React UI pages or client-side React code. Its CRUD UI generation is designed for the established Suite-supported UI stacks, such as MVC, Blazor and Angular. The new [React UI](../framework/ui/react/index.md) belongs to ABP's modern template system and is designed for an AI-first, AI-oriented development flow with a modern frontend stack such as React, TypeScript, Vite, TanStack Router, TanStack Query, shadcn/ui, Zod and React Hook Form. In React UI solutions, ABP Suite can still be useful for the backend and application layers where applicable, but the React UI side is expected to be developed in the source-owned React application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages and evolve the UI more easily. + It's a .NET Core Global tool that can be installed from the command line. If you are using [ABP Studio](../studio/index.md), you don't even need to install it because it should already be installed, when you first installed the [ABP Studio](../studio/index.md). By using the ABP Suite, you can generate CRUD pages from the database to the front-end and directly get a kickstart for your application. ABP Suite is actively developed and new features are being added version by version according to the roadmap and your feedback. + +## Blazor UI library + +When the target solution uses any Blazor UI framework (Blazor, BlazorServer, BlazorWebApp or MAUIBlazor), ABP Suite detects the underlying Blazor component library by scanning the Blazor project `.csproj` for known package references: + +* A reference to `Volo.Abp.MudBlazorUI`, `Volo.Abp.AspNetCore.Components.*.Theming.MudBlazor` or `*.Blazor.MudBlazor*` selects the **MudBlazor** templates. +* Otherwise ABP Suite falls back to the **Blazorise** templates that have always shipped with Suite. + +The detected value is shown in the *solution info* tooltip of the CRUD Page Generator screen and is reused for two things: + +1. Template routing — `Frontend.Blazor.*` resource names are resolved to `Frontend.Blazor.MudBlazor.*` for MudBlazor solutions, so the generated `.razor` / `.razor.cs` files use `MudCard`, `MudDataGrid`, `MudDialog`, `MudForm` and the corresponding Mud input controls instead of the Blazorise `Card`, `DataGrid`, `Modal`, `Validations` markup. +2. Template Management UI — the "Manage Templates" screen and the "outdated templates" check only list the variant that matches the current solution, so the Blazorise and MudBlazor template trees never appear side-by-side. + +Customized templates are stored under `.suite/customized-templates/` keyed by the full resource name, so MudBlazor customizations (`Frontend.Blazor.MudBlazor.*`) are physically isolated from Blazorise customizations (`Frontend.Blazor.*`) and never clash. diff --git a/docs/en/testing/ui-tests.md b/docs/en/testing/ui-tests.md index ac9e6eb91f..f8256ac2b1 100644 --- a/docs/en/testing/ui-tests.md +++ b/docs/en/testing/ui-tests.md @@ -34,6 +34,7 @@ See the following documents to learn Non Visual UI Testing: * [Testing in ASP.NET Core MVC / Razor Pages](../framework/ui/mvc-razor-pages/testing.md) * [Testing in Angular](../framework/ui/angular/testing.md) * [Testing in Blazor](../framework/ui/blazor/testing.md) +* [Testing in React](../framework/ui/react/unit-testing.md) ## Visual Tests diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-05.md b/docs/en/tutorials/book-store-with-abp-suite/part-05.md index 73a1d9c348..865658d186 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-05.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-05.md @@ -11,7 +11,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp","NG","MAUIBlazor"], - "DB": ["EF", "Mongo"] + "DB": ["EF", "Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` diff --git a/docs/en/tutorials/book-store/part-02.md b/docs/en/tutorials/book-store/part-02.md index 1fe21c2848..429d7ec17c 100644 --- a/docs/en/tutorials/book-store/part-02.md +++ b/docs/en/tutorials/book-store/part-02.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp", "NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` ````json @@ -549,6 +550,8 @@ When you click on the Books menu item under the Book Store parent, you will be r ### Book List +{{if BlazorUI == "Blazorise"}} + We will use the [Blazorise library](https://blazorise.com/) as the UI component kit. It is a very powerful library that supports major HTML/CSS frameworks, including Bootstrap. ABP provides a generic base class - `AbpCrudPageBase<...>`, to create CRUD style pages. This base class is compatible with the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpCrudPageBase` to automate the code behind for the standard CRUD stuff. @@ -624,6 +627,81 @@ Open the `Books.razor` and replace the content as the following: While the code above is pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +We will use the [MudBlazor library](https://mudblazor.com/) as the UI component kit. It is a Material Design component library built natively for Blazor. + +ABP provides a generic base class — `AbpMudCrudPageBase<...>`, to create CRUD style pages. This base class is compatible with the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpMudCrudPageBase` to automate the code behind for the standard CRUD stuff. + +Open the `Books.razor` and replace the content as the following: + +````razor +@page "/books" +@using Volo.Abp.Application.Dtos +@using Acme.BookStore.Books +@using Acme.BookStore.Localization +@inherits AbpMudCrudPageBase + + + + + @L["Books"] + + + + + + + + + @L[$"Enum:BookType.{(int)context.Item.Type}"] + + + + + @context.Item.PublishDate.ToShortDateString() + + + + + + @context.Item.CreationTime.ToLongDateString() + + + + + + + +@code +{ + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + } +} +```` + +> If you see some syntax errors, you can ignore them if your application is properly built and running. Visual Studio still has some bugs with Blazor. + +* Inherited from `AbpMudCrudPageBase` which implements all the CRUD details for us. +* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class. +* `LocalizationResource` is set to the `BookStoreResource` to localize the texts. +* This page uses the standard MudBlazor `MudDataGrid` with `` definitions. ABP also ships an `AbpMudExtensibleDataGrid` that integrates with the [data table column extension system](../../framework/ui/blazor/data-table-column-extensions.md) when you need to extend module pages. + +While the code above is pretty easy to understand, you can check the MudBlazor [Card](https://mudblazor.com/components/card) and [DataGrid](https://mudblazor.com/components/datagrid) documents to understand them better. + +{{end}} + #### About the AbpCrudPageBase We will continue benefitting from `AbpCrudPageBase` for the books page. You could just inject the `IBookAppService` and perform all the server side calls yourself (thanks to the [Dynamic C# HTTP API Client Proxy](../../framework/api-development/dynamic-csharp-clients.md) system of the ABP). We will do it manually for the authors page to demonstrate how to call the server side HTTP APIs in your Blazor applications. diff --git a/docs/en/tutorials/book-store/part-03.md b/docs/en/tutorials/book-store/part-03.md index 73b2daea4e..259475b27d 100644 --- a/docs/en/tutorials/book-store/part-03.md +++ b/docs/en/tutorials/book-store/part-03.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -1094,6 +1095,8 @@ In this section, you will learn how to create a new modal dialog form to create ### Add a "New Button" Button +{{if BlazorUI == "Blazorise"}} + Open the `Books.razor` and replace the `` section with the following code: ````xml @@ -1110,6 +1113,27 @@ Open the `Books.razor` and replace the `` section with the following ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Open the `Books.razor` and replace the `` section with the following code: + +````razor + + + @L["Books"] + + + @L["NewBook"] + + +```` + +{{end}} + This will change the card header by adding a "New book" button to the right side: ![blazor-add-book-button](./images/blazor-add-book-button-2.png) @@ -1118,6 +1142,8 @@ Now, we can add a modal that will be opened when we click the button. ### Book Creation Modal +{{if BlazorUI == "Blazorise"}} + Open the `Books.razor` and add the following code to the end of the page: ````xml @@ -1184,6 +1210,57 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper` at * The form implements validation and the `AbpBlazorMessageLocalizerHelper` is used to simply localize the validation messages. * The `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` methods are defined by the base class. Check out the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Open the `Books.razor` and add the following code to the end of the page: + +````razor + + + @L["NewBook"] + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + +```` + +* The form uses `[Required]`/DataAnnotations for validation; messages are localized via the same `AbpResource` localization system. +* The `_createDialog` field, `CloseCreateDialogAsync`, `CreateFormRef` and `CreateEntityAsync` are all defined in `AbpMudCrudPageBase`. Check the [MudBlazor documentation](https://mudblazor.com/components/dialog) if you want to understand the `MudDialog` and other components. +* `MudDialog.Options` widens the dialog (`MaxWidth.Medium` + `FullWidth`) so the form fields are not cramped. +* `MudStack` with `Spacing="3"` keeps the inputs visually separated; without it MudBlazor inputs render flush against each other. +* `MudDatePicker.@bind-Date` requires a nullable `DateTime?`. If your DTO uses non-nullable `DateTime`, change it to `DateTime?` (`public DateTime? PublishDate { get; set; }`) when using the MudBlazor variant. + +{{end}} + That's all. Run the application and try to add a new book: ![blazor-new-book-modal](./images/blazor-new-book-modal-2.png) @@ -1194,6 +1271,8 @@ Editing a book is similar to creating a new book. ### Actions Dropdown +{{if BlazorUI == "Blazorise"}} + Open the `Books.razor` and add the following `DataGridEntityActionsColumn` section inside the `DataGridColumns` as the first item: ````xml @@ -1212,12 +1291,38 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it: +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Open the `Books.razor` and add the following `TemplateColumn` as the first column inside the `` section of the `MudDataGrid`: + +````razor + + + + + @L["Edit"] + + + + +```` + +* `OpenEditDialogAsync` is defined in the base class which takes the entity (book) to edit. + +This renders an "Actions" dropdown menu (`MudMenu`) for each row in the data grid. We will add the **Delete** menu item later in the *Deleting a Book* section. + +{{end}} + ![blazor-edit-book-action](./images/blazor-edit-book-action-3.png) ### Edit Modal We can now define a modal to edit the book. Add the following code to the end of the `Books.razor` page: +{{if BlazorUI == "Blazorise"}} + ````xml @@ -1273,6 +1378,49 @@ We can now define a modal to edit the book. Add the following code to the end of ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + + + @EditingEntity.Name + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + +```` + +{{end}} + ### Mapperly Configuration The base `AbpCrudPageBase` uses the [object to object mapping](../../framework/infrastructure/object-to-object-mapping.md) system to convert an incoming `BookDto` object to a `CreateUpdateBookDto` object. So, we need to define the mapping. @@ -1304,7 +1452,11 @@ You can now run the application and try to edit a book. ## Deleting a Book -Open the `Books.razor` page and add the following `EntityAction` code under the "Edit" action inside `EntityActions`: +Open the `Books.razor` page and add the following entity action code under the "Edit" action. + +{{if BlazorUI == "Blazorise"}} + +Add the following `EntityAction` code under the "Edit" action inside `EntityActions`: ````xml ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Add the following `MudMenuItem` after the "Edit" item inside the actions `MudMenu`: + +````razor + + @L["Delete"] + +```` + +{{end}} + * `DeleteEntityAsync` is defined in the base class that deletes the entity by performing a call to the server. * `ConfirmationMessage` is a callback to show a confirmation message before executing the action. * `GetDeleteConfirmationMessage` is defined in the base class. You can override this method (or pass another value to the `ConfirmationMessage` parameter) to customize the localization message. @@ -1327,6 +1498,8 @@ Run the application and try to delete a book. Here's the complete code to create the book management CRUD page, that has been developed in the last two parts: +{{if BlazorUI == "Blazorise"}} + ````xml @page "/books" @using Volo.Abp.Application.Dtos @@ -1521,3 +1694,142 @@ Here's the complete code to create the book management CRUD page, that has been {{end}} +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/books" +@using Volo.Abp.Application.Dtos +@using Acme.BookStore.Books +@using Acme.BookStore.Localization +@using Microsoft.Extensions.Localization +@inherits AbpMudCrudPageBase + + + + + @L["Books"] + + + @L["NewBook"] + + + + + + + + + @L["Edit"] + @L["Delete"] + + + + + + + @L[$"Enum:BookType.{(int)context.Item.Type}"] + + + + + @context.Item.PublishDate.ToShortDateString() + + + + + + @context.Item.CreationTime.ToLongDateString() + + + + + + + + + + @L["NewBook"] + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + + + + + @EditingEntity.Name + + + + + + + @foreach (BookType bookTypeValue in Enum.GetValues(typeof(BookType))) + { + @L[$"Enum:BookType.{(int)bookTypeValue}"] + } + + + + + + + + @L["Cancel"] + @L["Save"] + + + +@code +{ + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + } +} +```` + +{{end}} + +{{end}} + diff --git a/docs/en/tutorials/book-store/part-09.md b/docs/en/tutorials/book-store/part-09.md index b70fd3e875..22c144dd14 100644 --- a/docs/en/tutorials/book-store/part-09.md +++ b/docs/en/tutorials/book-store/part-09.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -837,6 +838,8 @@ That's all! This is a fully working CRUD page, you can create, edit and delete a Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project with the following content: +{{if BlazorUI == "Blazorise"}} + ````xml @page "/authors" @using Acme.BookStore.Authors @@ -1017,11 +1020,133 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B
```` -* This code is similar to the `Books.razor`, except it doesn't inherit from the `AbpCrudPageBase`, but uses its own implementation. +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@page "/authors" +@using Acme.BookStore.Authors +@using Acme.BookStore.Localization +@inherits BookStoreComponentBase +@inject IAuthorAppService AuthorAppService + + + + + @L["Authors"] + + + @if (CanCreateAuthor) + { + + @L["NewAuthor"] + + } + + + + + + + + + @if (CanEditAuthor) + { + + @L["Edit"] + + } + @if (CanDeleteAuthor) + { + + @L["Delete"] + + } + + + + + + + @context.Item.BirthDate.ToShortDateString() + + + + + + + + + + @L["NewAuthor"] + + + + + + + + + + + + @L["Cancel"] + + @L["Save"] + + + + + + + @EditingAuthor.Name + + + + + + + + + + + + @L["Cancel"] + + @L["Save"] + + + +```` + +{{end}} + +* This code is similar to the `Books.razor`, except it doesn't inherit from the `AbpCrudPageBase`/`AbpMudCrudPageBase`, but uses its own implementation. * Injects the `IAuthorAppService` to consume the server side HTTP APIs from the UI. We can directly inject application service interfaces and use just like regular method calls by the help of [Dynamic C# HTTP API Client Proxy System](../../framework/api-development/dynamic-csharp-clients.md), which performs REST API calls for us. See the `Authors` class below to see the usage. Create a new code behind file, `Authors.razor.cs`, under the `Pages` folder, with the following content: +{{if BlazorUI == "Blazorise"}} + ````csharp using System; using System.Collections.Generic; @@ -1195,6 +1320,197 @@ public partial class Authors } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Permissions; +using Microsoft.AspNetCore.Authorization; +using MudBlazor; +using Volo.Abp.Application.Dtos; + +{{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor.Pages;{{ else if UI == "MAUIBlazor" }}namespace Acme.BookStore.MauiBlazor.Pages;{{ else }}namespace Acme.BookStore.Blazor.Client.Pages;{{ end }} + +public partial class Authors +{ + private IReadOnlyList AuthorList { get; set; } + + private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; + private int CurrentPage { get; set; } + private string CurrentSorting { get; set; } + private int TotalCount { get; set; } + + private bool CanCreateAuthor { get; set; } + private bool CanEditAuthor { get; set; } + private bool CanDeleteAuthor { get; set; } + + private CreateAuthorDto NewAuthor { get; set; } + + private Guid EditingAuthorId { get; set; } + private UpdateAuthorDto EditingAuthor { get; set; } + + private MudDialog CreateAuthorDialog { get; set; } + private MudDialog EditAuthorDialog { get; set; } + + private MudForm CreateFormRef; + private MudForm EditFormRef; + + // MudDatePicker requires nullable DateTime, while AuthorDto.BirthDate is non-nullable. + // Bind to a nullable wrapper and sync back to the DTO before saving. + private DateTime? NewAuthorBirthDate + { + get => NewAuthor?.BirthDate; + set { if (NewAuthor != null && value.HasValue) NewAuthor.BirthDate = value.Value; } + } + + private DateTime? EditingAuthorBirthDate + { + get => EditingAuthor?.BirthDate; + set { if (EditingAuthor != null && value.HasValue) EditingAuthor.BirthDate = value.Value; } + } + + public Authors() + { + NewAuthor = new CreateAuthorDto(); + EditingAuthor = new UpdateAuthorDto(); + } + + protected override async Task OnInitializedAsync() + { + await SetPermissionsAsync(); + await GetAuthorsAsync(); + } + + private async Task SetPermissionsAsync() + { + CanCreateAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Create); + + CanEditAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Edit); + + CanDeleteAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Delete); + } + + private async Task GetAuthorsAsync() + { + var result = await AuthorAppService.GetListAsync( + new GetAuthorListDto + { + MaxResultCount = PageSize, + SkipCount = CurrentPage * PageSize, + Sorting = CurrentSorting + } + ); + + AuthorList = result.Items; + TotalCount = (int)result.TotalCount; + } + + private async Task> OnDataGridReadAsync(GridState state) + { + CurrentSorting = state.SortDefinitions + .Where(s => !string.IsNullOrWhiteSpace(s.SortBy?.ToString())) + .Select(s => $"{s.SortBy}{(s.Descending ? " DESC" : "")}") + .JoinAsString(","); + CurrentPage = state.Page; + + await GetAuthorsAsync(); + + return new GridData { Items = AuthorList, TotalItems = TotalCount }; + } + + private async Task OpenCreateAuthorDialogAsync() + { + NewAuthor = new CreateAuthorDto(); + if (CreateFormRef != null) await CreateFormRef.ResetAsync(); + await InvokeAsync(() => CreateAuthorDialog.ShowAsync()); + } + + private Task CloseCreateAuthorDialogAsync() + { + return InvokeAsync(() => CreateAuthorDialog.CloseAsync()); + } + + private async Task OpenEditAuthorDialogAsync(AuthorDto author) + { + EditingAuthorId = author.Id; + EditingAuthor = ObjectMapper.Map(author); + if (EditFormRef != null) await EditFormRef.ResetAsync(); + await InvokeAsync(() => EditAuthorDialog.ShowAsync()); + } + + private Task CloseEditAuthorDialogAsync() + { + return InvokeAsync(() => EditAuthorDialog.CloseAsync()); + } + + private async Task DeleteAuthorAsync(AuthorDto author) + { + try + { + var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; + if (!await Message.Confirm(confirmMessage)) + { + return; + } + + await AuthorAppService.DeleteAsync(author.Id); + await GetAuthorsAsync(); + } + catch(Exception ex) + { + await HandleErrorAsync(ex); + } + } + + private async Task CreateAuthorAsync() + { + try + { + await CreateFormRef.Validate(); + if (CreateFormRef.IsValid) + { + await AuthorAppService.CreateAsync(NewAuthor); + await GetAuthorsAsync(); + await InvokeAsync(() => CreateAuthorDialog.CloseAsync()); + } + } + catch(Exception ex) + { + await HandleErrorAsync(ex); + } + } + + private async Task UpdateAuthorAsync() + { + try + { + await EditFormRef.Validate(); + if (EditFormRef.IsValid) + { + await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); + await GetAuthorsAsync(); + await InvokeAsync(() => EditAuthorDialog.CloseAsync()); + } + } + catch(Exception ex) + { + await HandleErrorAsync(ex); + } + } +} +```` + +{{end}} + This class typically defines the properties and methods used by the `Authors.razor` page. ### Object Mapping diff --git a/docs/en/tutorials/book-store/part-10.md b/docs/en/tutorials/book-store/part-10.md index e03ada5a96..f8ff85e5d5 100644 --- a/docs/en/tutorials/book-store/part-10.md +++ b/docs/en/tutorials/book-store/part-10.md @@ -10,7 +10,8 @@ //[doc-params] { "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], - "DB": ["EF","Mongo"] + "DB": ["EF","Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -1115,7 +1116,9 @@ That's all. Just run the application and try to create or edit an author. ### The Book List -It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following `DataGridColumn` definition just after the `Name` (book name) column: +It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following column definition just after the `Name` (book name) column: + +{{if BlazorUI == "Blazorise"}} ````xml ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + +```` + +{{end}} + When you run the application, you can see the *Author* column on the table: ![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors-2.png) @@ -1147,6 +1160,8 @@ protected override async Task OnInitializedAsync() * It is essential to call the `base.OnInitializedAsync()` since `AbpCrudPageBase` has some initialization code to be executed. +{{if BlazorUI == "Blazorise"}} + Override the `OpenCreateModalAsync` method and adding the following code: ````csharp @@ -1199,7 +1214,67 @@ The final `@code` block should be the following: } ```` -Finally, add the following `Field` definition into the `ModalBody` of the *Create* modal, as the first item, before the `Name` field: +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Override the `OpenCreateDialogAsync` method and adding the following code: + +````csharp +protected override async Task OpenCreateDialogAsync() +{ + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateDialogAsync(); + NewEntity.AuthorId = authorList.First().Id; +} +```` + +The final `@code` block should be the following: + +````csharp +@code +{ + //ADDED A NEW FIELD + IReadOnlyList authorList = Array.Empty(); + + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Delete; + } + + //GET AUTHORS ON INITIALIZATION + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + authorList = (await AppService.GetAuthorLookupAsync()).Items; + } + + protected override async Task OpenCreateDialogAsync() + { + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateDialogAsync(); + NewEntity.AuthorId = authorList.First().Id; + } +} +```` + +{{end}} + +Finally, add the following field definition into the *Create* modal/dialog, as the first item, before the `Name` field: + +{{if BlazorUI == "Blazorise"}} ````xml @@ -1215,6 +1290,21 @@ Finally, add the following `Field` definition into the `ModalBody` of the *Creat ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + + @foreach (var author in authorList) + { + @author.Name + } + +```` + +{{end}} + This requires to add a new localization key to the `en.json` file: ````js @@ -1227,7 +1317,9 @@ You can run the application to see the *Author Selection* while creating a new b ### Edit Book Modal -Add the following `Field` definition into the `ModalBody` of the *Edit* modal, as the first item, before the `Name` field: +Add the following field definition into the *Edit* modal/dialog, as the first item, before the `Name` field: + +{{if BlazorUI == "Blazorise"}} ````xml @@ -1243,6 +1335,21 @@ Add the following `Field` definition into the `ModalBody` of the *Edit* modal, a ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor + + @foreach (var author in authorList) + { + @author.Name + } + +```` + +{{end}} + That's all. We are reusing the `authorList` defined for the *Create* modal. {{end}} diff --git a/docs/en/tutorials/modular-crm/part-03.md b/docs/en/tutorials/modular-crm/part-03.md index e0db36d617..d34861a7ab 100644 --- a/docs/en/tutorials/modular-crm/part-03.md +++ b/docs/en/tutorials/modular-crm/part-03.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["MVC", "BlazorWebApp", "NG"] + "UI": ["MVC", "BlazorWebApp", "NG"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -515,6 +516,8 @@ Open the `ModularCrm.Catalog` .NET solution in your IDE, and find the `Pages/Cat Replace the `Index.razor` file with the following content: +{{if BlazorUI == "Blazorise"}} + ````razor @page "/catalog" @using System.Collections.Generic @@ -547,6 +550,44 @@ Replace the `Index.razor` file with the following content: } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````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(); + } +} +```` + +{{end}} + Here, you inject `IProductAppService`, get all products in `OnInitializedAsync`, and then render the result in a simple list. {{end}} diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index 4387ff6908..4f5b4fc2b5 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["MVC", "BlazorWebApp", "NG"] + "UI": ["MVC", "BlazorWebApp", "NG"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -525,6 +526,8 @@ public class OrderingMenuContributor : IMenuContributor Replace the `Index.razor` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.Blazor` project with the following code block: +{{if BlazorUI == "Blazorise"}} + ````razor @page "/ordering" @using System.Collections.Generic @@ -559,6 +562,46 @@ Replace the `Index.razor` content in the `Pages/Ordering` folder of the `Modular } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````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(); + } +} +```` + +{{end}} + 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 diff --git a/docs/en/tutorials/modular-crm/part-06.md b/docs/en/tutorials/modular-crm/part-06.md index 31cc0cb48c..1065e5df6e 100644 --- a/docs/en/tutorials/modular-crm/part-06.md +++ b/docs/en/tutorials/modular-crm/part-06.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["MVC", "BlazorWebApp", "NG"] + "UI": ["MVC", "BlazorWebApp", "NG"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -296,6 +297,8 @@ As you can see, we can see the product names instead of product IDs. 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: +{{if BlazorUI == "Blazorise"}} + ````razor @page "/ordering" @using System.Collections.Generic @@ -330,6 +333,46 @@ Open the `Index.razor` file, and change the `@order.ProductId` part to `@order.P } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````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/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index c8d95b627c..f0616c7765 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -11,7 +11,8 @@ //[doc-params] { "UI": ["MVC", "Blazor", "BlazorServer", "BlazorWebApp" ,"NG", "MAUIBlazor"], - "DB": ["EF", "Mongo"] + "DB": ["EF", "Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -637,6 +638,8 @@ See the *Dynamic C# Proxies & Auto API Controllers* section below to learn how w Open the `Index.razor` file in the `Pages` folder of the {{if UI=="Blazor" || UI=="BlazorWebApp"}} *TodoApp.Blazor.Client* {{else if UI=="BlazorServer"}} *TodoApp.Blazor* {{else if UI=="MAUIBlazor"}} *TodoApp.MauiBlazor* {{end}} project and replace the content with the following code block: +{{if BlazorUI == "Blazorise"}} + ```xml @page "/" @inherits TodoAppComponentBase @@ -675,6 +678,47 @@ Open the `Index.razor` file in the `Pages` folder of the {{if UI=="Blazor" || UI ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@page "/" +@inherits TodoAppComponentBase + + + + + + TODO LIST + + + + + + + Submit + + + + @foreach (var todoItem in TodoItems) + { + + + @todoItem.Text + + } + + + + +``` + +{{end}} + ### Index.razor.css As the final touch, open the `Index.razor.css` file in the `Pages` folder of the {{if UI=="Blazor" || UI=="BlazorWebApp"}}*TodoApp.Blazor.Client*{{else if UI=="BlazorServer"}} *TodoApp.Blazor* {{else if UI=="MAUIBlazor"}} *TodoApp.MauiBlazor* {{end}} project and add the following content: diff --git a/docs/en/tutorials/todo/single-layer/index.md b/docs/en/tutorials/todo/single-layer/index.md index 6a7f45ce62..b62abfb2b9 100644 --- a/docs/en/tutorials/todo/single-layer/index.md +++ b/docs/en/tutorials/todo/single-layer/index.md @@ -11,7 +11,8 @@ //[doc-params] { "UI": ["MVC", "Blazor", "BlazorServer", "NG"], - "DB": ["EF", "Mongo"] + "DB": ["EF", "Mongo"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -621,6 +622,8 @@ This class uses the {{if UI=="Blazor"}}`ITodoAppService`{{else}}`TodoAppService` Open the `Index.razor` file in the {{if UI=="BlazorServer"}}`Components/Pages`{{else}}`Pages`{{end}} folder and replace the content with the following code block: +{{if BlazorUI == "Blazorise"}} + ```xml @page "/" @inherits TodoAppComponentBase @@ -660,6 +663,47 @@ Open the `Index.razor` file in the {{if UI=="BlazorServer"}}`Components/Pages`{{ ``` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +```razor +@page "/" +@inherits TodoAppComponentBase + + + + + + TODO LIST + + + + + + + Submit + + + + @foreach (var todoItem in TodoItems) + { + + + @todoItem.Text + + } + + + + +``` + +{{end}} + ### Index.razor.css As the final touch, open the `Index.razor.css` file in the {{if UI=="BlazorServer"}}`Components/Pages`{{else}}`Pages`{{end}} folder and add the following code block at the end of the file: diff --git a/docs/en/ui-themes/basic-theme/index.md b/docs/en/ui-themes/basic-theme/index.md index c87491d1b4..71ee739721 100644 --- a/docs/en/ui-themes/basic-theme/index.md +++ b/docs/en/ui-themes/basic-theme/index.md @@ -19,6 +19,8 @@ See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to lea The Basic Theme has implementation for the following UI types: - [MVC UI](../../framework/ui/mvc-razor-pages/basic-theme.md) -- [Blazor UI](../../framework/ui/blazor/basic-theme.md) +- [Blazor UI](../../framework/ui/blazor/basic-theme.md) — available in two Blazor UI library variants: + - **Blazorise** (default): `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.BasicTheme` + - **MudBlazor**: `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorBasicTheme` - [Angular UI](../../framework/ui/angular/basic-theme.md) diff --git a/docs/en/ui-themes/index.md b/docs/en/ui-themes/index.md index 2beaa0dc05..36df6270ae 100644 --- a/docs/en/ui-themes/index.md +++ b/docs/en/ui-themes/index.md @@ -33,6 +33,15 @@ See the following documents based on the UI type you are using: - [Basic Theme - Blazor UI](../framework/ui/blazor/basic-theme.md) - [Basic Theme - Angular UI](../framework/ui/angular/basic-theme.md) +## Blazor UI Library + +When you create an ABP solution with a Blazor host (Blazor Server, Blazor WebAssembly or Blazor WebApp), you can additionally choose the underlying Blazor component library: + +* **Blazorise** — the original ABP default, based on Bootstrap. +* **MudBlazor** — a Material-Design component library, available as an alternative variant. Each official theme has a MudBlazor version (e.g. `MudBlazorLeptonXTheme`, `MudBlazorLeptonXLiteTheme`, `MudBlazorBasicTheme`). + +The choice is made at solution creation time via the `--blazor-ui-library` option (`abp new ... -bul mudblazor`) or in the ABP Studio new-solution wizard. + ## See Also * [Theming - MVC UI](../framework/ui/mvc-razor-pages/theming.md) diff --git a/docs/en/ui-themes/lepton-x-lite/blazor.md b/docs/en/ui-themes/lepton-x-lite/blazor.md index 7190ac1fc2..6b15e1f134 100644 --- a/docs/en/ui-themes/lepton-x-lite/blazor.md +++ b/docs/en/ui-themes/lepton-x-lite/blazor.md @@ -10,7 +10,8 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` @@ -20,6 +21,19 @@ LeptonX Lite has implementation for the ABP Blazor WebAssembly & Blazor Server. > See the [Theming document](../../framework/ui/mvc-razor-pages/theming.md) to learn about themes. +{{if BlazorUI == "MudBlazor"}} + +> **MudBlazor Variant** — When the `--blazor-ui-library mudblazor` option is used, the LeptonX Lite theme ships as a MudBlazor variant. Replace `LeptonXLiteTheme` with `MudBlazorLeptonXLiteTheme` everywhere in this document (package names, module type names and namespaces). The installation steps, layout customization API and override mechanism are the same; only the package and namespace prefix change. The component implementations use MudBlazor primitives (`MudAppBar`, `MudDrawer`, `MudNavLink`, etc.) instead of Blazorise components. +> +> Concrete package names you will see when using the MudBlazor variant: +> +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXLiteTheme` +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXLiteTheme.Bundling` +> * Module types: `Abp{...}MudBlazorLeptonXLiteThemeModule`, `Abp{...}MudBlazorLeptonXLiteThemeBundlingModule` +> * Layout namespace: `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXLiteTheme.Themes.MudBlazorLeptonXLite` + +{{end}} + ## Installation This theme is **already installed** when you create a new solution using the startup templates. If you are using any other template, you can install this theme by following the steps below: diff --git a/docs/en/ui-themes/lepton-x/blazor.md b/docs/en/ui-themes/lepton-x/blazor.md index 29e8deb6c3..07d07eea00 100644 --- a/docs/en/ui-themes/lepton-x/blazor.md +++ b/docs/en/ui-themes/lepton-x/blazor.md @@ -10,12 +10,26 @@ ````json //[doc-params] { - "UI": ["Blazor", "BlazorServer"] + "UI": ["Blazor", "BlazorServer"], + "BlazorUI": ["Blazorise", "MudBlazor"] } ```` LeptonX theme is implemented and ready to use with ABP. No custom implementation is needed for Blazor Server & WebAssembly. +{{if BlazorUI == "MudBlazor"}} + +> **MudBlazor Variant** — When the `--blazor-ui-library mudblazor` option is used, the LeptonX theme ships as a MudBlazor variant. Replace `LeptonXTheme` with `MudBlazorLeptonXTheme` everywhere in this document (package names, module type names and namespaces). The installation steps, layout customization API and override mechanism are the same; only the package and namespace prefix change. Component implementations use MudBlazor primitives (`MudAppBar`, `MudDrawer`, `MudNavLink`, `MudMenu`, etc.) instead of Blazorise components. +> +> Concrete package names you will see when using the MudBlazor variant: +> +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXTheme` +> * `Volo.Abp.AspNetCore.Components.{Server,WebAssembly}.MudBlazorLeptonXTheme.Bundling` +> * Module types: `Abp{...}MudBlazorLeptonXThemeModule`, `Abp{...}MudBlazorLeptonXThemeBundlingModule` +> * Layout components live under `Volo.Abp.AspNetCore.Components.{Web,Server,WebAssembly}.MudBlazorLeptonXTheme.Components.ApplicationLayout` (with `SideMenu`, `TopMenu`, and `PublicWebsiteLayout` sub-namespaces). + +{{end}} + ## Installation {{if UI == "Blazor"}} diff --git a/framework/Volo.Abp.abpmdl b/framework/Volo.Abp.abpmdl index afc52ca801..0935fedade 100644 --- a/framework/Volo.Abp.abpmdl +++ b/framework/Volo.Abp.abpmdl @@ -478,6 +478,10 @@ "path": "test/Volo.Abp.ObjectMapping.Tests/Volo.Abp.ObjectMapping.Tests.abppkg", "folder": "test" }, + "Volo.Abp.RabbitMQ.Tests": { + "path": "test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg", + "folder": "test" + }, "Volo.Abp.Ddd.Application.Contracts": { "path": "src/Volo.Abp.Ddd.Application.Contracts/Volo.Abp.Ddd.Application.Contracts.abppkg", "folder": "src" diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index fa76c5930d..5f4adbd622 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -10,13 +10,19 @@ + + + + + + @@ -137,6 +143,7 @@ + @@ -241,6 +248,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorBundlingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorBundlingModule.cs new file mode 100644 index 0000000000..f803cd8985 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorBundlingModule.cs @@ -0,0 +1,34 @@ +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule) +)] +public class AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.GlobalAssets.Enabled = true; + options.GlobalAssets.GlobalStyleBundleName = MauiBlazorMudBlazorStandardBundles.Styles.Global; + options.GlobalAssets.GlobalScriptBundleName = MauiBlazorMudBlazorStandardBundles.Scripts.Global; + + options + .StyleBundles + .Add(MauiBlazorMudBlazorStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(MauiBlazorMudBlazorStyleContributor)); + }); + + options + .ScriptBundles + .Add(MauiBlazorMudBlazorStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(MauiBlazorMudBlazorScriptContributor)); + }); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..e6bb89a4a1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/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. + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs new file mode 100644 index 0000000000..e15c4e02ff --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorScriptContributor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling; + +public class MauiBlazorMudBlazorScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/MudBlazor/MudBlazor.min.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-popover-patch.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-ripple-patch.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorStandardBundles.cs new file mode 100644 index 0000000000..54a460d0c4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorStandardBundles.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling; + +public class MauiBlazorMudBlazorStandardBundles +{ + public static class Styles + { + public static string Global = "MauiBlazorMudBlazor.Global"; + } + + public static class Scripts + { + public static string Global = "MauiBlazorMudBlazor.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorStyleContributor.cs new file mode 100644 index 0000000000..c78f1474d2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/MauiBlazorMudBlazorStyleContributor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling; + +public class MauiBlazorMudBlazorStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("_content/MudBlazor/MudBlazor.min.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/volo.abp.mudblazorui.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/libs/flag-icon/css/flag-icon.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling.csproj new file mode 100644 index 0000000000..b33c5a2adf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorModule.cs new file mode 100644 index 0000000000..48cc5a794f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorModule.cs @@ -0,0 +1,15 @@ +using Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.Bundling; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.AspNetCore.Components.MauiBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor; + +[DependsOn( + typeof(AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorBundlingModule), + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpAspNetCoreComponentsMauiBlazorModule) +)] +public class AbpAspNetCoreComponentsMauiBlazorThemingMudBlazorModule : AbpModule +{ +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/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.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.csproj b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.csproj new file mode 100644 index 0000000000..5a24de82c0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.MudBlazor.csproj @@ -0,0 +1,18 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/AbpAspNetCoreComponentsServerThemingMudBlazorModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/AbpAspNetCoreComponentsServerThemingMudBlazorModule.cs new file mode 100644 index 0000000000..7d7a784256 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/AbpAspNetCoreComponentsServerThemingMudBlazorModule.cs @@ -0,0 +1,36 @@ +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; +using Volo.Abp.AspNetCore.Mvc.UI.Packages; + +namespace Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor; + +[DependsOn( + typeof(AbpAspNetCoreComponentsServerModule), + typeof(AbpAspNetCoreMvcUiPackagesModule), + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpAspNetCoreMvcUiBundlingModule) + )] +public class AbpAspNetCoreComponentsServerThemingMudBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .StyleBundles + .Add(BlazorServerMudBlazorStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(BlazorServerMudBlazorStyleContributor)); + }); + + options + .ScriptBundles + .Add(BlazorServerMudBlazorStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(BlazorServerMudBlazorScriptContributor)); + }); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorComponentBundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorComponentBundleManager.cs new file mode 100644 index 0000000000..09a67ef6fa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorComponentBundleManager.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Bundling; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Bundling; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling; + +public class BlazorServerMudBlazorComponentBundleManager : IComponentBundleManager, ITransientDependency +{ + protected IBundleManager BundleManager { get; } + + public BlazorServerMudBlazorComponentBundleManager(IBundleManager bundleManager) + { + BundleManager = bundleManager; + } + + public virtual async Task> GetStyleBundleFilesAsync(string bundleName) + { + return (await BundleManager.GetStyleBundleFilesAsync(bundleName)).Select(f => f.FileName).ToList(); + } + + public virtual async Task> GetScriptBundleFilesAsync(string bundleName) + { + return (await BundleManager.GetScriptBundleFilesAsync(bundleName)).Select(f => f.FileName).ToList(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs new file mode 100644 index 0000000000..d9818564a9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorScriptContributor.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Components.Web; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling; + +public class BlazorServerMudBlazorScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + var options = context.ServiceProvider.GetRequiredService>().Value; + if (!options.IsBlazorWebApp) + { + context.Files.AddIfNotContains("/_framework/blazor.server.js"); + } + context.Files.AddIfNotContains("/_content/MudBlazor/MudBlazor.min.js"); + context.Files.AddIfNotContains("/_content/Volo.Abp.MudBlazorUI/abp-mud-popover-patch.js"); + context.Files.AddIfNotContains("/_content/Volo.Abp.MudBlazorUI/abp-mud-ripple-patch.js"); + context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorStandardBundles.cs new file mode 100644 index 0000000000..1b463f8871 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorStandardBundles.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling; + +public class BlazorServerMudBlazorStandardBundles +{ + public static class Styles + { + public static string Global = "Blazor.Global"; + } + + public static class Scripts + { + public static string Global = "Blazor.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorStyleContributor.cs new file mode 100644 index 0000000000..c2f76d35ed --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Bundling/BlazorServerMudBlazorStyleContributor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling; + +public class BlazorServerMudBlazorStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("/_content/MudBlazor/MudBlazor.min.css"); + context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("/_content/Volo.Abp.MudBlazorUI/volo.abp.mudblazorui.css"); + context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/libs/flag-icon/css/flag-icon.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/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.AspNetCore.Components.Server.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.abppkg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.abppkg new file mode 100644 index 0000000000..e0c33eaab1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.csproj b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.csproj new file mode 100644 index 0000000000..b4464b5367 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.csproj @@ -0,0 +1,20 @@ + + + + + + + net10.0 + enable + Nullable + true + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.css b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.css new file mode 100644 index 0000000000..b54375090b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.css @@ -0,0 +1,1568 @@ +.flag-icon-background { + background-size: contain; + background-position: 50%; + background-repeat: no-repeat; +} +.flag-icon { + background-size: contain; + background-position: 50%; + background-repeat: no-repeat; + position: relative; + display: inline-block; + width: 1.33333333em; + line-height: 1em; +} +.flag-icon:before { + content: "\00a0"; +} +.flag-icon.flag-icon-squared { + width: 1em; +} +.flag-icon-ad { + background-image: url(../flags/4x3/ad.svg); +} +.flag-icon-ad.flag-icon-squared { + background-image: url(../flags/1x1/ad.svg); +} +.flag-icon-ae { + background-image: url(../flags/4x3/ae.svg); +} +.flag-icon-ae.flag-icon-squared { + background-image: url(../flags/1x1/ae.svg); +} +.flag-icon-af { + background-image: url(../flags/4x3/af.svg); +} +.flag-icon-af.flag-icon-squared { + background-image: url(../flags/1x1/af.svg); +} +.flag-icon-ag { + background-image: url(../flags/4x3/ag.svg); +} +.flag-icon-ag.flag-icon-squared { + background-image: url(../flags/1x1/ag.svg); +} +.flag-icon-ai { + background-image: url(../flags/4x3/ai.svg); +} +.flag-icon-ai.flag-icon-squared { + background-image: url(../flags/1x1/ai.svg); +} +.flag-icon-al { + background-image: url(../flags/4x3/al.svg); +} +.flag-icon-al.flag-icon-squared { + background-image: url(../flags/1x1/al.svg); +} +.flag-icon-am { + background-image: url(../flags/4x3/am.svg); +} +.flag-icon-am.flag-icon-squared { + background-image: url(../flags/1x1/am.svg); +} +.flag-icon-ao { + background-image: url(../flags/4x3/ao.svg); +} +.flag-icon-ao.flag-icon-squared { + background-image: url(../flags/1x1/ao.svg); +} +.flag-icon-aq { + background-image: url(../flags/4x3/aq.svg); +} +.flag-icon-aq.flag-icon-squared { + background-image: url(../flags/1x1/aq.svg); +} +.flag-icon-ar { + background-image: url(../flags/4x3/ar.svg); +} +.flag-icon-ar.flag-icon-squared { + background-image: url(../flags/1x1/ar.svg); +} +.flag-icon-as { + background-image: url(../flags/4x3/as.svg); +} +.flag-icon-as.flag-icon-squared { + background-image: url(../flags/1x1/as.svg); +} +.flag-icon-at { + background-image: url(../flags/4x3/at.svg); +} +.flag-icon-at.flag-icon-squared { + background-image: url(../flags/1x1/at.svg); +} +.flag-icon-au { + background-image: url(../flags/4x3/au.svg); +} +.flag-icon-au.flag-icon-squared { + background-image: url(../flags/1x1/au.svg); +} +.flag-icon-aw { + background-image: url(../flags/4x3/aw.svg); +} +.flag-icon-aw.flag-icon-squared { + background-image: url(../flags/1x1/aw.svg); +} +.flag-icon-ax { + background-image: url(../flags/4x3/ax.svg); +} +.flag-icon-ax.flag-icon-squared { + background-image: url(../flags/1x1/ax.svg); +} +.flag-icon-az { + background-image: url(../flags/4x3/az.svg); +} +.flag-icon-az.flag-icon-squared { + background-image: url(../flags/1x1/az.svg); +} +.flag-icon-ba { + background-image: url(../flags/4x3/ba.svg); +} +.flag-icon-ba.flag-icon-squared { + background-image: url(../flags/1x1/ba.svg); +} +.flag-icon-bb { + background-image: url(../flags/4x3/bb.svg); +} +.flag-icon-bb.flag-icon-squared { + background-image: url(../flags/1x1/bb.svg); +} +.flag-icon-bd { + background-image: url(../flags/4x3/bd.svg); +} +.flag-icon-bd.flag-icon-squared { + background-image: url(../flags/1x1/bd.svg); +} +.flag-icon-be { + background-image: url(../flags/4x3/be.svg); +} +.flag-icon-be.flag-icon-squared { + background-image: url(../flags/1x1/be.svg); +} +.flag-icon-bf { + background-image: url(../flags/4x3/bf.svg); +} +.flag-icon-bf.flag-icon-squared { + background-image: url(../flags/1x1/bf.svg); +} +.flag-icon-bg { + background-image: url(../flags/4x3/bg.svg); +} +.flag-icon-bg.flag-icon-squared { + background-image: url(../flags/1x1/bg.svg); +} +.flag-icon-bh { + background-image: url(../flags/4x3/bh.svg); +} +.flag-icon-bh.flag-icon-squared { + background-image: url(../flags/1x1/bh.svg); +} +.flag-icon-bi { + background-image: url(../flags/4x3/bi.svg); +} +.flag-icon-bi.flag-icon-squared { + background-image: url(../flags/1x1/bi.svg); +} +.flag-icon-bj { + background-image: url(../flags/4x3/bj.svg); +} +.flag-icon-bj.flag-icon-squared { + background-image: url(../flags/1x1/bj.svg); +} +.flag-icon-bl { + background-image: url(../flags/4x3/bl.svg); +} +.flag-icon-bl.flag-icon-squared { + background-image: url(../flags/1x1/bl.svg); +} +.flag-icon-bm { + background-image: url(../flags/4x3/bm.svg); +} +.flag-icon-bm.flag-icon-squared { + background-image: url(../flags/1x1/bm.svg); +} +.flag-icon-bn { + background-image: url(../flags/4x3/bn.svg); +} +.flag-icon-bn.flag-icon-squared { + background-image: url(../flags/1x1/bn.svg); +} +.flag-icon-bo { + background-image: url(../flags/4x3/bo.svg); +} +.flag-icon-bo.flag-icon-squared { + background-image: url(../flags/1x1/bo.svg); +} +.flag-icon-bq { + background-image: url(../flags/4x3/bq.svg); +} +.flag-icon-bq.flag-icon-squared { + background-image: url(../flags/1x1/bq.svg); +} +.flag-icon-br { + background-image: url(../flags/4x3/br.svg); +} +.flag-icon-br.flag-icon-squared { + background-image: url(../flags/1x1/br.svg); +} +.flag-icon-bs { + background-image: url(../flags/4x3/bs.svg); +} +.flag-icon-bs.flag-icon-squared { + background-image: url(../flags/1x1/bs.svg); +} +.flag-icon-bt { + background-image: url(../flags/4x3/bt.svg); +} +.flag-icon-bt.flag-icon-squared { + background-image: url(../flags/1x1/bt.svg); +} +.flag-icon-bv { + background-image: url(../flags/4x3/bv.svg); +} +.flag-icon-bv.flag-icon-squared { + background-image: url(../flags/1x1/bv.svg); +} +.flag-icon-bw { + background-image: url(../flags/4x3/bw.svg); +} +.flag-icon-bw.flag-icon-squared { + background-image: url(../flags/1x1/bw.svg); +} +.flag-icon-by { + background-image: url(../flags/4x3/by.svg); +} +.flag-icon-by.flag-icon-squared { + background-image: url(../flags/1x1/by.svg); +} +.flag-icon-bz { + background-image: url(../flags/4x3/bz.svg); +} +.flag-icon-bz.flag-icon-squared { + background-image: url(../flags/1x1/bz.svg); +} +.flag-icon-ca { + background-image: url(../flags/4x3/ca.svg); +} +.flag-icon-ca.flag-icon-squared { + background-image: url(../flags/1x1/ca.svg); +} +.flag-icon-cc { + background-image: url(../flags/4x3/cc.svg); +} +.flag-icon-cc.flag-icon-squared { + background-image: url(../flags/1x1/cc.svg); +} +.flag-icon-cd { + background-image: url(../flags/4x3/cd.svg); +} +.flag-icon-cd.flag-icon-squared { + background-image: url(../flags/1x1/cd.svg); +} +.flag-icon-cf { + background-image: url(../flags/4x3/cf.svg); +} +.flag-icon-cf.flag-icon-squared { + background-image: url(../flags/1x1/cf.svg); +} +.flag-icon-cg { + background-image: url(../flags/4x3/cg.svg); +} +.flag-icon-cg.flag-icon-squared { + background-image: url(../flags/1x1/cg.svg); +} +.flag-icon-ch { + background-image: url(../flags/4x3/ch.svg); +} +.flag-icon-ch.flag-icon-squared { + background-image: url(../flags/1x1/ch.svg); +} +.flag-icon-ci { + background-image: url(../flags/4x3/ci.svg); +} +.flag-icon-ci.flag-icon-squared { + background-image: url(../flags/1x1/ci.svg); +} +.flag-icon-ck { + background-image: url(../flags/4x3/ck.svg); +} +.flag-icon-ck.flag-icon-squared { + background-image: url(../flags/1x1/ck.svg); +} +.flag-icon-cl { + background-image: url(../flags/4x3/cl.svg); +} +.flag-icon-cl.flag-icon-squared { + background-image: url(../flags/1x1/cl.svg); +} +.flag-icon-cm { + background-image: url(../flags/4x3/cm.svg); +} +.flag-icon-cm.flag-icon-squared { + background-image: url(../flags/1x1/cm.svg); +} +.flag-icon-cn { + background-image: url(../flags/4x3/cn.svg); +} +.flag-icon-cn.flag-icon-squared { + background-image: url(../flags/1x1/cn.svg); +} +.flag-icon-co { + background-image: url(../flags/4x3/co.svg); +} +.flag-icon-co.flag-icon-squared { + background-image: url(../flags/1x1/co.svg); +} +.flag-icon-cr { + background-image: url(../flags/4x3/cr.svg); +} +.flag-icon-cr.flag-icon-squared { + background-image: url(../flags/1x1/cr.svg); +} +.flag-icon-cu { + background-image: url(../flags/4x3/cu.svg); +} +.flag-icon-cu.flag-icon-squared { + background-image: url(../flags/1x1/cu.svg); +} +.flag-icon-cv { + background-image: url(../flags/4x3/cv.svg); +} +.flag-icon-cv.flag-icon-squared { + background-image: url(../flags/1x1/cv.svg); +} +.flag-icon-cw { + background-image: url(../flags/4x3/cw.svg); +} +.flag-icon-cw.flag-icon-squared { + background-image: url(../flags/1x1/cw.svg); +} +.flag-icon-cx { + background-image: url(../flags/4x3/cx.svg); +} +.flag-icon-cx.flag-icon-squared { + background-image: url(../flags/1x1/cx.svg); +} +.flag-icon-cy { + background-image: url(../flags/4x3/cy.svg); +} +.flag-icon-cy.flag-icon-squared { + background-image: url(../flags/1x1/cy.svg); +} +.flag-icon-cz { + background-image: url(../flags/4x3/cz.svg); +} +.flag-icon-cz.flag-icon-squared { + background-image: url(../flags/1x1/cz.svg); +} +.flag-icon-de { + background-image: url(../flags/4x3/de.svg); +} +.flag-icon-de.flag-icon-squared { + background-image: url(../flags/1x1/de.svg); +} +.flag-icon-dj { + background-image: url(../flags/4x3/dj.svg); +} +.flag-icon-dj.flag-icon-squared { + background-image: url(../flags/1x1/dj.svg); +} +.flag-icon-dk { + background-image: url(../flags/4x3/dk.svg); +} +.flag-icon-dk.flag-icon-squared { + background-image: url(../flags/1x1/dk.svg); +} +.flag-icon-dm { + background-image: url(../flags/4x3/dm.svg); +} +.flag-icon-dm.flag-icon-squared { + background-image: url(../flags/1x1/dm.svg); +} +.flag-icon-do { + background-image: url(../flags/4x3/do.svg); +} +.flag-icon-do.flag-icon-squared { + background-image: url(../flags/1x1/do.svg); +} +.flag-icon-dz { + background-image: url(../flags/4x3/dz.svg); +} +.flag-icon-dz.flag-icon-squared { + background-image: url(../flags/1x1/dz.svg); +} +.flag-icon-ec { + background-image: url(../flags/4x3/ec.svg); +} +.flag-icon-ec.flag-icon-squared { + background-image: url(../flags/1x1/ec.svg); +} +.flag-icon-ee { + background-image: url(../flags/4x3/ee.svg); +} +.flag-icon-ee.flag-icon-squared { + background-image: url(../flags/1x1/ee.svg); +} +.flag-icon-eg { + background-image: url(../flags/4x3/eg.svg); +} +.flag-icon-eg.flag-icon-squared { + background-image: url(../flags/1x1/eg.svg); +} +.flag-icon-eh { + background-image: url(../flags/4x3/eh.svg); +} +.flag-icon-eh.flag-icon-squared { + background-image: url(../flags/1x1/eh.svg); +} +.flag-icon-er { + background-image: url(../flags/4x3/er.svg); +} +.flag-icon-er.flag-icon-squared { + background-image: url(../flags/1x1/er.svg); +} +.flag-icon-es { + background-image: url(../flags/4x3/es.svg); +} +.flag-icon-es.flag-icon-squared { + background-image: url(../flags/1x1/es.svg); +} +.flag-icon-et { + background-image: url(../flags/4x3/et.svg); +} +.flag-icon-et.flag-icon-squared { + background-image: url(../flags/1x1/et.svg); +} +.flag-icon-fi { + background-image: url(../flags/4x3/fi.svg); +} +.flag-icon-fi.flag-icon-squared { + background-image: url(../flags/1x1/fi.svg); +} +.flag-icon-fj { + background-image: url(../flags/4x3/fj.svg); +} +.flag-icon-fj.flag-icon-squared { + background-image: url(../flags/1x1/fj.svg); +} +.flag-icon-fk { + background-image: url(../flags/4x3/fk.svg); +} +.flag-icon-fk.flag-icon-squared { + background-image: url(../flags/1x1/fk.svg); +} +.flag-icon-fm { + background-image: url(../flags/4x3/fm.svg); +} +.flag-icon-fm.flag-icon-squared { + background-image: url(../flags/1x1/fm.svg); +} +.flag-icon-fo { + background-image: url(../flags/4x3/fo.svg); +} +.flag-icon-fo.flag-icon-squared { + background-image: url(../flags/1x1/fo.svg); +} +.flag-icon-fr { + background-image: url(../flags/4x3/fr.svg); +} +.flag-icon-fr.flag-icon-squared { + background-image: url(../flags/1x1/fr.svg); +} +.flag-icon-ga { + background-image: url(../flags/4x3/ga.svg); +} +.flag-icon-ga.flag-icon-squared { + background-image: url(../flags/1x1/ga.svg); +} +.flag-icon-gb { + background-image: url(../flags/4x3/gb.svg); +} +.flag-icon-gb.flag-icon-squared { + background-image: url(../flags/1x1/gb.svg); +} +.flag-icon-gd { + background-image: url(../flags/4x3/gd.svg); +} +.flag-icon-gd.flag-icon-squared { + background-image: url(../flags/1x1/gd.svg); +} +.flag-icon-ge { + background-image: url(../flags/4x3/ge.svg); +} +.flag-icon-ge.flag-icon-squared { + background-image: url(../flags/1x1/ge.svg); +} +.flag-icon-gf { + background-image: url(../flags/4x3/gf.svg); +} +.flag-icon-gf.flag-icon-squared { + background-image: url(../flags/1x1/gf.svg); +} +.flag-icon-gg { + background-image: url(../flags/4x3/gg.svg); +} +.flag-icon-gg.flag-icon-squared { + background-image: url(../flags/1x1/gg.svg); +} +.flag-icon-gh { + background-image: url(../flags/4x3/gh.svg); +} +.flag-icon-gh.flag-icon-squared { + background-image: url(../flags/1x1/gh.svg); +} +.flag-icon-gi { + background-image: url(../flags/4x3/gi.svg); +} +.flag-icon-gi.flag-icon-squared { + background-image: url(../flags/1x1/gi.svg); +} +.flag-icon-gl { + background-image: url(../flags/4x3/gl.svg); +} +.flag-icon-gl.flag-icon-squared { + background-image: url(../flags/1x1/gl.svg); +} +.flag-icon-gm { + background-image: url(../flags/4x3/gm.svg); +} +.flag-icon-gm.flag-icon-squared { + background-image: url(../flags/1x1/gm.svg); +} +.flag-icon-gn { + background-image: url(../flags/4x3/gn.svg); +} +.flag-icon-gn.flag-icon-squared { + background-image: url(../flags/1x1/gn.svg); +} +.flag-icon-gp { + background-image: url(../flags/4x3/gp.svg); +} +.flag-icon-gp.flag-icon-squared { + background-image: url(../flags/1x1/gp.svg); +} +.flag-icon-gq { + background-image: url(../flags/4x3/gq.svg); +} +.flag-icon-gq.flag-icon-squared { + background-image: url(../flags/1x1/gq.svg); +} +.flag-icon-gr { + background-image: url(../flags/4x3/gr.svg); +} +.flag-icon-gr.flag-icon-squared { + background-image: url(../flags/1x1/gr.svg); +} +.flag-icon-gs { + background-image: url(../flags/4x3/gs.svg); +} +.flag-icon-gs.flag-icon-squared { + background-image: url(../flags/1x1/gs.svg); +} +.flag-icon-gt { + background-image: url(../flags/4x3/gt.svg); +} +.flag-icon-gt.flag-icon-squared { + background-image: url(../flags/1x1/gt.svg); +} +.flag-icon-gu { + background-image: url(../flags/4x3/gu.svg); +} +.flag-icon-gu.flag-icon-squared { + background-image: url(../flags/1x1/gu.svg); +} +.flag-icon-gw { + background-image: url(../flags/4x3/gw.svg); +} +.flag-icon-gw.flag-icon-squared { + background-image: url(../flags/1x1/gw.svg); +} +.flag-icon-gy { + background-image: url(../flags/4x3/gy.svg); +} +.flag-icon-gy.flag-icon-squared { + background-image: url(../flags/1x1/gy.svg); +} +.flag-icon-hk { + background-image: url(../flags/4x3/hk.svg); +} +.flag-icon-hk.flag-icon-squared { + background-image: url(../flags/1x1/hk.svg); +} +.flag-icon-hm { + background-image: url(../flags/4x3/hm.svg); +} +.flag-icon-hm.flag-icon-squared { + background-image: url(../flags/1x1/hm.svg); +} +.flag-icon-hn { + background-image: url(../flags/4x3/hn.svg); +} +.flag-icon-hn.flag-icon-squared { + background-image: url(../flags/1x1/hn.svg); +} +.flag-icon-hr { + background-image: url(../flags/4x3/hr.svg); +} +.flag-icon-hr.flag-icon-squared { + background-image: url(../flags/1x1/hr.svg); +} +.flag-icon-ht { + background-image: url(../flags/4x3/ht.svg); +} +.flag-icon-ht.flag-icon-squared { + background-image: url(../flags/1x1/ht.svg); +} +.flag-icon-hu { + background-image: url(../flags/4x3/hu.svg); +} +.flag-icon-hu.flag-icon-squared { + background-image: url(../flags/1x1/hu.svg); +} +.flag-icon-id { + background-image: url(../flags/4x3/id.svg); +} +.flag-icon-id.flag-icon-squared { + background-image: url(../flags/1x1/id.svg); +} +.flag-icon-ie { + background-image: url(../flags/4x3/ie.svg); +} +.flag-icon-ie.flag-icon-squared { + background-image: url(../flags/1x1/ie.svg); +} +.flag-icon-il { + background-image: url(../flags/4x3/il.svg); +} +.flag-icon-il.flag-icon-squared { + background-image: url(../flags/1x1/il.svg); +} +.flag-icon-im { + background-image: url(../flags/4x3/im.svg); +} +.flag-icon-im.flag-icon-squared { + background-image: url(../flags/1x1/im.svg); +} +.flag-icon-in { + background-image: url(../flags/4x3/in.svg); +} +.flag-icon-in.flag-icon-squared { + background-image: url(../flags/1x1/in.svg); +} +.flag-icon-io { + background-image: url(../flags/4x3/io.svg); +} +.flag-icon-io.flag-icon-squared { + background-image: url(../flags/1x1/io.svg); +} +.flag-icon-iq { + background-image: url(../flags/4x3/iq.svg); +} +.flag-icon-iq.flag-icon-squared { + background-image: url(../flags/1x1/iq.svg); +} +.flag-icon-ir { + background-image: url(../flags/4x3/ir.svg); +} +.flag-icon-ir.flag-icon-squared { + background-image: url(../flags/1x1/ir.svg); +} +.flag-icon-is { + background-image: url(../flags/4x3/is.svg); +} +.flag-icon-is.flag-icon-squared { + background-image: url(../flags/1x1/is.svg); +} +.flag-icon-it { + background-image: url(../flags/4x3/it.svg); +} +.flag-icon-it.flag-icon-squared { + background-image: url(../flags/1x1/it.svg); +} +.flag-icon-je { + background-image: url(../flags/4x3/je.svg); +} +.flag-icon-je.flag-icon-squared { + background-image: url(../flags/1x1/je.svg); +} +.flag-icon-jm { + background-image: url(../flags/4x3/jm.svg); +} +.flag-icon-jm.flag-icon-squared { + background-image: url(../flags/1x1/jm.svg); +} +.flag-icon-jo { + background-image: url(../flags/4x3/jo.svg); +} +.flag-icon-jo.flag-icon-squared { + background-image: url(../flags/1x1/jo.svg); +} +.flag-icon-jp { + background-image: url(../flags/4x3/jp.svg); +} +.flag-icon-jp.flag-icon-squared { + background-image: url(../flags/1x1/jp.svg); +} +.flag-icon-ke { + background-image: url(../flags/4x3/ke.svg); +} +.flag-icon-ke.flag-icon-squared { + background-image: url(../flags/1x1/ke.svg); +} +.flag-icon-kg { + background-image: url(../flags/4x3/kg.svg); +} +.flag-icon-kg.flag-icon-squared { + background-image: url(../flags/1x1/kg.svg); +} +.flag-icon-kh { + background-image: url(../flags/4x3/kh.svg); +} +.flag-icon-kh.flag-icon-squared { + background-image: url(../flags/1x1/kh.svg); +} +.flag-icon-ki { + background-image: url(../flags/4x3/ki.svg); +} +.flag-icon-ki.flag-icon-squared { + background-image: url(../flags/1x1/ki.svg); +} +.flag-icon-km { + background-image: url(../flags/4x3/km.svg); +} +.flag-icon-km.flag-icon-squared { + background-image: url(../flags/1x1/km.svg); +} +.flag-icon-kn { + background-image: url(../flags/4x3/kn.svg); +} +.flag-icon-kn.flag-icon-squared { + background-image: url(../flags/1x1/kn.svg); +} +.flag-icon-kp { + background-image: url(../flags/4x3/kp.svg); +} +.flag-icon-kp.flag-icon-squared { + background-image: url(../flags/1x1/kp.svg); +} +.flag-icon-kr { + background-image: url(../flags/4x3/kr.svg); +} +.flag-icon-kr.flag-icon-squared { + background-image: url(../flags/1x1/kr.svg); +} +.flag-icon-kw { + background-image: url(../flags/4x3/kw.svg); +} +.flag-icon-kw.flag-icon-squared { + background-image: url(../flags/1x1/kw.svg); +} +.flag-icon-ky { + background-image: url(../flags/4x3/ky.svg); +} +.flag-icon-ky.flag-icon-squared { + background-image: url(../flags/1x1/ky.svg); +} +.flag-icon-kz { + background-image: url(../flags/4x3/kz.svg); +} +.flag-icon-kz.flag-icon-squared { + background-image: url(../flags/1x1/kz.svg); +} +.flag-icon-la { + background-image: url(../flags/4x3/la.svg); +} +.flag-icon-la.flag-icon-squared { + background-image: url(../flags/1x1/la.svg); +} +.flag-icon-lb { + background-image: url(../flags/4x3/lb.svg); +} +.flag-icon-lb.flag-icon-squared { + background-image: url(../flags/1x1/lb.svg); +} +.flag-icon-lc { + background-image: url(../flags/4x3/lc.svg); +} +.flag-icon-lc.flag-icon-squared { + background-image: url(../flags/1x1/lc.svg); +} +.flag-icon-li { + background-image: url(../flags/4x3/li.svg); +} +.flag-icon-li.flag-icon-squared { + background-image: url(../flags/1x1/li.svg); +} +.flag-icon-lk { + background-image: url(../flags/4x3/lk.svg); +} +.flag-icon-lk.flag-icon-squared { + background-image: url(../flags/1x1/lk.svg); +} +.flag-icon-lr { + background-image: url(../flags/4x3/lr.svg); +} +.flag-icon-lr.flag-icon-squared { + background-image: url(../flags/1x1/lr.svg); +} +.flag-icon-ls { + background-image: url(../flags/4x3/ls.svg); +} +.flag-icon-ls.flag-icon-squared { + background-image: url(../flags/1x1/ls.svg); +} +.flag-icon-lt { + background-image: url(../flags/4x3/lt.svg); +} +.flag-icon-lt.flag-icon-squared { + background-image: url(../flags/1x1/lt.svg); +} +.flag-icon-lu { + background-image: url(../flags/4x3/lu.svg); +} +.flag-icon-lu.flag-icon-squared { + background-image: url(../flags/1x1/lu.svg); +} +.flag-icon-lv { + background-image: url(../flags/4x3/lv.svg); +} +.flag-icon-lv.flag-icon-squared { + background-image: url(../flags/1x1/lv.svg); +} +.flag-icon-ly { + background-image: url(../flags/4x3/ly.svg); +} +.flag-icon-ly.flag-icon-squared { + background-image: url(../flags/1x1/ly.svg); +} +.flag-icon-ma { + background-image: url(../flags/4x3/ma.svg); +} +.flag-icon-ma.flag-icon-squared { + background-image: url(../flags/1x1/ma.svg); +} +.flag-icon-mc { + background-image: url(../flags/4x3/mc.svg); +} +.flag-icon-mc.flag-icon-squared { + background-image: url(../flags/1x1/mc.svg); +} +.flag-icon-md { + background-image: url(../flags/4x3/md.svg); +} +.flag-icon-md.flag-icon-squared { + background-image: url(../flags/1x1/md.svg); +} +.flag-icon-me { + background-image: url(../flags/4x3/me.svg); +} +.flag-icon-me.flag-icon-squared { + background-image: url(../flags/1x1/me.svg); +} +.flag-icon-mf { + background-image: url(../flags/4x3/mf.svg); +} +.flag-icon-mf.flag-icon-squared { + background-image: url(../flags/1x1/mf.svg); +} +.flag-icon-mg { + background-image: url(../flags/4x3/mg.svg); +} +.flag-icon-mg.flag-icon-squared { + background-image: url(../flags/1x1/mg.svg); +} +.flag-icon-mh { + background-image: url(../flags/4x3/mh.svg); +} +.flag-icon-mh.flag-icon-squared { + background-image: url(../flags/1x1/mh.svg); +} +.flag-icon-mk { + background-image: url(../flags/4x3/mk.svg); +} +.flag-icon-mk.flag-icon-squared { + background-image: url(../flags/1x1/mk.svg); +} +.flag-icon-ml { + background-image: url(../flags/4x3/ml.svg); +} +.flag-icon-ml.flag-icon-squared { + background-image: url(../flags/1x1/ml.svg); +} +.flag-icon-mm { + background-image: url(../flags/4x3/mm.svg); +} +.flag-icon-mm.flag-icon-squared { + background-image: url(../flags/1x1/mm.svg); +} +.flag-icon-mn { + background-image: url(../flags/4x3/mn.svg); +} +.flag-icon-mn.flag-icon-squared { + background-image: url(../flags/1x1/mn.svg); +} +.flag-icon-mo { + background-image: url(../flags/4x3/mo.svg); +} +.flag-icon-mo.flag-icon-squared { + background-image: url(../flags/1x1/mo.svg); +} +.flag-icon-mp { + background-image: url(../flags/4x3/mp.svg); +} +.flag-icon-mp.flag-icon-squared { + background-image: url(../flags/1x1/mp.svg); +} +.flag-icon-mq { + background-image: url(../flags/4x3/mq.svg); +} +.flag-icon-mq.flag-icon-squared { + background-image: url(../flags/1x1/mq.svg); +} +.flag-icon-mr { + background-image: url(../flags/4x3/mr.svg); +} +.flag-icon-mr.flag-icon-squared { + background-image: url(../flags/1x1/mr.svg); +} +.flag-icon-ms { + background-image: url(../flags/4x3/ms.svg); +} +.flag-icon-ms.flag-icon-squared { + background-image: url(../flags/1x1/ms.svg); +} +.flag-icon-mt { + background-image: url(../flags/4x3/mt.svg); +} +.flag-icon-mt.flag-icon-squared { + background-image: url(../flags/1x1/mt.svg); +} +.flag-icon-mu { + background-image: url(../flags/4x3/mu.svg); +} +.flag-icon-mu.flag-icon-squared { + background-image: url(../flags/1x1/mu.svg); +} +.flag-icon-mv { + background-image: url(../flags/4x3/mv.svg); +} +.flag-icon-mv.flag-icon-squared { + background-image: url(../flags/1x1/mv.svg); +} +.flag-icon-mw { + background-image: url(../flags/4x3/mw.svg); +} +.flag-icon-mw.flag-icon-squared { + background-image: url(../flags/1x1/mw.svg); +} +.flag-icon-mx { + background-image: url(../flags/4x3/mx.svg); +} +.flag-icon-mx.flag-icon-squared { + background-image: url(../flags/1x1/mx.svg); +} +.flag-icon-my { + background-image: url(../flags/4x3/my.svg); +} +.flag-icon-my.flag-icon-squared { + background-image: url(../flags/1x1/my.svg); +} +.flag-icon-mz { + background-image: url(../flags/4x3/mz.svg); +} +.flag-icon-mz.flag-icon-squared { + background-image: url(../flags/1x1/mz.svg); +} +.flag-icon-na { + background-image: url(../flags/4x3/na.svg); +} +.flag-icon-na.flag-icon-squared { + background-image: url(../flags/1x1/na.svg); +} +.flag-icon-nc { + background-image: url(../flags/4x3/nc.svg); +} +.flag-icon-nc.flag-icon-squared { + background-image: url(../flags/1x1/nc.svg); +} +.flag-icon-ne { + background-image: url(../flags/4x3/ne.svg); +} +.flag-icon-ne.flag-icon-squared { + background-image: url(../flags/1x1/ne.svg); +} +.flag-icon-nf { + background-image: url(../flags/4x3/nf.svg); +} +.flag-icon-nf.flag-icon-squared { + background-image: url(../flags/1x1/nf.svg); +} +.flag-icon-ng { + background-image: url(../flags/4x3/ng.svg); +} +.flag-icon-ng.flag-icon-squared { + background-image: url(../flags/1x1/ng.svg); +} +.flag-icon-ni { + background-image: url(../flags/4x3/ni.svg); +} +.flag-icon-ni.flag-icon-squared { + background-image: url(../flags/1x1/ni.svg); +} +.flag-icon-nl { + background-image: url(../flags/4x3/nl.svg); +} +.flag-icon-nl.flag-icon-squared { + background-image: url(../flags/1x1/nl.svg); +} +.flag-icon-no { + background-image: url(../flags/4x3/no.svg); +} +.flag-icon-no.flag-icon-squared { + background-image: url(../flags/1x1/no.svg); +} +.flag-icon-np { + background-image: url(../flags/4x3/np.svg); +} +.flag-icon-np.flag-icon-squared { + background-image: url(../flags/1x1/np.svg); +} +.flag-icon-nr { + background-image: url(../flags/4x3/nr.svg); +} +.flag-icon-nr.flag-icon-squared { + background-image: url(../flags/1x1/nr.svg); +} +.flag-icon-nu { + background-image: url(../flags/4x3/nu.svg); +} +.flag-icon-nu.flag-icon-squared { + background-image: url(../flags/1x1/nu.svg); +} +.flag-icon-nz { + background-image: url(../flags/4x3/nz.svg); +} +.flag-icon-nz.flag-icon-squared { + background-image: url(../flags/1x1/nz.svg); +} +.flag-icon-om { + background-image: url(../flags/4x3/om.svg); +} +.flag-icon-om.flag-icon-squared { + background-image: url(../flags/1x1/om.svg); +} +.flag-icon-pa { + background-image: url(../flags/4x3/pa.svg); +} +.flag-icon-pa.flag-icon-squared { + background-image: url(../flags/1x1/pa.svg); +} +.flag-icon-pe { + background-image: url(../flags/4x3/pe.svg); +} +.flag-icon-pe.flag-icon-squared { + background-image: url(../flags/1x1/pe.svg); +} +.flag-icon-pf { + background-image: url(../flags/4x3/pf.svg); +} +.flag-icon-pf.flag-icon-squared { + background-image: url(../flags/1x1/pf.svg); +} +.flag-icon-pg { + background-image: url(../flags/4x3/pg.svg); +} +.flag-icon-pg.flag-icon-squared { + background-image: url(../flags/1x1/pg.svg); +} +.flag-icon-ph { + background-image: url(../flags/4x3/ph.svg); +} +.flag-icon-ph.flag-icon-squared { + background-image: url(../flags/1x1/ph.svg); +} +.flag-icon-pk { + background-image: url(../flags/4x3/pk.svg); +} +.flag-icon-pk.flag-icon-squared { + background-image: url(../flags/1x1/pk.svg); +} +.flag-icon-pl { + background-image: url(../flags/4x3/pl.svg); +} +.flag-icon-pl.flag-icon-squared { + background-image: url(../flags/1x1/pl.svg); +} +.flag-icon-pm { + background-image: url(../flags/4x3/pm.svg); +} +.flag-icon-pm.flag-icon-squared { + background-image: url(../flags/1x1/pm.svg); +} +.flag-icon-pn { + background-image: url(../flags/4x3/pn.svg); +} +.flag-icon-pn.flag-icon-squared { + background-image: url(../flags/1x1/pn.svg); +} +.flag-icon-pr { + background-image: url(../flags/4x3/pr.svg); +} +.flag-icon-pr.flag-icon-squared { + background-image: url(../flags/1x1/pr.svg); +} +.flag-icon-ps { + background-image: url(../flags/4x3/ps.svg); +} +.flag-icon-ps.flag-icon-squared { + background-image: url(../flags/1x1/ps.svg); +} +.flag-icon-pt { + background-image: url(../flags/4x3/pt.svg); +} +.flag-icon-pt.flag-icon-squared { + background-image: url(../flags/1x1/pt.svg); +} +.flag-icon-pw { + background-image: url(../flags/4x3/pw.svg); +} +.flag-icon-pw.flag-icon-squared { + background-image: url(../flags/1x1/pw.svg); +} +.flag-icon-py { + background-image: url(../flags/4x3/py.svg); +} +.flag-icon-py.flag-icon-squared { + background-image: url(../flags/1x1/py.svg); +} +.flag-icon-qa { + background-image: url(../flags/4x3/qa.svg); +} +.flag-icon-qa.flag-icon-squared { + background-image: url(../flags/1x1/qa.svg); +} +.flag-icon-re { + background-image: url(../flags/4x3/re.svg); +} +.flag-icon-re.flag-icon-squared { + background-image: url(../flags/1x1/re.svg); +} +.flag-icon-ro { + background-image: url(../flags/4x3/ro.svg); +} +.flag-icon-ro.flag-icon-squared { + background-image: url(../flags/1x1/ro.svg); +} +.flag-icon-rs { + background-image: url(../flags/4x3/rs.svg); +} +.flag-icon-rs.flag-icon-squared { + background-image: url(../flags/1x1/rs.svg); +} +.flag-icon-ru { + background-image: url(../flags/4x3/ru.svg); +} +.flag-icon-ru.flag-icon-squared { + background-image: url(../flags/1x1/ru.svg); +} +.flag-icon-rw { + background-image: url(../flags/4x3/rw.svg); +} +.flag-icon-rw.flag-icon-squared { + background-image: url(../flags/1x1/rw.svg); +} +.flag-icon-sa { + background-image: url(../flags/4x3/sa.svg); +} +.flag-icon-sa.flag-icon-squared { + background-image: url(../flags/1x1/sa.svg); +} +.flag-icon-sb { + background-image: url(../flags/4x3/sb.svg); +} +.flag-icon-sb.flag-icon-squared { + background-image: url(../flags/1x1/sb.svg); +} +.flag-icon-sc { + background-image: url(../flags/4x3/sc.svg); +} +.flag-icon-sc.flag-icon-squared { + background-image: url(../flags/1x1/sc.svg); +} +.flag-icon-sd { + background-image: url(../flags/4x3/sd.svg); +} +.flag-icon-sd.flag-icon-squared { + background-image: url(../flags/1x1/sd.svg); +} +.flag-icon-se { + background-image: url(../flags/4x3/se.svg); +} +.flag-icon-se.flag-icon-squared { + background-image: url(../flags/1x1/se.svg); +} +.flag-icon-sg { + background-image: url(../flags/4x3/sg.svg); +} +.flag-icon-sg.flag-icon-squared { + background-image: url(../flags/1x1/sg.svg); +} +.flag-icon-sh { + background-image: url(../flags/4x3/sh.svg); +} +.flag-icon-sh.flag-icon-squared { + background-image: url(../flags/1x1/sh.svg); +} +.flag-icon-si { + background-image: url(../flags/4x3/si.svg); +} +.flag-icon-si.flag-icon-squared { + background-image: url(../flags/1x1/si.svg); +} +.flag-icon-sj { + background-image: url(../flags/4x3/sj.svg); +} +.flag-icon-sj.flag-icon-squared { + background-image: url(../flags/1x1/sj.svg); +} +.flag-icon-sk { + background-image: url(../flags/4x3/sk.svg); +} +.flag-icon-sk.flag-icon-squared { + background-image: url(../flags/1x1/sk.svg); +} +.flag-icon-sl { + background-image: url(../flags/4x3/sl.svg); +} +.flag-icon-sl.flag-icon-squared { + background-image: url(../flags/1x1/sl.svg); +} +.flag-icon-sm { + background-image: url(../flags/4x3/sm.svg); +} +.flag-icon-sm.flag-icon-squared { + background-image: url(../flags/1x1/sm.svg); +} +.flag-icon-sn { + background-image: url(../flags/4x3/sn.svg); +} +.flag-icon-sn.flag-icon-squared { + background-image: url(../flags/1x1/sn.svg); +} +.flag-icon-so { + background-image: url(../flags/4x3/so.svg); +} +.flag-icon-so.flag-icon-squared { + background-image: url(../flags/1x1/so.svg); +} +.flag-icon-sr { + background-image: url(../flags/4x3/sr.svg); +} +.flag-icon-sr.flag-icon-squared { + background-image: url(../flags/1x1/sr.svg); +} +.flag-icon-ss { + background-image: url(../flags/4x3/ss.svg); +} +.flag-icon-ss.flag-icon-squared { + background-image: url(../flags/1x1/ss.svg); +} +.flag-icon-st { + background-image: url(../flags/4x3/st.svg); +} +.flag-icon-st.flag-icon-squared { + background-image: url(../flags/1x1/st.svg); +} +.flag-icon-sv { + background-image: url(../flags/4x3/sv.svg); +} +.flag-icon-sv.flag-icon-squared { + background-image: url(../flags/1x1/sv.svg); +} +.flag-icon-sx { + background-image: url(../flags/4x3/sx.svg); +} +.flag-icon-sx.flag-icon-squared { + background-image: url(../flags/1x1/sx.svg); +} +.flag-icon-sy { + background-image: url(../flags/4x3/sy.svg); +} +.flag-icon-sy.flag-icon-squared { + background-image: url(../flags/1x1/sy.svg); +} +.flag-icon-sz { + background-image: url(../flags/4x3/sz.svg); +} +.flag-icon-sz.flag-icon-squared { + background-image: url(../flags/1x1/sz.svg); +} +.flag-icon-tc { + background-image: url(../flags/4x3/tc.svg); +} +.flag-icon-tc.flag-icon-squared { + background-image: url(../flags/1x1/tc.svg); +} +.flag-icon-td { + background-image: url(../flags/4x3/td.svg); +} +.flag-icon-td.flag-icon-squared { + background-image: url(../flags/1x1/td.svg); +} +.flag-icon-tf { + background-image: url(../flags/4x3/tf.svg); +} +.flag-icon-tf.flag-icon-squared { + background-image: url(../flags/1x1/tf.svg); +} +.flag-icon-tg { + background-image: url(../flags/4x3/tg.svg); +} +.flag-icon-tg.flag-icon-squared { + background-image: url(../flags/1x1/tg.svg); +} +.flag-icon-th { + background-image: url(../flags/4x3/th.svg); +} +.flag-icon-th.flag-icon-squared { + background-image: url(../flags/1x1/th.svg); +} +.flag-icon-tj { + background-image: url(../flags/4x3/tj.svg); +} +.flag-icon-tj.flag-icon-squared { + background-image: url(../flags/1x1/tj.svg); +} +.flag-icon-tk { + background-image: url(../flags/4x3/tk.svg); +} +.flag-icon-tk.flag-icon-squared { + background-image: url(../flags/1x1/tk.svg); +} +.flag-icon-tl { + background-image: url(../flags/4x3/tl.svg); +} +.flag-icon-tl.flag-icon-squared { + background-image: url(../flags/1x1/tl.svg); +} +.flag-icon-tm { + background-image: url(../flags/4x3/tm.svg); +} +.flag-icon-tm.flag-icon-squared { + background-image: url(../flags/1x1/tm.svg); +} +.flag-icon-tn { + background-image: url(../flags/4x3/tn.svg); +} +.flag-icon-tn.flag-icon-squared { + background-image: url(../flags/1x1/tn.svg); +} +.flag-icon-to { + background-image: url(../flags/4x3/to.svg); +} +.flag-icon-to.flag-icon-squared { + background-image: url(../flags/1x1/to.svg); +} +.flag-icon-tr { + background-image: url(../flags/4x3/tr.svg); +} +.flag-icon-tr.flag-icon-squared { + background-image: url(../flags/1x1/tr.svg); +} +.flag-icon-tt { + background-image: url(../flags/4x3/tt.svg); +} +.flag-icon-tt.flag-icon-squared { + background-image: url(../flags/1x1/tt.svg); +} +.flag-icon-tv { + background-image: url(../flags/4x3/tv.svg); +} +.flag-icon-tv.flag-icon-squared { + background-image: url(../flags/1x1/tv.svg); +} +.flag-icon-tw { + background-image: url(../flags/4x3/tw.svg); +} +.flag-icon-tw.flag-icon-squared { + background-image: url(../flags/1x1/tw.svg); +} +.flag-icon-tz { + background-image: url(../flags/4x3/tz.svg); +} +.flag-icon-tz.flag-icon-squared { + background-image: url(../flags/1x1/tz.svg); +} +.flag-icon-ua { + background-image: url(../flags/4x3/ua.svg); +} +.flag-icon-ua.flag-icon-squared { + background-image: url(../flags/1x1/ua.svg); +} +.flag-icon-ug { + background-image: url(../flags/4x3/ug.svg); +} +.flag-icon-ug.flag-icon-squared { + background-image: url(../flags/1x1/ug.svg); +} +.flag-icon-um { + background-image: url(../flags/4x3/um.svg); +} +.flag-icon-um.flag-icon-squared { + background-image: url(../flags/1x1/um.svg); +} +.flag-icon-us { + background-image: url(../flags/4x3/us.svg); +} +.flag-icon-us.flag-icon-squared { + background-image: url(../flags/1x1/us.svg); +} +.flag-icon-uy { + background-image: url(../flags/4x3/uy.svg); +} +.flag-icon-uy.flag-icon-squared { + background-image: url(../flags/1x1/uy.svg); +} +.flag-icon-uz { + background-image: url(../flags/4x3/uz.svg); +} +.flag-icon-uz.flag-icon-squared { + background-image: url(../flags/1x1/uz.svg); +} +.flag-icon-va { + background-image: url(../flags/4x3/va.svg); +} +.flag-icon-va.flag-icon-squared { + background-image: url(../flags/1x1/va.svg); +} +.flag-icon-vc { + background-image: url(../flags/4x3/vc.svg); +} +.flag-icon-vc.flag-icon-squared { + background-image: url(../flags/1x1/vc.svg); +} +.flag-icon-ve { + background-image: url(../flags/4x3/ve.svg); +} +.flag-icon-ve.flag-icon-squared { + background-image: url(../flags/1x1/ve.svg); +} +.flag-icon-vg { + background-image: url(../flags/4x3/vg.svg); +} +.flag-icon-vg.flag-icon-squared { + background-image: url(../flags/1x1/vg.svg); +} +.flag-icon-vi { + background-image: url(../flags/4x3/vi.svg); +} +.flag-icon-vi.flag-icon-squared { + background-image: url(../flags/1x1/vi.svg); +} +.flag-icon-vn { + background-image: url(../flags/4x3/vn.svg); +} +.flag-icon-vn.flag-icon-squared { + background-image: url(../flags/1x1/vn.svg); +} +.flag-icon-vu { + background-image: url(../flags/4x3/vu.svg); +} +.flag-icon-vu.flag-icon-squared { + background-image: url(../flags/1x1/vu.svg); +} +.flag-icon-wf { + background-image: url(../flags/4x3/wf.svg); +} +.flag-icon-wf.flag-icon-squared { + background-image: url(../flags/1x1/wf.svg); +} +.flag-icon-ws { + background-image: url(../flags/4x3/ws.svg); +} +.flag-icon-ws.flag-icon-squared { + background-image: url(../flags/1x1/ws.svg); +} +.flag-icon-ye { + background-image: url(../flags/4x3/ye.svg); +} +.flag-icon-ye.flag-icon-squared { + background-image: url(../flags/1x1/ye.svg); +} +.flag-icon-yt { + background-image: url(../flags/4x3/yt.svg); +} +.flag-icon-yt.flag-icon-squared { + background-image: url(../flags/1x1/yt.svg); +} +.flag-icon-za { + background-image: url(../flags/4x3/za.svg); +} +.flag-icon-za.flag-icon-squared { + background-image: url(../flags/1x1/za.svg); +} +.flag-icon-zm { + background-image: url(../flags/4x3/zm.svg); +} +.flag-icon-zm.flag-icon-squared { + background-image: url(../flags/1x1/zm.svg); +} +.flag-icon-zw { + background-image: url(../flags/4x3/zw.svg); +} +.flag-icon-zw.flag-icon-squared { + background-image: url(../flags/1x1/zw.svg); +} +.flag-icon-es-ca { + background-image: url(../flags/4x3/es-ca.svg); +} +.flag-icon-es-ca.flag-icon-squared { + background-image: url(../flags/1x1/es-ca.svg); +} +.flag-icon-es-ga { + background-image: url(../flags/4x3/es-ga.svg); +} +.flag-icon-es-ga.flag-icon-squared { + background-image: url(../flags/1x1/es-ga.svg); +} +.flag-icon-eu { + background-image: url(../flags/4x3/eu.svg); +} +.flag-icon-eu.flag-icon-squared { + background-image: url(../flags/1x1/eu.svg); +} +.flag-icon-gb-eng { + background-image: url(../flags/4x3/gb-eng.svg); +} +.flag-icon-gb-eng.flag-icon-squared { + background-image: url(../flags/1x1/gb-eng.svg); +} +.flag-icon-gb-nir { + background-image: url(../flags/4x3/gb-nir.svg); +} +.flag-icon-gb-nir.flag-icon-squared { + background-image: url(../flags/1x1/gb-nir.svg); +} +.flag-icon-gb-sct { + background-image: url(../flags/4x3/gb-sct.svg); +} +.flag-icon-gb-sct.flag-icon-squared { + background-image: url(../flags/1x1/gb-sct.svg); +} +.flag-icon-gb-wls { + background-image: url(../flags/4x3/gb-wls.svg); +} +.flag-icon-gb-wls.flag-icon-squared { + background-image: url(../flags/1x1/gb-wls.svg); +} +.flag-icon-un { + background-image: url(../flags/4x3/un.svg); +} +.flag-icon-un.flag-icon-squared { + background-image: url(../flags/1x1/un.svg); +} +.flag-icon-xk { + background-image: url(../flags/4x3/xk.svg); +} +.flag-icon-xk.flag-icon-squared { + background-image: url(../flags/1x1/xk.svg); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.min.css b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.min.css new file mode 100644 index 0000000000..3d049b3953 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.min.css @@ -0,0 +1 @@ +.flag-icon-background{background-size:contain;background-position:50%;background-repeat:no-repeat}.flag-icon{background-size:contain;background-position:50%;background-repeat:no-repeat;position:relative;display:inline-block;width:1.33333333em;line-height:1em}.flag-icon:before{content:'\00a0'}.flag-icon.flag-icon-squared{width:1em}.flag-icon-ad{background-image:url(../flags/4x3/ad.svg)}.flag-icon-ad.flag-icon-squared{background-image:url(../flags/1x1/ad.svg)}.flag-icon-ae{background-image:url(../flags/4x3/ae.svg)}.flag-icon-ae.flag-icon-squared{background-image:url(../flags/1x1/ae.svg)}.flag-icon-af{background-image:url(../flags/4x3/af.svg)}.flag-icon-af.flag-icon-squared{background-image:url(../flags/1x1/af.svg)}.flag-icon-ag{background-image:url(../flags/4x3/ag.svg)}.flag-icon-ag.flag-icon-squared{background-image:url(../flags/1x1/ag.svg)}.flag-icon-ai{background-image:url(../flags/4x3/ai.svg)}.flag-icon-ai.flag-icon-squared{background-image:url(../flags/1x1/ai.svg)}.flag-icon-al{background-image:url(../flags/4x3/al.svg)}.flag-icon-al.flag-icon-squared{background-image:url(../flags/1x1/al.svg)}.flag-icon-am{background-image:url(../flags/4x3/am.svg)}.flag-icon-am.flag-icon-squared{background-image:url(../flags/1x1/am.svg)}.flag-icon-ao{background-image:url(../flags/4x3/ao.svg)}.flag-icon-ao.flag-icon-squared{background-image:url(../flags/1x1/ao.svg)}.flag-icon-aq{background-image:url(../flags/4x3/aq.svg)}.flag-icon-aq.flag-icon-squared{background-image:url(../flags/1x1/aq.svg)}.flag-icon-ar{background-image:url(../flags/4x3/ar.svg)}.flag-icon-ar.flag-icon-squared{background-image:url(../flags/1x1/ar.svg)}.flag-icon-as{background-image:url(../flags/4x3/as.svg)}.flag-icon-as.flag-icon-squared{background-image:url(../flags/1x1/as.svg)}.flag-icon-at{background-image:url(../flags/4x3/at.svg)}.flag-icon-at.flag-icon-squared{background-image:url(../flags/1x1/at.svg)}.flag-icon-au{background-image:url(../flags/4x3/au.svg)}.flag-icon-au.flag-icon-squared{background-image:url(../flags/1x1/au.svg)}.flag-icon-aw{background-image:url(../flags/4x3/aw.svg)}.flag-icon-aw.flag-icon-squared{background-image:url(../flags/1x1/aw.svg)}.flag-icon-ax{background-image:url(../flags/4x3/ax.svg)}.flag-icon-ax.flag-icon-squared{background-image:url(../flags/1x1/ax.svg)}.flag-icon-az{background-image:url(../flags/4x3/az.svg)}.flag-icon-az.flag-icon-squared{background-image:url(../flags/1x1/az.svg)}.flag-icon-ba{background-image:url(../flags/4x3/ba.svg)}.flag-icon-ba.flag-icon-squared{background-image:url(../flags/1x1/ba.svg)}.flag-icon-bb{background-image:url(../flags/4x3/bb.svg)}.flag-icon-bb.flag-icon-squared{background-image:url(../flags/1x1/bb.svg)}.flag-icon-bd{background-image:url(../flags/4x3/bd.svg)}.flag-icon-bd.flag-icon-squared{background-image:url(../flags/1x1/bd.svg)}.flag-icon-be{background-image:url(../flags/4x3/be.svg)}.flag-icon-be.flag-icon-squared{background-image:url(../flags/1x1/be.svg)}.flag-icon-bf{background-image:url(../flags/4x3/bf.svg)}.flag-icon-bf.flag-icon-squared{background-image:url(../flags/1x1/bf.svg)}.flag-icon-bg{background-image:url(../flags/4x3/bg.svg)}.flag-icon-bg.flag-icon-squared{background-image:url(../flags/1x1/bg.svg)}.flag-icon-bh{background-image:url(../flags/4x3/bh.svg)}.flag-icon-bh.flag-icon-squared{background-image:url(../flags/1x1/bh.svg)}.flag-icon-bi{background-image:url(../flags/4x3/bi.svg)}.flag-icon-bi.flag-icon-squared{background-image:url(../flags/1x1/bi.svg)}.flag-icon-bj{background-image:url(../flags/4x3/bj.svg)}.flag-icon-bj.flag-icon-squared{background-image:url(../flags/1x1/bj.svg)}.flag-icon-bl{background-image:url(../flags/4x3/bl.svg)}.flag-icon-bl.flag-icon-squared{background-image:url(../flags/1x1/bl.svg)}.flag-icon-bm{background-image:url(../flags/4x3/bm.svg)}.flag-icon-bm.flag-icon-squared{background-image:url(../flags/1x1/bm.svg)}.flag-icon-bn{background-image:url(../flags/4x3/bn.svg)}.flag-icon-bn.flag-icon-squared{background-image:url(../flags/1x1/bn.svg)}.flag-icon-bo{background-image:url(../flags/4x3/bo.svg)}.flag-icon-bo.flag-icon-squared{background-image:url(../flags/1x1/bo.svg)}.flag-icon-bq{background-image:url(../flags/4x3/bq.svg)}.flag-icon-bq.flag-icon-squared{background-image:url(../flags/1x1/bq.svg)}.flag-icon-br{background-image:url(../flags/4x3/br.svg)}.flag-icon-br.flag-icon-squared{background-image:url(../flags/1x1/br.svg)}.flag-icon-bs{background-image:url(../flags/4x3/bs.svg)}.flag-icon-bs.flag-icon-squared{background-image:url(../flags/1x1/bs.svg)}.flag-icon-bt{background-image:url(../flags/4x3/bt.svg)}.flag-icon-bt.flag-icon-squared{background-image:url(../flags/1x1/bt.svg)}.flag-icon-bv{background-image:url(../flags/4x3/bv.svg)}.flag-icon-bv.flag-icon-squared{background-image:url(../flags/1x1/bv.svg)}.flag-icon-bw{background-image:url(../flags/4x3/bw.svg)}.flag-icon-bw.flag-icon-squared{background-image:url(../flags/1x1/bw.svg)}.flag-icon-by{background-image:url(../flags/4x3/by.svg)}.flag-icon-by.flag-icon-squared{background-image:url(../flags/1x1/by.svg)}.flag-icon-bz{background-image:url(../flags/4x3/bz.svg)}.flag-icon-bz.flag-icon-squared{background-image:url(../flags/1x1/bz.svg)}.flag-icon-ca{background-image:url(../flags/4x3/ca.svg)}.flag-icon-ca.flag-icon-squared{background-image:url(../flags/1x1/ca.svg)}.flag-icon-cc{background-image:url(../flags/4x3/cc.svg)}.flag-icon-cc.flag-icon-squared{background-image:url(../flags/1x1/cc.svg)}.flag-icon-cd{background-image:url(../flags/4x3/cd.svg)}.flag-icon-cd.flag-icon-squared{background-image:url(../flags/1x1/cd.svg)}.flag-icon-cf{background-image:url(../flags/4x3/cf.svg)}.flag-icon-cf.flag-icon-squared{background-image:url(../flags/1x1/cf.svg)}.flag-icon-cg{background-image:url(../flags/4x3/cg.svg)}.flag-icon-cg.flag-icon-squared{background-image:url(../flags/1x1/cg.svg)}.flag-icon-ch{background-image:url(../flags/4x3/ch.svg)}.flag-icon-ch.flag-icon-squared{background-image:url(../flags/1x1/ch.svg)}.flag-icon-ci{background-image:url(../flags/4x3/ci.svg)}.flag-icon-ci.flag-icon-squared{background-image:url(../flags/1x1/ci.svg)}.flag-icon-ck{background-image:url(../flags/4x3/ck.svg)}.flag-icon-ck.flag-icon-squared{background-image:url(../flags/1x1/ck.svg)}.flag-icon-cl{background-image:url(../flags/4x3/cl.svg)}.flag-icon-cl.flag-icon-squared{background-image:url(../flags/1x1/cl.svg)}.flag-icon-cm{background-image:url(../flags/4x3/cm.svg)}.flag-icon-cm.flag-icon-squared{background-image:url(../flags/1x1/cm.svg)}.flag-icon-cn{background-image:url(../flags/4x3/cn.svg)}.flag-icon-cn.flag-icon-squared{background-image:url(../flags/1x1/cn.svg)}.flag-icon-co{background-image:url(../flags/4x3/co.svg)}.flag-icon-co.flag-icon-squared{background-image:url(../flags/1x1/co.svg)}.flag-icon-cr{background-image:url(../flags/4x3/cr.svg)}.flag-icon-cr.flag-icon-squared{background-image:url(../flags/1x1/cr.svg)}.flag-icon-cu{background-image:url(../flags/4x3/cu.svg)}.flag-icon-cu.flag-icon-squared{background-image:url(../flags/1x1/cu.svg)}.flag-icon-cv{background-image:url(../flags/4x3/cv.svg)}.flag-icon-cv.flag-icon-squared{background-image:url(../flags/1x1/cv.svg)}.flag-icon-cw{background-image:url(../flags/4x3/cw.svg)}.flag-icon-cw.flag-icon-squared{background-image:url(../flags/1x1/cw.svg)}.flag-icon-cx{background-image:url(../flags/4x3/cx.svg)}.flag-icon-cx.flag-icon-squared{background-image:url(../flags/1x1/cx.svg)}.flag-icon-cy{background-image:url(../flags/4x3/cy.svg)}.flag-icon-cy.flag-icon-squared{background-image:url(../flags/1x1/cy.svg)}.flag-icon-cz{background-image:url(../flags/4x3/cz.svg)}.flag-icon-cz.flag-icon-squared{background-image:url(../flags/1x1/cz.svg)}.flag-icon-de{background-image:url(../flags/4x3/de.svg)}.flag-icon-de.flag-icon-squared{background-image:url(../flags/1x1/de.svg)}.flag-icon-dj{background-image:url(../flags/4x3/dj.svg)}.flag-icon-dj.flag-icon-squared{background-image:url(../flags/1x1/dj.svg)}.flag-icon-dk{background-image:url(../flags/4x3/dk.svg)}.flag-icon-dk.flag-icon-squared{background-image:url(../flags/1x1/dk.svg)}.flag-icon-dm{background-image:url(../flags/4x3/dm.svg)}.flag-icon-dm.flag-icon-squared{background-image:url(../flags/1x1/dm.svg)}.flag-icon-do{background-image:url(../flags/4x3/do.svg)}.flag-icon-do.flag-icon-squared{background-image:url(../flags/1x1/do.svg)}.flag-icon-dz{background-image:url(../flags/4x3/dz.svg)}.flag-icon-dz.flag-icon-squared{background-image:url(../flags/1x1/dz.svg)}.flag-icon-ec{background-image:url(../flags/4x3/ec.svg)}.flag-icon-ec.flag-icon-squared{background-image:url(../flags/1x1/ec.svg)}.flag-icon-ee{background-image:url(../flags/4x3/ee.svg)}.flag-icon-ee.flag-icon-squared{background-image:url(../flags/1x1/ee.svg)}.flag-icon-eg{background-image:url(../flags/4x3/eg.svg)}.flag-icon-eg.flag-icon-squared{background-image:url(../flags/1x1/eg.svg)}.flag-icon-eh{background-image:url(../flags/4x3/eh.svg)}.flag-icon-eh.flag-icon-squared{background-image:url(../flags/1x1/eh.svg)}.flag-icon-er{background-image:url(../flags/4x3/er.svg)}.flag-icon-er.flag-icon-squared{background-image:url(../flags/1x1/er.svg)}.flag-icon-es{background-image:url(../flags/4x3/es.svg)}.flag-icon-es.flag-icon-squared{background-image:url(../flags/1x1/es.svg)}.flag-icon-et{background-image:url(../flags/4x3/et.svg)}.flag-icon-et.flag-icon-squared{background-image:url(../flags/1x1/et.svg)}.flag-icon-fi{background-image:url(../flags/4x3/fi.svg)}.flag-icon-fi.flag-icon-squared{background-image:url(../flags/1x1/fi.svg)}.flag-icon-fj{background-image:url(../flags/4x3/fj.svg)}.flag-icon-fj.flag-icon-squared{background-image:url(../flags/1x1/fj.svg)}.flag-icon-fk{background-image:url(../flags/4x3/fk.svg)}.flag-icon-fk.flag-icon-squared{background-image:url(../flags/1x1/fk.svg)}.flag-icon-fm{background-image:url(../flags/4x3/fm.svg)}.flag-icon-fm.flag-icon-squared{background-image:url(../flags/1x1/fm.svg)}.flag-icon-fo{background-image:url(../flags/4x3/fo.svg)}.flag-icon-fo.flag-icon-squared{background-image:url(../flags/1x1/fo.svg)}.flag-icon-fr{background-image:url(../flags/4x3/fr.svg)}.flag-icon-fr.flag-icon-squared{background-image:url(../flags/1x1/fr.svg)}.flag-icon-ga{background-image:url(../flags/4x3/ga.svg)}.flag-icon-ga.flag-icon-squared{background-image:url(../flags/1x1/ga.svg)}.flag-icon-gb{background-image:url(../flags/4x3/gb.svg)}.flag-icon-gb.flag-icon-squared{background-image:url(../flags/1x1/gb.svg)}.flag-icon-gd{background-image:url(../flags/4x3/gd.svg)}.flag-icon-gd.flag-icon-squared{background-image:url(../flags/1x1/gd.svg)}.flag-icon-ge{background-image:url(../flags/4x3/ge.svg)}.flag-icon-ge.flag-icon-squared{background-image:url(../flags/1x1/ge.svg)}.flag-icon-gf{background-image:url(../flags/4x3/gf.svg)}.flag-icon-gf.flag-icon-squared{background-image:url(../flags/1x1/gf.svg)}.flag-icon-gg{background-image:url(../flags/4x3/gg.svg)}.flag-icon-gg.flag-icon-squared{background-image:url(../flags/1x1/gg.svg)}.flag-icon-gh{background-image:url(../flags/4x3/gh.svg)}.flag-icon-gh.flag-icon-squared{background-image:url(../flags/1x1/gh.svg)}.flag-icon-gi{background-image:url(../flags/4x3/gi.svg)}.flag-icon-gi.flag-icon-squared{background-image:url(../flags/1x1/gi.svg)}.flag-icon-gl{background-image:url(../flags/4x3/gl.svg)}.flag-icon-gl.flag-icon-squared{background-image:url(../flags/1x1/gl.svg)}.flag-icon-gm{background-image:url(../flags/4x3/gm.svg)}.flag-icon-gm.flag-icon-squared{background-image:url(../flags/1x1/gm.svg)}.flag-icon-gn{background-image:url(../flags/4x3/gn.svg)}.flag-icon-gn.flag-icon-squared{background-image:url(../flags/1x1/gn.svg)}.flag-icon-gp{background-image:url(../flags/4x3/gp.svg)}.flag-icon-gp.flag-icon-squared{background-image:url(../flags/1x1/gp.svg)}.flag-icon-gq{background-image:url(../flags/4x3/gq.svg)}.flag-icon-gq.flag-icon-squared{background-image:url(../flags/1x1/gq.svg)}.flag-icon-gr{background-image:url(../flags/4x3/gr.svg)}.flag-icon-gr.flag-icon-squared{background-image:url(../flags/1x1/gr.svg)}.flag-icon-gs{background-image:url(../flags/4x3/gs.svg)}.flag-icon-gs.flag-icon-squared{background-image:url(../flags/1x1/gs.svg)}.flag-icon-gt{background-image:url(../flags/4x3/gt.svg)}.flag-icon-gt.flag-icon-squared{background-image:url(../flags/1x1/gt.svg)}.flag-icon-gu{background-image:url(../flags/4x3/gu.svg)}.flag-icon-gu.flag-icon-squared{background-image:url(../flags/1x1/gu.svg)}.flag-icon-gw{background-image:url(../flags/4x3/gw.svg)}.flag-icon-gw.flag-icon-squared{background-image:url(../flags/1x1/gw.svg)}.flag-icon-gy{background-image:url(../flags/4x3/gy.svg)}.flag-icon-gy.flag-icon-squared{background-image:url(../flags/1x1/gy.svg)}.flag-icon-hk{background-image:url(../flags/4x3/hk.svg)}.flag-icon-hk.flag-icon-squared{background-image:url(../flags/1x1/hk.svg)}.flag-icon-hm{background-image:url(../flags/4x3/hm.svg)}.flag-icon-hm.flag-icon-squared{background-image:url(../flags/1x1/hm.svg)}.flag-icon-hn{background-image:url(../flags/4x3/hn.svg)}.flag-icon-hn.flag-icon-squared{background-image:url(../flags/1x1/hn.svg)}.flag-icon-hr{background-image:url(../flags/4x3/hr.svg)}.flag-icon-hr.flag-icon-squared{background-image:url(../flags/1x1/hr.svg)}.flag-icon-ht{background-image:url(../flags/4x3/ht.svg)}.flag-icon-ht.flag-icon-squared{background-image:url(../flags/1x1/ht.svg)}.flag-icon-hu{background-image:url(../flags/4x3/hu.svg)}.flag-icon-hu.flag-icon-squared{background-image:url(../flags/1x1/hu.svg)}.flag-icon-id{background-image:url(../flags/4x3/id.svg)}.flag-icon-id.flag-icon-squared{background-image:url(../flags/1x1/id.svg)}.flag-icon-ie{background-image:url(../flags/4x3/ie.svg)}.flag-icon-ie.flag-icon-squared{background-image:url(../flags/1x1/ie.svg)}.flag-icon-il{background-image:url(../flags/4x3/il.svg)}.flag-icon-il.flag-icon-squared{background-image:url(../flags/1x1/il.svg)}.flag-icon-im{background-image:url(../flags/4x3/im.svg)}.flag-icon-im.flag-icon-squared{background-image:url(../flags/1x1/im.svg)}.flag-icon-in{background-image:url(../flags/4x3/in.svg)}.flag-icon-in.flag-icon-squared{background-image:url(../flags/1x1/in.svg)}.flag-icon-io{background-image:url(../flags/4x3/io.svg)}.flag-icon-io.flag-icon-squared{background-image:url(../flags/1x1/io.svg)}.flag-icon-iq{background-image:url(../flags/4x3/iq.svg)}.flag-icon-iq.flag-icon-squared{background-image:url(../flags/1x1/iq.svg)}.flag-icon-ir{background-image:url(../flags/4x3/ir.svg)}.flag-icon-ir.flag-icon-squared{background-image:url(../flags/1x1/ir.svg)}.flag-icon-is{background-image:url(../flags/4x3/is.svg)}.flag-icon-is.flag-icon-squared{background-image:url(../flags/1x1/is.svg)}.flag-icon-it{background-image:url(../flags/4x3/it.svg)}.flag-icon-it.flag-icon-squared{background-image:url(../flags/1x1/it.svg)}.flag-icon-je{background-image:url(../flags/4x3/je.svg)}.flag-icon-je.flag-icon-squared{background-image:url(../flags/1x1/je.svg)}.flag-icon-jm{background-image:url(../flags/4x3/jm.svg)}.flag-icon-jm.flag-icon-squared{background-image:url(../flags/1x1/jm.svg)}.flag-icon-jo{background-image:url(../flags/4x3/jo.svg)}.flag-icon-jo.flag-icon-squared{background-image:url(../flags/1x1/jo.svg)}.flag-icon-jp{background-image:url(../flags/4x3/jp.svg)}.flag-icon-jp.flag-icon-squared{background-image:url(../flags/1x1/jp.svg)}.flag-icon-ke{background-image:url(../flags/4x3/ke.svg)}.flag-icon-ke.flag-icon-squared{background-image:url(../flags/1x1/ke.svg)}.flag-icon-kg{background-image:url(../flags/4x3/kg.svg)}.flag-icon-kg.flag-icon-squared{background-image:url(../flags/1x1/kg.svg)}.flag-icon-kh{background-image:url(../flags/4x3/kh.svg)}.flag-icon-kh.flag-icon-squared{background-image:url(../flags/1x1/kh.svg)}.flag-icon-ki{background-image:url(../flags/4x3/ki.svg)}.flag-icon-ki.flag-icon-squared{background-image:url(../flags/1x1/ki.svg)}.flag-icon-km{background-image:url(../flags/4x3/km.svg)}.flag-icon-km.flag-icon-squared{background-image:url(../flags/1x1/km.svg)}.flag-icon-kn{background-image:url(../flags/4x3/kn.svg)}.flag-icon-kn.flag-icon-squared{background-image:url(../flags/1x1/kn.svg)}.flag-icon-kp{background-image:url(../flags/4x3/kp.svg)}.flag-icon-kp.flag-icon-squared{background-image:url(../flags/1x1/kp.svg)}.flag-icon-kr{background-image:url(../flags/4x3/kr.svg)}.flag-icon-kr.flag-icon-squared{background-image:url(../flags/1x1/kr.svg)}.flag-icon-kw{background-image:url(../flags/4x3/kw.svg)}.flag-icon-kw.flag-icon-squared{background-image:url(../flags/1x1/kw.svg)}.flag-icon-ky{background-image:url(../flags/4x3/ky.svg)}.flag-icon-ky.flag-icon-squared{background-image:url(../flags/1x1/ky.svg)}.flag-icon-kz{background-image:url(../flags/4x3/kz.svg)}.flag-icon-kz.flag-icon-squared{background-image:url(../flags/1x1/kz.svg)}.flag-icon-la{background-image:url(../flags/4x3/la.svg)}.flag-icon-la.flag-icon-squared{background-image:url(../flags/1x1/la.svg)}.flag-icon-lb{background-image:url(../flags/4x3/lb.svg)}.flag-icon-lb.flag-icon-squared{background-image:url(../flags/1x1/lb.svg)}.flag-icon-lc{background-image:url(../flags/4x3/lc.svg)}.flag-icon-lc.flag-icon-squared{background-image:url(../flags/1x1/lc.svg)}.flag-icon-li{background-image:url(../flags/4x3/li.svg)}.flag-icon-li.flag-icon-squared{background-image:url(../flags/1x1/li.svg)}.flag-icon-lk{background-image:url(../flags/4x3/lk.svg)}.flag-icon-lk.flag-icon-squared{background-image:url(../flags/1x1/lk.svg)}.flag-icon-lr{background-image:url(../flags/4x3/lr.svg)}.flag-icon-lr.flag-icon-squared{background-image:url(../flags/1x1/lr.svg)}.flag-icon-ls{background-image:url(../flags/4x3/ls.svg)}.flag-icon-ls.flag-icon-squared{background-image:url(../flags/1x1/ls.svg)}.flag-icon-lt{background-image:url(../flags/4x3/lt.svg)}.flag-icon-lt.flag-icon-squared{background-image:url(../flags/1x1/lt.svg)}.flag-icon-lu{background-image:url(../flags/4x3/lu.svg)}.flag-icon-lu.flag-icon-squared{background-image:url(../flags/1x1/lu.svg)}.flag-icon-lv{background-image:url(../flags/4x3/lv.svg)}.flag-icon-lv.flag-icon-squared{background-image:url(../flags/1x1/lv.svg)}.flag-icon-ly{background-image:url(../flags/4x3/ly.svg)}.flag-icon-ly.flag-icon-squared{background-image:url(../flags/1x1/ly.svg)}.flag-icon-ma{background-image:url(../flags/4x3/ma.svg)}.flag-icon-ma.flag-icon-squared{background-image:url(../flags/1x1/ma.svg)}.flag-icon-mc{background-image:url(../flags/4x3/mc.svg)}.flag-icon-mc.flag-icon-squared{background-image:url(../flags/1x1/mc.svg)}.flag-icon-md{background-image:url(../flags/4x3/md.svg)}.flag-icon-md.flag-icon-squared{background-image:url(../flags/1x1/md.svg)}.flag-icon-me{background-image:url(../flags/4x3/me.svg)}.flag-icon-me.flag-icon-squared{background-image:url(../flags/1x1/me.svg)}.flag-icon-mf{background-image:url(../flags/4x3/mf.svg)}.flag-icon-mf.flag-icon-squared{background-image:url(../flags/1x1/mf.svg)}.flag-icon-mg{background-image:url(../flags/4x3/mg.svg)}.flag-icon-mg.flag-icon-squared{background-image:url(../flags/1x1/mg.svg)}.flag-icon-mh{background-image:url(../flags/4x3/mh.svg)}.flag-icon-mh.flag-icon-squared{background-image:url(../flags/1x1/mh.svg)}.flag-icon-mk{background-image:url(../flags/4x3/mk.svg)}.flag-icon-mk.flag-icon-squared{background-image:url(../flags/1x1/mk.svg)}.flag-icon-ml{background-image:url(../flags/4x3/ml.svg)}.flag-icon-ml.flag-icon-squared{background-image:url(../flags/1x1/ml.svg)}.flag-icon-mm{background-image:url(../flags/4x3/mm.svg)}.flag-icon-mm.flag-icon-squared{background-image:url(../flags/1x1/mm.svg)}.flag-icon-mn{background-image:url(../flags/4x3/mn.svg)}.flag-icon-mn.flag-icon-squared{background-image:url(../flags/1x1/mn.svg)}.flag-icon-mo{background-image:url(../flags/4x3/mo.svg)}.flag-icon-mo.flag-icon-squared{background-image:url(../flags/1x1/mo.svg)}.flag-icon-mp{background-image:url(../flags/4x3/mp.svg)}.flag-icon-mp.flag-icon-squared{background-image:url(../flags/1x1/mp.svg)}.flag-icon-mq{background-image:url(../flags/4x3/mq.svg)}.flag-icon-mq.flag-icon-squared{background-image:url(../flags/1x1/mq.svg)}.flag-icon-mr{background-image:url(../flags/4x3/mr.svg)}.flag-icon-mr.flag-icon-squared{background-image:url(../flags/1x1/mr.svg)}.flag-icon-ms{background-image:url(../flags/4x3/ms.svg)}.flag-icon-ms.flag-icon-squared{background-image:url(../flags/1x1/ms.svg)}.flag-icon-mt{background-image:url(../flags/4x3/mt.svg)}.flag-icon-mt.flag-icon-squared{background-image:url(../flags/1x1/mt.svg)}.flag-icon-mu{background-image:url(../flags/4x3/mu.svg)}.flag-icon-mu.flag-icon-squared{background-image:url(../flags/1x1/mu.svg)}.flag-icon-mv{background-image:url(../flags/4x3/mv.svg)}.flag-icon-mv.flag-icon-squared{background-image:url(../flags/1x1/mv.svg)}.flag-icon-mw{background-image:url(../flags/4x3/mw.svg)}.flag-icon-mw.flag-icon-squared{background-image:url(../flags/1x1/mw.svg)}.flag-icon-mx{background-image:url(../flags/4x3/mx.svg)}.flag-icon-mx.flag-icon-squared{background-image:url(../flags/1x1/mx.svg)}.flag-icon-my{background-image:url(../flags/4x3/my.svg)}.flag-icon-my.flag-icon-squared{background-image:url(../flags/1x1/my.svg)}.flag-icon-mz{background-image:url(../flags/4x3/mz.svg)}.flag-icon-mz.flag-icon-squared{background-image:url(../flags/1x1/mz.svg)}.flag-icon-na{background-image:url(../flags/4x3/na.svg)}.flag-icon-na.flag-icon-squared{background-image:url(../flags/1x1/na.svg)}.flag-icon-nc{background-image:url(../flags/4x3/nc.svg)}.flag-icon-nc.flag-icon-squared{background-image:url(../flags/1x1/nc.svg)}.flag-icon-ne{background-image:url(../flags/4x3/ne.svg)}.flag-icon-ne.flag-icon-squared{background-image:url(../flags/1x1/ne.svg)}.flag-icon-nf{background-image:url(../flags/4x3/nf.svg)}.flag-icon-nf.flag-icon-squared{background-image:url(../flags/1x1/nf.svg)}.flag-icon-ng{background-image:url(../flags/4x3/ng.svg)}.flag-icon-ng.flag-icon-squared{background-image:url(../flags/1x1/ng.svg)}.flag-icon-ni{background-image:url(../flags/4x3/ni.svg)}.flag-icon-ni.flag-icon-squared{background-image:url(../flags/1x1/ni.svg)}.flag-icon-nl{background-image:url(../flags/4x3/nl.svg)}.flag-icon-nl.flag-icon-squared{background-image:url(../flags/1x1/nl.svg)}.flag-icon-no{background-image:url(../flags/4x3/no.svg)}.flag-icon-no.flag-icon-squared{background-image:url(../flags/1x1/no.svg)}.flag-icon-np{background-image:url(../flags/4x3/np.svg)}.flag-icon-np.flag-icon-squared{background-image:url(../flags/1x1/np.svg)}.flag-icon-nr{background-image:url(../flags/4x3/nr.svg)}.flag-icon-nr.flag-icon-squared{background-image:url(../flags/1x1/nr.svg)}.flag-icon-nu{background-image:url(../flags/4x3/nu.svg)}.flag-icon-nu.flag-icon-squared{background-image:url(../flags/1x1/nu.svg)}.flag-icon-nz{background-image:url(../flags/4x3/nz.svg)}.flag-icon-nz.flag-icon-squared{background-image:url(../flags/1x1/nz.svg)}.flag-icon-om{background-image:url(../flags/4x3/om.svg)}.flag-icon-om.flag-icon-squared{background-image:url(../flags/1x1/om.svg)}.flag-icon-pa{background-image:url(../flags/4x3/pa.svg)}.flag-icon-pa.flag-icon-squared{background-image:url(../flags/1x1/pa.svg)}.flag-icon-pe{background-image:url(../flags/4x3/pe.svg)}.flag-icon-pe.flag-icon-squared{background-image:url(../flags/1x1/pe.svg)}.flag-icon-pf{background-image:url(../flags/4x3/pf.svg)}.flag-icon-pf.flag-icon-squared{background-image:url(../flags/1x1/pf.svg)}.flag-icon-pg{background-image:url(../flags/4x3/pg.svg)}.flag-icon-pg.flag-icon-squared{background-image:url(../flags/1x1/pg.svg)}.flag-icon-ph{background-image:url(../flags/4x3/ph.svg)}.flag-icon-ph.flag-icon-squared{background-image:url(../flags/1x1/ph.svg)}.flag-icon-pk{background-image:url(../flags/4x3/pk.svg)}.flag-icon-pk.flag-icon-squared{background-image:url(../flags/1x1/pk.svg)}.flag-icon-pl{background-image:url(../flags/4x3/pl.svg)}.flag-icon-pl.flag-icon-squared{background-image:url(../flags/1x1/pl.svg)}.flag-icon-pm{background-image:url(../flags/4x3/pm.svg)}.flag-icon-pm.flag-icon-squared{background-image:url(../flags/1x1/pm.svg)}.flag-icon-pn{background-image:url(../flags/4x3/pn.svg)}.flag-icon-pn.flag-icon-squared{background-image:url(../flags/1x1/pn.svg)}.flag-icon-pr{background-image:url(../flags/4x3/pr.svg)}.flag-icon-pr.flag-icon-squared{background-image:url(../flags/1x1/pr.svg)}.flag-icon-ps{background-image:url(../flags/4x3/ps.svg)}.flag-icon-ps.flag-icon-squared{background-image:url(../flags/1x1/ps.svg)}.flag-icon-pt{background-image:url(../flags/4x3/pt.svg)}.flag-icon-pt.flag-icon-squared{background-image:url(../flags/1x1/pt.svg)}.flag-icon-pw{background-image:url(../flags/4x3/pw.svg)}.flag-icon-pw.flag-icon-squared{background-image:url(../flags/1x1/pw.svg)}.flag-icon-py{background-image:url(../flags/4x3/py.svg)}.flag-icon-py.flag-icon-squared{background-image:url(../flags/1x1/py.svg)}.flag-icon-qa{background-image:url(../flags/4x3/qa.svg)}.flag-icon-qa.flag-icon-squared{background-image:url(../flags/1x1/qa.svg)}.flag-icon-re{background-image:url(../flags/4x3/re.svg)}.flag-icon-re.flag-icon-squared{background-image:url(../flags/1x1/re.svg)}.flag-icon-ro{background-image:url(../flags/4x3/ro.svg)}.flag-icon-ro.flag-icon-squared{background-image:url(../flags/1x1/ro.svg)}.flag-icon-rs{background-image:url(../flags/4x3/rs.svg)}.flag-icon-rs.flag-icon-squared{background-image:url(../flags/1x1/rs.svg)}.flag-icon-ru{background-image:url(../flags/4x3/ru.svg)}.flag-icon-ru.flag-icon-squared{background-image:url(../flags/1x1/ru.svg)}.flag-icon-rw{background-image:url(../flags/4x3/rw.svg)}.flag-icon-rw.flag-icon-squared{background-image:url(../flags/1x1/rw.svg)}.flag-icon-sa{background-image:url(../flags/4x3/sa.svg)}.flag-icon-sa.flag-icon-squared{background-image:url(../flags/1x1/sa.svg)}.flag-icon-sb{background-image:url(../flags/4x3/sb.svg)}.flag-icon-sb.flag-icon-squared{background-image:url(../flags/1x1/sb.svg)}.flag-icon-sc{background-image:url(../flags/4x3/sc.svg)}.flag-icon-sc.flag-icon-squared{background-image:url(../flags/1x1/sc.svg)}.flag-icon-sd{background-image:url(../flags/4x3/sd.svg)}.flag-icon-sd.flag-icon-squared{background-image:url(../flags/1x1/sd.svg)}.flag-icon-se{background-image:url(../flags/4x3/se.svg)}.flag-icon-se.flag-icon-squared{background-image:url(../flags/1x1/se.svg)}.flag-icon-sg{background-image:url(../flags/4x3/sg.svg)}.flag-icon-sg.flag-icon-squared{background-image:url(../flags/1x1/sg.svg)}.flag-icon-sh{background-image:url(../flags/4x3/sh.svg)}.flag-icon-sh.flag-icon-squared{background-image:url(../flags/1x1/sh.svg)}.flag-icon-si{background-image:url(../flags/4x3/si.svg)}.flag-icon-si.flag-icon-squared{background-image:url(../flags/1x1/si.svg)}.flag-icon-sj{background-image:url(../flags/4x3/sj.svg)}.flag-icon-sj.flag-icon-squared{background-image:url(../flags/1x1/sj.svg)}.flag-icon-sk{background-image:url(../flags/4x3/sk.svg)}.flag-icon-sk.flag-icon-squared{background-image:url(../flags/1x1/sk.svg)}.flag-icon-sl{background-image:url(../flags/4x3/sl.svg)}.flag-icon-sl.flag-icon-squared{background-image:url(../flags/1x1/sl.svg)}.flag-icon-sm{background-image:url(../flags/4x3/sm.svg)}.flag-icon-sm.flag-icon-squared{background-image:url(../flags/1x1/sm.svg)}.flag-icon-sn{background-image:url(../flags/4x3/sn.svg)}.flag-icon-sn.flag-icon-squared{background-image:url(../flags/1x1/sn.svg)}.flag-icon-so{background-image:url(../flags/4x3/so.svg)}.flag-icon-so.flag-icon-squared{background-image:url(../flags/1x1/so.svg)}.flag-icon-sr{background-image:url(../flags/4x3/sr.svg)}.flag-icon-sr.flag-icon-squared{background-image:url(../flags/1x1/sr.svg)}.flag-icon-ss{background-image:url(../flags/4x3/ss.svg)}.flag-icon-ss.flag-icon-squared{background-image:url(../flags/1x1/ss.svg)}.flag-icon-st{background-image:url(../flags/4x3/st.svg)}.flag-icon-st.flag-icon-squared{background-image:url(../flags/1x1/st.svg)}.flag-icon-sv{background-image:url(../flags/4x3/sv.svg)}.flag-icon-sv.flag-icon-squared{background-image:url(../flags/1x1/sv.svg)}.flag-icon-sx{background-image:url(../flags/4x3/sx.svg)}.flag-icon-sx.flag-icon-squared{background-image:url(../flags/1x1/sx.svg)}.flag-icon-sy{background-image:url(../flags/4x3/sy.svg)}.flag-icon-sy.flag-icon-squared{background-image:url(../flags/1x1/sy.svg)}.flag-icon-sz{background-image:url(../flags/4x3/sz.svg)}.flag-icon-sz.flag-icon-squared{background-image:url(../flags/1x1/sz.svg)}.flag-icon-tc{background-image:url(../flags/4x3/tc.svg)}.flag-icon-tc.flag-icon-squared{background-image:url(../flags/1x1/tc.svg)}.flag-icon-td{background-image:url(../flags/4x3/td.svg)}.flag-icon-td.flag-icon-squared{background-image:url(../flags/1x1/td.svg)}.flag-icon-tf{background-image:url(../flags/4x3/tf.svg)}.flag-icon-tf.flag-icon-squared{background-image:url(../flags/1x1/tf.svg)}.flag-icon-tg{background-image:url(../flags/4x3/tg.svg)}.flag-icon-tg.flag-icon-squared{background-image:url(../flags/1x1/tg.svg)}.flag-icon-th{background-image:url(../flags/4x3/th.svg)}.flag-icon-th.flag-icon-squared{background-image:url(../flags/1x1/th.svg)}.flag-icon-tj{background-image:url(../flags/4x3/tj.svg)}.flag-icon-tj.flag-icon-squared{background-image:url(../flags/1x1/tj.svg)}.flag-icon-tk{background-image:url(../flags/4x3/tk.svg)}.flag-icon-tk.flag-icon-squared{background-image:url(../flags/1x1/tk.svg)}.flag-icon-tl{background-image:url(../flags/4x3/tl.svg)}.flag-icon-tl.flag-icon-squared{background-image:url(../flags/1x1/tl.svg)}.flag-icon-tm{background-image:url(../flags/4x3/tm.svg)}.flag-icon-tm.flag-icon-squared{background-image:url(../flags/1x1/tm.svg)}.flag-icon-tn{background-image:url(../flags/4x3/tn.svg)}.flag-icon-tn.flag-icon-squared{background-image:url(../flags/1x1/tn.svg)}.flag-icon-to{background-image:url(../flags/4x3/to.svg)}.flag-icon-to.flag-icon-squared{background-image:url(../flags/1x1/to.svg)}.flag-icon-tr{background-image:url(../flags/4x3/tr.svg)}.flag-icon-tr.flag-icon-squared{background-image:url(../flags/1x1/tr.svg)}.flag-icon-tt{background-image:url(../flags/4x3/tt.svg)}.flag-icon-tt.flag-icon-squared{background-image:url(../flags/1x1/tt.svg)}.flag-icon-tv{background-image:url(../flags/4x3/tv.svg)}.flag-icon-tv.flag-icon-squared{background-image:url(../flags/1x1/tv.svg)}.flag-icon-tw{background-image:url(../flags/4x3/tw.svg)}.flag-icon-tw.flag-icon-squared{background-image:url(../flags/1x1/tw.svg)}.flag-icon-tz{background-image:url(../flags/4x3/tz.svg)}.flag-icon-tz.flag-icon-squared{background-image:url(../flags/1x1/tz.svg)}.flag-icon-ua{background-image:url(../flags/4x3/ua.svg)}.flag-icon-ua.flag-icon-squared{background-image:url(../flags/1x1/ua.svg)}.flag-icon-ug{background-image:url(../flags/4x3/ug.svg)}.flag-icon-ug.flag-icon-squared{background-image:url(../flags/1x1/ug.svg)}.flag-icon-um{background-image:url(../flags/4x3/um.svg)}.flag-icon-um.flag-icon-squared{background-image:url(../flags/1x1/um.svg)}.flag-icon-us{background-image:url(../flags/4x3/us.svg)}.flag-icon-us.flag-icon-squared{background-image:url(../flags/1x1/us.svg)}.flag-icon-uy{background-image:url(../flags/4x3/uy.svg)}.flag-icon-uy.flag-icon-squared{background-image:url(../flags/1x1/uy.svg)}.flag-icon-uz{background-image:url(../flags/4x3/uz.svg)}.flag-icon-uz.flag-icon-squared{background-image:url(../flags/1x1/uz.svg)}.flag-icon-va{background-image:url(../flags/4x3/va.svg)}.flag-icon-va.flag-icon-squared{background-image:url(../flags/1x1/va.svg)}.flag-icon-vc{background-image:url(../flags/4x3/vc.svg)}.flag-icon-vc.flag-icon-squared{background-image:url(../flags/1x1/vc.svg)}.flag-icon-ve{background-image:url(../flags/4x3/ve.svg)}.flag-icon-ve.flag-icon-squared{background-image:url(../flags/1x1/ve.svg)}.flag-icon-vg{background-image:url(../flags/4x3/vg.svg)}.flag-icon-vg.flag-icon-squared{background-image:url(../flags/1x1/vg.svg)}.flag-icon-vi{background-image:url(../flags/4x3/vi.svg)}.flag-icon-vi.flag-icon-squared{background-image:url(../flags/1x1/vi.svg)}.flag-icon-vn{background-image:url(../flags/4x3/vn.svg)}.flag-icon-vn.flag-icon-squared{background-image:url(../flags/1x1/vn.svg)}.flag-icon-vu{background-image:url(../flags/4x3/vu.svg)}.flag-icon-vu.flag-icon-squared{background-image:url(../flags/1x1/vu.svg)}.flag-icon-wf{background-image:url(../flags/4x3/wf.svg)}.flag-icon-wf.flag-icon-squared{background-image:url(../flags/1x1/wf.svg)}.flag-icon-ws{background-image:url(../flags/4x3/ws.svg)}.flag-icon-ws.flag-icon-squared{background-image:url(../flags/1x1/ws.svg)}.flag-icon-ye{background-image:url(../flags/4x3/ye.svg)}.flag-icon-ye.flag-icon-squared{background-image:url(../flags/1x1/ye.svg)}.flag-icon-yt{background-image:url(../flags/4x3/yt.svg)}.flag-icon-yt.flag-icon-squared{background-image:url(../flags/1x1/yt.svg)}.flag-icon-za{background-image:url(../flags/4x3/za.svg)}.flag-icon-za.flag-icon-squared{background-image:url(../flags/1x1/za.svg)}.flag-icon-zm{background-image:url(../flags/4x3/zm.svg)}.flag-icon-zm.flag-icon-squared{background-image:url(../flags/1x1/zm.svg)}.flag-icon-zw{background-image:url(../flags/4x3/zw.svg)}.flag-icon-zw.flag-icon-squared{background-image:url(../flags/1x1/zw.svg)}.flag-icon-es-ca{background-image:url(../flags/4x3/es-ca.svg)}.flag-icon-es-ca.flag-icon-squared{background-image:url(../flags/1x1/es-ca.svg)}.flag-icon-es-ga{background-image:url(../flags/4x3/es-ga.svg)}.flag-icon-es-ga.flag-icon-squared{background-image:url(../flags/1x1/es-ga.svg)}.flag-icon-eu{background-image:url(../flags/4x3/eu.svg)}.flag-icon-eu.flag-icon-squared{background-image:url(../flags/1x1/eu.svg)}.flag-icon-gb-eng{background-image:url(../flags/4x3/gb-eng.svg)}.flag-icon-gb-eng.flag-icon-squared{background-image:url(../flags/1x1/gb-eng.svg)}.flag-icon-gb-nir{background-image:url(../flags/4x3/gb-nir.svg)}.flag-icon-gb-nir.flag-icon-squared{background-image:url(../flags/1x1/gb-nir.svg)}.flag-icon-gb-sct{background-image:url(../flags/4x3/gb-sct.svg)}.flag-icon-gb-sct.flag-icon-squared{background-image:url(../flags/1x1/gb-sct.svg)}.flag-icon-gb-wls{background-image:url(../flags/4x3/gb-wls.svg)}.flag-icon-gb-wls.flag-icon-squared{background-image:url(../flags/1x1/gb-wls.svg)}.flag-icon-un{background-image:url(../flags/4x3/un.svg)}.flag-icon-un.flag-icon-squared{background-image:url(../flags/1x1/un.svg)}.flag-icon-xk{background-image:url(../flags/4x3/xk.svg)}.flag-icon-xk.flag-icon-squared{background-image:url(../flags/1x1/xk.svg)} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ad.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ad.svg new file mode 100644 index 0000000000..e7fc56abbc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ad.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ae.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ae.svg new file mode 100644 index 0000000000..739c5d4646 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ae.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/af.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/af.svg new file mode 100644 index 0000000000..90c34b8d5a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/af.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ag.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ag.svg new file mode 100644 index 0000000000..d0b2a83cbe --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ag.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ai.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ai.svg new file mode 100644 index 0000000000..472be200e7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ai.svg @@ -0,0 +1,763 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/al.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/al.svg new file mode 100644 index 0000000000..75995ecde6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/al.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/am.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/am.svg new file mode 100644 index 0000000000..1198be035f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/am.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ao.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ao.svg new file mode 100644 index 0000000000..a5a25bf107 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ao.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aq.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aq.svg new file mode 100644 index 0000000000..80e682abd6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ar.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ar.svg new file mode 100644 index 0000000000..1730ecac9e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ar.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/as.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/as.svg new file mode 100644 index 0000000000..b8d8162d0a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/as.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/at.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/at.svg new file mode 100644 index 0000000000..649d6efe0a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/at.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/au.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/au.svg new file mode 100644 index 0000000000..ca5d607ea0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/au.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aw.svg new file mode 100644 index 0000000000..248a08d69b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aw.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ax.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ax.svg new file mode 100644 index 0000000000..cdeb07e273 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ax.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/az.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/az.svg new file mode 100644 index 0000000000..0119e1ab4e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/az.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ba.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ba.svg new file mode 100644 index 0000000000..5b92b0ab54 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ba.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bb.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bb.svg new file mode 100644 index 0000000000..9d627842f1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bb.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bd.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bd.svg new file mode 100644 index 0000000000..4cb38cf503 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bd.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/be.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/be.svg new file mode 100644 index 0000000000..01496c3cac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/be.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bf.svg new file mode 100644 index 0000000000..a3c7c44a4f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bg.svg new file mode 100644 index 0000000000..5abe67f642 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bh.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bh.svg new file mode 100644 index 0000000000..22fba621a9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bh.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bi.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bi.svg new file mode 100644 index 0000000000..cc11dcff2c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bi.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bj.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bj.svg new file mode 100644 index 0000000000..07c4c117ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bj.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bl.svg new file mode 100644 index 0000000000..0fa74e1c3c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bl.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bm.svg new file mode 100644 index 0000000000..a7057d24c0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bm.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bn.svg new file mode 100644 index 0000000000..2e93aea326 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bn.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bo.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bo.svg new file mode 100644 index 0000000000..52a534fe2b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bo.svg @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bq.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bq.svg new file mode 100644 index 0000000000..cc872ef1b8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/br.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/br.svg new file mode 100644 index 0000000000..8353e823fc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/br.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bs.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bs.svg new file mode 100644 index 0000000000..decdebb1ef --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bt.svg new file mode 100644 index 0000000000..3bbbfaac74 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bt.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bv.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bv.svg new file mode 100644 index 0000000000..01c9ee147d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bv.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bw.svg new file mode 100644 index 0000000000..0bc5d3d34f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/by.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/by.svg new file mode 100644 index 0000000000..73e14f7a23 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/by.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bz.svg new file mode 100644 index 0000000000..0e9a27ca6a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bz.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ca.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ca.svg new file mode 100644 index 0000000000..6882f6da65 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ca.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cc.svg new file mode 100644 index 0000000000..dd6e20003e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cc.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cd.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cd.svg new file mode 100644 index 0000000000..5da2a96892 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cd.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cf.svg new file mode 100644 index 0000000000..e924621528 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cg.svg new file mode 100644 index 0000000000..a52ba7e211 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ch.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ch.svg new file mode 100644 index 0000000000..773cdc8a3e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ch.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ci.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ci.svg new file mode 100644 index 0000000000..bd1e3f4141 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ci.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ck.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ck.svg new file mode 100644 index 0000000000..f2df0dbe34 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ck.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cl.svg new file mode 100644 index 0000000000..b808896721 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cm.svg new file mode 100644 index 0000000000..08b710bb87 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cn.svg new file mode 100644 index 0000000000..7873c1b4fa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/co.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/co.svg new file mode 100644 index 0000000000..18d1c5f450 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/co.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cr.svg new file mode 100644 index 0000000000..a60a6dd614 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cu.svg new file mode 100644 index 0000000000..396817620d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cu.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cv.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cv.svg new file mode 100644 index 0000000000..a8311b2f83 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cv.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cw.svg new file mode 100644 index 0000000000..d7ba21865b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cw.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cx.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cx.svg new file mode 100644 index 0000000000..ef82c45363 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cx.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cy.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cy.svg new file mode 100644 index 0000000000..ba2b0f8908 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cz.svg new file mode 100644 index 0000000000..9557b6e88d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/de.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/de.svg new file mode 100644 index 0000000000..b9ea8a6134 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/de.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dj.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dj.svg new file mode 100644 index 0000000000..3f6b2e4f35 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dj.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dk.svg new file mode 100644 index 0000000000..51ff69feb0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dm.svg new file mode 100644 index 0000000000..405a4b6b96 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dm.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/do.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/do.svg new file mode 100644 index 0000000000..03d3f3547b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/do.svg @@ -0,0 +1,6745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dz.svg new file mode 100644 index 0000000000..37df0c8bba --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ec.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ec.svg new file mode 100644 index 0000000000..65fd0bad2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ec.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ee.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ee.svg new file mode 100644 index 0000000000..fbc9e33943 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ee.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eg.svg new file mode 100644 index 0000000000..2965b6afa0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eg.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eh.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eh.svg new file mode 100644 index 0000000000..4c3feba1b3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eh.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/er.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/er.svg new file mode 100644 index 0000000000..86343349e3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/er.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ca.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ca.svg new file mode 100644 index 0000000000..2a50685ddb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ca.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ga.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ga.svg new file mode 100644 index 0000000000..5c55ff855b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ga.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es.svg new file mode 100644 index 0000000000..d7030eb292 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es.svg @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/et.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/et.svg new file mode 100644 index 0000000000..8b02f6b7ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/et.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eu.svg new file mode 100644 index 0000000000..b031d2d336 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eu.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fi.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fi.svg new file mode 100644 index 0000000000..aff1304c6b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fj.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fj.svg new file mode 100644 index 0000000000..9d9c3029e1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fj.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fk.svg new file mode 100644 index 0000000000..12a34c46dc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fk.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fm.svg new file mode 100644 index 0000000000..791fde9929 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fm.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fo.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fo.svg new file mode 100644 index 0000000000..b28915c013 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fr.svg new file mode 100644 index 0000000000..f8e3ca0d57 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ga.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ga.svg new file mode 100644 index 0000000000..16c81b3029 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ga.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-eng.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-eng.svg new file mode 100644 index 0000000000..18026d2941 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-eng.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-nir.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-nir.svg new file mode 100644 index 0000000000..6d8a3a3974 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-nir.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-sct.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-sct.svg new file mode 100644 index 0000000000..6987b0886b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-sct.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-wls.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-wls.svg new file mode 100644 index 0000000000..3931a17954 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-wls.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb.svg new file mode 100644 index 0000000000..ef048dc2a1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gd.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gd.svg new file mode 100644 index 0000000000..cca37ba483 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gd.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ge.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ge.svg new file mode 100644 index 0000000000..ac1d87bb49 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gf.svg new file mode 100644 index 0000000000..c00a576342 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gg.svg new file mode 100644 index 0000000000..2d06a9f86f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gh.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gh.svg new file mode 100644 index 0000000000..4b6446d645 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gi.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gi.svg new file mode 100644 index 0000000000..39f5b2770c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gi.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gl.svg new file mode 100644 index 0000000000..7a026d995f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gm.svg new file mode 100644 index 0000000000..b06ab6cb58 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gm.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gn.svg new file mode 100644 index 0000000000..8f8855da4b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gn.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gp.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gp.svg new file mode 100644 index 0000000000..0a5bdb0056 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gq.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gq.svg new file mode 100644 index 0000000000..8149406de4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gq.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gr.svg new file mode 100644 index 0000000000..4bc68fc1fa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gs.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gs.svg new file mode 100644 index 0000000000..48f68b8b67 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gs.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L + + + E + + + O + + + T + + + E + + + R + + + R + + + R + + + R + + + R + + + E + + + O + + + O + + + A + + + A + + + A + + + M + + + P + + + P + + + P + + + I + + + T + + + T + + + M + + + G + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gt.svg new file mode 100644 index 0000000000..761801ca47 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gt.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gu.svg new file mode 100644 index 0000000000..d6f5d535ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gu.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + G + + + U + + + A + + + M + + + + + + + + G + + + U + + + A + + + M + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gw.svg new file mode 100644 index 0000000000..064a593473 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gw.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gy.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gy.svg new file mode 100644 index 0000000000..57eb520903 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hk.svg new file mode 100644 index 0000000000..024c070555 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hk.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hm.svg new file mode 100644 index 0000000000..7e1f7e7e64 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hm.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hn.svg new file mode 100644 index 0000000000..c7c4c4acfd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hn.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hr.svg new file mode 100644 index 0000000000..7ea00410d5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hr.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ht.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ht.svg new file mode 100644 index 0000000000..920833a66e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ht.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hu.svg new file mode 100644 index 0000000000..94bc29f134 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/id.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/id.svg new file mode 100644 index 0000000000..6d2cf0941e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/id.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ie.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ie.svg new file mode 100644 index 0000000000..60448a9ded --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ie.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/il.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/il.svg new file mode 100644 index 0000000000..6cb4b1c1f7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/il.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/im.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/im.svg new file mode 100644 index 0000000000..0f487f67d7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/im.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/in.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/in.svg new file mode 100644 index 0000000000..e6557cd01a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/in.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/io.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/io.svg new file mode 100644 index 0000000000..4d809e03e6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/io.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/iq.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/iq.svg new file mode 100644 index 0000000000..6b96774d97 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/iq.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ir.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ir.svg new file mode 100644 index 0000000000..79f66324f1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ir.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/is.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/is.svg new file mode 100644 index 0000000000..08d1e68383 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/is.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/it.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/it.svg new file mode 100644 index 0000000000..615c58fb8f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/it.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/je.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/je.svg new file mode 100644 index 0000000000..c63ccb29ec --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/je.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jm.svg new file mode 100644 index 0000000000..c261da0971 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jm.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jo.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jo.svg new file mode 100644 index 0000000000..ab1c62aaa1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jp.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jp.svg new file mode 100644 index 0000000000..dc7a64a5c7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ke.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ke.svg new file mode 100644 index 0000000000..0b82f3a6c3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ke.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kg.svg new file mode 100644 index 0000000000..71ee7b8df5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kg.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kh.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kh.svg new file mode 100644 index 0000000000..8c888f1681 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kh.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ki.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ki.svg new file mode 100644 index 0000000000..bfc5ccab46 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ki.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/km.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/km.svg new file mode 100644 index 0000000000..8f842ea8c0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/km.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kn.svg new file mode 100644 index 0000000000..4b2a2488be --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kn.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kp.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kp.svg new file mode 100644 index 0000000000..8eda6be874 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kr.svg new file mode 100644 index 0000000000..2db51b02a6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kr.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kw.svg new file mode 100644 index 0000000000..3d4047fff5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ky.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ky.svg new file mode 100644 index 0000000000..b4ae00aa86 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ky.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kz.svg new file mode 100644 index 0000000000..f17bd6e0f8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kz.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/la.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/la.svg new file mode 100644 index 0000000000..1e7686a935 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/la.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lb.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lb.svg new file mode 100644 index 0000000000..a047b0b948 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lb.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lc.svg new file mode 100644 index 0000000000..b13b8852aa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/li.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/li.svg new file mode 100644 index 0000000000..cbed5cc84c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/li.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lk.svg new file mode 100644 index 0000000000..2b112155f6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lk.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lr.svg new file mode 100644 index 0000000000..0ae34e7282 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lr.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ls.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ls.svg new file mode 100644 index 0000000000..e71bb5bb70 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ls.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lt.svg new file mode 100644 index 0000000000..aa96cf3236 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lu.svg new file mode 100644 index 0000000000..6293671602 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lv.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lv.svg new file mode 100644 index 0000000000..5556de1a3d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ly.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ly.svg new file mode 100644 index 0000000000..fe0ed81b60 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ly.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ma.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ma.svg new file mode 100644 index 0000000000..85c99b3b9e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ma.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mc.svg new file mode 100644 index 0000000000..d38822ddac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mc.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/md.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/md.svg new file mode 100644 index 0000000000..86b2a96149 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/md.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/me.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/me.svg new file mode 100644 index 0000000000..56a19ed0a0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/me.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mf.svg new file mode 100644 index 0000000000..310afce407 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mg.svg new file mode 100644 index 0000000000..f0375cc6be --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mh.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mh.svg new file mode 100644 index 0000000000..97f34631af --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mh.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mk.svg new file mode 100644 index 0000000000..da2e9a4c40 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ml.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ml.svg new file mode 100644 index 0000000000..1e4d98900c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mm.svg new file mode 100644 index 0000000000..5076184c32 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mm.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mn.svg new file mode 100644 index 0000000000..568fda088a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mo.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mo.svg new file mode 100644 index 0000000000..83d04ea28a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mp.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mp.svg new file mode 100644 index 0000000000..54a0ede9f2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mp.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mq.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mq.svg new file mode 100644 index 0000000000..7a69fb5842 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mq.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mr.svg new file mode 100644 index 0000000000..7da23e5f4e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ms.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ms.svg new file mode 100644 index 0000000000..d7d910d020 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ms.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mt.svg new file mode 100644 index 0000000000..96acc15e73 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mt.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mu.svg new file mode 100644 index 0000000000..773d3d59ee --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mu.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mv.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mv.svg new file mode 100644 index 0000000000..aa5ed5333e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mw.svg new file mode 100644 index 0000000000..a9521a08c1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mw.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mx.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mx.svg new file mode 100644 index 0000000000..a440656807 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mx.svg @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/my.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/my.svg new file mode 100644 index 0000000000..7ebe064b74 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/my.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mz.svg new file mode 100644 index 0000000000..113a205792 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mz.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/na.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/na.svg new file mode 100644 index 0000000000..b934fc157c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/na.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nc.svg new file mode 100644 index 0000000000..2bdf6ee5b5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nc.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ne.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ne.svg new file mode 100644 index 0000000000..e76e44c0bb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ne.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nf.svg new file mode 100644 index 0000000000..21495222f6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nf.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ng.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ng.svg new file mode 100644 index 0000000000..57d65d380a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ng.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ni.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ni.svg new file mode 100644 index 0000000000..8f68d42255 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ni.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nl.svg new file mode 100644 index 0000000000..9db233dd4b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nl.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/no.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/no.svg new file mode 100644 index 0000000000..08ea5728d2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/no.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/np.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/np.svg new file mode 100644 index 0000000000..f34ee8c638 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/np.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nr.svg new file mode 100644 index 0000000000..282d80fa14 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nr.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nu.svg new file mode 100644 index 0000000000..aced440dc1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nu.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nz.svg new file mode 100644 index 0000000000..5283a96ed4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nz.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/om.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/om.svg new file mode 100644 index 0000000000..055d1e6986 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/om.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pa.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pa.svg new file mode 100644 index 0000000000..57965b9ea2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pa.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pe.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pe.svg new file mode 100644 index 0000000000..40b87badbb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pe.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pf.svg new file mode 100644 index 0000000000..94ff90cb2c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pf.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pg.svg new file mode 100644 index 0000000000..7397754182 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pg.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ph.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ph.svg new file mode 100644 index 0000000000..681cf23ff5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ph.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pk.svg new file mode 100644 index 0000000000..06b6022f0c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pk.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pl.svg new file mode 100644 index 0000000000..f7c12a1843 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pm.svg new file mode 100644 index 0000000000..3d4014a414 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pn.svg new file mode 100644 index 0000000000..47b0749a0a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pn.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pr.svg new file mode 100644 index 0000000000..79cf4c0991 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pr.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ps.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ps.svg new file mode 100644 index 0000000000..7c1ea3f9a6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ps.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pt.svg new file mode 100644 index 0000000000..425515054a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pt.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pw.svg new file mode 100644 index 0000000000..83bc3f71d5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/py.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/py.svg new file mode 100644 index 0000000000..88f55e6f02 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/py.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/qa.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/qa.svg new file mode 100644 index 0000000000..0bf30ea37f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/qa.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/re.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/re.svg new file mode 100644 index 0000000000..027c9f32ce --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/re.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ro.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ro.svg new file mode 100644 index 0000000000..994992ab66 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ro.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rs.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rs.svg new file mode 100644 index 0000000000..3270f304c4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rs.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ru.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ru.svg new file mode 100644 index 0000000000..d643087430 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ru.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rw.svg new file mode 100644 index 0000000000..26e41f6ad7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sa.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sa.svg new file mode 100644 index 0000000000..1cc41b80bf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sa.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sb.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sb.svg new file mode 100644 index 0000000000..f61bafd6a9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sb.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sc.svg new file mode 100644 index 0000000000..65d8943d64 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sc.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sd.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sd.svg new file mode 100644 index 0000000000..72a729708b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sd.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/se.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/se.svg new file mode 100644 index 0000000000..e824395ebb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/se.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sg.svg new file mode 100644 index 0000000000..1444e5b6ab --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sg.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sh.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sh.svg new file mode 100644 index 0000000000..599a09d73b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sh.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/si.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/si.svg new file mode 100644 index 0000000000..6de77ef473 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/si.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sj.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sj.svg new file mode 100644 index 0000000000..f4e5829735 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sj.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sk.svg new file mode 100644 index 0000000000..fd728680b0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sk.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sl.svg new file mode 100644 index 0000000000..18c76d781b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sl.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sm.svg new file mode 100644 index 0000000000..1ed18eed6c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sm.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L + + + I + + + B + + + E + + + R + + + T + + + A + + + S + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sn.svg new file mode 100644 index 0000000000..fbbad5e026 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sn.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/so.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/so.svg new file mode 100644 index 0000000000..96d88a992a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/so.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sr.svg new file mode 100644 index 0000000000..a0ca03e367 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ss.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ss.svg new file mode 100644 index 0000000000..7e6a47ffd9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ss.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/st.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/st.svg new file mode 100644 index 0000000000..da5df2990d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/st.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sv.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sv.svg new file mode 100644 index 0000000000..65b44baeec --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sv.svg @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sx.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sx.svg new file mode 100644 index 0000000000..5d9fadfa2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sx.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sy.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sy.svg new file mode 100644 index 0000000000..904dc1f65c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sz.svg new file mode 100644 index 0000000000..b3009e4d92 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sz.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tc.svg new file mode 100644 index 0000000000..1029615f55 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tc.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/td.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/td.svg new file mode 100644 index 0000000000..e3e81ce2fd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/td.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tf.svg new file mode 100644 index 0000000000..2061867c96 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tg.svg new file mode 100644 index 0000000000..2c1fd98fd4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/th.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/th.svg new file mode 100644 index 0000000000..86850f5fbf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/th.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tj.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tj.svg new file mode 100644 index 0000000000..853a4a4555 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tj.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tk.svg new file mode 100644 index 0000000000..c5ff6b4cb4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tl.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tl.svg new file mode 100644 index 0000000000..ec6d44bd13 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tm.svg new file mode 100644 index 0000000000..d856424fe6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tm.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tn.svg new file mode 100644 index 0000000000..d46a1cd9aa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/to.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/to.svg new file mode 100644 index 0000000000..201d6bc39d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/to.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tr.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tr.svg new file mode 100644 index 0000000000..861d4ea580 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tr.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tt.svg new file mode 100644 index 0000000000..87e439a07d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tv.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tv.svg new file mode 100644 index 0000000000..f30c8f3710 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tv.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tw.svg new file mode 100644 index 0000000000..5f284fc612 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tw.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tz.svg new file mode 100644 index 0000000000..f993ff15d6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tz.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ua.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ua.svg new file mode 100644 index 0000000000..18ebe0d49d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ua.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ug.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ug.svg new file mode 100644 index 0000000000..d9be945945 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ug.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/um.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/um.svg new file mode 100644 index 0000000000..25b08ce6ab --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/um.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/un.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/un.svg new file mode 100644 index 0000000000..1d50ea9250 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/un.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/us.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/us.svg new file mode 100644 index 0000000000..31f90c6c3a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/us.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uy.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uy.svg new file mode 100644 index 0000000000..0194a7cb0b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uy.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uz.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uz.svg new file mode 100644 index 0000000000..641af1b826 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/va.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/va.svg new file mode 100644 index 0000000000..7da6d3e739 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/va.svg @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vc.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vc.svg new file mode 100644 index 0000000000..ee72f781ac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ve.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ve.svg new file mode 100644 index 0000000000..205fe848a3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ve.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vg.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vg.svg new file mode 100644 index 0000000000..9572de3480 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vg.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vi.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vi.svg new file mode 100644 index 0000000000..2740f24ffa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vi.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vn.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vn.svg new file mode 100644 index 0000000000..6b158145ad --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vu.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vu.svg new file mode 100644 index 0000000000..397d30e60b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vu.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/wf.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/wf.svg new file mode 100644 index 0000000000..bb726a7c2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/wf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ws.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ws.svg new file mode 100644 index 0000000000..155ad7b558 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ws.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/xk.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/xk.svg new file mode 100644 index 0000000000..69146ca449 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/xk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ye.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ye.svg new file mode 100644 index 0000000000..d49d2c414e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ye.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/yt.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/yt.svg new file mode 100644 index 0000000000..7bf3837379 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/yt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/za.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/za.svg new file mode 100644 index 0000000000..9bae96fe3e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/za.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zm.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zm.svg new file mode 100644 index 0000000000..105f1076ad --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zm.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zw.svg b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zw.svg new file mode 100644 index 0000000000..ca5b7a20f9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zw.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/css/all.css b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/css/all.css new file mode 100644 index 0000000000..1893247a7d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/css/all.css @@ -0,0 +1,8003 @@ +/*! + * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2023 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); } + +.fa, +.fa-classic, +.fa-sharp, +.fas, +.fa-solid, +.far, +.fa-regular, +.fab, +.fa-brands { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; } + +.fas, +.fa-classic, +.fa-solid, +.far, +.fa-regular { + font-family: 'Font Awesome 6 Free'; } + +.fab, +.fa-brands { + font-family: 'Font Awesome 6 Brands'; } + +.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-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; } + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; } + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; } + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; } + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; } + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; } + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); } + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); } + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); } + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin-reverse { + --fa-animation-direction: reverse; } + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); } + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-bounce, + .fa-fade, + .fa-beat-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + -webkit-transition-delay: 0s; + transition-delay: 0s; + -webkit-transition-duration: 0s; + transition-duration: 0s; } } + +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@-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 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, 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%; + z-index: var(--fa-stack-z-index, auto); } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: var(--fa-inverse, #fff); } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ + +.fa-0::before { + content: "\30"; } + +.fa-1::before { + content: "\31"; } + +.fa-2::before { + content: "\32"; } + +.fa-3::before { + content: "\33"; } + +.fa-4::before { + content: "\34"; } + +.fa-5::before { + content: "\35"; } + +.fa-6::before { + content: "\36"; } + +.fa-7::before { + content: "\37"; } + +.fa-8::before { + content: "\38"; } + +.fa-9::before { + content: "\39"; } + +.fa-fill-drip::before { + content: "\f576"; } + +.fa-arrows-to-circle::before { + content: "\e4bd"; } + +.fa-circle-chevron-right::before { + content: "\f138"; } + +.fa-chevron-circle-right::before { + content: "\f138"; } + +.fa-at::before { + content: "\40"; } + +.fa-trash-can::before { + content: "\f2ed"; } + +.fa-trash-alt::before { + content: "\f2ed"; } + +.fa-text-height::before { + content: "\f034"; } + +.fa-user-xmark::before { + content: "\f235"; } + +.fa-user-times::before { + content: "\f235"; } + +.fa-stethoscope::before { + content: "\f0f1"; } + +.fa-message::before { + content: "\f27a"; } + +.fa-comment-alt::before { + content: "\f27a"; } + +.fa-info::before { + content: "\f129"; } + +.fa-down-left-and-up-right-to-center::before { + content: "\f422"; } + +.fa-compress-alt::before { + content: "\f422"; } + +.fa-explosion::before { + content: "\e4e9"; } + +.fa-file-lines::before { + content: "\f15c"; } + +.fa-file-alt::before { + content: "\f15c"; } + +.fa-file-text::before { + content: "\f15c"; } + +.fa-wave-square::before { + content: "\f83e"; } + +.fa-ring::before { + content: "\f70b"; } + +.fa-building-un::before { + content: "\e4d9"; } + +.fa-dice-three::before { + content: "\f527"; } + +.fa-calendar-days::before { + content: "\f073"; } + +.fa-calendar-alt::before { + content: "\f073"; } + +.fa-anchor-circle-check::before { + content: "\e4aa"; } + +.fa-building-circle-arrow-right::before { + content: "\e4d1"; } + +.fa-volleyball::before { + content: "\f45f"; } + +.fa-volleyball-ball::before { + content: "\f45f"; } + +.fa-arrows-up-to-line::before { + content: "\e4c2"; } + +.fa-sort-down::before { + content: "\f0dd"; } + +.fa-sort-desc::before { + content: "\f0dd"; } + +.fa-circle-minus::before { + content: "\f056"; } + +.fa-minus-circle::before { + content: "\f056"; } + +.fa-door-open::before { + content: "\f52b"; } + +.fa-right-from-bracket::before { + content: "\f2f5"; } + +.fa-sign-out-alt::before { + content: "\f2f5"; } + +.fa-atom::before { + content: "\f5d2"; } + +.fa-soap::before { + content: "\e06e"; } + +.fa-icons::before { + content: "\f86d"; } + +.fa-heart-music-camera-bolt::before { + content: "\f86d"; } + +.fa-microphone-lines-slash::before { + content: "\f539"; } + +.fa-microphone-alt-slash::before { + content: "\f539"; } + +.fa-bridge-circle-check::before { + content: "\e4c9"; } + +.fa-pump-medical::before { + content: "\e06a"; } + +.fa-fingerprint::before { + content: "\f577"; } + +.fa-hand-point-right::before { + content: "\f0a4"; } + +.fa-magnifying-glass-location::before { + content: "\f689"; } + +.fa-search-location::before { + content: "\f689"; } + +.fa-forward-step::before { + content: "\f051"; } + +.fa-step-forward::before { + content: "\f051"; } + +.fa-face-smile-beam::before { + content: "\f5b8"; } + +.fa-smile-beam::before { + content: "\f5b8"; } + +.fa-flag-checkered::before { + content: "\f11e"; } + +.fa-football::before { + content: "\f44e"; } + +.fa-football-ball::before { + content: "\f44e"; } + +.fa-school-circle-exclamation::before { + content: "\e56c"; } + +.fa-crop::before { + content: "\f125"; } + +.fa-angles-down::before { + content: "\f103"; } + +.fa-angle-double-down::before { + content: "\f103"; } + +.fa-users-rectangle::before { + content: "\e594"; } + +.fa-people-roof::before { + content: "\e537"; } + +.fa-people-line::before { + content: "\e534"; } + +.fa-beer-mug-empty::before { + content: "\f0fc"; } + +.fa-beer::before { + content: "\f0fc"; } + +.fa-diagram-predecessor::before { + content: "\e477"; } + +.fa-arrow-up-long::before { + content: "\f176"; } + +.fa-long-arrow-up::before { + content: "\f176"; } + +.fa-fire-flame-simple::before { + content: "\f46a"; } + +.fa-burn::before { + content: "\f46a"; } + +.fa-person::before { + content: "\f183"; } + +.fa-male::before { + content: "\f183"; } + +.fa-laptop::before { + content: "\f109"; } + +.fa-file-csv::before { + content: "\f6dd"; } + +.fa-menorah::before { + content: "\f676"; } + +.fa-truck-plane::before { + content: "\e58f"; } + +.fa-record-vinyl::before { + content: "\f8d9"; } + +.fa-face-grin-stars::before { + content: "\f587"; } + +.fa-grin-stars::before { + content: "\f587"; } + +.fa-bong::before { + content: "\f55c"; } + +.fa-spaghetti-monster-flying::before { + content: "\f67b"; } + +.fa-pastafarianism::before { + content: "\f67b"; } + +.fa-arrow-down-up-across-line::before { + content: "\e4af"; } + +.fa-spoon::before { + content: "\f2e5"; } + +.fa-utensil-spoon::before { + content: "\f2e5"; } + +.fa-jar-wheat::before { + content: "\e517"; } + +.fa-envelopes-bulk::before { + content: "\f674"; } + +.fa-mail-bulk::before { + content: "\f674"; } + +.fa-file-circle-exclamation::before { + content: "\e4eb"; } + +.fa-circle-h::before { + content: "\f47e"; } + +.fa-hospital-symbol::before { + content: "\f47e"; } + +.fa-pager::before { + content: "\f815"; } + +.fa-address-book::before { + content: "\f2b9"; } + +.fa-contact-book::before { + content: "\f2b9"; } + +.fa-strikethrough::before { + content: "\f0cc"; } + +.fa-k::before { + content: "\4b"; } + +.fa-landmark-flag::before { + content: "\e51c"; } + +.fa-pencil::before { + content: "\f303"; } + +.fa-pencil-alt::before { + content: "\f303"; } + +.fa-backward::before { + content: "\f04a"; } + +.fa-caret-right::before { + content: "\f0da"; } + +.fa-comments::before { + content: "\f086"; } + +.fa-paste::before { + content: "\f0ea"; } + +.fa-file-clipboard::before { + content: "\f0ea"; } + +.fa-code-pull-request::before { + content: "\e13c"; } + +.fa-clipboard-list::before { + content: "\f46d"; } + +.fa-truck-ramp-box::before { + content: "\f4de"; } + +.fa-truck-loading::before { + content: "\f4de"; } + +.fa-user-check::before { + content: "\f4fc"; } + +.fa-vial-virus::before { + content: "\e597"; } + +.fa-sheet-plastic::before { + content: "\e571"; } + +.fa-blog::before { + content: "\f781"; } + +.fa-user-ninja::before { + content: "\f504"; } + +.fa-person-arrow-up-from-line::before { + content: "\e539"; } + +.fa-scroll-torah::before { + content: "\f6a0"; } + +.fa-torah::before { + content: "\f6a0"; } + +.fa-broom-ball::before { + content: "\f458"; } + +.fa-quidditch::before { + content: "\f458"; } + +.fa-quidditch-broom-ball::before { + content: "\f458"; } + +.fa-toggle-off::before { + content: "\f204"; } + +.fa-box-archive::before { + content: "\f187"; } + +.fa-archive::before { + content: "\f187"; } + +.fa-person-drowning::before { + content: "\e545"; } + +.fa-arrow-down-9-1::before { + content: "\f886"; } + +.fa-sort-numeric-desc::before { + content: "\f886"; } + +.fa-sort-numeric-down-alt::before { + content: "\f886"; } + +.fa-face-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-spray-can::before { + content: "\f5bd"; } + +.fa-truck-monster::before { + content: "\f63b"; } + +.fa-w::before { + content: "\57"; } + +.fa-earth-africa::before { + content: "\f57c"; } + +.fa-globe-africa::before { + content: "\f57c"; } + +.fa-rainbow::before { + content: "\f75b"; } + +.fa-circle-notch::before { + content: "\f1ce"; } + +.fa-tablet-screen-button::before { + content: "\f3fa"; } + +.fa-tablet-alt::before { + content: "\f3fa"; } + +.fa-paw::before { + content: "\f1b0"; } + +.fa-cloud::before { + content: "\f0c2"; } + +.fa-trowel-bricks::before { + content: "\e58a"; } + +.fa-face-flushed::before { + content: "\f579"; } + +.fa-flushed::before { + content: "\f579"; } + +.fa-hospital-user::before { + content: "\f80d"; } + +.fa-tent-arrow-left-right::before { + content: "\e57f"; } + +.fa-gavel::before { + content: "\f0e3"; } + +.fa-legal::before { + content: "\f0e3"; } + +.fa-binoculars::before { + content: "\f1e5"; } + +.fa-microphone-slash::before { + content: "\f131"; } + +.fa-box-tissue::before { + content: "\e05b"; } + +.fa-motorcycle::before { + content: "\f21c"; } + +.fa-bell-concierge::before { + content: "\f562"; } + +.fa-concierge-bell::before { + content: "\f562"; } + +.fa-pen-ruler::before { + content: "\f5ae"; } + +.fa-pencil-ruler::before { + content: "\f5ae"; } + +.fa-people-arrows::before { + content: "\e068"; } + +.fa-people-arrows-left-right::before { + content: "\e068"; } + +.fa-mars-and-venus-burst::before { + content: "\e523"; } + +.fa-square-caret-right::before { + content: "\f152"; } + +.fa-caret-square-right::before { + content: "\f152"; } + +.fa-scissors::before { + content: "\f0c4"; } + +.fa-cut::before { + content: "\f0c4"; } + +.fa-sun-plant-wilt::before { + content: "\e57a"; } + +.fa-toilets-portable::before { + content: "\e584"; } + +.fa-hockey-puck::before { + content: "\f453"; } + +.fa-table::before { + content: "\f0ce"; } + +.fa-magnifying-glass-arrow-right::before { + content: "\e521"; } + +.fa-tachograph-digital::before { + content: "\f566"; } + +.fa-digital-tachograph::before { + content: "\f566"; } + +.fa-users-slash::before { + content: "\e073"; } + +.fa-clover::before { + content: "\e139"; } + +.fa-reply::before { + content: "\f3e5"; } + +.fa-mail-reply::before { + content: "\f3e5"; } + +.fa-star-and-crescent::before { + content: "\f699"; } + +.fa-house-fire::before { + content: "\e50c"; } + +.fa-square-minus::before { + content: "\f146"; } + +.fa-minus-square::before { + content: "\f146"; } + +.fa-helicopter::before { + content: "\f533"; } + +.fa-compass::before { + content: "\f14e"; } + +.fa-square-caret-down::before { + content: "\f150"; } + +.fa-caret-square-down::before { + content: "\f150"; } + +.fa-file-circle-question::before { + content: "\e4ef"; } + +.fa-laptop-code::before { + content: "\f5fc"; } + +.fa-swatchbook::before { + content: "\f5c3"; } + +.fa-prescription-bottle::before { + content: "\f485"; } + +.fa-bars::before { + content: "\f0c9"; } + +.fa-navicon::before { + content: "\f0c9"; } + +.fa-people-group::before { + content: "\e533"; } + +.fa-hourglass-end::before { + content: "\f253"; } + +.fa-hourglass-3::before { + content: "\f253"; } + +.fa-heart-crack::before { + content: "\f7a9"; } + +.fa-heart-broken::before { + content: "\f7a9"; } + +.fa-square-up-right::before { + content: "\f360"; } + +.fa-external-link-square-alt::before { + content: "\f360"; } + +.fa-face-kiss-beam::before { + content: "\f597"; } + +.fa-kiss-beam::before { + content: "\f597"; } + +.fa-film::before { + content: "\f008"; } + +.fa-ruler-horizontal::before { + content: "\f547"; } + +.fa-people-robbery::before { + content: "\e536"; } + +.fa-lightbulb::before { + content: "\f0eb"; } + +.fa-caret-left::before { + content: "\f0d9"; } + +.fa-circle-exclamation::before { + content: "\f06a"; } + +.fa-exclamation-circle::before { + content: "\f06a"; } + +.fa-school-circle-xmark::before { + content: "\e56d"; } + +.fa-arrow-right-from-bracket::before { + content: "\f08b"; } + +.fa-sign-out::before { + content: "\f08b"; } + +.fa-circle-chevron-down::before { + content: "\f13a"; } + +.fa-chevron-circle-down::before { + content: "\f13a"; } + +.fa-unlock-keyhole::before { + content: "\f13e"; } + +.fa-unlock-alt::before { + content: "\f13e"; } + +.fa-cloud-showers-heavy::before { + content: "\f740"; } + +.fa-headphones-simple::before { + content: "\f58f"; } + +.fa-headphones-alt::before { + content: "\f58f"; } + +.fa-sitemap::before { + content: "\f0e8"; } + +.fa-circle-dollar-to-slot::before { + content: "\f4b9"; } + +.fa-donate::before { + content: "\f4b9"; } + +.fa-memory::before { + content: "\f538"; } + +.fa-road-spikes::before { + content: "\e568"; } + +.fa-fire-burner::before { + content: "\e4f1"; } + +.fa-flag::before { + content: "\f024"; } + +.fa-hanukiah::before { + content: "\f6e6"; } + +.fa-feather::before { + content: "\f52d"; } + +.fa-volume-low::before { + content: "\f027"; } + +.fa-volume-down::before { + content: "\f027"; } + +.fa-comment-slash::before { + content: "\f4b3"; } + +.fa-cloud-sun-rain::before { + content: "\f743"; } + +.fa-compress::before { + content: "\f066"; } + +.fa-wheat-awn::before { + content: "\e2cd"; } + +.fa-wheat-alt::before { + content: "\e2cd"; } + +.fa-ankh::before { + content: "\f644"; } + +.fa-hands-holding-child::before { + content: "\e4fa"; } + +.fa-asterisk::before { + content: "\2a"; } + +.fa-square-check::before { + content: "\f14a"; } + +.fa-check-square::before { + content: "\f14a"; } + +.fa-peseta-sign::before { + content: "\e221"; } + +.fa-heading::before { + content: "\f1dc"; } + +.fa-header::before { + content: "\f1dc"; } + +.fa-ghost::before { + content: "\f6e2"; } + +.fa-list::before { + content: "\f03a"; } + +.fa-list-squares::before { + content: "\f03a"; } + +.fa-square-phone-flip::before { + content: "\f87b"; } + +.fa-phone-square-alt::before { + content: "\f87b"; } + +.fa-cart-plus::before { + content: "\f217"; } + +.fa-gamepad::before { + content: "\f11b"; } + +.fa-circle-dot::before { + content: "\f192"; } + +.fa-dot-circle::before { + content: "\f192"; } + +.fa-face-dizzy::before { + content: "\f567"; } + +.fa-dizzy::before { + content: "\f567"; } + +.fa-egg::before { + content: "\f7fb"; } + +.fa-house-medical-circle-xmark::before { + content: "\e513"; } + +.fa-campground::before { + content: "\f6bb"; } + +.fa-folder-plus::before { + content: "\f65e"; } + +.fa-futbol::before { + content: "\f1e3"; } + +.fa-futbol-ball::before { + content: "\f1e3"; } + +.fa-soccer-ball::before { + content: "\f1e3"; } + +.fa-paintbrush::before { + content: "\f1fc"; } + +.fa-paint-brush::before { + content: "\f1fc"; } + +.fa-lock::before { + content: "\f023"; } + +.fa-gas-pump::before { + content: "\f52f"; } + +.fa-hot-tub-person::before { + content: "\f593"; } + +.fa-hot-tub::before { + content: "\f593"; } + +.fa-map-location::before { + content: "\f59f"; } + +.fa-map-marked::before { + content: "\f59f"; } + +.fa-house-flood-water::before { + content: "\e50e"; } + +.fa-tree::before { + content: "\f1bb"; } + +.fa-bridge-lock::before { + content: "\e4cc"; } + +.fa-sack-dollar::before { + content: "\f81d"; } + +.fa-pen-to-square::before { + content: "\f044"; } + +.fa-edit::before { + content: "\f044"; } + +.fa-car-side::before { + content: "\f5e4"; } + +.fa-share-nodes::before { + content: "\f1e0"; } + +.fa-share-alt::before { + content: "\f1e0"; } + +.fa-heart-circle-minus::before { + content: "\e4ff"; } + +.fa-hourglass-half::before { + content: "\f252"; } + +.fa-hourglass-2::before { + content: "\f252"; } + +.fa-microscope::before { + content: "\f610"; } + +.fa-sink::before { + content: "\e06d"; } + +.fa-bag-shopping::before { + content: "\f290"; } + +.fa-shopping-bag::before { + content: "\f290"; } + +.fa-arrow-down-z-a::before { + content: "\f881"; } + +.fa-sort-alpha-desc::before { + content: "\f881"; } + +.fa-sort-alpha-down-alt::before { + content: "\f881"; } + +.fa-mitten::before { + content: "\f7b5"; } + +.fa-person-rays::before { + content: "\e54d"; } + +.fa-users::before { + content: "\f0c0"; } + +.fa-eye-slash::before { + content: "\f070"; } + +.fa-flask-vial::before { + content: "\e4f3"; } + +.fa-hand::before { + content: "\f256"; } + +.fa-hand-paper::before { + content: "\f256"; } + +.fa-om::before { + content: "\f679"; } + +.fa-worm::before { + content: "\e599"; } + +.fa-house-circle-xmark::before { + content: "\e50b"; } + +.fa-plug::before { + content: "\f1e6"; } + +.fa-chevron-up::before { + content: "\f077"; } + +.fa-hand-spock::before { + content: "\f259"; } + +.fa-stopwatch::before { + content: "\f2f2"; } + +.fa-face-kiss::before { + content: "\f596"; } + +.fa-kiss::before { + content: "\f596"; } + +.fa-bridge-circle-xmark::before { + content: "\e4cb"; } + +.fa-face-grin-tongue::before { + content: "\f589"; } + +.fa-grin-tongue::before { + content: "\f589"; } + +.fa-chess-bishop::before { + content: "\f43a"; } + +.fa-face-grin-wink::before { + content: "\f58c"; } + +.fa-grin-wink::before { + content: "\f58c"; } + +.fa-ear-deaf::before { + content: "\f2a4"; } + +.fa-deaf::before { + content: "\f2a4"; } + +.fa-deafness::before { + content: "\f2a4"; } + +.fa-hard-of-hearing::before { + content: "\f2a4"; } + +.fa-road-circle-check::before { + content: "\e564"; } + +.fa-dice-five::before { + content: "\f523"; } + +.fa-square-rss::before { + content: "\f143"; } + +.fa-rss-square::before { + content: "\f143"; } + +.fa-land-mine-on::before { + content: "\e51b"; } + +.fa-i-cursor::before { + content: "\f246"; } + +.fa-stamp::before { + content: "\f5bf"; } + +.fa-stairs::before { + content: "\e289"; } + +.fa-i::before { + content: "\49"; } + +.fa-hryvnia-sign::before { + content: "\f6f2"; } + +.fa-hryvnia::before { + content: "\f6f2"; } + +.fa-pills::before { + content: "\f484"; } + +.fa-face-grin-wide::before { + content: "\f581"; } + +.fa-grin-alt::before { + content: "\f581"; } + +.fa-tooth::before { + content: "\f5c9"; } + +.fa-v::before { + content: "\56"; } + +.fa-bangladeshi-taka-sign::before { + content: "\e2e6"; } + +.fa-bicycle::before { + content: "\f206"; } + +.fa-staff-snake::before { + content: "\e579"; } + +.fa-rod-asclepius::before { + content: "\e579"; } + +.fa-rod-snake::before { + content: "\e579"; } + +.fa-staff-aesculapius::before { + content: "\e579"; } + +.fa-head-side-cough-slash::before { + content: "\e062"; } + +.fa-truck-medical::before { + content: "\f0f9"; } + +.fa-ambulance::before { + content: "\f0f9"; } + +.fa-wheat-awn-circle-exclamation::before { + content: "\e598"; } + +.fa-snowman::before { + content: "\f7d0"; } + +.fa-mortar-pestle::before { + content: "\f5a7"; } + +.fa-road-barrier::before { + content: "\e562"; } + +.fa-school::before { + content: "\f549"; } + +.fa-igloo::before { + content: "\f7ae"; } + +.fa-joint::before { + content: "\f595"; } + +.fa-angle-right::before { + content: "\f105"; } + +.fa-horse::before { + content: "\f6f0"; } + +.fa-q::before { + content: "\51"; } + +.fa-g::before { + content: "\47"; } + +.fa-notes-medical::before { + content: "\f481"; } + +.fa-temperature-half::before { + content: "\f2c9"; } + +.fa-temperature-2::before { + content: "\f2c9"; } + +.fa-thermometer-2::before { + content: "\f2c9"; } + +.fa-thermometer-half::before { + content: "\f2c9"; } + +.fa-dong-sign::before { + content: "\e169"; } + +.fa-capsules::before { + content: "\f46b"; } + +.fa-poo-storm::before { + content: "\f75a"; } + +.fa-poo-bolt::before { + content: "\f75a"; } + +.fa-face-frown-open::before { + content: "\f57a"; } + +.fa-frown-open::before { + content: "\f57a"; } + +.fa-hand-point-up::before { + content: "\f0a6"; } + +.fa-money-bill::before { + content: "\f0d6"; } + +.fa-bookmark::before { + content: "\f02e"; } + +.fa-align-justify::before { + content: "\f039"; } + +.fa-umbrella-beach::before { + content: "\f5ca"; } + +.fa-helmet-un::before { + content: "\e503"; } + +.fa-bullseye::before { + content: "\f140"; } + +.fa-bacon::before { + content: "\f7e5"; } + +.fa-hand-point-down::before { + content: "\f0a7"; } + +.fa-arrow-up-from-bracket::before { + content: "\e09a"; } + +.fa-folder::before { + content: "\f07b"; } + +.fa-folder-blank::before { + content: "\f07b"; } + +.fa-file-waveform::before { + content: "\f478"; } + +.fa-file-medical-alt::before { + content: "\f478"; } + +.fa-radiation::before { + content: "\f7b9"; } + +.fa-chart-simple::before { + content: "\e473"; } + +.fa-mars-stroke::before { + content: "\f229"; } + +.fa-vial::before { + content: "\f492"; } + +.fa-gauge::before { + content: "\f624"; } + +.fa-dashboard::before { + content: "\f624"; } + +.fa-gauge-med::before { + content: "\f624"; } + +.fa-tachometer-alt-average::before { + content: "\f624"; } + +.fa-wand-magic-sparkles::before { + content: "\e2ca"; } + +.fa-magic-wand-sparkles::before { + content: "\e2ca"; } + +.fa-e::before { + content: "\45"; } + +.fa-pen-clip::before { + content: "\f305"; } + +.fa-pen-alt::before { + content: "\f305"; } + +.fa-bridge-circle-exclamation::before { + content: "\e4ca"; } + +.fa-user::before { + content: "\f007"; } + +.fa-school-circle-check::before { + content: "\e56b"; } + +.fa-dumpster::before { + content: "\f793"; } + +.fa-van-shuttle::before { + content: "\f5b6"; } + +.fa-shuttle-van::before { + content: "\f5b6"; } + +.fa-building-user::before { + content: "\e4da"; } + +.fa-square-caret-left::before { + content: "\f191"; } + +.fa-caret-square-left::before { + content: "\f191"; } + +.fa-highlighter::before { + content: "\f591"; } + +.fa-key::before { + content: "\f084"; } + +.fa-bullhorn::before { + content: "\f0a1"; } + +.fa-globe::before { + content: "\f0ac"; } + +.fa-synagogue::before { + content: "\f69b"; } + +.fa-person-half-dress::before { + content: "\e548"; } + +.fa-road-bridge::before { + content: "\e563"; } + +.fa-location-arrow::before { + content: "\f124"; } + +.fa-c::before { + content: "\43"; } + +.fa-tablet-button::before { + content: "\f10a"; } + +.fa-building-lock::before { + content: "\e4d6"; } + +.fa-pizza-slice::before { + content: "\f818"; } + +.fa-money-bill-wave::before { + content: "\f53a"; } + +.fa-chart-area::before { + content: "\f1fe"; } + +.fa-area-chart::before { + content: "\f1fe"; } + +.fa-house-flag::before { + content: "\e50d"; } + +.fa-person-circle-minus::before { + content: "\e540"; } + +.fa-ban::before { + content: "\f05e"; } + +.fa-cancel::before { + content: "\f05e"; } + +.fa-camera-rotate::before { + content: "\e0d8"; } + +.fa-spray-can-sparkles::before { + content: "\f5d0"; } + +.fa-air-freshener::before { + content: "\f5d0"; } + +.fa-star::before { + content: "\f005"; } + +.fa-repeat::before { + content: "\f363"; } + +.fa-cross::before { + content: "\f654"; } + +.fa-box::before { + content: "\f466"; } + +.fa-venus-mars::before { + content: "\f228"; } + +.fa-arrow-pointer::before { + content: "\f245"; } + +.fa-mouse-pointer::before { + content: "\f245"; } + +.fa-maximize::before { + content: "\f31e"; } + +.fa-expand-arrows-alt::before { + content: "\f31e"; } + +.fa-charging-station::before { + content: "\f5e7"; } + +.fa-shapes::before { + content: "\f61f"; } + +.fa-triangle-circle-square::before { + content: "\f61f"; } + +.fa-shuffle::before { + content: "\f074"; } + +.fa-random::before { + content: "\f074"; } + +.fa-person-running::before { + content: "\f70c"; } + +.fa-running::before { + content: "\f70c"; } + +.fa-mobile-retro::before { + content: "\e527"; } + +.fa-grip-lines-vertical::before { + content: "\f7a5"; } + +.fa-spider::before { + content: "\f717"; } + +.fa-hands-bound::before { + content: "\e4f9"; } + +.fa-file-invoice-dollar::before { + content: "\f571"; } + +.fa-plane-circle-exclamation::before { + content: "\e556"; } + +.fa-x-ray::before { + content: "\f497"; } + +.fa-spell-check::before { + content: "\f891"; } + +.fa-slash::before { + content: "\f715"; } + +.fa-computer-mouse::before { + content: "\f8cc"; } + +.fa-mouse::before { + content: "\f8cc"; } + +.fa-arrow-right-to-bracket::before { + content: "\f090"; } + +.fa-sign-in::before { + content: "\f090"; } + +.fa-shop-slash::before { + content: "\e070"; } + +.fa-store-alt-slash::before { + content: "\e070"; } + +.fa-server::before { + content: "\f233"; } + +.fa-virus-covid-slash::before { + content: "\e4a9"; } + +.fa-shop-lock::before { + content: "\e4a5"; } + +.fa-hourglass-start::before { + content: "\f251"; } + +.fa-hourglass-1::before { + content: "\f251"; } + +.fa-blender-phone::before { + content: "\f6b6"; } + +.fa-building-wheat::before { + content: "\e4db"; } + +.fa-person-breastfeeding::before { + content: "\e53a"; } + +.fa-right-to-bracket::before { + content: "\f2f6"; } + +.fa-sign-in-alt::before { + content: "\f2f6"; } + +.fa-venus::before { + content: "\f221"; } + +.fa-passport::before { + content: "\f5ab"; } + +.fa-heart-pulse::before { + content: "\f21e"; } + +.fa-heartbeat::before { + content: "\f21e"; } + +.fa-people-carry-box::before { + content: "\f4ce"; } + +.fa-people-carry::before { + content: "\f4ce"; } + +.fa-temperature-high::before { + content: "\f769"; } + +.fa-microchip::before { + content: "\f2db"; } + +.fa-crown::before { + content: "\f521"; } + +.fa-weight-hanging::before { + content: "\f5cd"; } + +.fa-xmarks-lines::before { + content: "\e59a"; } + +.fa-file-prescription::before { + content: "\f572"; } + +.fa-weight-scale::before { + content: "\f496"; } + +.fa-weight::before { + content: "\f496"; } + +.fa-user-group::before { + content: "\f500"; } + +.fa-user-friends::before { + content: "\f500"; } + +.fa-arrow-up-a-z::before { + content: "\f15e"; } + +.fa-sort-alpha-up::before { + content: "\f15e"; } + +.fa-chess-knight::before { + content: "\f441"; } + +.fa-face-laugh-squint::before { + content: "\f59b"; } + +.fa-laugh-squint::before { + content: "\f59b"; } + +.fa-wheelchair::before { + content: "\f193"; } + +.fa-circle-arrow-up::before { + content: "\f0aa"; } + +.fa-arrow-circle-up::before { + content: "\f0aa"; } + +.fa-toggle-on::before { + content: "\f205"; } + +.fa-person-walking::before { + content: "\f554"; } + +.fa-walking::before { + content: "\f554"; } + +.fa-l::before { + content: "\4c"; } + +.fa-fire::before { + content: "\f06d"; } + +.fa-bed-pulse::before { + content: "\f487"; } + +.fa-procedures::before { + content: "\f487"; } + +.fa-shuttle-space::before { + content: "\f197"; } + +.fa-space-shuttle::before { + content: "\f197"; } + +.fa-face-laugh::before { + content: "\f599"; } + +.fa-laugh::before { + content: "\f599"; } + +.fa-folder-open::before { + content: "\f07c"; } + +.fa-heart-circle-plus::before { + content: "\e500"; } + +.fa-code-fork::before { + content: "\e13b"; } + +.fa-city::before { + content: "\f64f"; } + +.fa-microphone-lines::before { + content: "\f3c9"; } + +.fa-microphone-alt::before { + content: "\f3c9"; } + +.fa-pepper-hot::before { + content: "\f816"; } + +.fa-unlock::before { + content: "\f09c"; } + +.fa-colon-sign::before { + content: "\e140"; } + +.fa-headset::before { + content: "\f590"; } + +.fa-store-slash::before { + content: "\e071"; } + +.fa-road-circle-xmark::before { + content: "\e566"; } + +.fa-user-minus::before { + content: "\f503"; } + +.fa-mars-stroke-up::before { + content: "\f22a"; } + +.fa-mars-stroke-v::before { + content: "\f22a"; } + +.fa-champagne-glasses::before { + content: "\f79f"; } + +.fa-glass-cheers::before { + content: "\f79f"; } + +.fa-clipboard::before { + content: "\f328"; } + +.fa-house-circle-exclamation::before { + content: "\e50a"; } + +.fa-file-arrow-up::before { + content: "\f574"; } + +.fa-file-upload::before { + content: "\f574"; } + +.fa-wifi::before { + content: "\f1eb"; } + +.fa-wifi-3::before { + content: "\f1eb"; } + +.fa-wifi-strong::before { + content: "\f1eb"; } + +.fa-bath::before { + content: "\f2cd"; } + +.fa-bathtub::before { + content: "\f2cd"; } + +.fa-underline::before { + content: "\f0cd"; } + +.fa-user-pen::before { + content: "\f4ff"; } + +.fa-user-edit::before { + content: "\f4ff"; } + +.fa-signature::before { + content: "\f5b7"; } + +.fa-stroopwafel::before { + content: "\f551"; } + +.fa-bold::before { + content: "\f032"; } + +.fa-anchor-lock::before { + content: "\e4ad"; } + +.fa-building-ngo::before { + content: "\e4d7"; } + +.fa-manat-sign::before { + content: "\e1d5"; } + +.fa-not-equal::before { + content: "\f53e"; } + +.fa-border-top-left::before { + content: "\f853"; } + +.fa-border-style::before { + content: "\f853"; } + +.fa-map-location-dot::before { + content: "\f5a0"; } + +.fa-map-marked-alt::before { + content: "\f5a0"; } + +.fa-jedi::before { + content: "\f669"; } + +.fa-square-poll-vertical::before { + content: "\f681"; } + +.fa-poll::before { + content: "\f681"; } + +.fa-mug-hot::before { + content: "\f7b6"; } + +.fa-car-battery::before { + content: "\f5df"; } + +.fa-battery-car::before { + content: "\f5df"; } + +.fa-gift::before { + content: "\f06b"; } + +.fa-dice-two::before { + content: "\f528"; } + +.fa-chess-queen::before { + content: "\f445"; } + +.fa-glasses::before { + content: "\f530"; } + +.fa-chess-board::before { + content: "\f43c"; } + +.fa-building-circle-check::before { + content: "\e4d2"; } + +.fa-person-chalkboard::before { + content: "\e53d"; } + +.fa-mars-stroke-right::before { + content: "\f22b"; } + +.fa-mars-stroke-h::before { + content: "\f22b"; } + +.fa-hand-back-fist::before { + content: "\f255"; } + +.fa-hand-rock::before { + content: "\f255"; } + +.fa-square-caret-up::before { + content: "\f151"; } + +.fa-caret-square-up::before { + content: "\f151"; } + +.fa-cloud-showers-water::before { + content: "\e4e4"; } + +.fa-chart-bar::before { + content: "\f080"; } + +.fa-bar-chart::before { + content: "\f080"; } + +.fa-hands-bubbles::before { + content: "\e05e"; } + +.fa-hands-wash::before { + content: "\e05e"; } + +.fa-less-than-equal::before { + content: "\f537"; } + +.fa-train::before { + content: "\f238"; } + +.fa-eye-low-vision::before { + content: "\f2a8"; } + +.fa-low-vision::before { + content: "\f2a8"; } + +.fa-crow::before { + content: "\f520"; } + +.fa-sailboat::before { + content: "\e445"; } + +.fa-window-restore::before { + content: "\f2d2"; } + +.fa-square-plus::before { + content: "\f0fe"; } + +.fa-plus-square::before { + content: "\f0fe"; } + +.fa-torii-gate::before { + content: "\f6a1"; } + +.fa-frog::before { + content: "\f52e"; } + +.fa-bucket::before { + content: "\e4cf"; } + +.fa-image::before { + content: "\f03e"; } + +.fa-microphone::before { + content: "\f130"; } + +.fa-cow::before { + content: "\f6c8"; } + +.fa-caret-up::before { + content: "\f0d8"; } + +.fa-screwdriver::before { + content: "\f54a"; } + +.fa-folder-closed::before { + content: "\e185"; } + +.fa-house-tsunami::before { + content: "\e515"; } + +.fa-square-nfi::before { + content: "\e576"; } + +.fa-arrow-up-from-ground-water::before { + content: "\e4b5"; } + +.fa-martini-glass::before { + content: "\f57b"; } + +.fa-glass-martini-alt::before { + content: "\f57b"; } + +.fa-rotate-left::before { + content: "\f2ea"; } + +.fa-rotate-back::before { + content: "\f2ea"; } + +.fa-rotate-backward::before { + content: "\f2ea"; } + +.fa-undo-alt::before { + content: "\f2ea"; } + +.fa-table-columns::before { + content: "\f0db"; } + +.fa-columns::before { + content: "\f0db"; } + +.fa-lemon::before { + content: "\f094"; } + +.fa-head-side-mask::before { + content: "\e063"; } + +.fa-handshake::before { + content: "\f2b5"; } + +.fa-gem::before { + content: "\f3a5"; } + +.fa-dolly::before { + content: "\f472"; } + +.fa-dolly-box::before { + content: "\f472"; } + +.fa-smoking::before { + content: "\f48d"; } + +.fa-minimize::before { + content: "\f78c"; } + +.fa-compress-arrows-alt::before { + content: "\f78c"; } + +.fa-monument::before { + content: "\f5a6"; } + +.fa-snowplow::before { + content: "\f7d2"; } + +.fa-angles-right::before { + content: "\f101"; } + +.fa-angle-double-right::before { + content: "\f101"; } + +.fa-cannabis::before { + content: "\f55f"; } + +.fa-circle-play::before { + content: "\f144"; } + +.fa-play-circle::before { + content: "\f144"; } + +.fa-tablets::before { + content: "\f490"; } + +.fa-ethernet::before { + content: "\f796"; } + +.fa-euro-sign::before { + content: "\f153"; } + +.fa-eur::before { + content: "\f153"; } + +.fa-euro::before { + content: "\f153"; } + +.fa-chair::before { + content: "\f6c0"; } + +.fa-circle-check::before { + content: "\f058"; } + +.fa-check-circle::before { + content: "\f058"; } + +.fa-circle-stop::before { + content: "\f28d"; } + +.fa-stop-circle::before { + content: "\f28d"; } + +.fa-compass-drafting::before { + content: "\f568"; } + +.fa-drafting-compass::before { + content: "\f568"; } + +.fa-plate-wheat::before { + content: "\e55a"; } + +.fa-icicles::before { + content: "\f7ad"; } + +.fa-person-shelter::before { + content: "\e54f"; } + +.fa-neuter::before { + content: "\f22c"; } + +.fa-id-badge::before { + content: "\f2c1"; } + +.fa-marker::before { + content: "\f5a1"; } + +.fa-face-laugh-beam::before { + content: "\f59a"; } + +.fa-laugh-beam::before { + content: "\f59a"; } + +.fa-helicopter-symbol::before { + content: "\e502"; } + +.fa-universal-access::before { + content: "\f29a"; } + +.fa-circle-chevron-up::before { + content: "\f139"; } + +.fa-chevron-circle-up::before { + content: "\f139"; } + +.fa-lari-sign::before { + content: "\e1c8"; } + +.fa-volcano::before { + content: "\f770"; } + +.fa-person-walking-dashed-line-arrow-right::before { + content: "\e553"; } + +.fa-sterling-sign::before { + content: "\f154"; } + +.fa-gbp::before { + content: "\f154"; } + +.fa-pound-sign::before { + content: "\f154"; } + +.fa-viruses::before { + content: "\e076"; } + +.fa-square-person-confined::before { + content: "\e577"; } + +.fa-user-tie::before { + content: "\f508"; } + +.fa-arrow-down-long::before { + content: "\f175"; } + +.fa-long-arrow-down::before { + content: "\f175"; } + +.fa-tent-arrow-down-to-line::before { + content: "\e57e"; } + +.fa-certificate::before { + content: "\f0a3"; } + +.fa-reply-all::before { + content: "\f122"; } + +.fa-mail-reply-all::before { + content: "\f122"; } + +.fa-suitcase::before { + content: "\f0f2"; } + +.fa-person-skating::before { + content: "\f7c5"; } + +.fa-skating::before { + content: "\f7c5"; } + +.fa-filter-circle-dollar::before { + content: "\f662"; } + +.fa-funnel-dollar::before { + content: "\f662"; } + +.fa-camera-retro::before { + content: "\f083"; } + +.fa-circle-arrow-down::before { + content: "\f0ab"; } + +.fa-arrow-circle-down::before { + content: "\f0ab"; } + +.fa-file-import::before { + content: "\f56f"; } + +.fa-arrow-right-to-file::before { + content: "\f56f"; } + +.fa-square-arrow-up-right::before { + content: "\f14c"; } + +.fa-external-link-square::before { + content: "\f14c"; } + +.fa-box-open::before { + content: "\f49e"; } + +.fa-scroll::before { + content: "\f70e"; } + +.fa-spa::before { + content: "\f5bb"; } + +.fa-location-pin-lock::before { + content: "\e51f"; } + +.fa-pause::before { + content: "\f04c"; } + +.fa-hill-avalanche::before { + content: "\e507"; } + +.fa-temperature-empty::before { + content: "\f2cb"; } + +.fa-temperature-0::before { + content: "\f2cb"; } + +.fa-thermometer-0::before { + content: "\f2cb"; } + +.fa-thermometer-empty::before { + content: "\f2cb"; } + +.fa-bomb::before { + content: "\f1e2"; } + +.fa-registered::before { + content: "\f25d"; } + +.fa-address-card::before { + content: "\f2bb"; } + +.fa-contact-card::before { + content: "\f2bb"; } + +.fa-vcard::before { + content: "\f2bb"; } + +.fa-scale-unbalanced-flip::before { + content: "\f516"; } + +.fa-balance-scale-right::before { + content: "\f516"; } + +.fa-subscript::before { + content: "\f12c"; } + +.fa-diamond-turn-right::before { + content: "\f5eb"; } + +.fa-directions::before { + content: "\f5eb"; } + +.fa-burst::before { + content: "\e4dc"; } + +.fa-house-laptop::before { + content: "\e066"; } + +.fa-laptop-house::before { + content: "\e066"; } + +.fa-face-tired::before { + content: "\f5c8"; } + +.fa-tired::before { + content: "\f5c8"; } + +.fa-money-bills::before { + content: "\e1f3"; } + +.fa-smog::before { + content: "\f75f"; } + +.fa-crutch::before { + content: "\f7f7"; } + +.fa-cloud-arrow-up::before { + content: "\f0ee"; } + +.fa-cloud-upload::before { + content: "\f0ee"; } + +.fa-cloud-upload-alt::before { + content: "\f0ee"; } + +.fa-palette::before { + content: "\f53f"; } + +.fa-arrows-turn-right::before { + content: "\e4c0"; } + +.fa-vest::before { + content: "\e085"; } + +.fa-ferry::before { + content: "\e4ea"; } + +.fa-arrows-down-to-people::before { + content: "\e4b9"; } + +.fa-seedling::before { + content: "\f4d8"; } + +.fa-sprout::before { + content: "\f4d8"; } + +.fa-left-right::before { + content: "\f337"; } + +.fa-arrows-alt-h::before { + content: "\f337"; } + +.fa-boxes-packing::before { + content: "\e4c7"; } + +.fa-circle-arrow-left::before { + content: "\f0a8"; } + +.fa-arrow-circle-left::before { + content: "\f0a8"; } + +.fa-group-arrows-rotate::before { + content: "\e4f6"; } + +.fa-bowl-food::before { + content: "\e4c6"; } + +.fa-candy-cane::before { + content: "\f786"; } + +.fa-arrow-down-wide-short::before { + content: "\f160"; } + +.fa-sort-amount-asc::before { + content: "\f160"; } + +.fa-sort-amount-down::before { + content: "\f160"; } + +.fa-cloud-bolt::before { + content: "\f76c"; } + +.fa-thunderstorm::before { + content: "\f76c"; } + +.fa-text-slash::before { + content: "\f87d"; } + +.fa-remove-format::before { + content: "\f87d"; } + +.fa-face-smile-wink::before { + content: "\f4da"; } + +.fa-smile-wink::before { + content: "\f4da"; } + +.fa-file-word::before { + content: "\f1c2"; } + +.fa-file-powerpoint::before { + content: "\f1c4"; } + +.fa-arrows-left-right::before { + content: "\f07e"; } + +.fa-arrows-h::before { + content: "\f07e"; } + +.fa-house-lock::before { + content: "\e510"; } + +.fa-cloud-arrow-down::before { + content: "\f0ed"; } + +.fa-cloud-download::before { + content: "\f0ed"; } + +.fa-cloud-download-alt::before { + content: "\f0ed"; } + +.fa-children::before { + content: "\e4e1"; } + +.fa-chalkboard::before { + content: "\f51b"; } + +.fa-blackboard::before { + content: "\f51b"; } + +.fa-user-large-slash::before { + content: "\f4fa"; } + +.fa-user-alt-slash::before { + content: "\f4fa"; } + +.fa-envelope-open::before { + content: "\f2b6"; } + +.fa-handshake-simple-slash::before { + content: "\e05f"; } + +.fa-handshake-alt-slash::before { + content: "\e05f"; } + +.fa-mattress-pillow::before { + content: "\e525"; } + +.fa-guarani-sign::before { + content: "\e19a"; } + +.fa-arrows-rotate::before { + content: "\f021"; } + +.fa-refresh::before { + content: "\f021"; } + +.fa-sync::before { + content: "\f021"; } + +.fa-fire-extinguisher::before { + content: "\f134"; } + +.fa-cruzeiro-sign::before { + content: "\e152"; } + +.fa-greater-than-equal::before { + content: "\f532"; } + +.fa-shield-halved::before { + content: "\f3ed"; } + +.fa-shield-alt::before { + content: "\f3ed"; } + +.fa-book-atlas::before { + content: "\f558"; } + +.fa-atlas::before { + content: "\f558"; } + +.fa-virus::before { + content: "\e074"; } + +.fa-envelope-circle-check::before { + content: "\e4e8"; } + +.fa-layer-group::before { + content: "\f5fd"; } + +.fa-arrows-to-dot::before { + content: "\e4be"; } + +.fa-archway::before { + content: "\f557"; } + +.fa-heart-circle-check::before { + content: "\e4fd"; } + +.fa-house-chimney-crack::before { + content: "\f6f1"; } + +.fa-house-damage::before { + content: "\f6f1"; } + +.fa-file-zipper::before { + content: "\f1c6"; } + +.fa-file-archive::before { + content: "\f1c6"; } + +.fa-square::before { + content: "\f0c8"; } + +.fa-martini-glass-empty::before { + content: "\f000"; } + +.fa-glass-martini::before { + content: "\f000"; } + +.fa-couch::before { + content: "\f4b8"; } + +.fa-cedi-sign::before { + content: "\e0df"; } + +.fa-italic::before { + content: "\f033"; } + +.fa-church::before { + content: "\f51d"; } + +.fa-comments-dollar::before { + content: "\f653"; } + +.fa-democrat::before { + content: "\f747"; } + +.fa-z::before { + content: "\5a"; } + +.fa-person-skiing::before { + content: "\f7c9"; } + +.fa-skiing::before { + content: "\f7c9"; } + +.fa-road-lock::before { + content: "\e567"; } + +.fa-a::before { + content: "\41"; } + +.fa-temperature-arrow-down::before { + content: "\e03f"; } + +.fa-temperature-down::before { + content: "\e03f"; } + +.fa-feather-pointed::before { + content: "\f56b"; } + +.fa-feather-alt::before { + content: "\f56b"; } + +.fa-p::before { + content: "\50"; } + +.fa-snowflake::before { + content: "\f2dc"; } + +.fa-newspaper::before { + content: "\f1ea"; } + +.fa-rectangle-ad::before { + content: "\f641"; } + +.fa-ad::before { + content: "\f641"; } + +.fa-circle-arrow-right::before { + content: "\f0a9"; } + +.fa-arrow-circle-right::before { + content: "\f0a9"; } + +.fa-filter-circle-xmark::before { + content: "\e17b"; } + +.fa-locust::before { + content: "\e520"; } + +.fa-sort::before { + content: "\f0dc"; } + +.fa-unsorted::before { + content: "\f0dc"; } + +.fa-list-ol::before { + content: "\f0cb"; } + +.fa-list-1-2::before { + content: "\f0cb"; } + +.fa-list-numeric::before { + content: "\f0cb"; } + +.fa-person-dress-burst::before { + content: "\e544"; } + +.fa-money-check-dollar::before { + content: "\f53d"; } + +.fa-money-check-alt::before { + content: "\f53d"; } + +.fa-vector-square::before { + content: "\f5cb"; } + +.fa-bread-slice::before { + content: "\f7ec"; } + +.fa-language::before { + content: "\f1ab"; } + +.fa-face-kiss-wink-heart::before { + content: "\f598"; } + +.fa-kiss-wink-heart::before { + content: "\f598"; } + +.fa-filter::before { + content: "\f0b0"; } + +.fa-question::before { + content: "\3f"; } + +.fa-file-signature::before { + content: "\f573"; } + +.fa-up-down-left-right::before { + content: "\f0b2"; } + +.fa-arrows-alt::before { + content: "\f0b2"; } + +.fa-house-chimney-user::before { + content: "\e065"; } + +.fa-hand-holding-heart::before { + content: "\f4be"; } + +.fa-puzzle-piece::before { + content: "\f12e"; } + +.fa-money-check::before { + content: "\f53c"; } + +.fa-star-half-stroke::before { + content: "\f5c0"; } + +.fa-star-half-alt::before { + content: "\f5c0"; } + +.fa-code::before { + content: "\f121"; } + +.fa-whiskey-glass::before { + content: "\f7a0"; } + +.fa-glass-whiskey::before { + content: "\f7a0"; } + +.fa-building-circle-exclamation::before { + content: "\e4d3"; } + +.fa-magnifying-glass-chart::before { + content: "\e522"; } + +.fa-arrow-up-right-from-square::before { + content: "\f08e"; } + +.fa-external-link::before { + content: "\f08e"; } + +.fa-cubes-stacked::before { + content: "\e4e6"; } + +.fa-won-sign::before { + content: "\f159"; } + +.fa-krw::before { + content: "\f159"; } + +.fa-won::before { + content: "\f159"; } + +.fa-virus-covid::before { + content: "\e4a8"; } + +.fa-austral-sign::before { + content: "\e0a9"; } + +.fa-f::before { + content: "\46"; } + +.fa-leaf::before { + content: "\f06c"; } + +.fa-road::before { + content: "\f018"; } + +.fa-taxi::before { + content: "\f1ba"; } + +.fa-cab::before { + content: "\f1ba"; } + +.fa-person-circle-plus::before { + content: "\e541"; } + +.fa-chart-pie::before { + content: "\f200"; } + +.fa-pie-chart::before { + content: "\f200"; } + +.fa-bolt-lightning::before { + content: "\e0b7"; } + +.fa-sack-xmark::before { + content: "\e56a"; } + +.fa-file-excel::before { + content: "\f1c3"; } + +.fa-file-contract::before { + content: "\f56c"; } + +.fa-fish-fins::before { + content: "\e4f2"; } + +.fa-building-flag::before { + content: "\e4d5"; } + +.fa-face-grin-beam::before { + content: "\f582"; } + +.fa-grin-beam::before { + content: "\f582"; } + +.fa-object-ungroup::before { + content: "\f248"; } + +.fa-poop::before { + content: "\f619"; } + +.fa-location-pin::before { + content: "\f041"; } + +.fa-map-marker::before { + content: "\f041"; } + +.fa-kaaba::before { + content: "\f66b"; } + +.fa-toilet-paper::before { + content: "\f71e"; } + +.fa-helmet-safety::before { + content: "\f807"; } + +.fa-hard-hat::before { + content: "\f807"; } + +.fa-hat-hard::before { + content: "\f807"; } + +.fa-eject::before { + content: "\f052"; } + +.fa-circle-right::before { + content: "\f35a"; } + +.fa-arrow-alt-circle-right::before { + content: "\f35a"; } + +.fa-plane-circle-check::before { + content: "\e555"; } + +.fa-face-rolling-eyes::before { + content: "\f5a5"; } + +.fa-meh-rolling-eyes::before { + content: "\f5a5"; } + +.fa-object-group::before { + content: "\f247"; } + +.fa-chart-line::before { + content: "\f201"; } + +.fa-line-chart::before { + content: "\f201"; } + +.fa-mask-ventilator::before { + content: "\e524"; } + +.fa-arrow-right::before { + content: "\f061"; } + +.fa-signs-post::before { + content: "\f277"; } + +.fa-map-signs::before { + content: "\f277"; } + +.fa-cash-register::before { + content: "\f788"; } + +.fa-person-circle-question::before { + content: "\e542"; } + +.fa-h::before { + content: "\48"; } + +.fa-tarp::before { + content: "\e57b"; } + +.fa-screwdriver-wrench::before { + content: "\f7d9"; } + +.fa-tools::before { + content: "\f7d9"; } + +.fa-arrows-to-eye::before { + content: "\e4bf"; } + +.fa-plug-circle-bolt::before { + content: "\e55b"; } + +.fa-heart::before { + content: "\f004"; } + +.fa-mars-and-venus::before { + content: "\f224"; } + +.fa-house-user::before { + content: "\e1b0"; } + +.fa-home-user::before { + content: "\e1b0"; } + +.fa-dumpster-fire::before { + content: "\f794"; } + +.fa-house-crack::before { + content: "\e3b1"; } + +.fa-martini-glass-citrus::before { + content: "\f561"; } + +.fa-cocktail::before { + content: "\f561"; } + +.fa-face-surprise::before { + content: "\f5c2"; } + +.fa-surprise::before { + content: "\f5c2"; } + +.fa-bottle-water::before { + content: "\e4c5"; } + +.fa-circle-pause::before { + content: "\f28b"; } + +.fa-pause-circle::before { + content: "\f28b"; } + +.fa-toilet-paper-slash::before { + content: "\e072"; } + +.fa-apple-whole::before { + content: "\f5d1"; } + +.fa-apple-alt::before { + content: "\f5d1"; } + +.fa-kitchen-set::before { + content: "\e51a"; } + +.fa-r::before { + content: "\52"; } + +.fa-temperature-quarter::before { + content: "\f2ca"; } + +.fa-temperature-1::before { + content: "\f2ca"; } + +.fa-thermometer-1::before { + content: "\f2ca"; } + +.fa-thermometer-quarter::before { + content: "\f2ca"; } + +.fa-cube::before { + content: "\f1b2"; } + +.fa-bitcoin-sign::before { + content: "\e0b4"; } + +.fa-shield-dog::before { + content: "\e573"; } + +.fa-solar-panel::before { + content: "\f5ba"; } + +.fa-lock-open::before { + content: "\f3c1"; } + +.fa-elevator::before { + content: "\e16d"; } + +.fa-money-bill-transfer::before { + content: "\e528"; } + +.fa-money-bill-trend-up::before { + content: "\e529"; } + +.fa-house-flood-water-circle-arrow-right::before { + content: "\e50f"; } + +.fa-square-poll-horizontal::before { + content: "\f682"; } + +.fa-poll-h::before { + content: "\f682"; } + +.fa-circle::before { + content: "\f111"; } + +.fa-backward-fast::before { + content: "\f049"; } + +.fa-fast-backward::before { + content: "\f049"; } + +.fa-recycle::before { + content: "\f1b8"; } + +.fa-user-astronaut::before { + content: "\f4fb"; } + +.fa-plane-slash::before { + content: "\e069"; } + +.fa-trademark::before { + content: "\f25c"; } + +.fa-basketball::before { + content: "\f434"; } + +.fa-basketball-ball::before { + content: "\f434"; } + +.fa-satellite-dish::before { + content: "\f7c0"; } + +.fa-circle-up::before { + content: "\f35b"; } + +.fa-arrow-alt-circle-up::before { + content: "\f35b"; } + +.fa-mobile-screen-button::before { + content: "\f3cd"; } + +.fa-mobile-alt::before { + content: "\f3cd"; } + +.fa-volume-high::before { + content: "\f028"; } + +.fa-volume-up::before { + content: "\f028"; } + +.fa-users-rays::before { + content: "\e593"; } + +.fa-wallet::before { + content: "\f555"; } + +.fa-clipboard-check::before { + content: "\f46c"; } + +.fa-file-audio::before { + content: "\f1c7"; } + +.fa-burger::before { + content: "\f805"; } + +.fa-hamburger::before { + content: "\f805"; } + +.fa-wrench::before { + content: "\f0ad"; } + +.fa-bugs::before { + content: "\e4d0"; } + +.fa-rupee-sign::before { + content: "\f156"; } + +.fa-rupee::before { + content: "\f156"; } + +.fa-file-image::before { + content: "\f1c5"; } + +.fa-circle-question::before { + content: "\f059"; } + +.fa-question-circle::before { + content: "\f059"; } + +.fa-plane-departure::before { + content: "\f5b0"; } + +.fa-handshake-slash::before { + content: "\e060"; } + +.fa-book-bookmark::before { + content: "\e0bb"; } + +.fa-code-branch::before { + content: "\f126"; } + +.fa-hat-cowboy::before { + content: "\f8c0"; } + +.fa-bridge::before { + content: "\e4c8"; } + +.fa-phone-flip::before { + content: "\f879"; } + +.fa-phone-alt::before { + content: "\f879"; } + +.fa-truck-front::before { + content: "\e2b7"; } + +.fa-cat::before { + content: "\f6be"; } + +.fa-anchor-circle-exclamation::before { + content: "\e4ab"; } + +.fa-truck-field::before { + content: "\e58d"; } + +.fa-route::before { + content: "\f4d7"; } + +.fa-clipboard-question::before { + content: "\e4e3"; } + +.fa-panorama::before { + content: "\e209"; } + +.fa-comment-medical::before { + content: "\f7f5"; } + +.fa-teeth-open::before { + content: "\f62f"; } + +.fa-file-circle-minus::before { + content: "\e4ed"; } + +.fa-tags::before { + content: "\f02c"; } + +.fa-wine-glass::before { + content: "\f4e3"; } + +.fa-forward-fast::before { + content: "\f050"; } + +.fa-fast-forward::before { + content: "\f050"; } + +.fa-face-meh-blank::before { + content: "\f5a4"; } + +.fa-meh-blank::before { + content: "\f5a4"; } + +.fa-square-parking::before { + content: "\f540"; } + +.fa-parking::before { + content: "\f540"; } + +.fa-house-signal::before { + content: "\e012"; } + +.fa-bars-progress::before { + content: "\f828"; } + +.fa-tasks-alt::before { + content: "\f828"; } + +.fa-faucet-drip::before { + content: "\e006"; } + +.fa-cart-flatbed::before { + content: "\f474"; } + +.fa-dolly-flatbed::before { + content: "\f474"; } + +.fa-ban-smoking::before { + content: "\f54d"; } + +.fa-smoking-ban::before { + content: "\f54d"; } + +.fa-terminal::before { + content: "\f120"; } + +.fa-mobile-button::before { + content: "\f10b"; } + +.fa-house-medical-flag::before { + content: "\e514"; } + +.fa-basket-shopping::before { + content: "\f291"; } + +.fa-shopping-basket::before { + content: "\f291"; } + +.fa-tape::before { + content: "\f4db"; } + +.fa-bus-simple::before { + content: "\f55e"; } + +.fa-bus-alt::before { + content: "\f55e"; } + +.fa-eye::before { + content: "\f06e"; } + +.fa-face-sad-cry::before { + content: "\f5b3"; } + +.fa-sad-cry::before { + content: "\f5b3"; } + +.fa-audio-description::before { + content: "\f29e"; } + +.fa-person-military-to-person::before { + content: "\e54c"; } + +.fa-file-shield::before { + content: "\e4f0"; } + +.fa-user-slash::before { + content: "\f506"; } + +.fa-pen::before { + content: "\f304"; } + +.fa-tower-observation::before { + content: "\e586"; } + +.fa-file-code::before { + content: "\f1c9"; } + +.fa-signal::before { + content: "\f012"; } + +.fa-signal-5::before { + content: "\f012"; } + +.fa-signal-perfect::before { + content: "\f012"; } + +.fa-bus::before { + content: "\f207"; } + +.fa-heart-circle-xmark::before { + content: "\e501"; } + +.fa-house-chimney::before { + content: "\e3af"; } + +.fa-home-lg::before { + content: "\e3af"; } + +.fa-window-maximize::before { + content: "\f2d0"; } + +.fa-face-frown::before { + content: "\f119"; } + +.fa-frown::before { + content: "\f119"; } + +.fa-prescription::before { + content: "\f5b1"; } + +.fa-shop::before { + content: "\f54f"; } + +.fa-store-alt::before { + content: "\f54f"; } + +.fa-floppy-disk::before { + content: "\f0c7"; } + +.fa-save::before { + content: "\f0c7"; } + +.fa-vihara::before { + content: "\f6a7"; } + +.fa-scale-unbalanced::before { + content: "\f515"; } + +.fa-balance-scale-left::before { + content: "\f515"; } + +.fa-sort-up::before { + content: "\f0de"; } + +.fa-sort-asc::before { + content: "\f0de"; } + +.fa-comment-dots::before { + content: "\f4ad"; } + +.fa-commenting::before { + content: "\f4ad"; } + +.fa-plant-wilt::before { + content: "\e5aa"; } + +.fa-diamond::before { + content: "\f219"; } + +.fa-face-grin-squint::before { + content: "\f585"; } + +.fa-grin-squint::before { + content: "\f585"; } + +.fa-hand-holding-dollar::before { + content: "\f4c0"; } + +.fa-hand-holding-usd::before { + content: "\f4c0"; } + +.fa-bacterium::before { + content: "\e05a"; } + +.fa-hand-pointer::before { + content: "\f25a"; } + +.fa-drum-steelpan::before { + content: "\f56a"; } + +.fa-hand-scissors::before { + content: "\f257"; } + +.fa-hands-praying::before { + content: "\f684"; } + +.fa-praying-hands::before { + content: "\f684"; } + +.fa-arrow-rotate-right::before { + content: "\f01e"; } + +.fa-arrow-right-rotate::before { + content: "\f01e"; } + +.fa-arrow-rotate-forward::before { + content: "\f01e"; } + +.fa-redo::before { + content: "\f01e"; } + +.fa-biohazard::before { + content: "\f780"; } + +.fa-location-crosshairs::before { + content: "\f601"; } + +.fa-location::before { + content: "\f601"; } + +.fa-mars-double::before { + content: "\f227"; } + +.fa-child-dress::before { + content: "\e59c"; } + +.fa-users-between-lines::before { + content: "\e591"; } + +.fa-lungs-virus::before { + content: "\e067"; } + +.fa-face-grin-tears::before { + content: "\f588"; } + +.fa-grin-tears::before { + content: "\f588"; } + +.fa-phone::before { + content: "\f095"; } + +.fa-calendar-xmark::before { + content: "\f273"; } + +.fa-calendar-times::before { + content: "\f273"; } + +.fa-child-reaching::before { + content: "\e59d"; } + +.fa-head-side-virus::before { + content: "\e064"; } + +.fa-user-gear::before { + content: "\f4fe"; } + +.fa-user-cog::before { + content: "\f4fe"; } + +.fa-arrow-up-1-9::before { + content: "\f163"; } + +.fa-sort-numeric-up::before { + content: "\f163"; } + +.fa-door-closed::before { + content: "\f52a"; } + +.fa-shield-virus::before { + content: "\e06c"; } + +.fa-dice-six::before { + content: "\f526"; } + +.fa-mosquito-net::before { + content: "\e52c"; } + +.fa-bridge-water::before { + content: "\e4ce"; } + +.fa-person-booth::before { + content: "\f756"; } + +.fa-text-width::before { + content: "\f035"; } + +.fa-hat-wizard::before { + content: "\f6e8"; } + +.fa-pen-fancy::before { + content: "\f5ac"; } + +.fa-person-digging::before { + content: "\f85e"; } + +.fa-digging::before { + content: "\f85e"; } + +.fa-trash::before { + content: "\f1f8"; } + +.fa-gauge-simple::before { + content: "\f629"; } + +.fa-gauge-simple-med::before { + content: "\f629"; } + +.fa-tachometer-average::before { + content: "\f629"; } + +.fa-book-medical::before { + content: "\f7e6"; } + +.fa-poo::before { + content: "\f2fe"; } + +.fa-quote-right::before { + content: "\f10e"; } + +.fa-quote-right-alt::before { + content: "\f10e"; } + +.fa-shirt::before { + content: "\f553"; } + +.fa-t-shirt::before { + content: "\f553"; } + +.fa-tshirt::before { + content: "\f553"; } + +.fa-cubes::before { + content: "\f1b3"; } + +.fa-divide::before { + content: "\f529"; } + +.fa-tenge-sign::before { + content: "\f7d7"; } + +.fa-tenge::before { + content: "\f7d7"; } + +.fa-headphones::before { + content: "\f025"; } + +.fa-hands-holding::before { + content: "\f4c2"; } + +.fa-hands-clapping::before { + content: "\e1a8"; } + +.fa-republican::before { + content: "\f75e"; } + +.fa-arrow-left::before { + content: "\f060"; } + +.fa-person-circle-xmark::before { + content: "\e543"; } + +.fa-ruler::before { + content: "\f545"; } + +.fa-align-left::before { + content: "\f036"; } + +.fa-dice-d6::before { + content: "\f6d1"; } + +.fa-restroom::before { + content: "\f7bd"; } + +.fa-j::before { + content: "\4a"; } + +.fa-users-viewfinder::before { + content: "\e595"; } + +.fa-file-video::before { + content: "\f1c8"; } + +.fa-up-right-from-square::before { + content: "\f35d"; } + +.fa-external-link-alt::before { + content: "\f35d"; } + +.fa-table-cells::before { + content: "\f00a"; } + +.fa-th::before { + content: "\f00a"; } + +.fa-file-pdf::before { + content: "\f1c1"; } + +.fa-book-bible::before { + content: "\f647"; } + +.fa-bible::before { + content: "\f647"; } + +.fa-o::before { + content: "\4f"; } + +.fa-suitcase-medical::before { + content: "\f0fa"; } + +.fa-medkit::before { + content: "\f0fa"; } + +.fa-user-secret::before { + content: "\f21b"; } + +.fa-otter::before { + content: "\f700"; } + +.fa-person-dress::before { + content: "\f182"; } + +.fa-female::before { + content: "\f182"; } + +.fa-comment-dollar::before { + content: "\f651"; } + +.fa-business-time::before { + content: "\f64a"; } + +.fa-briefcase-clock::before { + content: "\f64a"; } + +.fa-table-cells-large::before { + content: "\f009"; } + +.fa-th-large::before { + content: "\f009"; } + +.fa-book-tanakh::before { + content: "\f827"; } + +.fa-tanakh::before { + content: "\f827"; } + +.fa-phone-volume::before { + content: "\f2a0"; } + +.fa-volume-control-phone::before { + content: "\f2a0"; } + +.fa-hat-cowboy-side::before { + content: "\f8c1"; } + +.fa-clipboard-user::before { + content: "\f7f3"; } + +.fa-child::before { + content: "\f1ae"; } + +.fa-lira-sign::before { + content: "\f195"; } + +.fa-satellite::before { + content: "\f7bf"; } + +.fa-plane-lock::before { + content: "\e558"; } + +.fa-tag::before { + content: "\f02b"; } + +.fa-comment::before { + content: "\f075"; } + +.fa-cake-candles::before { + content: "\f1fd"; } + +.fa-birthday-cake::before { + content: "\f1fd"; } + +.fa-cake::before { + content: "\f1fd"; } + +.fa-envelope::before { + content: "\f0e0"; } + +.fa-angles-up::before { + content: "\f102"; } + +.fa-angle-double-up::before { + content: "\f102"; } + +.fa-paperclip::before { + content: "\f0c6"; } + +.fa-arrow-right-to-city::before { + content: "\e4b3"; } + +.fa-ribbon::before { + content: "\f4d6"; } + +.fa-lungs::before { + content: "\f604"; } + +.fa-arrow-up-9-1::before { + content: "\f887"; } + +.fa-sort-numeric-up-alt::before { + content: "\f887"; } + +.fa-litecoin-sign::before { + content: "\e1d3"; } + +.fa-border-none::before { + content: "\f850"; } + +.fa-circle-nodes::before { + content: "\e4e2"; } + +.fa-parachute-box::before { + content: "\f4cd"; } + +.fa-indent::before { + content: "\f03c"; } + +.fa-truck-field-un::before { + content: "\e58e"; } + +.fa-hourglass::before { + content: "\f254"; } + +.fa-hourglass-empty::before { + content: "\f254"; } + +.fa-mountain::before { + content: "\f6fc"; } + +.fa-user-doctor::before { + content: "\f0f0"; } + +.fa-user-md::before { + content: "\f0f0"; } + +.fa-circle-info::before { + content: "\f05a"; } + +.fa-info-circle::before { + content: "\f05a"; } + +.fa-cloud-meatball::before { + content: "\f73b"; } + +.fa-camera::before { + content: "\f030"; } + +.fa-camera-alt::before { + content: "\f030"; } + +.fa-square-virus::before { + content: "\e578"; } + +.fa-meteor::before { + content: "\f753"; } + +.fa-car-on::before { + content: "\e4dd"; } + +.fa-sleigh::before { + content: "\f7cc"; } + +.fa-arrow-down-1-9::before { + content: "\f162"; } + +.fa-sort-numeric-asc::before { + content: "\f162"; } + +.fa-sort-numeric-down::before { + content: "\f162"; } + +.fa-hand-holding-droplet::before { + content: "\f4c1"; } + +.fa-hand-holding-water::before { + content: "\f4c1"; } + +.fa-water::before { + content: "\f773"; } + +.fa-calendar-check::before { + content: "\f274"; } + +.fa-braille::before { + content: "\f2a1"; } + +.fa-prescription-bottle-medical::before { + content: "\f486"; } + +.fa-prescription-bottle-alt::before { + content: "\f486"; } + +.fa-landmark::before { + content: "\f66f"; } + +.fa-truck::before { + content: "\f0d1"; } + +.fa-crosshairs::before { + content: "\f05b"; } + +.fa-person-cane::before { + content: "\e53c"; } + +.fa-tent::before { + content: "\e57d"; } + +.fa-vest-patches::before { + content: "\e086"; } + +.fa-check-double::before { + content: "\f560"; } + +.fa-arrow-down-a-z::before { + content: "\f15d"; } + +.fa-sort-alpha-asc::before { + content: "\f15d"; } + +.fa-sort-alpha-down::before { + content: "\f15d"; } + +.fa-money-bill-wheat::before { + content: "\e52a"; } + +.fa-cookie::before { + content: "\f563"; } + +.fa-arrow-rotate-left::before { + content: "\f0e2"; } + +.fa-arrow-left-rotate::before { + content: "\f0e2"; } + +.fa-arrow-rotate-back::before { + content: "\f0e2"; } + +.fa-arrow-rotate-backward::before { + content: "\f0e2"; } + +.fa-undo::before { + content: "\f0e2"; } + +.fa-hard-drive::before { + content: "\f0a0"; } + +.fa-hdd::before { + content: "\f0a0"; } + +.fa-face-grin-squint-tears::before { + content: "\f586"; } + +.fa-grin-squint-tears::before { + content: "\f586"; } + +.fa-dumbbell::before { + content: "\f44b"; } + +.fa-rectangle-list::before { + content: "\f022"; } + +.fa-list-alt::before { + content: "\f022"; } + +.fa-tarp-droplet::before { + content: "\e57c"; } + +.fa-house-medical-circle-check::before { + content: "\e511"; } + +.fa-person-skiing-nordic::before { + content: "\f7ca"; } + +.fa-skiing-nordic::before { + content: "\f7ca"; } + +.fa-calendar-plus::before { + content: "\f271"; } + +.fa-plane-arrival::before { + content: "\f5af"; } + +.fa-circle-left::before { + content: "\f359"; } + +.fa-arrow-alt-circle-left::before { + content: "\f359"; } + +.fa-train-subway::before { + content: "\f239"; } + +.fa-subway::before { + content: "\f239"; } + +.fa-chart-gantt::before { + content: "\e0e4"; } + +.fa-indian-rupee-sign::before { + content: "\e1bc"; } + +.fa-indian-rupee::before { + content: "\e1bc"; } + +.fa-inr::before { + content: "\e1bc"; } + +.fa-crop-simple::before { + content: "\f565"; } + +.fa-crop-alt::before { + content: "\f565"; } + +.fa-money-bill-1::before { + content: "\f3d1"; } + +.fa-money-bill-alt::before { + content: "\f3d1"; } + +.fa-left-long::before { + content: "\f30a"; } + +.fa-long-arrow-alt-left::before { + content: "\f30a"; } + +.fa-dna::before { + content: "\f471"; } + +.fa-virus-slash::before { + content: "\e075"; } + +.fa-minus::before { + content: "\f068"; } + +.fa-subtract::before { + content: "\f068"; } + +.fa-chess::before { + content: "\f439"; } + +.fa-arrow-left-long::before { + content: "\f177"; } + +.fa-long-arrow-left::before { + content: "\f177"; } + +.fa-plug-circle-check::before { + content: "\e55c"; } + +.fa-street-view::before { + content: "\f21d"; } + +.fa-franc-sign::before { + content: "\e18f"; } + +.fa-volume-off::before { + content: "\f026"; } + +.fa-hands-asl-interpreting::before { + content: "\f2a3"; } + +.fa-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-asl-interpreting::before { + content: "\f2a3"; } + +.fa-hands-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-gear::before { + content: "\f013"; } + +.fa-cog::before { + content: "\f013"; } + +.fa-droplet-slash::before { + content: "\f5c7"; } + +.fa-tint-slash::before { + content: "\f5c7"; } + +.fa-mosque::before { + content: "\f678"; } + +.fa-mosquito::before { + content: "\e52b"; } + +.fa-star-of-david::before { + content: "\f69a"; } + +.fa-person-military-rifle::before { + content: "\e54b"; } + +.fa-cart-shopping::before { + content: "\f07a"; } + +.fa-shopping-cart::before { + content: "\f07a"; } + +.fa-vials::before { + content: "\f493"; } + +.fa-plug-circle-plus::before { + content: "\e55f"; } + +.fa-place-of-worship::before { + content: "\f67f"; } + +.fa-grip-vertical::before { + content: "\f58e"; } + +.fa-arrow-turn-up::before { + content: "\f148"; } + +.fa-level-up::before { + content: "\f148"; } + +.fa-u::before { + content: "\55"; } + +.fa-square-root-variable::before { + content: "\f698"; } + +.fa-square-root-alt::before { + content: "\f698"; } + +.fa-clock::before { + content: "\f017"; } + +.fa-clock-four::before { + content: "\f017"; } + +.fa-backward-step::before { + content: "\f048"; } + +.fa-step-backward::before { + content: "\f048"; } + +.fa-pallet::before { + content: "\f482"; } + +.fa-faucet::before { + content: "\e005"; } + +.fa-baseball-bat-ball::before { + content: "\f432"; } + +.fa-s::before { + content: "\53"; } + +.fa-timeline::before { + content: "\e29c"; } + +.fa-keyboard::before { + content: "\f11c"; } + +.fa-caret-down::before { + content: "\f0d7"; } + +.fa-house-chimney-medical::before { + content: "\f7f2"; } + +.fa-clinic-medical::before { + content: "\f7f2"; } + +.fa-temperature-three-quarters::before { + content: "\f2c8"; } + +.fa-temperature-3::before { + content: "\f2c8"; } + +.fa-thermometer-3::before { + content: "\f2c8"; } + +.fa-thermometer-three-quarters::before { + content: "\f2c8"; } + +.fa-mobile-screen::before { + content: "\f3cf"; } + +.fa-mobile-android-alt::before { + content: "\f3cf"; } + +.fa-plane-up::before { + content: "\e22d"; } + +.fa-piggy-bank::before { + content: "\f4d3"; } + +.fa-battery-half::before { + content: "\f242"; } + +.fa-battery-3::before { + content: "\f242"; } + +.fa-mountain-city::before { + content: "\e52e"; } + +.fa-coins::before { + content: "\f51e"; } + +.fa-khanda::before { + content: "\f66d"; } + +.fa-sliders::before { + content: "\f1de"; } + +.fa-sliders-h::before { + content: "\f1de"; } + +.fa-folder-tree::before { + content: "\f802"; } + +.fa-network-wired::before { + content: "\f6ff"; } + +.fa-map-pin::before { + content: "\f276"; } + +.fa-hamsa::before { + content: "\f665"; } + +.fa-cent-sign::before { + content: "\e3f5"; } + +.fa-flask::before { + content: "\f0c3"; } + +.fa-person-pregnant::before { + content: "\e31e"; } + +.fa-wand-sparkles::before { + content: "\f72b"; } + +.fa-ellipsis-vertical::before { + content: "\f142"; } + +.fa-ellipsis-v::before { + content: "\f142"; } + +.fa-ticket::before { + content: "\f145"; } + +.fa-power-off::before { + content: "\f011"; } + +.fa-right-long::before { + content: "\f30b"; } + +.fa-long-arrow-alt-right::before { + content: "\f30b"; } + +.fa-flag-usa::before { + content: "\f74d"; } + +.fa-laptop-file::before { + content: "\e51d"; } + +.fa-tty::before { + content: "\f1e4"; } + +.fa-teletype::before { + content: "\f1e4"; } + +.fa-diagram-next::before { + content: "\e476"; } + +.fa-person-rifle::before { + content: "\e54e"; } + +.fa-house-medical-circle-exclamation::before { + content: "\e512"; } + +.fa-closed-captioning::before { + content: "\f20a"; } + +.fa-person-hiking::before { + content: "\f6ec"; } + +.fa-hiking::before { + content: "\f6ec"; } + +.fa-venus-double::before { + content: "\f226"; } + +.fa-images::before { + content: "\f302"; } + +.fa-calculator::before { + content: "\f1ec"; } + +.fa-people-pulling::before { + content: "\e535"; } + +.fa-n::before { + content: "\4e"; } + +.fa-cable-car::before { + content: "\f7da"; } + +.fa-tram::before { + content: "\f7da"; } + +.fa-cloud-rain::before { + content: "\f73d"; } + +.fa-building-circle-xmark::before { + content: "\e4d4"; } + +.fa-ship::before { + content: "\f21a"; } + +.fa-arrows-down-to-line::before { + content: "\e4b8"; } + +.fa-download::before { + content: "\f019"; } + +.fa-face-grin::before { + content: "\f580"; } + +.fa-grin::before { + content: "\f580"; } + +.fa-delete-left::before { + content: "\f55a"; } + +.fa-backspace::before { + content: "\f55a"; } + +.fa-eye-dropper::before { + content: "\f1fb"; } + +.fa-eye-dropper-empty::before { + content: "\f1fb"; } + +.fa-eyedropper::before { + content: "\f1fb"; } + +.fa-file-circle-check::before { + content: "\e5a0"; } + +.fa-forward::before { + content: "\f04e"; } + +.fa-mobile::before { + content: "\f3ce"; } + +.fa-mobile-android::before { + content: "\f3ce"; } + +.fa-mobile-phone::before { + content: "\f3ce"; } + +.fa-face-meh::before { + content: "\f11a"; } + +.fa-meh::before { + content: "\f11a"; } + +.fa-align-center::before { + content: "\f037"; } + +.fa-book-skull::before { + content: "\f6b7"; } + +.fa-book-dead::before { + content: "\f6b7"; } + +.fa-id-card::before { + content: "\f2c2"; } + +.fa-drivers-license::before { + content: "\f2c2"; } + +.fa-outdent::before { + content: "\f03b"; } + +.fa-dedent::before { + content: "\f03b"; } + +.fa-heart-circle-exclamation::before { + content: "\e4fe"; } + +.fa-house::before { + content: "\f015"; } + +.fa-home::before { + content: "\f015"; } + +.fa-home-alt::before { + content: "\f015"; } + +.fa-home-lg-alt::before { + content: "\f015"; } + +.fa-calendar-week::before { + content: "\f784"; } + +.fa-laptop-medical::before { + content: "\f812"; } + +.fa-b::before { + content: "\42"; } + +.fa-file-medical::before { + content: "\f477"; } + +.fa-dice-one::before { + content: "\f525"; } + +.fa-kiwi-bird::before { + content: "\f535"; } + +.fa-arrow-right-arrow-left::before { + content: "\f0ec"; } + +.fa-exchange::before { + content: "\f0ec"; } + +.fa-rotate-right::before { + content: "\f2f9"; } + +.fa-redo-alt::before { + content: "\f2f9"; } + +.fa-rotate-forward::before { + content: "\f2f9"; } + +.fa-utensils::before { + content: "\f2e7"; } + +.fa-cutlery::before { + content: "\f2e7"; } + +.fa-arrow-up-wide-short::before { + content: "\f161"; } + +.fa-sort-amount-up::before { + content: "\f161"; } + +.fa-mill-sign::before { + content: "\e1ed"; } + +.fa-bowl-rice::before { + content: "\e2eb"; } + +.fa-skull::before { + content: "\f54c"; } + +.fa-tower-broadcast::before { + content: "\f519"; } + +.fa-broadcast-tower::before { + content: "\f519"; } + +.fa-truck-pickup::before { + content: "\f63c"; } + +.fa-up-long::before { + content: "\f30c"; } + +.fa-long-arrow-alt-up::before { + content: "\f30c"; } + +.fa-stop::before { + content: "\f04d"; } + +.fa-code-merge::before { + content: "\f387"; } + +.fa-upload::before { + content: "\f093"; } + +.fa-hurricane::before { + content: "\f751"; } + +.fa-mound::before { + content: "\e52d"; } + +.fa-toilet-portable::before { + content: "\e583"; } + +.fa-compact-disc::before { + content: "\f51f"; } + +.fa-file-arrow-down::before { + content: "\f56d"; } + +.fa-file-download::before { + content: "\f56d"; } + +.fa-caravan::before { + content: "\f8ff"; } + +.fa-shield-cat::before { + content: "\e572"; } + +.fa-bolt::before { + content: "\f0e7"; } + +.fa-zap::before { + content: "\f0e7"; } + +.fa-glass-water::before { + content: "\e4f4"; } + +.fa-oil-well::before { + content: "\e532"; } + +.fa-vault::before { + content: "\e2c5"; } + +.fa-mars::before { + content: "\f222"; } + +.fa-toilet::before { + content: "\f7d8"; } + +.fa-plane-circle-xmark::before { + content: "\e557"; } + +.fa-yen-sign::before { + content: "\f157"; } + +.fa-cny::before { + content: "\f157"; } + +.fa-jpy::before { + content: "\f157"; } + +.fa-rmb::before { + content: "\f157"; } + +.fa-yen::before { + content: "\f157"; } + +.fa-ruble-sign::before { + content: "\f158"; } + +.fa-rouble::before { + content: "\f158"; } + +.fa-rub::before { + content: "\f158"; } + +.fa-ruble::before { + content: "\f158"; } + +.fa-sun::before { + content: "\f185"; } + +.fa-guitar::before { + content: "\f7a6"; } + +.fa-face-laugh-wink::before { + content: "\f59c"; } + +.fa-laugh-wink::before { + content: "\f59c"; } + +.fa-horse-head::before { + content: "\f7ab"; } + +.fa-bore-hole::before { + content: "\e4c3"; } + +.fa-industry::before { + content: "\f275"; } + +.fa-circle-down::before { + content: "\f358"; } + +.fa-arrow-alt-circle-down::before { + content: "\f358"; } + +.fa-arrows-turn-to-dots::before { + content: "\e4c1"; } + +.fa-florin-sign::before { + content: "\e184"; } + +.fa-arrow-down-short-wide::before { + content: "\f884"; } + +.fa-sort-amount-desc::before { + content: "\f884"; } + +.fa-sort-amount-down-alt::before { + content: "\f884"; } + +.fa-less-than::before { + content: "\3c"; } + +.fa-angle-down::before { + content: "\f107"; } + +.fa-car-tunnel::before { + content: "\e4de"; } + +.fa-head-side-cough::before { + content: "\e061"; } + +.fa-grip-lines::before { + content: "\f7a4"; } + +.fa-thumbs-down::before { + content: "\f165"; } + +.fa-user-lock::before { + content: "\f502"; } + +.fa-arrow-right-long::before { + content: "\f178"; } + +.fa-long-arrow-right::before { + content: "\f178"; } + +.fa-anchor-circle-xmark::before { + content: "\e4ac"; } + +.fa-ellipsis::before { + content: "\f141"; } + +.fa-ellipsis-h::before { + content: "\f141"; } + +.fa-chess-pawn::before { + content: "\f443"; } + +.fa-kit-medical::before { + content: "\f479"; } + +.fa-first-aid::before { + content: "\f479"; } + +.fa-person-through-window::before { + content: "\e5a9"; } + +.fa-toolbox::before { + content: "\f552"; } + +.fa-hands-holding-circle::before { + content: "\e4fb"; } + +.fa-bug::before { + content: "\f188"; } + +.fa-credit-card::before { + content: "\f09d"; } + +.fa-credit-card-alt::before { + content: "\f09d"; } + +.fa-car::before { + content: "\f1b9"; } + +.fa-automobile::before { + content: "\f1b9"; } + +.fa-hand-holding-hand::before { + content: "\e4f7"; } + +.fa-book-open-reader::before { + content: "\f5da"; } + +.fa-book-reader::before { + content: "\f5da"; } + +.fa-mountain-sun::before { + content: "\e52f"; } + +.fa-arrows-left-right-to-line::before { + content: "\e4ba"; } + +.fa-dice-d20::before { + content: "\f6cf"; } + +.fa-truck-droplet::before { + content: "\e58c"; } + +.fa-file-circle-xmark::before { + content: "\e5a1"; } + +.fa-temperature-arrow-up::before { + content: "\e040"; } + +.fa-temperature-up::before { + content: "\e040"; } + +.fa-medal::before { + content: "\f5a2"; } + +.fa-bed::before { + content: "\f236"; } + +.fa-square-h::before { + content: "\f0fd"; } + +.fa-h-square::before { + content: "\f0fd"; } + +.fa-podcast::before { + content: "\f2ce"; } + +.fa-temperature-full::before { + content: "\f2c7"; } + +.fa-temperature-4::before { + content: "\f2c7"; } + +.fa-thermometer-4::before { + content: "\f2c7"; } + +.fa-thermometer-full::before { + content: "\f2c7"; } + +.fa-bell::before { + content: "\f0f3"; } + +.fa-superscript::before { + content: "\f12b"; } + +.fa-plug-circle-xmark::before { + content: "\e560"; } + +.fa-star-of-life::before { + content: "\f621"; } + +.fa-phone-slash::before { + content: "\f3dd"; } + +.fa-paint-roller::before { + content: "\f5aa"; } + +.fa-handshake-angle::before { + content: "\f4c4"; } + +.fa-hands-helping::before { + content: "\f4c4"; } + +.fa-location-dot::before { + content: "\f3c5"; } + +.fa-map-marker-alt::before { + content: "\f3c5"; } + +.fa-file::before { + content: "\f15b"; } + +.fa-greater-than::before { + content: "\3e"; } + +.fa-person-swimming::before { + content: "\f5c4"; } + +.fa-swimmer::before { + content: "\f5c4"; } + +.fa-arrow-down::before { + content: "\f063"; } + +.fa-droplet::before { + content: "\f043"; } + +.fa-tint::before { + content: "\f043"; } + +.fa-eraser::before { + content: "\f12d"; } + +.fa-earth-americas::before { + content: "\f57d"; } + +.fa-earth::before { + content: "\f57d"; } + +.fa-earth-america::before { + content: "\f57d"; } + +.fa-globe-americas::before { + content: "\f57d"; } + +.fa-person-burst::before { + content: "\e53b"; } + +.fa-dove::before { + content: "\f4ba"; } + +.fa-battery-empty::before { + content: "\f244"; } + +.fa-battery-0::before { + content: "\f244"; } + +.fa-socks::before { + content: "\f696"; } + +.fa-inbox::before { + content: "\f01c"; } + +.fa-section::before { + content: "\e447"; } + +.fa-gauge-high::before { + content: "\f625"; } + +.fa-tachometer-alt::before { + content: "\f625"; } + +.fa-tachometer-alt-fast::before { + content: "\f625"; } + +.fa-envelope-open-text::before { + content: "\f658"; } + +.fa-hospital::before { + content: "\f0f8"; } + +.fa-hospital-alt::before { + content: "\f0f8"; } + +.fa-hospital-wide::before { + content: "\f0f8"; } + +.fa-wine-bottle::before { + content: "\f72f"; } + +.fa-chess-rook::before { + content: "\f447"; } + +.fa-bars-staggered::before { + content: "\f550"; } + +.fa-reorder::before { + content: "\f550"; } + +.fa-stream::before { + content: "\f550"; } + +.fa-dharmachakra::before { + content: "\f655"; } + +.fa-hotdog::before { + content: "\f80f"; } + +.fa-person-walking-with-cane::before { + content: "\f29d"; } + +.fa-blind::before { + content: "\f29d"; } + +.fa-drum::before { + content: "\f569"; } + +.fa-ice-cream::before { + content: "\f810"; } + +.fa-heart-circle-bolt::before { + content: "\e4fc"; } + +.fa-fax::before { + content: "\f1ac"; } + +.fa-paragraph::before { + content: "\f1dd"; } + +.fa-check-to-slot::before { + content: "\f772"; } + +.fa-vote-yea::before { + content: "\f772"; } + +.fa-star-half::before { + content: "\f089"; } + +.fa-boxes-stacked::before { + content: "\f468"; } + +.fa-boxes::before { + content: "\f468"; } + +.fa-boxes-alt::before { + content: "\f468"; } + +.fa-link::before { + content: "\f0c1"; } + +.fa-chain::before { + content: "\f0c1"; } + +.fa-ear-listen::before { + content: "\f2a2"; } + +.fa-assistive-listening-systems::before { + content: "\f2a2"; } + +.fa-tree-city::before { + content: "\e587"; } + +.fa-play::before { + content: "\f04b"; } + +.fa-font::before { + content: "\f031"; } + +.fa-rupiah-sign::before { + content: "\e23d"; } + +.fa-magnifying-glass::before { + content: "\f002"; } + +.fa-search::before { + content: "\f002"; } + +.fa-table-tennis-paddle-ball::before { + content: "\f45d"; } + +.fa-ping-pong-paddle-ball::before { + content: "\f45d"; } + +.fa-table-tennis::before { + content: "\f45d"; } + +.fa-person-dots-from-line::before { + content: "\f470"; } + +.fa-diagnoses::before { + content: "\f470"; } + +.fa-trash-can-arrow-up::before { + content: "\f82a"; } + +.fa-trash-restore-alt::before { + content: "\f82a"; } + +.fa-naira-sign::before { + content: "\e1f6"; } + +.fa-cart-arrow-down::before { + content: "\f218"; } + +.fa-walkie-talkie::before { + content: "\f8ef"; } + +.fa-file-pen::before { + content: "\f31c"; } + +.fa-file-edit::before { + content: "\f31c"; } + +.fa-receipt::before { + content: "\f543"; } + +.fa-square-pen::before { + content: "\f14b"; } + +.fa-pen-square::before { + content: "\f14b"; } + +.fa-pencil-square::before { + content: "\f14b"; } + +.fa-suitcase-rolling::before { + content: "\f5c1"; } + +.fa-person-circle-exclamation::before { + content: "\e53f"; } + +.fa-chevron-down::before { + content: "\f078"; } + +.fa-battery-full::before { + content: "\f240"; } + +.fa-battery::before { + content: "\f240"; } + +.fa-battery-5::before { + content: "\f240"; } + +.fa-skull-crossbones::before { + content: "\f714"; } + +.fa-code-compare::before { + content: "\e13a"; } + +.fa-list-ul::before { + content: "\f0ca"; } + +.fa-list-dots::before { + content: "\f0ca"; } + +.fa-school-lock::before { + content: "\e56f"; } + +.fa-tower-cell::before { + content: "\e585"; } + +.fa-down-long::before { + content: "\f309"; } + +.fa-long-arrow-alt-down::before { + content: "\f309"; } + +.fa-ranking-star::before { + content: "\e561"; } + +.fa-chess-king::before { + content: "\f43f"; } + +.fa-person-harassing::before { + content: "\e549"; } + +.fa-brazilian-real-sign::before { + content: "\e46c"; } + +.fa-landmark-dome::before { + content: "\f752"; } + +.fa-landmark-alt::before { + content: "\f752"; } + +.fa-arrow-up::before { + content: "\f062"; } + +.fa-tv::before { + content: "\f26c"; } + +.fa-television::before { + content: "\f26c"; } + +.fa-tv-alt::before { + content: "\f26c"; } + +.fa-shrimp::before { + content: "\e448"; } + +.fa-list-check::before { + content: "\f0ae"; } + +.fa-tasks::before { + content: "\f0ae"; } + +.fa-jug-detergent::before { + content: "\e519"; } + +.fa-circle-user::before { + content: "\f2bd"; } + +.fa-user-circle::before { + content: "\f2bd"; } + +.fa-user-shield::before { + content: "\f505"; } + +.fa-wind::before { + content: "\f72e"; } + +.fa-car-burst::before { + content: "\f5e1"; } + +.fa-car-crash::before { + content: "\f5e1"; } + +.fa-y::before { + content: "\59"; } + +.fa-person-snowboarding::before { + content: "\f7ce"; } + +.fa-snowboarding::before { + content: "\f7ce"; } + +.fa-truck-fast::before { + content: "\f48b"; } + +.fa-shipping-fast::before { + content: "\f48b"; } + +.fa-fish::before { + content: "\f578"; } + +.fa-user-graduate::before { + content: "\f501"; } + +.fa-circle-half-stroke::before { + content: "\f042"; } + +.fa-adjust::before { + content: "\f042"; } + +.fa-clapperboard::before { + content: "\e131"; } + +.fa-circle-radiation::before { + content: "\f7ba"; } + +.fa-radiation-alt::before { + content: "\f7ba"; } + +.fa-baseball::before { + content: "\f433"; } + +.fa-baseball-ball::before { + content: "\f433"; } + +.fa-jet-fighter-up::before { + content: "\e518"; } + +.fa-diagram-project::before { + content: "\f542"; } + +.fa-project-diagram::before { + content: "\f542"; } + +.fa-copy::before { + content: "\f0c5"; } + +.fa-volume-xmark::before { + content: "\f6a9"; } + +.fa-volume-mute::before { + content: "\f6a9"; } + +.fa-volume-times::before { + content: "\f6a9"; } + +.fa-hand-sparkles::before { + content: "\e05d"; } + +.fa-grip::before { + content: "\f58d"; } + +.fa-grip-horizontal::before { + content: "\f58d"; } + +.fa-share-from-square::before { + content: "\f14d"; } + +.fa-share-square::before { + content: "\f14d"; } + +.fa-child-combatant::before { + content: "\e4e0"; } + +.fa-child-rifle::before { + content: "\e4e0"; } + +.fa-gun::before { + content: "\e19b"; } + +.fa-square-phone::before { + content: "\f098"; } + +.fa-phone-square::before { + content: "\f098"; } + +.fa-plus::before { + content: "\2b"; } + +.fa-add::before { + content: "\2b"; } + +.fa-expand::before { + content: "\f065"; } + +.fa-computer::before { + content: "\e4e5"; } + +.fa-xmark::before { + content: "\f00d"; } + +.fa-close::before { + content: "\f00d"; } + +.fa-multiply::before { + content: "\f00d"; } + +.fa-remove::before { + content: "\f00d"; } + +.fa-times::before { + content: "\f00d"; } + +.fa-arrows-up-down-left-right::before { + content: "\f047"; } + +.fa-arrows::before { + content: "\f047"; } + +.fa-chalkboard-user::before { + content: "\f51c"; } + +.fa-chalkboard-teacher::before { + content: "\f51c"; } + +.fa-peso-sign::before { + content: "\e222"; } + +.fa-building-shield::before { + content: "\e4d8"; } + +.fa-baby::before { + content: "\f77c"; } + +.fa-users-line::before { + content: "\e592"; } + +.fa-quote-left::before { + content: "\f10d"; } + +.fa-quote-left-alt::before { + content: "\f10d"; } + +.fa-tractor::before { + content: "\f722"; } + +.fa-trash-arrow-up::before { + content: "\f829"; } + +.fa-trash-restore::before { + content: "\f829"; } + +.fa-arrow-down-up-lock::before { + content: "\e4b0"; } + +.fa-lines-leaning::before { + content: "\e51e"; } + +.fa-ruler-combined::before { + content: "\f546"; } + +.fa-copyright::before { + content: "\f1f9"; } + +.fa-equals::before { + content: "\3d"; } + +.fa-blender::before { + content: "\f517"; } + +.fa-teeth::before { + content: "\f62e"; } + +.fa-shekel-sign::before { + content: "\f20b"; } + +.fa-ils::before { + content: "\f20b"; } + +.fa-shekel::before { + content: "\f20b"; } + +.fa-sheqel::before { + content: "\f20b"; } + +.fa-sheqel-sign::before { + content: "\f20b"; } + +.fa-map::before { + content: "\f279"; } + +.fa-rocket::before { + content: "\f135"; } + +.fa-photo-film::before { + content: "\f87c"; } + +.fa-photo-video::before { + content: "\f87c"; } + +.fa-folder-minus::before { + content: "\f65d"; } + +.fa-store::before { + content: "\f54e"; } + +.fa-arrow-trend-up::before { + content: "\e098"; } + +.fa-plug-circle-minus::before { + content: "\e55e"; } + +.fa-sign-hanging::before { + content: "\f4d9"; } + +.fa-sign::before { + content: "\f4d9"; } + +.fa-bezier-curve::before { + content: "\f55b"; } + +.fa-bell-slash::before { + content: "\f1f6"; } + +.fa-tablet::before { + content: "\f3fb"; } + +.fa-tablet-android::before { + content: "\f3fb"; } + +.fa-school-flag::before { + content: "\e56e"; } + +.fa-fill::before { + content: "\f575"; } + +.fa-angle-up::before { + content: "\f106"; } + +.fa-drumstick-bite::before { + content: "\f6d7"; } + +.fa-holly-berry::before { + content: "\f7aa"; } + +.fa-chevron-left::before { + content: "\f053"; } + +.fa-bacteria::before { + content: "\e059"; } + +.fa-hand-lizard::before { + content: "\f258"; } + +.fa-notdef::before { + content: "\e1fe"; } + +.fa-disease::before { + content: "\f7fa"; } + +.fa-briefcase-medical::before { + content: "\f469"; } + +.fa-genderless::before { + content: "\f22d"; } + +.fa-chevron-right::before { + content: "\f054"; } + +.fa-retweet::before { + content: "\f079"; } + +.fa-car-rear::before { + content: "\f5de"; } + +.fa-car-alt::before { + content: "\f5de"; } + +.fa-pump-soap::before { + content: "\e06b"; } + +.fa-video-slash::before { + content: "\f4e2"; } + +.fa-battery-quarter::before { + content: "\f243"; } + +.fa-battery-2::before { + content: "\f243"; } + +.fa-radio::before { + content: "\f8d7"; } + +.fa-baby-carriage::before { + content: "\f77d"; } + +.fa-carriage-baby::before { + content: "\f77d"; } + +.fa-traffic-light::before { + content: "\f637"; } + +.fa-thermometer::before { + content: "\f491"; } + +.fa-vr-cardboard::before { + content: "\f729"; } + +.fa-hand-middle-finger::before { + content: "\f806"; } + +.fa-percent::before { + content: "\25"; } + +.fa-percentage::before { + content: "\25"; } + +.fa-truck-moving::before { + content: "\f4df"; } + +.fa-glass-water-droplet::before { + content: "\e4f5"; } + +.fa-display::before { + content: "\e163"; } + +.fa-face-smile::before { + content: "\f118"; } + +.fa-smile::before { + content: "\f118"; } + +.fa-thumbtack::before { + content: "\f08d"; } + +.fa-thumb-tack::before { + content: "\f08d"; } + +.fa-trophy::before { + content: "\f091"; } + +.fa-person-praying::before { + content: "\f683"; } + +.fa-pray::before { + content: "\f683"; } + +.fa-hammer::before { + content: "\f6e3"; } + +.fa-hand-peace::before { + content: "\f25b"; } + +.fa-rotate::before { + content: "\f2f1"; } + +.fa-sync-alt::before { + content: "\f2f1"; } + +.fa-spinner::before { + content: "\f110"; } + +.fa-robot::before { + content: "\f544"; } + +.fa-peace::before { + content: "\f67c"; } + +.fa-gears::before { + content: "\f085"; } + +.fa-cogs::before { + content: "\f085"; } + +.fa-warehouse::before { + content: "\f494"; } + +.fa-arrow-up-right-dots::before { + content: "\e4b7"; } + +.fa-splotch::before { + content: "\f5bc"; } + +.fa-face-grin-hearts::before { + content: "\f584"; } + +.fa-grin-hearts::before { + content: "\f584"; } + +.fa-dice-four::before { + content: "\f524"; } + +.fa-sim-card::before { + content: "\f7c4"; } + +.fa-transgender::before { + content: "\f225"; } + +.fa-transgender-alt::before { + content: "\f225"; } + +.fa-mercury::before { + content: "\f223"; } + +.fa-arrow-turn-down::before { + content: "\f149"; } + +.fa-level-down::before { + content: "\f149"; } + +.fa-person-falling-burst::before { + content: "\e547"; } + +.fa-award::before { + content: "\f559"; } + +.fa-ticket-simple::before { + content: "\f3ff"; } + +.fa-ticket-alt::before { + content: "\f3ff"; } + +.fa-building::before { + content: "\f1ad"; } + +.fa-angles-left::before { + content: "\f100"; } + +.fa-angle-double-left::before { + content: "\f100"; } + +.fa-qrcode::before { + content: "\f029"; } + +.fa-clock-rotate-left::before { + content: "\f1da"; } + +.fa-history::before { + content: "\f1da"; } + +.fa-face-grin-beam-sweat::before { + content: "\f583"; } + +.fa-grin-beam-sweat::before { + content: "\f583"; } + +.fa-file-export::before { + content: "\f56e"; } + +.fa-arrow-right-from-file::before { + content: "\f56e"; } + +.fa-shield::before { + content: "\f132"; } + +.fa-shield-blank::before { + content: "\f132"; } + +.fa-arrow-up-short-wide::before { + content: "\f885"; } + +.fa-sort-amount-up-alt::before { + content: "\f885"; } + +.fa-house-medical::before { + content: "\e3b2"; } + +.fa-golf-ball-tee::before { + content: "\f450"; } + +.fa-golf-ball::before { + content: "\f450"; } + +.fa-circle-chevron-left::before { + content: "\f137"; } + +.fa-chevron-circle-left::before { + content: "\f137"; } + +.fa-house-chimney-window::before { + content: "\e00d"; } + +.fa-pen-nib::before { + content: "\f5ad"; } + +.fa-tent-arrow-turn-left::before { + content: "\e580"; } + +.fa-tents::before { + content: "\e582"; } + +.fa-wand-magic::before { + content: "\f0d0"; } + +.fa-magic::before { + content: "\f0d0"; } + +.fa-dog::before { + content: "\f6d3"; } + +.fa-carrot::before { + content: "\f787"; } + +.fa-moon::before { + content: "\f186"; } + +.fa-wine-glass-empty::before { + content: "\f5ce"; } + +.fa-wine-glass-alt::before { + content: "\f5ce"; } + +.fa-cheese::before { + content: "\f7ef"; } + +.fa-yin-yang::before { + content: "\f6ad"; } + +.fa-music::before { + content: "\f001"; } + +.fa-code-commit::before { + content: "\f386"; } + +.fa-temperature-low::before { + content: "\f76b"; } + +.fa-person-biking::before { + content: "\f84a"; } + +.fa-biking::before { + content: "\f84a"; } + +.fa-broom::before { + content: "\f51a"; } + +.fa-shield-heart::before { + content: "\e574"; } + +.fa-gopuram::before { + content: "\f664"; } + +.fa-earth-oceania::before { + content: "\e47b"; } + +.fa-globe-oceania::before { + content: "\e47b"; } + +.fa-square-xmark::before { + content: "\f2d3"; } + +.fa-times-square::before { + content: "\f2d3"; } + +.fa-xmark-square::before { + content: "\f2d3"; } + +.fa-hashtag::before { + content: "\23"; } + +.fa-up-right-and-down-left-from-center::before { + content: "\f424"; } + +.fa-expand-alt::before { + content: "\f424"; } + +.fa-oil-can::before { + content: "\f613"; } + +.fa-t::before { + content: "\54"; } + +.fa-hippo::before { + content: "\f6ed"; } + +.fa-chart-column::before { + content: "\e0e3"; } + +.fa-infinity::before { + content: "\f534"; } + +.fa-vial-circle-check::before { + content: "\e596"; } + +.fa-person-arrow-down-to-line::before { + content: "\e538"; } + +.fa-voicemail::before { + content: "\f897"; } + +.fa-fan::before { + content: "\f863"; } + +.fa-person-walking-luggage::before { + content: "\e554"; } + +.fa-up-down::before { + content: "\f338"; } + +.fa-arrows-alt-v::before { + content: "\f338"; } + +.fa-cloud-moon-rain::before { + content: "\f73c"; } + +.fa-calendar::before { + content: "\f133"; } + +.fa-trailer::before { + content: "\e041"; } + +.fa-bahai::before { + content: "\f666"; } + +.fa-haykal::before { + content: "\f666"; } + +.fa-sd-card::before { + content: "\f7c2"; } + +.fa-dragon::before { + content: "\f6d5"; } + +.fa-shoe-prints::before { + content: "\f54b"; } + +.fa-circle-plus::before { + content: "\f055"; } + +.fa-plus-circle::before { + content: "\f055"; } + +.fa-face-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-hand-holding::before { + content: "\f4bd"; } + +.fa-plug-circle-exclamation::before { + content: "\e55d"; } + +.fa-link-slash::before { + content: "\f127"; } + +.fa-chain-broken::before { + content: "\f127"; } + +.fa-chain-slash::before { + content: "\f127"; } + +.fa-unlink::before { + content: "\f127"; } + +.fa-clone::before { + content: "\f24d"; } + +.fa-person-walking-arrow-loop-left::before { + content: "\e551"; } + +.fa-arrow-up-z-a::before { + content: "\f882"; } + +.fa-sort-alpha-up-alt::before { + content: "\f882"; } + +.fa-fire-flame-curved::before { + content: "\f7e4"; } + +.fa-fire-alt::before { + content: "\f7e4"; } + +.fa-tornado::before { + content: "\f76f"; } + +.fa-file-circle-plus::before { + content: "\e494"; } + +.fa-book-quran::before { + content: "\f687"; } + +.fa-quran::before { + content: "\f687"; } + +.fa-anchor::before { + content: "\f13d"; } + +.fa-border-all::before { + content: "\f84c"; } + +.fa-face-angry::before { + content: "\f556"; } + +.fa-angry::before { + content: "\f556"; } + +.fa-cookie-bite::before { + content: "\f564"; } + +.fa-arrow-trend-down::before { + content: "\e097"; } + +.fa-rss::before { + content: "\f09e"; } + +.fa-feed::before { + content: "\f09e"; } + +.fa-draw-polygon::before { + content: "\f5ee"; } + +.fa-scale-balanced::before { + content: "\f24e"; } + +.fa-balance-scale::before { + content: "\f24e"; } + +.fa-gauge-simple-high::before { + content: "\f62a"; } + +.fa-tachometer::before { + content: "\f62a"; } + +.fa-tachometer-fast::before { + content: "\f62a"; } + +.fa-shower::before { + content: "\f2cc"; } + +.fa-desktop::before { + content: "\f390"; } + +.fa-desktop-alt::before { + content: "\f390"; } + +.fa-m::before { + content: "\4d"; } + +.fa-table-list::before { + content: "\f00b"; } + +.fa-th-list::before { + content: "\f00b"; } + +.fa-comment-sms::before { + content: "\f7cd"; } + +.fa-sms::before { + content: "\f7cd"; } + +.fa-book::before { + content: "\f02d"; } + +.fa-user-plus::before { + content: "\f234"; } + +.fa-check::before { + content: "\f00c"; } + +.fa-battery-three-quarters::before { + content: "\f241"; } + +.fa-battery-4::before { + content: "\f241"; } + +.fa-house-circle-check::before { + content: "\e509"; } + +.fa-angle-left::before { + content: "\f104"; } + +.fa-diagram-successor::before { + content: "\e47a"; } + +.fa-truck-arrow-right::before { + content: "\e58b"; } + +.fa-arrows-split-up-and-left::before { + content: "\e4bc"; } + +.fa-hand-fist::before { + content: "\f6de"; } + +.fa-fist-raised::before { + content: "\f6de"; } + +.fa-cloud-moon::before { + content: "\f6c3"; } + +.fa-briefcase::before { + content: "\f0b1"; } + +.fa-person-falling::before { + content: "\e546"; } + +.fa-image-portrait::before { + content: "\f3e0"; } + +.fa-portrait::before { + content: "\f3e0"; } + +.fa-user-tag::before { + content: "\f507"; } + +.fa-rug::before { + content: "\e569"; } + +.fa-earth-europe::before { + content: "\f7a2"; } + +.fa-globe-europe::before { + content: "\f7a2"; } + +.fa-cart-flatbed-suitcase::before { + content: "\f59d"; } + +.fa-luggage-cart::before { + content: "\f59d"; } + +.fa-rectangle-xmark::before { + content: "\f410"; } + +.fa-rectangle-times::before { + content: "\f410"; } + +.fa-times-rectangle::before { + content: "\f410"; } + +.fa-window-close::before { + content: "\f410"; } + +.fa-baht-sign::before { + content: "\e0ac"; } + +.fa-book-open::before { + content: "\f518"; } + +.fa-book-journal-whills::before { + content: "\f66a"; } + +.fa-journal-whills::before { + content: "\f66a"; } + +.fa-handcuffs::before { + content: "\e4f8"; } + +.fa-triangle-exclamation::before { + content: "\f071"; } + +.fa-exclamation-triangle::before { + content: "\f071"; } + +.fa-warning::before { + content: "\f071"; } + +.fa-database::before { + content: "\f1c0"; } + +.fa-share::before { + content: "\f064"; } + +.fa-mail-forward::before { + content: "\f064"; } + +.fa-bottle-droplet::before { + content: "\e4c4"; } + +.fa-mask-face::before { + content: "\e1d7"; } + +.fa-hill-rockslide::before { + content: "\e508"; } + +.fa-right-left::before { + content: "\f362"; } + +.fa-exchange-alt::before { + content: "\f362"; } + +.fa-paper-plane::before { + content: "\f1d8"; } + +.fa-road-circle-exclamation::before { + content: "\e565"; } + +.fa-dungeon::before { + content: "\f6d9"; } + +.fa-align-right::before { + content: "\f038"; } + +.fa-money-bill-1-wave::before { + content: "\f53b"; } + +.fa-money-bill-wave-alt::before { + content: "\f53b"; } + +.fa-life-ring::before { + content: "\f1cd"; } + +.fa-hands::before { + content: "\f2a7"; } + +.fa-sign-language::before { + content: "\f2a7"; } + +.fa-signing::before { + content: "\f2a7"; } + +.fa-calendar-day::before { + content: "\f783"; } + +.fa-water-ladder::before { + content: "\f5c5"; } + +.fa-ladder-water::before { + content: "\f5c5"; } + +.fa-swimming-pool::before { + content: "\f5c5"; } + +.fa-arrows-up-down::before { + content: "\f07d"; } + +.fa-arrows-v::before { + content: "\f07d"; } + +.fa-face-grimace::before { + content: "\f57f"; } + +.fa-grimace::before { + content: "\f57f"; } + +.fa-wheelchair-move::before { + content: "\e2ce"; } + +.fa-wheelchair-alt::before { + content: "\e2ce"; } + +.fa-turn-down::before { + content: "\f3be"; } + +.fa-level-down-alt::before { + content: "\f3be"; } + +.fa-person-walking-arrow-right::before { + content: "\e552"; } + +.fa-square-envelope::before { + content: "\f199"; } + +.fa-envelope-square::before { + content: "\f199"; } + +.fa-dice::before { + content: "\f522"; } + +.fa-bowling-ball::before { + content: "\f436"; } + +.fa-brain::before { + content: "\f5dc"; } + +.fa-bandage::before { + content: "\f462"; } + +.fa-band-aid::before { + content: "\f462"; } + +.fa-calendar-minus::before { + content: "\f272"; } + +.fa-circle-xmark::before { + content: "\f057"; } + +.fa-times-circle::before { + content: "\f057"; } + +.fa-xmark-circle::before { + content: "\f057"; } + +.fa-gifts::before { + content: "\f79c"; } + +.fa-hotel::before { + content: "\f594"; } + +.fa-earth-asia::before { + content: "\f57e"; } + +.fa-globe-asia::before { + content: "\f57e"; } + +.fa-id-card-clip::before { + content: "\f47f"; } + +.fa-id-card-alt::before { + content: "\f47f"; } + +.fa-magnifying-glass-plus::before { + content: "\f00e"; } + +.fa-search-plus::before { + content: "\f00e"; } + +.fa-thumbs-up::before { + content: "\f164"; } + +.fa-user-clock::before { + content: "\f4fd"; } + +.fa-hand-dots::before { + content: "\f461"; } + +.fa-allergies::before { + content: "\f461"; } + +.fa-file-invoice::before { + content: "\f570"; } + +.fa-window-minimize::before { + content: "\f2d1"; } + +.fa-mug-saucer::before { + content: "\f0f4"; } + +.fa-coffee::before { + content: "\f0f4"; } + +.fa-brush::before { + content: "\f55d"; } + +.fa-mask::before { + content: "\f6fa"; } + +.fa-magnifying-glass-minus::before { + content: "\f010"; } + +.fa-search-minus::before { + content: "\f010"; } + +.fa-ruler-vertical::before { + content: "\f548"; } + +.fa-user-large::before { + content: "\f406"; } + +.fa-user-alt::before { + content: "\f406"; } + +.fa-train-tram::before { + content: "\e5b4"; } + +.fa-user-nurse::before { + content: "\f82f"; } + +.fa-syringe::before { + content: "\f48e"; } + +.fa-cloud-sun::before { + content: "\f6c4"; } + +.fa-stopwatch-20::before { + content: "\e06f"; } + +.fa-square-full::before { + content: "\f45c"; } + +.fa-magnet::before { + content: "\f076"; } + +.fa-jar::before { + content: "\e516"; } + +.fa-note-sticky::before { + content: "\f249"; } + +.fa-sticky-note::before { + content: "\f249"; } + +.fa-bug-slash::before { + content: "\e490"; } + +.fa-arrow-up-from-water-pump::before { + content: "\e4b6"; } + +.fa-bone::before { + content: "\f5d7"; } + +.fa-user-injured::before { + content: "\f728"; } + +.fa-face-sad-tear::before { + content: "\f5b4"; } + +.fa-sad-tear::before { + content: "\f5b4"; } + +.fa-plane::before { + content: "\f072"; } + +.fa-tent-arrows-down::before { + content: "\e581"; } + +.fa-exclamation::before { + content: "\21"; } + +.fa-arrows-spin::before { + content: "\e4bb"; } + +.fa-print::before { + content: "\f02f"; } + +.fa-turkish-lira-sign::before { + content: "\e2bb"; } + +.fa-try::before { + content: "\e2bb"; } + +.fa-turkish-lira::before { + content: "\e2bb"; } + +.fa-dollar-sign::before { + content: "\24"; } + +.fa-dollar::before { + content: "\24"; } + +.fa-usd::before { + content: "\24"; } + +.fa-x::before { + content: "\58"; } + +.fa-magnifying-glass-dollar::before { + content: "\f688"; } + +.fa-search-dollar::before { + content: "\f688"; } + +.fa-users-gear::before { + content: "\f509"; } + +.fa-users-cog::before { + content: "\f509"; } + +.fa-person-military-pointing::before { + content: "\e54a"; } + +.fa-building-columns::before { + content: "\f19c"; } + +.fa-bank::before { + content: "\f19c"; } + +.fa-institution::before { + content: "\f19c"; } + +.fa-museum::before { + content: "\f19c"; } + +.fa-university::before { + content: "\f19c"; } + +.fa-umbrella::before { + content: "\f0e9"; } + +.fa-trowel::before { + content: "\e589"; } + +.fa-d::before { + content: "\44"; } + +.fa-stapler::before { + content: "\e5af"; } + +.fa-masks-theater::before { + content: "\f630"; } + +.fa-theater-masks::before { + content: "\f630"; } + +.fa-kip-sign::before { + content: "\e1c4"; } + +.fa-hand-point-left::before { + content: "\f0a5"; } + +.fa-handshake-simple::before { + content: "\f4c6"; } + +.fa-handshake-alt::before { + content: "\f4c6"; } + +.fa-jet-fighter::before { + content: "\f0fb"; } + +.fa-fighter-jet::before { + content: "\f0fb"; } + +.fa-square-share-nodes::before { + content: "\f1e1"; } + +.fa-share-alt-square::before { + content: "\f1e1"; } + +.fa-barcode::before { + content: "\f02a"; } + +.fa-plus-minus::before { + content: "\e43c"; } + +.fa-video::before { + content: "\f03d"; } + +.fa-video-camera::before { + content: "\f03d"; } + +.fa-graduation-cap::before { + content: "\f19d"; } + +.fa-mortar-board::before { + content: "\f19d"; } + +.fa-hand-holding-medical::before { + content: "\e05c"; } + +.fa-person-circle-check::before { + content: "\e53e"; } + +.fa-turn-up::before { + content: "\f3bf"; } + +.fa-level-up-alt::before { + content: "\f3bf"; } + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } +:root, :host { + --fa-style-family-brands: 'Font Awesome 6 Brands'; + --fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; } + +@font-face { + font-family: 'Font Awesome 6 Brands'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +.fab, +.fa-brands { + font-weight: 400; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-shopware:before { + content: "\f5b5"; } + +.fa-creative-commons-nc:before { + content: "\f4e8"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-redhat:before { + content: "\f7bc"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-cloudflare:before { + content: "\e07d"; } + +.fa-ups:before { + content: "\f7e0"; } + +.fa-pixiv:before { + content: "\e640"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-stackpath:before { + content: "\f842"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-guilded:before { + content: "\e07e"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-square-js:before { + content: "\f3b9"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-orcid:before { + content: "\f8d2"; } + +.fa-java:before { + content: "\f4e4"; } + +.fa-invision:before { + content: "\f7b0"; } + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-creative-commons-by:before { + content: "\f4e7"; } + +.fa-unity:before { + content: "\e049"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-mailchimp:before { + content: "\f59e"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-square-reddit:before { + content: "\f1a2"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-square-font-awesome:before { + content: "\e5ad"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-brave:before { + content: "\e63c"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-square-instagram:before { + content: "\e055"; } + +.fa-instagram-square:before { + content: "\e055"; } + +.fa-battle-net:before { + content: "\f835"; } + +.fa-the-red-yeti:before { + content: "\f69d"; } + +.fa-square-hacker-news:before { + content: "\f3af"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-threads:before { + content: "\e618"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-square-snapchat:before { + content: "\f2ad"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-artstation:before { + content: "\f77a"; } + +.fa-markdown:before { + content: "\f60f"; } + +.fa-sourcetree:before { + content: "\f7d3"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-diaspora:before { + content: "\f791"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-phoenix-squadron:before { + content: "\f511"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-creative-commons-sa:before { + content: "\f4ef"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-square-font-awesome-stroke:before { + content: "\f35c"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-atlassian:before { + content: "\f77b"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-nimblr:before { + content: "\f5a8"; } + +.fa-chromecast:before { + content: "\f838"; } + +.fa-evernote:before { + content: "\f839"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-creative-commons-sampling:before { + content: "\f4f0"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-watchman-monitoring:before { + content: "\e087"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-git-alt:before { + content: "\f841"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-rev:before { + content: "\f5b2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-wizards-of-the-coast:before { + content: "\f730"; } + +.fa-square-viadeo:before { + content: "\f2aa"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-centos:before { + content: "\f789"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-opensuse:before { + content: "\e62b"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-square-dribbble:before { + content: "\f397"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-node:before { + content: "\f419"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-debian:before { + content: "\e60b"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-instalod:before { + content: "\e081"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-square-twitter:before { + content: "\f081"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-r-project:before { + content: "\f4f7"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-golang:before { + content: "\e40f"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-uncharted:before { + content: "\e084"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-square-youtube:before { + content: "\f431"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-wpressr:before { + content: "\f3e4"; } + +.fa-rendact:before { + content: "\f3e4"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-galactic-republic:before { + content: "\f50c"; } + +.fa-nfc-directional:before { + content: "\e530"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-fedora:before { + content: "\f798"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-meta:before { + content: "\e49b"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-square-letterboxd:before { + content: "\e62e"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-creative-commons-zero:before { + content: "\f4f3"; } + +.fa-hips:before { + content: "\f452"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-confluence:before { + content: "\f78d"; } + +.fa-shoelace:before { + content: "\e60c"; } + +.fa-mdb:before { + content: "\f8ca"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-ebay:before { + content: "\f4f4"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-unsplash:before { + content: "\e07c"; } + +.fa-yarn:before { + content: "\f7e3"; } + +.fa-square-steam:before { + content: "\f1b7"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-500px:before { + content: "\f26e"; } + +.fa-square-vimeo:before { + content: "\f194"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-flag:before { + content: "\f2b4"; } + +.fa-font-awesome-logo-full:before { + content: "\f2b4"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-hive:before { + content: "\e07f"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-keybase:before { + content: "\f4f5"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-padlet:before { + content: "\e4a0"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-square-github:before { + content: "\f092"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-fedex:before { + content: "\f797"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-shopify:before { + content: "\e057"; } + +.fa-neos:before { + content: "\f612"; } + +.fa-square-threads:before { + content: "\e619"; } + +.fa-hackerrank:before { + content: "\f5f7"; } + +.fa-researchgate:before { + content: "\f4f8"; } + +.fa-swift:before { + content: "\f8e1"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-google-scholar:before { + content: "\e63b"; } + +.fa-square-gitlab:before { + content: "\e5ae"; } + +.fa-gitlab-square:before { + content: "\e5ae"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-square-odnoklassniki:before { + content: "\f264"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-sith:before { + content: "\f512"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-hashnode:before { + content: "\e499"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-squarespace:before { + content: "\f5be"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-creative-commons-share:before { + content: "\f4f2"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-itch-io:before { + content: "\f83a"; } + +.fa-umbraco:before { + content: "\f8e8"; } + +.fa-galactic-senate:before { + content: "\f50d"; } + +.fa-ubuntu:before { + content: "\f7df"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-dhl:before { + content: "\f790"; } + +.fa-square-pinterest:before { + content: "\f0d3"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-creative-commons-pd:before { + content: "\f4ec"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-quinscape:before { + content: "\f459"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-signal-messenger:before { + content: "\e663"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-dailymotion:before { + content: "\e052"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-nfc-symbol:before { + content: "\e531"; } + +.fa-mintbit:before { + content: "\e62f"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-ello:before { + content: "\f5f1"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-brave-reverse:before { + content: "\e63d"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-square-google-plus:before { + content: "\f0d4"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-mandalorian:before { + content: "\f50f"; } + +.fa-first-order-alt:before { + content: "\f50a"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-d-and-d-beyond:before { + content: "\f6ca"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-fulcrum:before { + content: "\f50b"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-square-xing:before { + content: "\f169"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-usps:before { + content: "\f7e1"; } + +.fa-megaport:before { + content: "\f5a3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-letterboxd:before { + content: "\e62d"; } + +.fa-symfony:before { + content: "\f83d"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-think-peaks:before { + content: "\f731"; } + +.fa-bilibili:before { + content: "\e3d9"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-x-twitter:before { + content: "\e61b"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-42-group:before { + content: "\e080"; } + +.fa-innosoft:before { + content: "\e080"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-square-pied-piper:before { + content: "\e01e"; } + +.fa-pied-piper-square:before { + content: "\e01e"; } + +.fa-creative-commons-nd:before { + content: "\f4eb"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-tiktok:before { + content: "\e07b"; } + +.fa-square-facebook:before { + content: "\f082"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-mixer:before { + content: "\e056"; } + +.fa-square-lastfm:before { + content: "\f203"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-mendeley:before { + content: "\f7b3"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-figma:before { + content: "\f799"; } + +.fa-creative-commons-remix:before { + content: "\f4ee"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-cmplid:before { + content: "\e360"; } + +.fa-upwork:before { + content: "\e641"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-jedi-order:before { + content: "\f50e"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-trade-federation:before { + content: "\f513"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-deezer:before { + content: "\e077"; } + +.fa-raspberry-pi:before { + content: "\f7bb"; } + +.fa-jira:before { + content: "\f7b1"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-screenpal:before { + content: "\e570"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-microblog:before { + content: "\e01a"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-pied-piper-hat:before { + content: "\f4e5"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-readme:before { + content: "\f4d5"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-wirsindhandwerk:before { + content: "\e2d0"; } + +.fa-wsh:before { + content: "\e2d0"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-octopus-deploy:before { + content: "\e082"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ab"; } + +.fa-fantasy-flight-games:before { + content: "\f6dc"; } + +.fa-rust:before { + content: "\e07a"; } + +.fa-wix:before { + content: "\f5cf"; } + +.fa-square-behance:before { + content: "\f1b5"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-webflow:before { + content: "\e65c"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-kaggle:before { + content: "\f5fa"; } + +.fa-space-awesome:before { + content: "\e5ac"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-square-git:before { + content: "\f1d2"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-square-tumblr:before { + content: "\f174"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-perbyte:before { + content: "\e083"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-weebly:before { + content: "\f5cc"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-themeco:before { + content: "\f5c6"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-bots:before { + content: "\e340"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-hornbill:before { + content: "\f592"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-ideal:before { + content: "\e013"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-dev:before { + content: "\f6cc"; } + +.fa-sketch:before { + content: "\f7c6"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-php:before { + content: "\f457"; } + +.fa-alipay:before { + content: "\f642"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-firefox-browser:before { + content: "\e007"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-suse:before { + content: "\f7d6"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-buffer:before { + content: "\f837"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-yammer:before { + content: "\f840"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-stubber:before { + content: "\e5c7"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f2c6"; } + +.fa-old-republic:before { + content: "\f510"; } + +.fa-odysee:before { + content: "\e5c6"; } + +.fa-square-whatsapp:before { + content: "\f40c"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-edge-legacy:before { + content: "\e078"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f198"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-square-x-twitter:before { + content: "\e61a"; } + +.fa-reacteurope:before { + content: "\f75d"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f23a"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-flipboard:before { + content: "\f44d"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-critical-role:before { + content: "\f6c9"; } + +.fa-sitrox:before { + content: "\e44a"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-mastodon:before { + content: "\f4f6"; } + +.fa-airbnb:before { + content: "\f834"; } + +.fa-wolf-pack-battalion:before { + content: "\f514"; } + +.fa-buy-n-large:before { + content: "\f8a6"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-canadian-maple-leaf:before { + content: "\f785"; } + +.fa-teamspeak:before { + content: "\f4f9"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-wodu:before { + content: "\e088"; } + +.fa-google-pay:before { + content: "\e079"; } + +.fa-intercom:before { + content: "\f7af"; } + +.fa-zhihu:before { + content: "\f63f"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-pix:before { + content: "\e43a"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } +:root, :host { + --fa-style-family-classic: 'Font Awesome 6 Free'; + --fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } + +.far, +.fa-regular { + font-weight: 400; } +:root, :host { + --fa-style-family-classic: 'Font Awesome 6 Free'; + --fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 900; + font-display: block; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +.fas, +.fa-solid { + font-weight: 900; } +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-display: block; + font-weight: 400; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-display: block; + font-weight: 900; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-display: block; + font-weight: 400; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); + unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; } + +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype"); + unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.ttf b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000..5efb1d4f96 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000000..36fbda7d33 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.ttf b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000000..838b4e2cfe Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000..b6cabbacb6 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.ttf b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000..ec24749db9 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000..824d518eb4 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.ttf b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000000..b175aa8ece Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000000..e09b5a5500 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/AbpAspNetCoreComponentsWebThemingMudBlazorModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/AbpAspNetCoreComponentsWebThemingMudBlazorModule.cs new file mode 100644 index 0000000000..08b43b5cb1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/AbpAspNetCoreComponentsWebThemingMudBlazorModule.cs @@ -0,0 +1,22 @@ +using Volo.Abp.AspNetCore.Components.Web.Security; +using Volo.Abp.Modularity; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; + +[DependsOn( + typeof(AbpMudBlazorUIModule), + typeof(AbpUiNavigationModule) +)] +public class AbpAspNetCoreComponentsWebThemingMudBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Components.Add(typeof(AbpAuthenticationState), null); + }); + } +} + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/AbpDynamicLayoutComponentOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/AbpDynamicLayoutComponentOptions.cs new file mode 100644 index 0000000000..ab6b516004 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/AbpDynamicLayoutComponentOptions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; + +public class AbpDynamicLayoutComponentOptions +{ + /// + /// Used to define components that renders in the layout + /// + [NotNull] + public Dictionary?> Components { get; set; } + + public AbpDynamicLayoutComponentOptions() + { + Components = new Dictionary?>(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/AbpScripts.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/AbpScripts.razor new file mode 100644 index 0000000000..436a22fc0e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/AbpScripts.razor @@ -0,0 +1,61 @@ +@implements IDisposable +@inject IComponentBundleManager BundleManager +@inject PersistentComponentState ApplicationState +@if (ScriptFiles != null) +{ + foreach (var file in ScriptFiles) + { + var src = file; + if (!AppBasePath.IsNullOrWhiteSpace()) + { + src = AppBasePath.EnsureEndsWith('/') + file.RemovePreFix("/"); + } + + } +} + +@code { + + private const string PrerenderedKey = "abp_script_prerendered"; + + [Parameter] + public List? WebAssemblyScriptFiles { get; set; } + + [Parameter] + public string? BundleName { get; set; } + + [Parameter] + public string? AppBasePath { get; set; } + + private List? ScriptFiles { get; set; } + + private PersistingComponentStateSubscription _persistingSubscription; + + protected override async Task OnInitializedAsync() + { + _persistingSubscription = ApplicationState.RegisterOnPersisting(Callback); + if (!ApplicationState.TryTakeFromJson(PrerenderedKey, out _)) + { + // We are in prerendering mode + if (!BundleName.IsNullOrWhiteSpace()) + { + ScriptFiles = (await BundleManager.GetScriptBundleFilesAsync(BundleName!)).ToList(); + } + } + else + { + if (OperatingSystem.IsBrowser() && WebAssemblyScriptFiles != null) + { + ScriptFiles = WebAssemblyScriptFiles; + } + } + } + + private Task Callback() + { + ApplicationState.PersistAsJson(PrerenderedKey, PrerenderedKey); + return Task.CompletedTask; + } + + public void Dispose() => _persistingSubscription.Dispose(); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/AbpStyles.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/AbpStyles.razor new file mode 100644 index 0000000000..1c2ede488f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/AbpStyles.razor @@ -0,0 +1,75 @@ +@implements IDisposable +@inject IComponentBundleManager BundleManager +@inject PersistentComponentState ApplicationState +@if (StyleFiles != null) +{ + foreach (var file in StyleFiles) + { + var href = file; + if (!AppBasePath.IsNullOrWhiteSpace()) + { + href = AppBasePath.EnsureEndsWith('/') + file.RemovePreFix("/"); + } + + } +} + +@code { + + private const string PrerenderedKey = "abp_style_prerendered"; + + [Parameter] + public List? WebAssemblyStyleFiles { get; set; } + + [Parameter] + public string? BundleName { get; set; } + + [Parameter] + public string? AppBasePath { get; set; } + + private List? StyleFiles { get; set; } + + private PersistingComponentStateSubscription _persistingSubscription; + + protected override async Task OnInitializedAsync() + { + _persistingSubscription = ApplicationState.RegisterOnPersisting(Callback); + if (!ApplicationState.TryTakeFromJson>(PrerenderedKey, out var scriptFiles)) + { + // We are in prerendering mode + if (!BundleName.IsNullOrWhiteSpace()) + { + StyleFiles = (await BundleManager.GetStyleBundleFilesAsync(BundleName!)).ToList(); + } + } + else + { + StyleFiles = scriptFiles; + if (OperatingSystem.IsBrowser() && StyleFiles != null && WebAssemblyStyleFiles != null) + { + StyleFiles.AddIfNotContains(WebAssemblyStyleFiles); + } + } + } + + private bool _hasRemoveServerStyle = false; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!_hasRemoveServerStyle && OperatingSystem.IsBrowser() && WebAssemblyStyleFiles != null) + { + _hasRemoveServerStyle = true; + await Task.Delay(3000); + StyleFiles = WebAssemblyStyleFiles; + StateHasChanged(); + } + } + + private Task Callback() + { + ApplicationState.PersistAsJson(PrerenderedKey, StyleFiles); + return Task.CompletedTask; + } + + public void Dispose() => _persistingSubscription.Dispose(); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/IComponentBundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/IComponentBundleManager.cs new file mode 100644 index 0000000000..8fe8aa710f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Bundling/IComponentBundleManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Bundling; + +public interface IComponentBundleManager +{ + Task> GetStyleBundleFilesAsync(string bundleName); + + Task> GetScriptBundleFilesAsync(string bundleName); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/DynamicLayoutComponent.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/DynamicLayoutComponent.razor new file mode 100644 index 0000000000..7119f6a54c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/DynamicLayoutComponent.razor @@ -0,0 +1,7 @@ +@if (AbpDynamicLayoutComponentOptions.Value.Components.Any()) +{ + foreach (var (componentType, parameters) in AbpDynamicLayoutComponentOptions.Value.Components) + { + + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/DynamicLayoutComponent.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/DynamicLayoutComponent.razor.cs new file mode 100644 index 0000000000..5b9ba2bb66 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/DynamicLayoutComponent.razor.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components; + +public partial class DynamicLayoutComponent : ComponentBase +{ + [Inject] + protected IOptions AbpDynamicLayoutComponentOptions { get; set; } = default!; +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/LayoutHooks/LayoutHook.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/LayoutHooks/LayoutHook.razor new file mode 100644 index 0000000000..d7c5718d7b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/LayoutHooks/LayoutHook.razor @@ -0,0 +1,7 @@ +@if (LayoutHookViewModel.Hooks.Any()) +{ + foreach (var hook in LayoutHookViewModel.Hooks) + { + + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/LayoutHooks/LayoutHook.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/LayoutHooks/LayoutHook.razor.cs new file mode 100644 index 0000000000..3cf75df16f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Components/LayoutHooks/LayoutHook.razor.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using Volo.Abp.Ui.LayoutHooks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components.LayoutHooks; + +public partial class LayoutHook : ComponentBase +{ + [Parameter] + public string Name { get; set; } = default!; + + [Parameter] + public string? Layout { get; set; } + + [Inject] + protected IOptions LayoutHookOptions { get; set; } = default!; + + protected LayoutHookViewModel LayoutHookViewModel { get; private set; } = default!; + + protected override Task OnInitializedAsync() + { + if (LayoutHookOptions.Value.Hooks.TryGetValue(Name, out var layoutHooks)) + { + layoutHooks = layoutHooks + .Where(x => IsComponentBase(x) && (string.IsNullOrWhiteSpace(x.Layout) || x.Layout == Layout)) + .ToList(); + } + + layoutHooks ??= new List(); + + LayoutHookViewModel = new LayoutHookViewModel(layoutHooks.ToArray(), Layout); + + return Task.CompletedTask; + } + + protected virtual bool IsComponentBase(LayoutHookInfo layoutHook) + { + return typeof(ComponentBase).IsAssignableFrom(layoutHook.ComponentType); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/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.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeader.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeader.razor new file mode 100644 index 0000000000..0560bcb970 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeader.razor @@ -0,0 +1,65 @@ +@using global::MudBlazor +@using Microsoft.Extensions.Options +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.Ui.Branding + +@inject IOptions Options +@inject IBrandingProvider BrandingProvider + + + + @if (PageLayout.Title.IsNullOrEmpty()) + { + @BrandingProvider.AppName + } + else + { + @PageLayout.Title | @BrandingProvider.AppName + } + + @if(Options.Value.RenderPageTitle) + { + + @PageLayout.Title + + } + + @if (Options.Value.RenderBreadcrumbs && PageLayout.BreadcrumbItems.Any()) + { + + + + + @if (!string.IsNullOrEmpty(item.Icon)) + { + + } + @item.Text + + + + + + + + } + + @if(Options.Value.RenderToolbar) + { + + + @if (Toolbar == null) + { + @ChildContent + } + + @foreach (var toolbarItemRender in ToolbarItemRenders) + { + + @toolbarItemRender + + } + + + } + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeader.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeader.razor.cs new file mode 100644 index 0000000000..301045d6b7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeader.razor.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout; + +public partial class PageHeader : ComponentBase, IDisposable +{ + protected List ToolbarItemRenders { get; set; } + + private List _breadcrumbItems = new(); + + public IPageToolbarManager PageToolbarManager { get; set; } = default!; + + [Inject] + public PageLayout PageLayout { get; private set; } = default!; + + [Parameter] // TODO: Consider removing this property in future and use only PageLayout. + public string? Title { get => PageLayout.Title; set => PageLayout.Title = value; } + + [Parameter] + public bool BreadcrumbShowHome { get; set; } = true; + + [Parameter] + public bool BreadcrumbShowCurrent { get; set; } = true; + + [Parameter] + public RenderFragment ChildContent { get; set; } = default!; + + [Parameter] // TODO: Consider removing this property in future and use only PageLayout. + public List BreadcrumbItems { + get => PageLayout.BreadcrumbItems.ToList(); + set + { + PageLayout.BreadcrumbItems.Clear(); + foreach (var item in value) + { + PageLayout.BreadcrumbItems.Add(item); + } + } + } + + [Parameter] + public PageToolbar? Toolbar { get; set; } + + public PageHeader() + { + ToolbarItemRenders = new List(); + } + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + + // Build MudBlazor breadcrumb items + BuildBreadcrumbItems(); + + if (Toolbar != null) + { + var toolbarItems = await PageToolbarManager.GetItemsAsync(Toolbar); + + if (!ShouldRenderToolbarItems(toolbarItems)) + { + return; + } + + ToolbarItemRenders.Clear(); + + if (!Options.Value.RenderToolbar) + { + PageLayout.ToolbarItems.Clear(); + foreach (var item in toolbarItems) + { + PageLayout.ToolbarItems.Add(item); + } + return; + } + + foreach (var item in toolbarItems) + { + var sequence = 0; + ToolbarItemRenders.Add(builder => + { + builder.OpenComponent(sequence, item.ComponentType); + if (item.Arguments != null) + { + foreach (var argument in item.Arguments) + { + sequence++; + builder.AddAttribute(sequence, argument.Key, argument.Value); + } + } + builder.CloseComponent(); + }); + } + } + } + + private void BuildBreadcrumbItems() + { + _breadcrumbItems.Clear(); + + if (BreadcrumbShowHome) + { + _breadcrumbItems.Add(new BreadcrumbItem(string.Empty, href: "/", icon: Icons.Material.Filled.Home)); + } + + var pageItems = PageLayout.BreadcrumbItems.ToList(); + for (var i = 0; i < pageItems.Count; i++) + { + var item = pageItems[i]; + var isLast = i == pageItems.Count - 1; + + var mudIcon = ConvertToMudIcon(item.Icon); + + // If BreadcrumbShowCurrent is false, skip the last item or make it non-clickable + if (isLast && !BreadcrumbShowCurrent) + { + continue; + } + + _breadcrumbItems.Add(new BreadcrumbItem( + item.Text, + href: isLast ? null : item.Href, + disabled: isLast, + icon: mudIcon)); + } + } + + private static string? ConvertToMudIcon(object? icon) + { + if (icon == null) + { + return null; + } + + if (icon is string iconString) + { + return iconString; + } + + var iconName = icon.ToString(); + + return iconName switch + { + "Home" => Icons.Material.Filled.Home, + "Edit" => Icons.Material.Filled.Edit, + "Delete" => Icons.Material.Filled.Delete, + "Add" => Icons.Material.Filled.Add, + "Search" => Icons.Material.Filled.Search, + "Settings" => Icons.Material.Filled.Settings, + "User" => Icons.Material.Filled.Person, + "Users" => Icons.Material.Filled.People, + "File" => Icons.Material.Filled.InsertDriveFile, + "Folder" => Icons.Material.Filled.Folder, + "Dashboard" => Icons.Material.Filled.Dashboard, + "List" => Icons.Material.Filled.List, + "Check" => Icons.Material.Filled.Check, + "Times" => Icons.Material.Filled.Close, + "Warning" => Icons.Material.Filled.Warning, + "Info" => Icons.Material.Filled.Info, + "Question" => Icons.Material.Filled.Help, + _ => null // Return null for unmapped icons + }; + } + + protected virtual bool ShouldRenderToolbarItems(PageToolbarItem[] items) + { + if (items.Length != PageLayout.ToolbarItems.Count) + { + return true; + } + + return items.Where((t, i) => t.ComponentType != PageLayout.ToolbarItems[i].ComponentType).Any(); + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + } + + public void Dispose() + { + PageLayout.Reset(); + ToolbarItemRenders.Clear(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeaderOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeaderOptions.cs new file mode 100644 index 0000000000..68492898d5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageHeaderOptions.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout; + +public class PageHeaderOptions +{ + public bool RenderPageTitle { get; set; } = true; + public bool RenderBreadcrumbs { get; set; } = true; + public bool RenderToolbar { get; set; } = true; +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageLayout.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageLayout.cs new file mode 100644 index 0000000000..de1ba5e35b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/PageLayout.cs @@ -0,0 +1,54 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout; + +public class PageLayout : IScopedDependency, INotifyPropertyChanged +{ + private string? title; + + // TODO: Consider using this property for setting Page Title too. + public virtual string? Title { + get => title; + set { + title = value; + OnPropertyChanged(); + } + } + + private string? menuItemName; + + public string? MenuItemName { + get => menuItemName; + set + { + menuItemName = value; + OnPropertyChanged(); + } + } + + public bool ShowToolbar { get; set; } = true; + + public virtual ObservableCollection BreadcrumbItems { get; } = new(); + + public virtual ObservableCollection ToolbarItems { get; } = new(); + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void Reset() + { + Title = string.Empty; + MenuItemName = string.Empty; + BreadcrumbItems.Clear(); + ToolbarItems.Clear(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/StandardLayouts.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/StandardLayouts.cs new file mode 100644 index 0000000000..bc25f503bd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Layout/StandardLayouts.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout; + +public static class StandardLayouts +{ + public const string Application = "Application"; + public const string Account = "Account"; + public const string Public = "Public"; + public const string Empty = "Empty"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/IPageToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/IPageToolbarContributor.cs new file mode 100644 index 0000000000..f1f5b230f4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/IPageToolbarContributor.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public interface IPageToolbarContributor +{ + Task ContributeAsync(PageToolbarContributionContext context); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/IPageToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/IPageToolbarManager.cs new file mode 100644 index 0000000000..58f5833249 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/IPageToolbarManager.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public interface IPageToolbarManager +{ + Task GetItemsAsync(PageToolbar toolbar); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/MudPageToolbarButton.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/MudPageToolbarButton.razor new file mode 100644 index 0000000000..e37ccee5c3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/MudPageToolbarButton.razor @@ -0,0 +1,21 @@ +@using global::MudBlazor + + + @Text + + +@code { + private string? IconString => Icon?.ToString(); + + private async Task OnClickHandler() + { + if (OnClickAsync != null) + { + await OnClickAsync(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/MudPageToolbarButton.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/MudPageToolbarButton.razor.cs new file mode 100644 index 0000000000..a8918a5f18 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/MudPageToolbarButton.razor.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public partial class MudPageToolbarButton : ComponentBase +{ + [Parameter] + public Color Color { get; set; } = Color.Primary; + + [Parameter] + public object? Icon { get; set; } + + [Parameter] + public string Text { get; set; } = default!; + + [Parameter] + public Func? OnClickAsync { get; set; } + + [Parameter] + public bool Disabled { get; set; } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbar.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbar.cs new file mode 100644 index 0000000000..1f808816ab --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbar.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbar +{ + public PageToolbarContributorList Contributors { get; set; } + + public PageToolbar() + { + Contributors = new PageToolbarContributorList(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributionContext.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributionContext.cs new file mode 100644 index 0000000000..37f7677cf9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributionContext.cs @@ -0,0 +1,20 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbarContributionContext +{ + [NotNull] + public IServiceProvider ServiceProvider { get; } + + [NotNull] + public PageToolbarItemList Items { get; } + + public PageToolbarContributionContext( + [NotNull] IServiceProvider serviceProvider) + { + ServiceProvider = Check.NotNull(serviceProvider, nameof(serviceProvider)); + Items = new PageToolbarItemList(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributor.cs new file mode 100644 index 0000000000..92fb22838e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributor.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public abstract class PageToolbarContributor : IPageToolbarContributor +{ + public abstract Task ContributeAsync(PageToolbarContributionContext context); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributorList.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributorList.cs new file mode 100644 index 0000000000..0723bbe571 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarContributorList.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbarContributorList : List +{ +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarDictionary.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarDictionary.cs new file mode 100644 index 0000000000..2016008a5e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarDictionary.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbarDictionary : Dictionary +{ + +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarExtensions.cs new file mode 100644 index 0000000000..136a95c689 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarExtensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MudBlazor; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public static class PageToolbarExtensions +{ + public static PageToolbar AddComponent( + this PageToolbar toolbar, + Dictionary? arguments = null, + int order = 0, + string? requiredPolicyName = null) + { + return toolbar.AddComponent( + typeof(TComponent), + arguments, + order, + requiredPolicyName + ); + } + + public static PageToolbar AddComponent( + this PageToolbar toolbar, + Type componentType, + Dictionary? arguments = null, + int order = 0, + string? requiredPolicyName = null) + { + toolbar.Contributors.Add( + new SimplePageToolbarContributor( + componentType, + arguments, + order, + requiredPolicyName + ) + ); + + return toolbar; + } + + public static PageToolbar AddButton( + this PageToolbar toolbar, + string text, + Func clicked, + object? icon = null, + Color? color = null, + bool disabled = false, + int order = 0, + string? requiredPolicyName = null) + { + toolbar.AddComponent( + new Dictionary + { + { nameof(MudPageToolbarButton.Color), color ?? Color.Primary }, + { nameof(MudPageToolbarButton.Text), text }, + { nameof(MudPageToolbarButton.Disabled), disabled }, + { nameof(MudPageToolbarButton.Icon), icon }, + { nameof(MudPageToolbarButton.OnClickAsync), clicked }, + }, + order, + requiredPolicyName + ); + + return toolbar; + } + + public static PageToolbar AddMudButton( + this PageToolbar toolbar, + string text, + Func clicked, + string? icon = null, + Color? color = null, + bool disabled = false, + int order = 0, + string? requiredPolicyName = null) + { + return toolbar.AddButton(text, clicked, icon, color, disabled, order, requiredPolicyName); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarItem.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarItem.cs new file mode 100644 index 0000000000..f5820599d4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarItem.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbarItem +{ + [NotNull] + public Type ComponentType { get; } + + public Dictionary? Arguments { get; set; } + + public int Order { get; set; } + + public PageToolbarItem( + [NotNull] Type componentType, + Dictionary? arguments = null, + int order = 0) + { + ComponentType = Check.NotNull(componentType, nameof(componentType)); + Arguments = arguments; + Order = order; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarItemList.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarItemList.cs new file mode 100644 index 0000000000..761efa01ed --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarItemList.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbarItemList : List +{ + +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarManager.cs new file mode 100644 index 0000000000..24efd89e1b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/PageToolbarManager.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class PageToolbarManager : IPageToolbarManager, ITransientDependency +{ + protected IServiceScopeFactory ServiceScopeFactory { get; } + + public PageToolbarManager( + IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + } + + public virtual async Task GetItemsAsync(PageToolbar toolbar) + { + if (toolbar == null || !toolbar.Contributors.Any()) + { + return Array.Empty(); + } + + using (var scope = ServiceScopeFactory.CreateScope()) + { + var context = new PageToolbarContributionContext(scope.ServiceProvider); + + foreach (var contributor in toolbar.Contributors) + { + await contributor.ContributeAsync(context); + } + + return context.Items.OrderBy(i => i.Order).ToArray(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/SimplePageToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/SimplePageToolbarContributor.cs new file mode 100644 index 0000000000..4b333e2b90 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/PageToolbars/SimplePageToolbarContributor.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; + +public class SimplePageToolbarContributor : IPageToolbarContributor +{ + public Type ComponentType { get; } + + public Dictionary? Arguments { get; set; } + + public int Order { get; } + + public string? RequiredPolicyName { get; } + + private bool? _shouldAddComponent; + + public SimplePageToolbarContributor( + Type componentType, + Dictionary? arguments = null, + int order = 0, + string? requiredPolicyName = null) + { + ComponentType = componentType; + Arguments = arguments; + Order = order; + RequiredPolicyName = requiredPolicyName; + } + + public async Task ContributeAsync(PageToolbarContributionContext context) + { + if (await ShouldAddComponentAsync(context)) + { + context.Items.Add(new PageToolbarItem(ComponentType, Arguments, Order)); + } + } + + protected virtual async Task ShouldAddComponentAsync(PageToolbarContributionContext context) + { + if (_shouldAddComponent.HasValue) + { + return _shouldAddComponent.Value; + } + + if (RequiredPolicyName == null) + { + _shouldAddComponent = true; + return _shouldAddComponent.Value; + } + + var authorizationService = context.ServiceProvider.GetRequiredService(); + _shouldAddComponent = await authorizationService.IsGrantedAsync(RequiredPolicyName); + return _shouldAddComponent.Value; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Routing/AbpRouterOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Routing/AbpRouterOptions.cs new file mode 100644 index 0000000000..390322da3b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Routing/AbpRouterOptions.cs @@ -0,0 +1,15 @@ +using System.Reflection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; + +public class AbpRouterOptions +{ + public Assembly AppAssembly { get; set; } = default!; + + public RouterAssemblyList AdditionalAssemblies { get; } + + public AbpRouterOptions() + { + AdditionalAssemblies = new RouterAssemblyList(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Routing/RouterAssemblyList.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Routing/RouterAssemblyList.cs new file mode 100644 index 0000000000..a23841c13e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Routing/RouterAssemblyList.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; + +public class RouterAssemblyList : List +{ + +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/AbpThemingOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/AbpThemingOptions.cs new file mode 100644 index 0000000000..4fd7d8958c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/AbpThemingOptions.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public class AbpThemingOptions +{ + public ThemeDictionary Themes { get; } + + public string? DefaultThemeName { get; set; } + + public AbpThemingOptions() + { + Themes = new ThemeDictionary(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/DefaultThemeManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/DefaultThemeManager.cs new file mode 100644 index 0000000000..30bf198a9f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/DefaultThemeManager.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public class DefaultThemeManager : IThemeManager, IScopedDependency, IServiceProviderAccessor +{ + public IServiceProvider ServiceProvider { get; } + public ITheme CurrentTheme => GetCurrentTheme(); + + private ITheme? _currentTheme; + + protected IThemeSelector ThemeSelector { get; } + + public DefaultThemeManager( + IServiceProvider serviceProvider, + IThemeSelector themeSelector) + { + ServiceProvider = serviceProvider; + ThemeSelector = themeSelector; + } + + protected virtual ITheme GetCurrentTheme() + { + if (_currentTheme != null) + { + return _currentTheme; + } + + _currentTheme = (ITheme)ServiceProvider.GetRequiredService(ThemeSelector.GetCurrentThemeInfo().ThemeType); + return _currentTheme; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/DefaultThemeSelector.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/DefaultThemeSelector.cs new file mode 100644 index 0000000000..18fa99744c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/DefaultThemeSelector.cs @@ -0,0 +1,36 @@ +using System.Linq; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public class DefaultThemeSelector : IThemeSelector, ITransientDependency +{ + protected AbpThemingOptions Options { get; } + + public DefaultThemeSelector(IOptions options) + { + Options = options.Value; + } + + public virtual ThemeInfo GetCurrentThemeInfo() + { + if (!Options.Themes.Any()) + { + throw new AbpException($"No theme registered! Use {nameof(AbpThemingOptions)} to register themes."); + } + + if (Options.DefaultThemeName == null) + { + return Options.Themes.Values.First(); + } + + var themeInfo = Options.Themes.Values.FirstOrDefault(t => t.Name == Options.DefaultThemeName); + if (themeInfo == null) + { + throw new AbpException("Default theme is configured but it's not found in the registered themes: " + Options.DefaultThemeName); + } + + return themeInfo; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ITheme.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ITheme.cs new file mode 100644 index 0000000000..270ff2c427 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ITheme.cs @@ -0,0 +1,8 @@ +using System; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public interface ITheme +{ + Type GetLayout(string name, bool fallbackToDefault = true); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/IThemeManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/IThemeManager.cs new file mode 100644 index 0000000000..663ce4716c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/IThemeManager.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public interface IThemeManager +{ + ITheme CurrentTheme { get; } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/IThemeSelector.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/IThemeSelector.cs new file mode 100644 index 0000000000..51809a6f54 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/IThemeSelector.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public interface IThemeSelector +{ + ThemeInfo GetCurrentThemeInfo(); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeDictionary.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeDictionary.cs new file mode 100644 index 0000000000..e2361c2c6a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeDictionary.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public class ThemeDictionary : Dictionary +{ + public ThemeInfo Add() + where TTheme : ITheme + { + return Add(typeof(TTheme)); + } + + public ThemeInfo Add(Type themeType) + { + if (ContainsKey(themeType)) + { + throw new AbpException("This theme is already added before: " + themeType.AssemblyQualifiedName); + } + + return this[themeType] = new ThemeInfo(themeType); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeExtensions.cs new file mode 100644 index 0000000000..09798cecea --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeExtensions.cs @@ -0,0 +1,27 @@ +using System; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public static class ThemeExtensions +{ + public static Type GetApplicationLayout(this ITheme theme, bool fallbackToDefault = true) + { + return theme.GetLayout(StandardLayouts.Application, fallbackToDefault); + } + + public static Type GetAccountLayout(this ITheme theme, bool fallbackToDefault = true) + { + return theme.GetLayout(StandardLayouts.Account, fallbackToDefault); + } + + public static Type GetPublicLayout(this ITheme theme, bool fallbackToDefault = true) + { + return theme.GetLayout(StandardLayouts.Public, fallbackToDefault); + } + + public static Type GetEmptyLayout(this ITheme theme, bool fallbackToDefault = true) + { + return theme.GetLayout(StandardLayouts.Empty, fallbackToDefault); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeInfo.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeInfo.cs new file mode 100644 index 0000000000..d9525cd758 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeInfo.cs @@ -0,0 +1,24 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +public class ThemeInfo +{ + public Type ThemeType { get; } + + public string Name { get; } + + public ThemeInfo([NotNull] Type themeType) + { + Check.NotNull(themeType, nameof(themeType)); + + if (!typeof(ITheme).IsAssignableFrom(themeType)) + { + throw new AbpException($"Given {nameof(themeType)} ({themeType.AssemblyQualifiedName}) should be type of {typeof(ITheme).AssemblyQualifiedName}"); + } + + ThemeType = themeType; + Name = ThemeNameAttribute.GetName(themeType); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeNameAttribute.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeNameAttribute.cs new file mode 100644 index 0000000000..fc85e1e5cd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Theming/ThemeNameAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; + +[AttributeUsage(AttributeTargets.Class)] +public class ThemeNameAttribute : Attribute +{ + public string Name { get; set; } + + public ThemeNameAttribute(string name) + { + Name = name; + } + + public static string GetName(Type themeType) + { + return themeType + .GetCustomAttributes(true) + .OfType() + .FirstOrDefault()?.Name ?? themeType.Name; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/AbpToolbarOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/AbpToolbarOptions.cs new file mode 100644 index 0000000000..e096cebbc2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/AbpToolbarOptions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public class AbpToolbarOptions +{ + [NotNull] + public List Contributors { get; } + + public AbpToolbarOptions() + { + Contributors = new List(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarConfigurationContext.cs new file mode 100644 index 0000000000..b019b269d8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarConfigurationContext.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Localization; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public interface IToolbarConfigurationContext : IServiceProviderAccessor +{ + Toolbar Toolbar { get; } + + IAuthorizationService AuthorizationService { get; } + + IStringLocalizerFactory StringLocalizerFactory { get; } + + Task IsGrantedAsync(string policyName); + + IStringLocalizer? GetDefaultLocalizer(); + + [NotNull] + public IStringLocalizer GetLocalizer(); + + [NotNull] + public IStringLocalizer GetLocalizer(Type resourceType); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarContributor.cs new file mode 100644 index 0000000000..973509464c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarContributor.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public interface IToolbarContributor +{ + Task ConfigureToolbarAsync(IToolbarConfigurationContext context); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarManager.cs new file mode 100644 index 0000000000..cb483092d9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/IToolbarManager.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public interface IToolbarManager +{ + Task GetAsync(string name); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/StandardToolbars.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/StandardToolbars.cs new file mode 100644 index 0000000000..c3443f7efd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/StandardToolbars.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public static class StandardToolbars +{ + public const string Main = "Main"; +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/Toolbar.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/Toolbar.cs new file mode 100644 index 0000000000..5a0d829124 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/Toolbar.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public class Toolbar +{ + public string Name { get; } + + public List Items { get; } + + public Toolbar([NotNull] string name) + { + Name = Check.NotNull(name, nameof(name)); + Items = new List(); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarConfigurationContext.cs new file mode 100644 index 0000000000..1fdff594b0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarConfigurationContext.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public class ToolbarConfigurationContext : IToolbarConfigurationContext +{ + public IServiceProvider ServiceProvider { get; } + + private readonly IAbpLazyServiceProvider _lazyServiceProvider; + + public IAuthorizationService AuthorizationService => _lazyServiceProvider.LazyGetRequiredService(); + + public IStringLocalizerFactory StringLocalizerFactory => _lazyServiceProvider.LazyGetRequiredService(); + + public Toolbar Toolbar { get; } + + public ToolbarConfigurationContext(Toolbar toolbar, IServiceProvider serviceProvider) + { + Toolbar = toolbar; + ServiceProvider = serviceProvider; + _lazyServiceProvider = ServiceProvider.GetRequiredService(); + } + + public Task IsGrantedAsync(string policyName) + { + return AuthorizationService.IsGrantedAsync(policyName); + } + + public IStringLocalizer? GetDefaultLocalizer() + { + return StringLocalizerFactory.CreateDefaultOrNull(); + } + + [NotNull] + public IStringLocalizer GetLocalizer() + { + return StringLocalizerFactory.Create(); + } + + [NotNull] + public IStringLocalizer GetLocalizer(Type resourceType) + { + return StringLocalizerFactory.Create(resourceType); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarItem.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarItem.cs new file mode 100644 index 0000000000..31b2839cc6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarItem.cs @@ -0,0 +1,21 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public class ToolbarItem +{ + public Type ComponentType { + get => _componentType; + set => _componentType = Check.NotNull(value, nameof(value)); + } + private Type _componentType = default!; + + public int Order { get; set; } + + public ToolbarItem([NotNull] Type componentType, int order = 0) + { + Order = order; + ComponentType = Check.NotNull(componentType, nameof(componentType)); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarManager.cs new file mode 100644 index 0000000000..cfdfcfd1e7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Toolbars/ToolbarManager.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +public class ToolbarManager : IToolbarManager, ITransientDependency +{ + protected AbpToolbarOptions Options { get; } + protected IServiceProvider ServiceProvider { get; } + + public ToolbarManager( + IOptions options, + IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + Options = options.Value; + } + + public async Task GetAsync(string name) + { + var toolbar = new Toolbar(name); + + using (var scope = ServiceProvider.CreateScope()) + { + var context = new ToolbarConfigurationContext(toolbar, scope.ServiceProvider); + + foreach (var contributor in Options.Contributors) + { + await contributor.ConfigureToolbarAsync(context); + } + } + + return toolbar; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.abppkg b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.abppkg new file mode 100644 index 0000000000..e0c33eaab1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.csproj b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.csproj new file mode 100644 index 0000000000..0e174f1ab8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.csproj @@ -0,0 +1,22 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorBundlingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorBundlingModule.cs new file mode 100644 index 0000000000..aabe62e604 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorBundlingModule.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.GlobalAssets.Enabled = true; + options.GlobalAssets.GlobalStyleBundleName = BlazorWebAssemblyMudBlazorStandardBundles.Styles.Global; + options.GlobalAssets.GlobalScriptBundleName = BlazorWebAssemblyMudBlazorStandardBundles.Scripts.Global; + + options + .StyleBundles + .Add(BlazorWebAssemblyMudBlazorStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyMudBlazorStyleContributor)); + }); + + options + .ScriptBundles + .Add(BlazorWebAssemblyMudBlazorStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyMudBlazorScriptContributor)); + }); + + options.MinificationIgnoredFiles.Add("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs new file mode 100644 index 0000000000..7949bf0d72 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorScriptContributor.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling; + +public class BlazorWebAssemblyMudBlazorScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); + context.Files.AddIfNotContains("_content/MudBlazor/MudBlazor.min.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-popover-patch.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/abp-mud-ripple-patch.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorStandardBundles.cs new file mode 100644 index 0000000000..de5b72cc2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorStandardBundles.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling; + +public class BlazorWebAssemblyMudBlazorStandardBundles +{ + public static class Styles + { + public static string Global = "BlazorWebAssemblyMudBlazor.Global"; + } + + public static class Scripts + { + public static string Global = "BlazorWebAssemblyMudBlazor.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorStyleContributor.cs new file mode 100644 index 0000000000..32d6563d74 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/BlazorWebAssemblyMudBlazorStyleContributor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling; + +public class BlazorWebAssemblyMudBlazorStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("_content/MudBlazor/MudBlazor.min.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.MudBlazorUI/volo.abp.mudblazorui.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/libs/flag-icon/css/flag-icon.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..e6bb89a4a1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/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. + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling.csproj new file mode 100644 index 0000000000..adb6c6260d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule.cs new file mode 100644 index 0000000000..6476687904 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule.cs @@ -0,0 +1,35 @@ +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling; +using Volo.Abp.AspNetCore.Components.WebAssembly; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorBundlingModule), + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpAspNetCoreComponentsWebAssemblyModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .StyleBundles + .Add(BlazorWebAssemblyMudBlazorStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyMudBlazorStyleContributor)); + }); + + options + .ScriptBundles + .Add(BlazorWebAssemblyMudBlazorStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyMudBlazorScriptContributor)); + }); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/CultureAwareRedirectToLoginHelper.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/CultureAwareRedirectToLoginHelper.cs new file mode 100644 index 0000000000..2ad0bdea79 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/CultureAwareRedirectToLoginHelper.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Components.Web; +using Volo.Abp.AspNetCore.Components.WebAssembly; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; + +public static class CultureAwareRedirectToLoginHelper +{ + public static async Task RedirectAsync( + NavigationManager navigation, + string loginUrl, + IRouteBasedCultureUrlHelper cultureUrlHelper, + IOptions webOptions) + { + var cultureLoginUrl = await cultureUrlHelper.PrependCulturePrefixAsync(loginUrl); + if (webOptions.Value.IsBlazorWebApp) + { + navigation.NavigateTo(cultureLoginUrl, forceLoad: true); + } + else + { + navigation.NavigateToLogin(cultureLoginUrl); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/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.AspNetCore.Components.WebAssembly.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.abppkg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.abppkg new file mode 100644 index 0000000000..e0c33eaab1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.csproj new file mode 100644 index 0000000000..1f830fa162 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.csproj @@ -0,0 +1,18 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/WebAssemblyMudBlazorComponentBundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/WebAssemblyMudBlazorComponentBundleManager.cs new file mode 100644 index 0000000000..c6a3a446f3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/WebAssemblyMudBlazorComponentBundleManager.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Bundling; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; + +public class WebAssemblyMudBlazorComponentBundleManager: IComponentBundleManager, ITransientDependency +{ + public virtual Task> GetStyleBundleFilesAsync(string bundleName) + { + return Task.FromResult>(new List()); + } + + public virtual Task> GetScriptBundleFilesAsync(string bundleName) + { + return Task.FromResult>(new List()); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.css b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.css new file mode 100644 index 0000000000..b54375090b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.css @@ -0,0 +1,1568 @@ +.flag-icon-background { + background-size: contain; + background-position: 50%; + background-repeat: no-repeat; +} +.flag-icon { + background-size: contain; + background-position: 50%; + background-repeat: no-repeat; + position: relative; + display: inline-block; + width: 1.33333333em; + line-height: 1em; +} +.flag-icon:before { + content: "\00a0"; +} +.flag-icon.flag-icon-squared { + width: 1em; +} +.flag-icon-ad { + background-image: url(../flags/4x3/ad.svg); +} +.flag-icon-ad.flag-icon-squared { + background-image: url(../flags/1x1/ad.svg); +} +.flag-icon-ae { + background-image: url(../flags/4x3/ae.svg); +} +.flag-icon-ae.flag-icon-squared { + background-image: url(../flags/1x1/ae.svg); +} +.flag-icon-af { + background-image: url(../flags/4x3/af.svg); +} +.flag-icon-af.flag-icon-squared { + background-image: url(../flags/1x1/af.svg); +} +.flag-icon-ag { + background-image: url(../flags/4x3/ag.svg); +} +.flag-icon-ag.flag-icon-squared { + background-image: url(../flags/1x1/ag.svg); +} +.flag-icon-ai { + background-image: url(../flags/4x3/ai.svg); +} +.flag-icon-ai.flag-icon-squared { + background-image: url(../flags/1x1/ai.svg); +} +.flag-icon-al { + background-image: url(../flags/4x3/al.svg); +} +.flag-icon-al.flag-icon-squared { + background-image: url(../flags/1x1/al.svg); +} +.flag-icon-am { + background-image: url(../flags/4x3/am.svg); +} +.flag-icon-am.flag-icon-squared { + background-image: url(../flags/1x1/am.svg); +} +.flag-icon-ao { + background-image: url(../flags/4x3/ao.svg); +} +.flag-icon-ao.flag-icon-squared { + background-image: url(../flags/1x1/ao.svg); +} +.flag-icon-aq { + background-image: url(../flags/4x3/aq.svg); +} +.flag-icon-aq.flag-icon-squared { + background-image: url(../flags/1x1/aq.svg); +} +.flag-icon-ar { + background-image: url(../flags/4x3/ar.svg); +} +.flag-icon-ar.flag-icon-squared { + background-image: url(../flags/1x1/ar.svg); +} +.flag-icon-as { + background-image: url(../flags/4x3/as.svg); +} +.flag-icon-as.flag-icon-squared { + background-image: url(../flags/1x1/as.svg); +} +.flag-icon-at { + background-image: url(../flags/4x3/at.svg); +} +.flag-icon-at.flag-icon-squared { + background-image: url(../flags/1x1/at.svg); +} +.flag-icon-au { + background-image: url(../flags/4x3/au.svg); +} +.flag-icon-au.flag-icon-squared { + background-image: url(../flags/1x1/au.svg); +} +.flag-icon-aw { + background-image: url(../flags/4x3/aw.svg); +} +.flag-icon-aw.flag-icon-squared { + background-image: url(../flags/1x1/aw.svg); +} +.flag-icon-ax { + background-image: url(../flags/4x3/ax.svg); +} +.flag-icon-ax.flag-icon-squared { + background-image: url(../flags/1x1/ax.svg); +} +.flag-icon-az { + background-image: url(../flags/4x3/az.svg); +} +.flag-icon-az.flag-icon-squared { + background-image: url(../flags/1x1/az.svg); +} +.flag-icon-ba { + background-image: url(../flags/4x3/ba.svg); +} +.flag-icon-ba.flag-icon-squared { + background-image: url(../flags/1x1/ba.svg); +} +.flag-icon-bb { + background-image: url(../flags/4x3/bb.svg); +} +.flag-icon-bb.flag-icon-squared { + background-image: url(../flags/1x1/bb.svg); +} +.flag-icon-bd { + background-image: url(../flags/4x3/bd.svg); +} +.flag-icon-bd.flag-icon-squared { + background-image: url(../flags/1x1/bd.svg); +} +.flag-icon-be { + background-image: url(../flags/4x3/be.svg); +} +.flag-icon-be.flag-icon-squared { + background-image: url(../flags/1x1/be.svg); +} +.flag-icon-bf { + background-image: url(../flags/4x3/bf.svg); +} +.flag-icon-bf.flag-icon-squared { + background-image: url(../flags/1x1/bf.svg); +} +.flag-icon-bg { + background-image: url(../flags/4x3/bg.svg); +} +.flag-icon-bg.flag-icon-squared { + background-image: url(../flags/1x1/bg.svg); +} +.flag-icon-bh { + background-image: url(../flags/4x3/bh.svg); +} +.flag-icon-bh.flag-icon-squared { + background-image: url(../flags/1x1/bh.svg); +} +.flag-icon-bi { + background-image: url(../flags/4x3/bi.svg); +} +.flag-icon-bi.flag-icon-squared { + background-image: url(../flags/1x1/bi.svg); +} +.flag-icon-bj { + background-image: url(../flags/4x3/bj.svg); +} +.flag-icon-bj.flag-icon-squared { + background-image: url(../flags/1x1/bj.svg); +} +.flag-icon-bl { + background-image: url(../flags/4x3/bl.svg); +} +.flag-icon-bl.flag-icon-squared { + background-image: url(../flags/1x1/bl.svg); +} +.flag-icon-bm { + background-image: url(../flags/4x3/bm.svg); +} +.flag-icon-bm.flag-icon-squared { + background-image: url(../flags/1x1/bm.svg); +} +.flag-icon-bn { + background-image: url(../flags/4x3/bn.svg); +} +.flag-icon-bn.flag-icon-squared { + background-image: url(../flags/1x1/bn.svg); +} +.flag-icon-bo { + background-image: url(../flags/4x3/bo.svg); +} +.flag-icon-bo.flag-icon-squared { + background-image: url(../flags/1x1/bo.svg); +} +.flag-icon-bq { + background-image: url(../flags/4x3/bq.svg); +} +.flag-icon-bq.flag-icon-squared { + background-image: url(../flags/1x1/bq.svg); +} +.flag-icon-br { + background-image: url(../flags/4x3/br.svg); +} +.flag-icon-br.flag-icon-squared { + background-image: url(../flags/1x1/br.svg); +} +.flag-icon-bs { + background-image: url(../flags/4x3/bs.svg); +} +.flag-icon-bs.flag-icon-squared { + background-image: url(../flags/1x1/bs.svg); +} +.flag-icon-bt { + background-image: url(../flags/4x3/bt.svg); +} +.flag-icon-bt.flag-icon-squared { + background-image: url(../flags/1x1/bt.svg); +} +.flag-icon-bv { + background-image: url(../flags/4x3/bv.svg); +} +.flag-icon-bv.flag-icon-squared { + background-image: url(../flags/1x1/bv.svg); +} +.flag-icon-bw { + background-image: url(../flags/4x3/bw.svg); +} +.flag-icon-bw.flag-icon-squared { + background-image: url(../flags/1x1/bw.svg); +} +.flag-icon-by { + background-image: url(../flags/4x3/by.svg); +} +.flag-icon-by.flag-icon-squared { + background-image: url(../flags/1x1/by.svg); +} +.flag-icon-bz { + background-image: url(../flags/4x3/bz.svg); +} +.flag-icon-bz.flag-icon-squared { + background-image: url(../flags/1x1/bz.svg); +} +.flag-icon-ca { + background-image: url(../flags/4x3/ca.svg); +} +.flag-icon-ca.flag-icon-squared { + background-image: url(../flags/1x1/ca.svg); +} +.flag-icon-cc { + background-image: url(../flags/4x3/cc.svg); +} +.flag-icon-cc.flag-icon-squared { + background-image: url(../flags/1x1/cc.svg); +} +.flag-icon-cd { + background-image: url(../flags/4x3/cd.svg); +} +.flag-icon-cd.flag-icon-squared { + background-image: url(../flags/1x1/cd.svg); +} +.flag-icon-cf { + background-image: url(../flags/4x3/cf.svg); +} +.flag-icon-cf.flag-icon-squared { + background-image: url(../flags/1x1/cf.svg); +} +.flag-icon-cg { + background-image: url(../flags/4x3/cg.svg); +} +.flag-icon-cg.flag-icon-squared { + background-image: url(../flags/1x1/cg.svg); +} +.flag-icon-ch { + background-image: url(../flags/4x3/ch.svg); +} +.flag-icon-ch.flag-icon-squared { + background-image: url(../flags/1x1/ch.svg); +} +.flag-icon-ci { + background-image: url(../flags/4x3/ci.svg); +} +.flag-icon-ci.flag-icon-squared { + background-image: url(../flags/1x1/ci.svg); +} +.flag-icon-ck { + background-image: url(../flags/4x3/ck.svg); +} +.flag-icon-ck.flag-icon-squared { + background-image: url(../flags/1x1/ck.svg); +} +.flag-icon-cl { + background-image: url(../flags/4x3/cl.svg); +} +.flag-icon-cl.flag-icon-squared { + background-image: url(../flags/1x1/cl.svg); +} +.flag-icon-cm { + background-image: url(../flags/4x3/cm.svg); +} +.flag-icon-cm.flag-icon-squared { + background-image: url(../flags/1x1/cm.svg); +} +.flag-icon-cn { + background-image: url(../flags/4x3/cn.svg); +} +.flag-icon-cn.flag-icon-squared { + background-image: url(../flags/1x1/cn.svg); +} +.flag-icon-co { + background-image: url(../flags/4x3/co.svg); +} +.flag-icon-co.flag-icon-squared { + background-image: url(../flags/1x1/co.svg); +} +.flag-icon-cr { + background-image: url(../flags/4x3/cr.svg); +} +.flag-icon-cr.flag-icon-squared { + background-image: url(../flags/1x1/cr.svg); +} +.flag-icon-cu { + background-image: url(../flags/4x3/cu.svg); +} +.flag-icon-cu.flag-icon-squared { + background-image: url(../flags/1x1/cu.svg); +} +.flag-icon-cv { + background-image: url(../flags/4x3/cv.svg); +} +.flag-icon-cv.flag-icon-squared { + background-image: url(../flags/1x1/cv.svg); +} +.flag-icon-cw { + background-image: url(../flags/4x3/cw.svg); +} +.flag-icon-cw.flag-icon-squared { + background-image: url(../flags/1x1/cw.svg); +} +.flag-icon-cx { + background-image: url(../flags/4x3/cx.svg); +} +.flag-icon-cx.flag-icon-squared { + background-image: url(../flags/1x1/cx.svg); +} +.flag-icon-cy { + background-image: url(../flags/4x3/cy.svg); +} +.flag-icon-cy.flag-icon-squared { + background-image: url(../flags/1x1/cy.svg); +} +.flag-icon-cz { + background-image: url(../flags/4x3/cz.svg); +} +.flag-icon-cz.flag-icon-squared { + background-image: url(../flags/1x1/cz.svg); +} +.flag-icon-de { + background-image: url(../flags/4x3/de.svg); +} +.flag-icon-de.flag-icon-squared { + background-image: url(../flags/1x1/de.svg); +} +.flag-icon-dj { + background-image: url(../flags/4x3/dj.svg); +} +.flag-icon-dj.flag-icon-squared { + background-image: url(../flags/1x1/dj.svg); +} +.flag-icon-dk { + background-image: url(../flags/4x3/dk.svg); +} +.flag-icon-dk.flag-icon-squared { + background-image: url(../flags/1x1/dk.svg); +} +.flag-icon-dm { + background-image: url(../flags/4x3/dm.svg); +} +.flag-icon-dm.flag-icon-squared { + background-image: url(../flags/1x1/dm.svg); +} +.flag-icon-do { + background-image: url(../flags/4x3/do.svg); +} +.flag-icon-do.flag-icon-squared { + background-image: url(../flags/1x1/do.svg); +} +.flag-icon-dz { + background-image: url(../flags/4x3/dz.svg); +} +.flag-icon-dz.flag-icon-squared { + background-image: url(../flags/1x1/dz.svg); +} +.flag-icon-ec { + background-image: url(../flags/4x3/ec.svg); +} +.flag-icon-ec.flag-icon-squared { + background-image: url(../flags/1x1/ec.svg); +} +.flag-icon-ee { + background-image: url(../flags/4x3/ee.svg); +} +.flag-icon-ee.flag-icon-squared { + background-image: url(../flags/1x1/ee.svg); +} +.flag-icon-eg { + background-image: url(../flags/4x3/eg.svg); +} +.flag-icon-eg.flag-icon-squared { + background-image: url(../flags/1x1/eg.svg); +} +.flag-icon-eh { + background-image: url(../flags/4x3/eh.svg); +} +.flag-icon-eh.flag-icon-squared { + background-image: url(../flags/1x1/eh.svg); +} +.flag-icon-er { + background-image: url(../flags/4x3/er.svg); +} +.flag-icon-er.flag-icon-squared { + background-image: url(../flags/1x1/er.svg); +} +.flag-icon-es { + background-image: url(../flags/4x3/es.svg); +} +.flag-icon-es.flag-icon-squared { + background-image: url(../flags/1x1/es.svg); +} +.flag-icon-et { + background-image: url(../flags/4x3/et.svg); +} +.flag-icon-et.flag-icon-squared { + background-image: url(../flags/1x1/et.svg); +} +.flag-icon-fi { + background-image: url(../flags/4x3/fi.svg); +} +.flag-icon-fi.flag-icon-squared { + background-image: url(../flags/1x1/fi.svg); +} +.flag-icon-fj { + background-image: url(../flags/4x3/fj.svg); +} +.flag-icon-fj.flag-icon-squared { + background-image: url(../flags/1x1/fj.svg); +} +.flag-icon-fk { + background-image: url(../flags/4x3/fk.svg); +} +.flag-icon-fk.flag-icon-squared { + background-image: url(../flags/1x1/fk.svg); +} +.flag-icon-fm { + background-image: url(../flags/4x3/fm.svg); +} +.flag-icon-fm.flag-icon-squared { + background-image: url(../flags/1x1/fm.svg); +} +.flag-icon-fo { + background-image: url(../flags/4x3/fo.svg); +} +.flag-icon-fo.flag-icon-squared { + background-image: url(../flags/1x1/fo.svg); +} +.flag-icon-fr { + background-image: url(../flags/4x3/fr.svg); +} +.flag-icon-fr.flag-icon-squared { + background-image: url(../flags/1x1/fr.svg); +} +.flag-icon-ga { + background-image: url(../flags/4x3/ga.svg); +} +.flag-icon-ga.flag-icon-squared { + background-image: url(../flags/1x1/ga.svg); +} +.flag-icon-gb { + background-image: url(../flags/4x3/gb.svg); +} +.flag-icon-gb.flag-icon-squared { + background-image: url(../flags/1x1/gb.svg); +} +.flag-icon-gd { + background-image: url(../flags/4x3/gd.svg); +} +.flag-icon-gd.flag-icon-squared { + background-image: url(../flags/1x1/gd.svg); +} +.flag-icon-ge { + background-image: url(../flags/4x3/ge.svg); +} +.flag-icon-ge.flag-icon-squared { + background-image: url(../flags/1x1/ge.svg); +} +.flag-icon-gf { + background-image: url(../flags/4x3/gf.svg); +} +.flag-icon-gf.flag-icon-squared { + background-image: url(../flags/1x1/gf.svg); +} +.flag-icon-gg { + background-image: url(../flags/4x3/gg.svg); +} +.flag-icon-gg.flag-icon-squared { + background-image: url(../flags/1x1/gg.svg); +} +.flag-icon-gh { + background-image: url(../flags/4x3/gh.svg); +} +.flag-icon-gh.flag-icon-squared { + background-image: url(../flags/1x1/gh.svg); +} +.flag-icon-gi { + background-image: url(../flags/4x3/gi.svg); +} +.flag-icon-gi.flag-icon-squared { + background-image: url(../flags/1x1/gi.svg); +} +.flag-icon-gl { + background-image: url(../flags/4x3/gl.svg); +} +.flag-icon-gl.flag-icon-squared { + background-image: url(../flags/1x1/gl.svg); +} +.flag-icon-gm { + background-image: url(../flags/4x3/gm.svg); +} +.flag-icon-gm.flag-icon-squared { + background-image: url(../flags/1x1/gm.svg); +} +.flag-icon-gn { + background-image: url(../flags/4x3/gn.svg); +} +.flag-icon-gn.flag-icon-squared { + background-image: url(../flags/1x1/gn.svg); +} +.flag-icon-gp { + background-image: url(../flags/4x3/gp.svg); +} +.flag-icon-gp.flag-icon-squared { + background-image: url(../flags/1x1/gp.svg); +} +.flag-icon-gq { + background-image: url(../flags/4x3/gq.svg); +} +.flag-icon-gq.flag-icon-squared { + background-image: url(../flags/1x1/gq.svg); +} +.flag-icon-gr { + background-image: url(../flags/4x3/gr.svg); +} +.flag-icon-gr.flag-icon-squared { + background-image: url(../flags/1x1/gr.svg); +} +.flag-icon-gs { + background-image: url(../flags/4x3/gs.svg); +} +.flag-icon-gs.flag-icon-squared { + background-image: url(../flags/1x1/gs.svg); +} +.flag-icon-gt { + background-image: url(../flags/4x3/gt.svg); +} +.flag-icon-gt.flag-icon-squared { + background-image: url(../flags/1x1/gt.svg); +} +.flag-icon-gu { + background-image: url(../flags/4x3/gu.svg); +} +.flag-icon-gu.flag-icon-squared { + background-image: url(../flags/1x1/gu.svg); +} +.flag-icon-gw { + background-image: url(../flags/4x3/gw.svg); +} +.flag-icon-gw.flag-icon-squared { + background-image: url(../flags/1x1/gw.svg); +} +.flag-icon-gy { + background-image: url(../flags/4x3/gy.svg); +} +.flag-icon-gy.flag-icon-squared { + background-image: url(../flags/1x1/gy.svg); +} +.flag-icon-hk { + background-image: url(../flags/4x3/hk.svg); +} +.flag-icon-hk.flag-icon-squared { + background-image: url(../flags/1x1/hk.svg); +} +.flag-icon-hm { + background-image: url(../flags/4x3/hm.svg); +} +.flag-icon-hm.flag-icon-squared { + background-image: url(../flags/1x1/hm.svg); +} +.flag-icon-hn { + background-image: url(../flags/4x3/hn.svg); +} +.flag-icon-hn.flag-icon-squared { + background-image: url(../flags/1x1/hn.svg); +} +.flag-icon-hr { + background-image: url(../flags/4x3/hr.svg); +} +.flag-icon-hr.flag-icon-squared { + background-image: url(../flags/1x1/hr.svg); +} +.flag-icon-ht { + background-image: url(../flags/4x3/ht.svg); +} +.flag-icon-ht.flag-icon-squared { + background-image: url(../flags/1x1/ht.svg); +} +.flag-icon-hu { + background-image: url(../flags/4x3/hu.svg); +} +.flag-icon-hu.flag-icon-squared { + background-image: url(../flags/1x1/hu.svg); +} +.flag-icon-id { + background-image: url(../flags/4x3/id.svg); +} +.flag-icon-id.flag-icon-squared { + background-image: url(../flags/1x1/id.svg); +} +.flag-icon-ie { + background-image: url(../flags/4x3/ie.svg); +} +.flag-icon-ie.flag-icon-squared { + background-image: url(../flags/1x1/ie.svg); +} +.flag-icon-il { + background-image: url(../flags/4x3/il.svg); +} +.flag-icon-il.flag-icon-squared { + background-image: url(../flags/1x1/il.svg); +} +.flag-icon-im { + background-image: url(../flags/4x3/im.svg); +} +.flag-icon-im.flag-icon-squared { + background-image: url(../flags/1x1/im.svg); +} +.flag-icon-in { + background-image: url(../flags/4x3/in.svg); +} +.flag-icon-in.flag-icon-squared { + background-image: url(../flags/1x1/in.svg); +} +.flag-icon-io { + background-image: url(../flags/4x3/io.svg); +} +.flag-icon-io.flag-icon-squared { + background-image: url(../flags/1x1/io.svg); +} +.flag-icon-iq { + background-image: url(../flags/4x3/iq.svg); +} +.flag-icon-iq.flag-icon-squared { + background-image: url(../flags/1x1/iq.svg); +} +.flag-icon-ir { + background-image: url(../flags/4x3/ir.svg); +} +.flag-icon-ir.flag-icon-squared { + background-image: url(../flags/1x1/ir.svg); +} +.flag-icon-is { + background-image: url(../flags/4x3/is.svg); +} +.flag-icon-is.flag-icon-squared { + background-image: url(../flags/1x1/is.svg); +} +.flag-icon-it { + background-image: url(../flags/4x3/it.svg); +} +.flag-icon-it.flag-icon-squared { + background-image: url(../flags/1x1/it.svg); +} +.flag-icon-je { + background-image: url(../flags/4x3/je.svg); +} +.flag-icon-je.flag-icon-squared { + background-image: url(../flags/1x1/je.svg); +} +.flag-icon-jm { + background-image: url(../flags/4x3/jm.svg); +} +.flag-icon-jm.flag-icon-squared { + background-image: url(../flags/1x1/jm.svg); +} +.flag-icon-jo { + background-image: url(../flags/4x3/jo.svg); +} +.flag-icon-jo.flag-icon-squared { + background-image: url(../flags/1x1/jo.svg); +} +.flag-icon-jp { + background-image: url(../flags/4x3/jp.svg); +} +.flag-icon-jp.flag-icon-squared { + background-image: url(../flags/1x1/jp.svg); +} +.flag-icon-ke { + background-image: url(../flags/4x3/ke.svg); +} +.flag-icon-ke.flag-icon-squared { + background-image: url(../flags/1x1/ke.svg); +} +.flag-icon-kg { + background-image: url(../flags/4x3/kg.svg); +} +.flag-icon-kg.flag-icon-squared { + background-image: url(../flags/1x1/kg.svg); +} +.flag-icon-kh { + background-image: url(../flags/4x3/kh.svg); +} +.flag-icon-kh.flag-icon-squared { + background-image: url(../flags/1x1/kh.svg); +} +.flag-icon-ki { + background-image: url(../flags/4x3/ki.svg); +} +.flag-icon-ki.flag-icon-squared { + background-image: url(../flags/1x1/ki.svg); +} +.flag-icon-km { + background-image: url(../flags/4x3/km.svg); +} +.flag-icon-km.flag-icon-squared { + background-image: url(../flags/1x1/km.svg); +} +.flag-icon-kn { + background-image: url(../flags/4x3/kn.svg); +} +.flag-icon-kn.flag-icon-squared { + background-image: url(../flags/1x1/kn.svg); +} +.flag-icon-kp { + background-image: url(../flags/4x3/kp.svg); +} +.flag-icon-kp.flag-icon-squared { + background-image: url(../flags/1x1/kp.svg); +} +.flag-icon-kr { + background-image: url(../flags/4x3/kr.svg); +} +.flag-icon-kr.flag-icon-squared { + background-image: url(../flags/1x1/kr.svg); +} +.flag-icon-kw { + background-image: url(../flags/4x3/kw.svg); +} +.flag-icon-kw.flag-icon-squared { + background-image: url(../flags/1x1/kw.svg); +} +.flag-icon-ky { + background-image: url(../flags/4x3/ky.svg); +} +.flag-icon-ky.flag-icon-squared { + background-image: url(../flags/1x1/ky.svg); +} +.flag-icon-kz { + background-image: url(../flags/4x3/kz.svg); +} +.flag-icon-kz.flag-icon-squared { + background-image: url(../flags/1x1/kz.svg); +} +.flag-icon-la { + background-image: url(../flags/4x3/la.svg); +} +.flag-icon-la.flag-icon-squared { + background-image: url(../flags/1x1/la.svg); +} +.flag-icon-lb { + background-image: url(../flags/4x3/lb.svg); +} +.flag-icon-lb.flag-icon-squared { + background-image: url(../flags/1x1/lb.svg); +} +.flag-icon-lc { + background-image: url(../flags/4x3/lc.svg); +} +.flag-icon-lc.flag-icon-squared { + background-image: url(../flags/1x1/lc.svg); +} +.flag-icon-li { + background-image: url(../flags/4x3/li.svg); +} +.flag-icon-li.flag-icon-squared { + background-image: url(../flags/1x1/li.svg); +} +.flag-icon-lk { + background-image: url(../flags/4x3/lk.svg); +} +.flag-icon-lk.flag-icon-squared { + background-image: url(../flags/1x1/lk.svg); +} +.flag-icon-lr { + background-image: url(../flags/4x3/lr.svg); +} +.flag-icon-lr.flag-icon-squared { + background-image: url(../flags/1x1/lr.svg); +} +.flag-icon-ls { + background-image: url(../flags/4x3/ls.svg); +} +.flag-icon-ls.flag-icon-squared { + background-image: url(../flags/1x1/ls.svg); +} +.flag-icon-lt { + background-image: url(../flags/4x3/lt.svg); +} +.flag-icon-lt.flag-icon-squared { + background-image: url(../flags/1x1/lt.svg); +} +.flag-icon-lu { + background-image: url(../flags/4x3/lu.svg); +} +.flag-icon-lu.flag-icon-squared { + background-image: url(../flags/1x1/lu.svg); +} +.flag-icon-lv { + background-image: url(../flags/4x3/lv.svg); +} +.flag-icon-lv.flag-icon-squared { + background-image: url(../flags/1x1/lv.svg); +} +.flag-icon-ly { + background-image: url(../flags/4x3/ly.svg); +} +.flag-icon-ly.flag-icon-squared { + background-image: url(../flags/1x1/ly.svg); +} +.flag-icon-ma { + background-image: url(../flags/4x3/ma.svg); +} +.flag-icon-ma.flag-icon-squared { + background-image: url(../flags/1x1/ma.svg); +} +.flag-icon-mc { + background-image: url(../flags/4x3/mc.svg); +} +.flag-icon-mc.flag-icon-squared { + background-image: url(../flags/1x1/mc.svg); +} +.flag-icon-md { + background-image: url(../flags/4x3/md.svg); +} +.flag-icon-md.flag-icon-squared { + background-image: url(../flags/1x1/md.svg); +} +.flag-icon-me { + background-image: url(../flags/4x3/me.svg); +} +.flag-icon-me.flag-icon-squared { + background-image: url(../flags/1x1/me.svg); +} +.flag-icon-mf { + background-image: url(../flags/4x3/mf.svg); +} +.flag-icon-mf.flag-icon-squared { + background-image: url(../flags/1x1/mf.svg); +} +.flag-icon-mg { + background-image: url(../flags/4x3/mg.svg); +} +.flag-icon-mg.flag-icon-squared { + background-image: url(../flags/1x1/mg.svg); +} +.flag-icon-mh { + background-image: url(../flags/4x3/mh.svg); +} +.flag-icon-mh.flag-icon-squared { + background-image: url(../flags/1x1/mh.svg); +} +.flag-icon-mk { + background-image: url(../flags/4x3/mk.svg); +} +.flag-icon-mk.flag-icon-squared { + background-image: url(../flags/1x1/mk.svg); +} +.flag-icon-ml { + background-image: url(../flags/4x3/ml.svg); +} +.flag-icon-ml.flag-icon-squared { + background-image: url(../flags/1x1/ml.svg); +} +.flag-icon-mm { + background-image: url(../flags/4x3/mm.svg); +} +.flag-icon-mm.flag-icon-squared { + background-image: url(../flags/1x1/mm.svg); +} +.flag-icon-mn { + background-image: url(../flags/4x3/mn.svg); +} +.flag-icon-mn.flag-icon-squared { + background-image: url(../flags/1x1/mn.svg); +} +.flag-icon-mo { + background-image: url(../flags/4x3/mo.svg); +} +.flag-icon-mo.flag-icon-squared { + background-image: url(../flags/1x1/mo.svg); +} +.flag-icon-mp { + background-image: url(../flags/4x3/mp.svg); +} +.flag-icon-mp.flag-icon-squared { + background-image: url(../flags/1x1/mp.svg); +} +.flag-icon-mq { + background-image: url(../flags/4x3/mq.svg); +} +.flag-icon-mq.flag-icon-squared { + background-image: url(../flags/1x1/mq.svg); +} +.flag-icon-mr { + background-image: url(../flags/4x3/mr.svg); +} +.flag-icon-mr.flag-icon-squared { + background-image: url(../flags/1x1/mr.svg); +} +.flag-icon-ms { + background-image: url(../flags/4x3/ms.svg); +} +.flag-icon-ms.flag-icon-squared { + background-image: url(../flags/1x1/ms.svg); +} +.flag-icon-mt { + background-image: url(../flags/4x3/mt.svg); +} +.flag-icon-mt.flag-icon-squared { + background-image: url(../flags/1x1/mt.svg); +} +.flag-icon-mu { + background-image: url(../flags/4x3/mu.svg); +} +.flag-icon-mu.flag-icon-squared { + background-image: url(../flags/1x1/mu.svg); +} +.flag-icon-mv { + background-image: url(../flags/4x3/mv.svg); +} +.flag-icon-mv.flag-icon-squared { + background-image: url(../flags/1x1/mv.svg); +} +.flag-icon-mw { + background-image: url(../flags/4x3/mw.svg); +} +.flag-icon-mw.flag-icon-squared { + background-image: url(../flags/1x1/mw.svg); +} +.flag-icon-mx { + background-image: url(../flags/4x3/mx.svg); +} +.flag-icon-mx.flag-icon-squared { + background-image: url(../flags/1x1/mx.svg); +} +.flag-icon-my { + background-image: url(../flags/4x3/my.svg); +} +.flag-icon-my.flag-icon-squared { + background-image: url(../flags/1x1/my.svg); +} +.flag-icon-mz { + background-image: url(../flags/4x3/mz.svg); +} +.flag-icon-mz.flag-icon-squared { + background-image: url(../flags/1x1/mz.svg); +} +.flag-icon-na { + background-image: url(../flags/4x3/na.svg); +} +.flag-icon-na.flag-icon-squared { + background-image: url(../flags/1x1/na.svg); +} +.flag-icon-nc { + background-image: url(../flags/4x3/nc.svg); +} +.flag-icon-nc.flag-icon-squared { + background-image: url(../flags/1x1/nc.svg); +} +.flag-icon-ne { + background-image: url(../flags/4x3/ne.svg); +} +.flag-icon-ne.flag-icon-squared { + background-image: url(../flags/1x1/ne.svg); +} +.flag-icon-nf { + background-image: url(../flags/4x3/nf.svg); +} +.flag-icon-nf.flag-icon-squared { + background-image: url(../flags/1x1/nf.svg); +} +.flag-icon-ng { + background-image: url(../flags/4x3/ng.svg); +} +.flag-icon-ng.flag-icon-squared { + background-image: url(../flags/1x1/ng.svg); +} +.flag-icon-ni { + background-image: url(../flags/4x3/ni.svg); +} +.flag-icon-ni.flag-icon-squared { + background-image: url(../flags/1x1/ni.svg); +} +.flag-icon-nl { + background-image: url(../flags/4x3/nl.svg); +} +.flag-icon-nl.flag-icon-squared { + background-image: url(../flags/1x1/nl.svg); +} +.flag-icon-no { + background-image: url(../flags/4x3/no.svg); +} +.flag-icon-no.flag-icon-squared { + background-image: url(../flags/1x1/no.svg); +} +.flag-icon-np { + background-image: url(../flags/4x3/np.svg); +} +.flag-icon-np.flag-icon-squared { + background-image: url(../flags/1x1/np.svg); +} +.flag-icon-nr { + background-image: url(../flags/4x3/nr.svg); +} +.flag-icon-nr.flag-icon-squared { + background-image: url(../flags/1x1/nr.svg); +} +.flag-icon-nu { + background-image: url(../flags/4x3/nu.svg); +} +.flag-icon-nu.flag-icon-squared { + background-image: url(../flags/1x1/nu.svg); +} +.flag-icon-nz { + background-image: url(../flags/4x3/nz.svg); +} +.flag-icon-nz.flag-icon-squared { + background-image: url(../flags/1x1/nz.svg); +} +.flag-icon-om { + background-image: url(../flags/4x3/om.svg); +} +.flag-icon-om.flag-icon-squared { + background-image: url(../flags/1x1/om.svg); +} +.flag-icon-pa { + background-image: url(../flags/4x3/pa.svg); +} +.flag-icon-pa.flag-icon-squared { + background-image: url(../flags/1x1/pa.svg); +} +.flag-icon-pe { + background-image: url(../flags/4x3/pe.svg); +} +.flag-icon-pe.flag-icon-squared { + background-image: url(../flags/1x1/pe.svg); +} +.flag-icon-pf { + background-image: url(../flags/4x3/pf.svg); +} +.flag-icon-pf.flag-icon-squared { + background-image: url(../flags/1x1/pf.svg); +} +.flag-icon-pg { + background-image: url(../flags/4x3/pg.svg); +} +.flag-icon-pg.flag-icon-squared { + background-image: url(../flags/1x1/pg.svg); +} +.flag-icon-ph { + background-image: url(../flags/4x3/ph.svg); +} +.flag-icon-ph.flag-icon-squared { + background-image: url(../flags/1x1/ph.svg); +} +.flag-icon-pk { + background-image: url(../flags/4x3/pk.svg); +} +.flag-icon-pk.flag-icon-squared { + background-image: url(../flags/1x1/pk.svg); +} +.flag-icon-pl { + background-image: url(../flags/4x3/pl.svg); +} +.flag-icon-pl.flag-icon-squared { + background-image: url(../flags/1x1/pl.svg); +} +.flag-icon-pm { + background-image: url(../flags/4x3/pm.svg); +} +.flag-icon-pm.flag-icon-squared { + background-image: url(../flags/1x1/pm.svg); +} +.flag-icon-pn { + background-image: url(../flags/4x3/pn.svg); +} +.flag-icon-pn.flag-icon-squared { + background-image: url(../flags/1x1/pn.svg); +} +.flag-icon-pr { + background-image: url(../flags/4x3/pr.svg); +} +.flag-icon-pr.flag-icon-squared { + background-image: url(../flags/1x1/pr.svg); +} +.flag-icon-ps { + background-image: url(../flags/4x3/ps.svg); +} +.flag-icon-ps.flag-icon-squared { + background-image: url(../flags/1x1/ps.svg); +} +.flag-icon-pt { + background-image: url(../flags/4x3/pt.svg); +} +.flag-icon-pt.flag-icon-squared { + background-image: url(../flags/1x1/pt.svg); +} +.flag-icon-pw { + background-image: url(../flags/4x3/pw.svg); +} +.flag-icon-pw.flag-icon-squared { + background-image: url(../flags/1x1/pw.svg); +} +.flag-icon-py { + background-image: url(../flags/4x3/py.svg); +} +.flag-icon-py.flag-icon-squared { + background-image: url(../flags/1x1/py.svg); +} +.flag-icon-qa { + background-image: url(../flags/4x3/qa.svg); +} +.flag-icon-qa.flag-icon-squared { + background-image: url(../flags/1x1/qa.svg); +} +.flag-icon-re { + background-image: url(../flags/4x3/re.svg); +} +.flag-icon-re.flag-icon-squared { + background-image: url(../flags/1x1/re.svg); +} +.flag-icon-ro { + background-image: url(../flags/4x3/ro.svg); +} +.flag-icon-ro.flag-icon-squared { + background-image: url(../flags/1x1/ro.svg); +} +.flag-icon-rs { + background-image: url(../flags/4x3/rs.svg); +} +.flag-icon-rs.flag-icon-squared { + background-image: url(../flags/1x1/rs.svg); +} +.flag-icon-ru { + background-image: url(../flags/4x3/ru.svg); +} +.flag-icon-ru.flag-icon-squared { + background-image: url(../flags/1x1/ru.svg); +} +.flag-icon-rw { + background-image: url(../flags/4x3/rw.svg); +} +.flag-icon-rw.flag-icon-squared { + background-image: url(../flags/1x1/rw.svg); +} +.flag-icon-sa { + background-image: url(../flags/4x3/sa.svg); +} +.flag-icon-sa.flag-icon-squared { + background-image: url(../flags/1x1/sa.svg); +} +.flag-icon-sb { + background-image: url(../flags/4x3/sb.svg); +} +.flag-icon-sb.flag-icon-squared { + background-image: url(../flags/1x1/sb.svg); +} +.flag-icon-sc { + background-image: url(../flags/4x3/sc.svg); +} +.flag-icon-sc.flag-icon-squared { + background-image: url(../flags/1x1/sc.svg); +} +.flag-icon-sd { + background-image: url(../flags/4x3/sd.svg); +} +.flag-icon-sd.flag-icon-squared { + background-image: url(../flags/1x1/sd.svg); +} +.flag-icon-se { + background-image: url(../flags/4x3/se.svg); +} +.flag-icon-se.flag-icon-squared { + background-image: url(../flags/1x1/se.svg); +} +.flag-icon-sg { + background-image: url(../flags/4x3/sg.svg); +} +.flag-icon-sg.flag-icon-squared { + background-image: url(../flags/1x1/sg.svg); +} +.flag-icon-sh { + background-image: url(../flags/4x3/sh.svg); +} +.flag-icon-sh.flag-icon-squared { + background-image: url(../flags/1x1/sh.svg); +} +.flag-icon-si { + background-image: url(../flags/4x3/si.svg); +} +.flag-icon-si.flag-icon-squared { + background-image: url(../flags/1x1/si.svg); +} +.flag-icon-sj { + background-image: url(../flags/4x3/sj.svg); +} +.flag-icon-sj.flag-icon-squared { + background-image: url(../flags/1x1/sj.svg); +} +.flag-icon-sk { + background-image: url(../flags/4x3/sk.svg); +} +.flag-icon-sk.flag-icon-squared { + background-image: url(../flags/1x1/sk.svg); +} +.flag-icon-sl { + background-image: url(../flags/4x3/sl.svg); +} +.flag-icon-sl.flag-icon-squared { + background-image: url(../flags/1x1/sl.svg); +} +.flag-icon-sm { + background-image: url(../flags/4x3/sm.svg); +} +.flag-icon-sm.flag-icon-squared { + background-image: url(../flags/1x1/sm.svg); +} +.flag-icon-sn { + background-image: url(../flags/4x3/sn.svg); +} +.flag-icon-sn.flag-icon-squared { + background-image: url(../flags/1x1/sn.svg); +} +.flag-icon-so { + background-image: url(../flags/4x3/so.svg); +} +.flag-icon-so.flag-icon-squared { + background-image: url(../flags/1x1/so.svg); +} +.flag-icon-sr { + background-image: url(../flags/4x3/sr.svg); +} +.flag-icon-sr.flag-icon-squared { + background-image: url(../flags/1x1/sr.svg); +} +.flag-icon-ss { + background-image: url(../flags/4x3/ss.svg); +} +.flag-icon-ss.flag-icon-squared { + background-image: url(../flags/1x1/ss.svg); +} +.flag-icon-st { + background-image: url(../flags/4x3/st.svg); +} +.flag-icon-st.flag-icon-squared { + background-image: url(../flags/1x1/st.svg); +} +.flag-icon-sv { + background-image: url(../flags/4x3/sv.svg); +} +.flag-icon-sv.flag-icon-squared { + background-image: url(../flags/1x1/sv.svg); +} +.flag-icon-sx { + background-image: url(../flags/4x3/sx.svg); +} +.flag-icon-sx.flag-icon-squared { + background-image: url(../flags/1x1/sx.svg); +} +.flag-icon-sy { + background-image: url(../flags/4x3/sy.svg); +} +.flag-icon-sy.flag-icon-squared { + background-image: url(../flags/1x1/sy.svg); +} +.flag-icon-sz { + background-image: url(../flags/4x3/sz.svg); +} +.flag-icon-sz.flag-icon-squared { + background-image: url(../flags/1x1/sz.svg); +} +.flag-icon-tc { + background-image: url(../flags/4x3/tc.svg); +} +.flag-icon-tc.flag-icon-squared { + background-image: url(../flags/1x1/tc.svg); +} +.flag-icon-td { + background-image: url(../flags/4x3/td.svg); +} +.flag-icon-td.flag-icon-squared { + background-image: url(../flags/1x1/td.svg); +} +.flag-icon-tf { + background-image: url(../flags/4x3/tf.svg); +} +.flag-icon-tf.flag-icon-squared { + background-image: url(../flags/1x1/tf.svg); +} +.flag-icon-tg { + background-image: url(../flags/4x3/tg.svg); +} +.flag-icon-tg.flag-icon-squared { + background-image: url(../flags/1x1/tg.svg); +} +.flag-icon-th { + background-image: url(../flags/4x3/th.svg); +} +.flag-icon-th.flag-icon-squared { + background-image: url(../flags/1x1/th.svg); +} +.flag-icon-tj { + background-image: url(../flags/4x3/tj.svg); +} +.flag-icon-tj.flag-icon-squared { + background-image: url(../flags/1x1/tj.svg); +} +.flag-icon-tk { + background-image: url(../flags/4x3/tk.svg); +} +.flag-icon-tk.flag-icon-squared { + background-image: url(../flags/1x1/tk.svg); +} +.flag-icon-tl { + background-image: url(../flags/4x3/tl.svg); +} +.flag-icon-tl.flag-icon-squared { + background-image: url(../flags/1x1/tl.svg); +} +.flag-icon-tm { + background-image: url(../flags/4x3/tm.svg); +} +.flag-icon-tm.flag-icon-squared { + background-image: url(../flags/1x1/tm.svg); +} +.flag-icon-tn { + background-image: url(../flags/4x3/tn.svg); +} +.flag-icon-tn.flag-icon-squared { + background-image: url(../flags/1x1/tn.svg); +} +.flag-icon-to { + background-image: url(../flags/4x3/to.svg); +} +.flag-icon-to.flag-icon-squared { + background-image: url(../flags/1x1/to.svg); +} +.flag-icon-tr { + background-image: url(../flags/4x3/tr.svg); +} +.flag-icon-tr.flag-icon-squared { + background-image: url(../flags/1x1/tr.svg); +} +.flag-icon-tt { + background-image: url(../flags/4x3/tt.svg); +} +.flag-icon-tt.flag-icon-squared { + background-image: url(../flags/1x1/tt.svg); +} +.flag-icon-tv { + background-image: url(../flags/4x3/tv.svg); +} +.flag-icon-tv.flag-icon-squared { + background-image: url(../flags/1x1/tv.svg); +} +.flag-icon-tw { + background-image: url(../flags/4x3/tw.svg); +} +.flag-icon-tw.flag-icon-squared { + background-image: url(../flags/1x1/tw.svg); +} +.flag-icon-tz { + background-image: url(../flags/4x3/tz.svg); +} +.flag-icon-tz.flag-icon-squared { + background-image: url(../flags/1x1/tz.svg); +} +.flag-icon-ua { + background-image: url(../flags/4x3/ua.svg); +} +.flag-icon-ua.flag-icon-squared { + background-image: url(../flags/1x1/ua.svg); +} +.flag-icon-ug { + background-image: url(../flags/4x3/ug.svg); +} +.flag-icon-ug.flag-icon-squared { + background-image: url(../flags/1x1/ug.svg); +} +.flag-icon-um { + background-image: url(../flags/4x3/um.svg); +} +.flag-icon-um.flag-icon-squared { + background-image: url(../flags/1x1/um.svg); +} +.flag-icon-us { + background-image: url(../flags/4x3/us.svg); +} +.flag-icon-us.flag-icon-squared { + background-image: url(../flags/1x1/us.svg); +} +.flag-icon-uy { + background-image: url(../flags/4x3/uy.svg); +} +.flag-icon-uy.flag-icon-squared { + background-image: url(../flags/1x1/uy.svg); +} +.flag-icon-uz { + background-image: url(../flags/4x3/uz.svg); +} +.flag-icon-uz.flag-icon-squared { + background-image: url(../flags/1x1/uz.svg); +} +.flag-icon-va { + background-image: url(../flags/4x3/va.svg); +} +.flag-icon-va.flag-icon-squared { + background-image: url(../flags/1x1/va.svg); +} +.flag-icon-vc { + background-image: url(../flags/4x3/vc.svg); +} +.flag-icon-vc.flag-icon-squared { + background-image: url(../flags/1x1/vc.svg); +} +.flag-icon-ve { + background-image: url(../flags/4x3/ve.svg); +} +.flag-icon-ve.flag-icon-squared { + background-image: url(../flags/1x1/ve.svg); +} +.flag-icon-vg { + background-image: url(../flags/4x3/vg.svg); +} +.flag-icon-vg.flag-icon-squared { + background-image: url(../flags/1x1/vg.svg); +} +.flag-icon-vi { + background-image: url(../flags/4x3/vi.svg); +} +.flag-icon-vi.flag-icon-squared { + background-image: url(../flags/1x1/vi.svg); +} +.flag-icon-vn { + background-image: url(../flags/4x3/vn.svg); +} +.flag-icon-vn.flag-icon-squared { + background-image: url(../flags/1x1/vn.svg); +} +.flag-icon-vu { + background-image: url(../flags/4x3/vu.svg); +} +.flag-icon-vu.flag-icon-squared { + background-image: url(../flags/1x1/vu.svg); +} +.flag-icon-wf { + background-image: url(../flags/4x3/wf.svg); +} +.flag-icon-wf.flag-icon-squared { + background-image: url(../flags/1x1/wf.svg); +} +.flag-icon-ws { + background-image: url(../flags/4x3/ws.svg); +} +.flag-icon-ws.flag-icon-squared { + background-image: url(../flags/1x1/ws.svg); +} +.flag-icon-ye { + background-image: url(../flags/4x3/ye.svg); +} +.flag-icon-ye.flag-icon-squared { + background-image: url(../flags/1x1/ye.svg); +} +.flag-icon-yt { + background-image: url(../flags/4x3/yt.svg); +} +.flag-icon-yt.flag-icon-squared { + background-image: url(../flags/1x1/yt.svg); +} +.flag-icon-za { + background-image: url(../flags/4x3/za.svg); +} +.flag-icon-za.flag-icon-squared { + background-image: url(../flags/1x1/za.svg); +} +.flag-icon-zm { + background-image: url(../flags/4x3/zm.svg); +} +.flag-icon-zm.flag-icon-squared { + background-image: url(../flags/1x1/zm.svg); +} +.flag-icon-zw { + background-image: url(../flags/4x3/zw.svg); +} +.flag-icon-zw.flag-icon-squared { + background-image: url(../flags/1x1/zw.svg); +} +.flag-icon-es-ca { + background-image: url(../flags/4x3/es-ca.svg); +} +.flag-icon-es-ca.flag-icon-squared { + background-image: url(../flags/1x1/es-ca.svg); +} +.flag-icon-es-ga { + background-image: url(../flags/4x3/es-ga.svg); +} +.flag-icon-es-ga.flag-icon-squared { + background-image: url(../flags/1x1/es-ga.svg); +} +.flag-icon-eu { + background-image: url(../flags/4x3/eu.svg); +} +.flag-icon-eu.flag-icon-squared { + background-image: url(../flags/1x1/eu.svg); +} +.flag-icon-gb-eng { + background-image: url(../flags/4x3/gb-eng.svg); +} +.flag-icon-gb-eng.flag-icon-squared { + background-image: url(../flags/1x1/gb-eng.svg); +} +.flag-icon-gb-nir { + background-image: url(../flags/4x3/gb-nir.svg); +} +.flag-icon-gb-nir.flag-icon-squared { + background-image: url(../flags/1x1/gb-nir.svg); +} +.flag-icon-gb-sct { + background-image: url(../flags/4x3/gb-sct.svg); +} +.flag-icon-gb-sct.flag-icon-squared { + background-image: url(../flags/1x1/gb-sct.svg); +} +.flag-icon-gb-wls { + background-image: url(../flags/4x3/gb-wls.svg); +} +.flag-icon-gb-wls.flag-icon-squared { + background-image: url(../flags/1x1/gb-wls.svg); +} +.flag-icon-un { + background-image: url(../flags/4x3/un.svg); +} +.flag-icon-un.flag-icon-squared { + background-image: url(../flags/1x1/un.svg); +} +.flag-icon-xk { + background-image: url(../flags/4x3/xk.svg); +} +.flag-icon-xk.flag-icon-squared { + background-image: url(../flags/1x1/xk.svg); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.min.css b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.min.css new file mode 100644 index 0000000000..3d049b3953 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/css/flag-icon.min.css @@ -0,0 +1 @@ +.flag-icon-background{background-size:contain;background-position:50%;background-repeat:no-repeat}.flag-icon{background-size:contain;background-position:50%;background-repeat:no-repeat;position:relative;display:inline-block;width:1.33333333em;line-height:1em}.flag-icon:before{content:'\00a0'}.flag-icon.flag-icon-squared{width:1em}.flag-icon-ad{background-image:url(../flags/4x3/ad.svg)}.flag-icon-ad.flag-icon-squared{background-image:url(../flags/1x1/ad.svg)}.flag-icon-ae{background-image:url(../flags/4x3/ae.svg)}.flag-icon-ae.flag-icon-squared{background-image:url(../flags/1x1/ae.svg)}.flag-icon-af{background-image:url(../flags/4x3/af.svg)}.flag-icon-af.flag-icon-squared{background-image:url(../flags/1x1/af.svg)}.flag-icon-ag{background-image:url(../flags/4x3/ag.svg)}.flag-icon-ag.flag-icon-squared{background-image:url(../flags/1x1/ag.svg)}.flag-icon-ai{background-image:url(../flags/4x3/ai.svg)}.flag-icon-ai.flag-icon-squared{background-image:url(../flags/1x1/ai.svg)}.flag-icon-al{background-image:url(../flags/4x3/al.svg)}.flag-icon-al.flag-icon-squared{background-image:url(../flags/1x1/al.svg)}.flag-icon-am{background-image:url(../flags/4x3/am.svg)}.flag-icon-am.flag-icon-squared{background-image:url(../flags/1x1/am.svg)}.flag-icon-ao{background-image:url(../flags/4x3/ao.svg)}.flag-icon-ao.flag-icon-squared{background-image:url(../flags/1x1/ao.svg)}.flag-icon-aq{background-image:url(../flags/4x3/aq.svg)}.flag-icon-aq.flag-icon-squared{background-image:url(../flags/1x1/aq.svg)}.flag-icon-ar{background-image:url(../flags/4x3/ar.svg)}.flag-icon-ar.flag-icon-squared{background-image:url(../flags/1x1/ar.svg)}.flag-icon-as{background-image:url(../flags/4x3/as.svg)}.flag-icon-as.flag-icon-squared{background-image:url(../flags/1x1/as.svg)}.flag-icon-at{background-image:url(../flags/4x3/at.svg)}.flag-icon-at.flag-icon-squared{background-image:url(../flags/1x1/at.svg)}.flag-icon-au{background-image:url(../flags/4x3/au.svg)}.flag-icon-au.flag-icon-squared{background-image:url(../flags/1x1/au.svg)}.flag-icon-aw{background-image:url(../flags/4x3/aw.svg)}.flag-icon-aw.flag-icon-squared{background-image:url(../flags/1x1/aw.svg)}.flag-icon-ax{background-image:url(../flags/4x3/ax.svg)}.flag-icon-ax.flag-icon-squared{background-image:url(../flags/1x1/ax.svg)}.flag-icon-az{background-image:url(../flags/4x3/az.svg)}.flag-icon-az.flag-icon-squared{background-image:url(../flags/1x1/az.svg)}.flag-icon-ba{background-image:url(../flags/4x3/ba.svg)}.flag-icon-ba.flag-icon-squared{background-image:url(../flags/1x1/ba.svg)}.flag-icon-bb{background-image:url(../flags/4x3/bb.svg)}.flag-icon-bb.flag-icon-squared{background-image:url(../flags/1x1/bb.svg)}.flag-icon-bd{background-image:url(../flags/4x3/bd.svg)}.flag-icon-bd.flag-icon-squared{background-image:url(../flags/1x1/bd.svg)}.flag-icon-be{background-image:url(../flags/4x3/be.svg)}.flag-icon-be.flag-icon-squared{background-image:url(../flags/1x1/be.svg)}.flag-icon-bf{background-image:url(../flags/4x3/bf.svg)}.flag-icon-bf.flag-icon-squared{background-image:url(../flags/1x1/bf.svg)}.flag-icon-bg{background-image:url(../flags/4x3/bg.svg)}.flag-icon-bg.flag-icon-squared{background-image:url(../flags/1x1/bg.svg)}.flag-icon-bh{background-image:url(../flags/4x3/bh.svg)}.flag-icon-bh.flag-icon-squared{background-image:url(../flags/1x1/bh.svg)}.flag-icon-bi{background-image:url(../flags/4x3/bi.svg)}.flag-icon-bi.flag-icon-squared{background-image:url(../flags/1x1/bi.svg)}.flag-icon-bj{background-image:url(../flags/4x3/bj.svg)}.flag-icon-bj.flag-icon-squared{background-image:url(../flags/1x1/bj.svg)}.flag-icon-bl{background-image:url(../flags/4x3/bl.svg)}.flag-icon-bl.flag-icon-squared{background-image:url(../flags/1x1/bl.svg)}.flag-icon-bm{background-image:url(../flags/4x3/bm.svg)}.flag-icon-bm.flag-icon-squared{background-image:url(../flags/1x1/bm.svg)}.flag-icon-bn{background-image:url(../flags/4x3/bn.svg)}.flag-icon-bn.flag-icon-squared{background-image:url(../flags/1x1/bn.svg)}.flag-icon-bo{background-image:url(../flags/4x3/bo.svg)}.flag-icon-bo.flag-icon-squared{background-image:url(../flags/1x1/bo.svg)}.flag-icon-bq{background-image:url(../flags/4x3/bq.svg)}.flag-icon-bq.flag-icon-squared{background-image:url(../flags/1x1/bq.svg)}.flag-icon-br{background-image:url(../flags/4x3/br.svg)}.flag-icon-br.flag-icon-squared{background-image:url(../flags/1x1/br.svg)}.flag-icon-bs{background-image:url(../flags/4x3/bs.svg)}.flag-icon-bs.flag-icon-squared{background-image:url(../flags/1x1/bs.svg)}.flag-icon-bt{background-image:url(../flags/4x3/bt.svg)}.flag-icon-bt.flag-icon-squared{background-image:url(../flags/1x1/bt.svg)}.flag-icon-bv{background-image:url(../flags/4x3/bv.svg)}.flag-icon-bv.flag-icon-squared{background-image:url(../flags/1x1/bv.svg)}.flag-icon-bw{background-image:url(../flags/4x3/bw.svg)}.flag-icon-bw.flag-icon-squared{background-image:url(../flags/1x1/bw.svg)}.flag-icon-by{background-image:url(../flags/4x3/by.svg)}.flag-icon-by.flag-icon-squared{background-image:url(../flags/1x1/by.svg)}.flag-icon-bz{background-image:url(../flags/4x3/bz.svg)}.flag-icon-bz.flag-icon-squared{background-image:url(../flags/1x1/bz.svg)}.flag-icon-ca{background-image:url(../flags/4x3/ca.svg)}.flag-icon-ca.flag-icon-squared{background-image:url(../flags/1x1/ca.svg)}.flag-icon-cc{background-image:url(../flags/4x3/cc.svg)}.flag-icon-cc.flag-icon-squared{background-image:url(../flags/1x1/cc.svg)}.flag-icon-cd{background-image:url(../flags/4x3/cd.svg)}.flag-icon-cd.flag-icon-squared{background-image:url(../flags/1x1/cd.svg)}.flag-icon-cf{background-image:url(../flags/4x3/cf.svg)}.flag-icon-cf.flag-icon-squared{background-image:url(../flags/1x1/cf.svg)}.flag-icon-cg{background-image:url(../flags/4x3/cg.svg)}.flag-icon-cg.flag-icon-squared{background-image:url(../flags/1x1/cg.svg)}.flag-icon-ch{background-image:url(../flags/4x3/ch.svg)}.flag-icon-ch.flag-icon-squared{background-image:url(../flags/1x1/ch.svg)}.flag-icon-ci{background-image:url(../flags/4x3/ci.svg)}.flag-icon-ci.flag-icon-squared{background-image:url(../flags/1x1/ci.svg)}.flag-icon-ck{background-image:url(../flags/4x3/ck.svg)}.flag-icon-ck.flag-icon-squared{background-image:url(../flags/1x1/ck.svg)}.flag-icon-cl{background-image:url(../flags/4x3/cl.svg)}.flag-icon-cl.flag-icon-squared{background-image:url(../flags/1x1/cl.svg)}.flag-icon-cm{background-image:url(../flags/4x3/cm.svg)}.flag-icon-cm.flag-icon-squared{background-image:url(../flags/1x1/cm.svg)}.flag-icon-cn{background-image:url(../flags/4x3/cn.svg)}.flag-icon-cn.flag-icon-squared{background-image:url(../flags/1x1/cn.svg)}.flag-icon-co{background-image:url(../flags/4x3/co.svg)}.flag-icon-co.flag-icon-squared{background-image:url(../flags/1x1/co.svg)}.flag-icon-cr{background-image:url(../flags/4x3/cr.svg)}.flag-icon-cr.flag-icon-squared{background-image:url(../flags/1x1/cr.svg)}.flag-icon-cu{background-image:url(../flags/4x3/cu.svg)}.flag-icon-cu.flag-icon-squared{background-image:url(../flags/1x1/cu.svg)}.flag-icon-cv{background-image:url(../flags/4x3/cv.svg)}.flag-icon-cv.flag-icon-squared{background-image:url(../flags/1x1/cv.svg)}.flag-icon-cw{background-image:url(../flags/4x3/cw.svg)}.flag-icon-cw.flag-icon-squared{background-image:url(../flags/1x1/cw.svg)}.flag-icon-cx{background-image:url(../flags/4x3/cx.svg)}.flag-icon-cx.flag-icon-squared{background-image:url(../flags/1x1/cx.svg)}.flag-icon-cy{background-image:url(../flags/4x3/cy.svg)}.flag-icon-cy.flag-icon-squared{background-image:url(../flags/1x1/cy.svg)}.flag-icon-cz{background-image:url(../flags/4x3/cz.svg)}.flag-icon-cz.flag-icon-squared{background-image:url(../flags/1x1/cz.svg)}.flag-icon-de{background-image:url(../flags/4x3/de.svg)}.flag-icon-de.flag-icon-squared{background-image:url(../flags/1x1/de.svg)}.flag-icon-dj{background-image:url(../flags/4x3/dj.svg)}.flag-icon-dj.flag-icon-squared{background-image:url(../flags/1x1/dj.svg)}.flag-icon-dk{background-image:url(../flags/4x3/dk.svg)}.flag-icon-dk.flag-icon-squared{background-image:url(../flags/1x1/dk.svg)}.flag-icon-dm{background-image:url(../flags/4x3/dm.svg)}.flag-icon-dm.flag-icon-squared{background-image:url(../flags/1x1/dm.svg)}.flag-icon-do{background-image:url(../flags/4x3/do.svg)}.flag-icon-do.flag-icon-squared{background-image:url(../flags/1x1/do.svg)}.flag-icon-dz{background-image:url(../flags/4x3/dz.svg)}.flag-icon-dz.flag-icon-squared{background-image:url(../flags/1x1/dz.svg)}.flag-icon-ec{background-image:url(../flags/4x3/ec.svg)}.flag-icon-ec.flag-icon-squared{background-image:url(../flags/1x1/ec.svg)}.flag-icon-ee{background-image:url(../flags/4x3/ee.svg)}.flag-icon-ee.flag-icon-squared{background-image:url(../flags/1x1/ee.svg)}.flag-icon-eg{background-image:url(../flags/4x3/eg.svg)}.flag-icon-eg.flag-icon-squared{background-image:url(../flags/1x1/eg.svg)}.flag-icon-eh{background-image:url(../flags/4x3/eh.svg)}.flag-icon-eh.flag-icon-squared{background-image:url(../flags/1x1/eh.svg)}.flag-icon-er{background-image:url(../flags/4x3/er.svg)}.flag-icon-er.flag-icon-squared{background-image:url(../flags/1x1/er.svg)}.flag-icon-es{background-image:url(../flags/4x3/es.svg)}.flag-icon-es.flag-icon-squared{background-image:url(../flags/1x1/es.svg)}.flag-icon-et{background-image:url(../flags/4x3/et.svg)}.flag-icon-et.flag-icon-squared{background-image:url(../flags/1x1/et.svg)}.flag-icon-fi{background-image:url(../flags/4x3/fi.svg)}.flag-icon-fi.flag-icon-squared{background-image:url(../flags/1x1/fi.svg)}.flag-icon-fj{background-image:url(../flags/4x3/fj.svg)}.flag-icon-fj.flag-icon-squared{background-image:url(../flags/1x1/fj.svg)}.flag-icon-fk{background-image:url(../flags/4x3/fk.svg)}.flag-icon-fk.flag-icon-squared{background-image:url(../flags/1x1/fk.svg)}.flag-icon-fm{background-image:url(../flags/4x3/fm.svg)}.flag-icon-fm.flag-icon-squared{background-image:url(../flags/1x1/fm.svg)}.flag-icon-fo{background-image:url(../flags/4x3/fo.svg)}.flag-icon-fo.flag-icon-squared{background-image:url(../flags/1x1/fo.svg)}.flag-icon-fr{background-image:url(../flags/4x3/fr.svg)}.flag-icon-fr.flag-icon-squared{background-image:url(../flags/1x1/fr.svg)}.flag-icon-ga{background-image:url(../flags/4x3/ga.svg)}.flag-icon-ga.flag-icon-squared{background-image:url(../flags/1x1/ga.svg)}.flag-icon-gb{background-image:url(../flags/4x3/gb.svg)}.flag-icon-gb.flag-icon-squared{background-image:url(../flags/1x1/gb.svg)}.flag-icon-gd{background-image:url(../flags/4x3/gd.svg)}.flag-icon-gd.flag-icon-squared{background-image:url(../flags/1x1/gd.svg)}.flag-icon-ge{background-image:url(../flags/4x3/ge.svg)}.flag-icon-ge.flag-icon-squared{background-image:url(../flags/1x1/ge.svg)}.flag-icon-gf{background-image:url(../flags/4x3/gf.svg)}.flag-icon-gf.flag-icon-squared{background-image:url(../flags/1x1/gf.svg)}.flag-icon-gg{background-image:url(../flags/4x3/gg.svg)}.flag-icon-gg.flag-icon-squared{background-image:url(../flags/1x1/gg.svg)}.flag-icon-gh{background-image:url(../flags/4x3/gh.svg)}.flag-icon-gh.flag-icon-squared{background-image:url(../flags/1x1/gh.svg)}.flag-icon-gi{background-image:url(../flags/4x3/gi.svg)}.flag-icon-gi.flag-icon-squared{background-image:url(../flags/1x1/gi.svg)}.flag-icon-gl{background-image:url(../flags/4x3/gl.svg)}.flag-icon-gl.flag-icon-squared{background-image:url(../flags/1x1/gl.svg)}.flag-icon-gm{background-image:url(../flags/4x3/gm.svg)}.flag-icon-gm.flag-icon-squared{background-image:url(../flags/1x1/gm.svg)}.flag-icon-gn{background-image:url(../flags/4x3/gn.svg)}.flag-icon-gn.flag-icon-squared{background-image:url(../flags/1x1/gn.svg)}.flag-icon-gp{background-image:url(../flags/4x3/gp.svg)}.flag-icon-gp.flag-icon-squared{background-image:url(../flags/1x1/gp.svg)}.flag-icon-gq{background-image:url(../flags/4x3/gq.svg)}.flag-icon-gq.flag-icon-squared{background-image:url(../flags/1x1/gq.svg)}.flag-icon-gr{background-image:url(../flags/4x3/gr.svg)}.flag-icon-gr.flag-icon-squared{background-image:url(../flags/1x1/gr.svg)}.flag-icon-gs{background-image:url(../flags/4x3/gs.svg)}.flag-icon-gs.flag-icon-squared{background-image:url(../flags/1x1/gs.svg)}.flag-icon-gt{background-image:url(../flags/4x3/gt.svg)}.flag-icon-gt.flag-icon-squared{background-image:url(../flags/1x1/gt.svg)}.flag-icon-gu{background-image:url(../flags/4x3/gu.svg)}.flag-icon-gu.flag-icon-squared{background-image:url(../flags/1x1/gu.svg)}.flag-icon-gw{background-image:url(../flags/4x3/gw.svg)}.flag-icon-gw.flag-icon-squared{background-image:url(../flags/1x1/gw.svg)}.flag-icon-gy{background-image:url(../flags/4x3/gy.svg)}.flag-icon-gy.flag-icon-squared{background-image:url(../flags/1x1/gy.svg)}.flag-icon-hk{background-image:url(../flags/4x3/hk.svg)}.flag-icon-hk.flag-icon-squared{background-image:url(../flags/1x1/hk.svg)}.flag-icon-hm{background-image:url(../flags/4x3/hm.svg)}.flag-icon-hm.flag-icon-squared{background-image:url(../flags/1x1/hm.svg)}.flag-icon-hn{background-image:url(../flags/4x3/hn.svg)}.flag-icon-hn.flag-icon-squared{background-image:url(../flags/1x1/hn.svg)}.flag-icon-hr{background-image:url(../flags/4x3/hr.svg)}.flag-icon-hr.flag-icon-squared{background-image:url(../flags/1x1/hr.svg)}.flag-icon-ht{background-image:url(../flags/4x3/ht.svg)}.flag-icon-ht.flag-icon-squared{background-image:url(../flags/1x1/ht.svg)}.flag-icon-hu{background-image:url(../flags/4x3/hu.svg)}.flag-icon-hu.flag-icon-squared{background-image:url(../flags/1x1/hu.svg)}.flag-icon-id{background-image:url(../flags/4x3/id.svg)}.flag-icon-id.flag-icon-squared{background-image:url(../flags/1x1/id.svg)}.flag-icon-ie{background-image:url(../flags/4x3/ie.svg)}.flag-icon-ie.flag-icon-squared{background-image:url(../flags/1x1/ie.svg)}.flag-icon-il{background-image:url(../flags/4x3/il.svg)}.flag-icon-il.flag-icon-squared{background-image:url(../flags/1x1/il.svg)}.flag-icon-im{background-image:url(../flags/4x3/im.svg)}.flag-icon-im.flag-icon-squared{background-image:url(../flags/1x1/im.svg)}.flag-icon-in{background-image:url(../flags/4x3/in.svg)}.flag-icon-in.flag-icon-squared{background-image:url(../flags/1x1/in.svg)}.flag-icon-io{background-image:url(../flags/4x3/io.svg)}.flag-icon-io.flag-icon-squared{background-image:url(../flags/1x1/io.svg)}.flag-icon-iq{background-image:url(../flags/4x3/iq.svg)}.flag-icon-iq.flag-icon-squared{background-image:url(../flags/1x1/iq.svg)}.flag-icon-ir{background-image:url(../flags/4x3/ir.svg)}.flag-icon-ir.flag-icon-squared{background-image:url(../flags/1x1/ir.svg)}.flag-icon-is{background-image:url(../flags/4x3/is.svg)}.flag-icon-is.flag-icon-squared{background-image:url(../flags/1x1/is.svg)}.flag-icon-it{background-image:url(../flags/4x3/it.svg)}.flag-icon-it.flag-icon-squared{background-image:url(../flags/1x1/it.svg)}.flag-icon-je{background-image:url(../flags/4x3/je.svg)}.flag-icon-je.flag-icon-squared{background-image:url(../flags/1x1/je.svg)}.flag-icon-jm{background-image:url(../flags/4x3/jm.svg)}.flag-icon-jm.flag-icon-squared{background-image:url(../flags/1x1/jm.svg)}.flag-icon-jo{background-image:url(../flags/4x3/jo.svg)}.flag-icon-jo.flag-icon-squared{background-image:url(../flags/1x1/jo.svg)}.flag-icon-jp{background-image:url(../flags/4x3/jp.svg)}.flag-icon-jp.flag-icon-squared{background-image:url(../flags/1x1/jp.svg)}.flag-icon-ke{background-image:url(../flags/4x3/ke.svg)}.flag-icon-ke.flag-icon-squared{background-image:url(../flags/1x1/ke.svg)}.flag-icon-kg{background-image:url(../flags/4x3/kg.svg)}.flag-icon-kg.flag-icon-squared{background-image:url(../flags/1x1/kg.svg)}.flag-icon-kh{background-image:url(../flags/4x3/kh.svg)}.flag-icon-kh.flag-icon-squared{background-image:url(../flags/1x1/kh.svg)}.flag-icon-ki{background-image:url(../flags/4x3/ki.svg)}.flag-icon-ki.flag-icon-squared{background-image:url(../flags/1x1/ki.svg)}.flag-icon-km{background-image:url(../flags/4x3/km.svg)}.flag-icon-km.flag-icon-squared{background-image:url(../flags/1x1/km.svg)}.flag-icon-kn{background-image:url(../flags/4x3/kn.svg)}.flag-icon-kn.flag-icon-squared{background-image:url(../flags/1x1/kn.svg)}.flag-icon-kp{background-image:url(../flags/4x3/kp.svg)}.flag-icon-kp.flag-icon-squared{background-image:url(../flags/1x1/kp.svg)}.flag-icon-kr{background-image:url(../flags/4x3/kr.svg)}.flag-icon-kr.flag-icon-squared{background-image:url(../flags/1x1/kr.svg)}.flag-icon-kw{background-image:url(../flags/4x3/kw.svg)}.flag-icon-kw.flag-icon-squared{background-image:url(../flags/1x1/kw.svg)}.flag-icon-ky{background-image:url(../flags/4x3/ky.svg)}.flag-icon-ky.flag-icon-squared{background-image:url(../flags/1x1/ky.svg)}.flag-icon-kz{background-image:url(../flags/4x3/kz.svg)}.flag-icon-kz.flag-icon-squared{background-image:url(../flags/1x1/kz.svg)}.flag-icon-la{background-image:url(../flags/4x3/la.svg)}.flag-icon-la.flag-icon-squared{background-image:url(../flags/1x1/la.svg)}.flag-icon-lb{background-image:url(../flags/4x3/lb.svg)}.flag-icon-lb.flag-icon-squared{background-image:url(../flags/1x1/lb.svg)}.flag-icon-lc{background-image:url(../flags/4x3/lc.svg)}.flag-icon-lc.flag-icon-squared{background-image:url(../flags/1x1/lc.svg)}.flag-icon-li{background-image:url(../flags/4x3/li.svg)}.flag-icon-li.flag-icon-squared{background-image:url(../flags/1x1/li.svg)}.flag-icon-lk{background-image:url(../flags/4x3/lk.svg)}.flag-icon-lk.flag-icon-squared{background-image:url(../flags/1x1/lk.svg)}.flag-icon-lr{background-image:url(../flags/4x3/lr.svg)}.flag-icon-lr.flag-icon-squared{background-image:url(../flags/1x1/lr.svg)}.flag-icon-ls{background-image:url(../flags/4x3/ls.svg)}.flag-icon-ls.flag-icon-squared{background-image:url(../flags/1x1/ls.svg)}.flag-icon-lt{background-image:url(../flags/4x3/lt.svg)}.flag-icon-lt.flag-icon-squared{background-image:url(../flags/1x1/lt.svg)}.flag-icon-lu{background-image:url(../flags/4x3/lu.svg)}.flag-icon-lu.flag-icon-squared{background-image:url(../flags/1x1/lu.svg)}.flag-icon-lv{background-image:url(../flags/4x3/lv.svg)}.flag-icon-lv.flag-icon-squared{background-image:url(../flags/1x1/lv.svg)}.flag-icon-ly{background-image:url(../flags/4x3/ly.svg)}.flag-icon-ly.flag-icon-squared{background-image:url(../flags/1x1/ly.svg)}.flag-icon-ma{background-image:url(../flags/4x3/ma.svg)}.flag-icon-ma.flag-icon-squared{background-image:url(../flags/1x1/ma.svg)}.flag-icon-mc{background-image:url(../flags/4x3/mc.svg)}.flag-icon-mc.flag-icon-squared{background-image:url(../flags/1x1/mc.svg)}.flag-icon-md{background-image:url(../flags/4x3/md.svg)}.flag-icon-md.flag-icon-squared{background-image:url(../flags/1x1/md.svg)}.flag-icon-me{background-image:url(../flags/4x3/me.svg)}.flag-icon-me.flag-icon-squared{background-image:url(../flags/1x1/me.svg)}.flag-icon-mf{background-image:url(../flags/4x3/mf.svg)}.flag-icon-mf.flag-icon-squared{background-image:url(../flags/1x1/mf.svg)}.flag-icon-mg{background-image:url(../flags/4x3/mg.svg)}.flag-icon-mg.flag-icon-squared{background-image:url(../flags/1x1/mg.svg)}.flag-icon-mh{background-image:url(../flags/4x3/mh.svg)}.flag-icon-mh.flag-icon-squared{background-image:url(../flags/1x1/mh.svg)}.flag-icon-mk{background-image:url(../flags/4x3/mk.svg)}.flag-icon-mk.flag-icon-squared{background-image:url(../flags/1x1/mk.svg)}.flag-icon-ml{background-image:url(../flags/4x3/ml.svg)}.flag-icon-ml.flag-icon-squared{background-image:url(../flags/1x1/ml.svg)}.flag-icon-mm{background-image:url(../flags/4x3/mm.svg)}.flag-icon-mm.flag-icon-squared{background-image:url(../flags/1x1/mm.svg)}.flag-icon-mn{background-image:url(../flags/4x3/mn.svg)}.flag-icon-mn.flag-icon-squared{background-image:url(../flags/1x1/mn.svg)}.flag-icon-mo{background-image:url(../flags/4x3/mo.svg)}.flag-icon-mo.flag-icon-squared{background-image:url(../flags/1x1/mo.svg)}.flag-icon-mp{background-image:url(../flags/4x3/mp.svg)}.flag-icon-mp.flag-icon-squared{background-image:url(../flags/1x1/mp.svg)}.flag-icon-mq{background-image:url(../flags/4x3/mq.svg)}.flag-icon-mq.flag-icon-squared{background-image:url(../flags/1x1/mq.svg)}.flag-icon-mr{background-image:url(../flags/4x3/mr.svg)}.flag-icon-mr.flag-icon-squared{background-image:url(../flags/1x1/mr.svg)}.flag-icon-ms{background-image:url(../flags/4x3/ms.svg)}.flag-icon-ms.flag-icon-squared{background-image:url(../flags/1x1/ms.svg)}.flag-icon-mt{background-image:url(../flags/4x3/mt.svg)}.flag-icon-mt.flag-icon-squared{background-image:url(../flags/1x1/mt.svg)}.flag-icon-mu{background-image:url(../flags/4x3/mu.svg)}.flag-icon-mu.flag-icon-squared{background-image:url(../flags/1x1/mu.svg)}.flag-icon-mv{background-image:url(../flags/4x3/mv.svg)}.flag-icon-mv.flag-icon-squared{background-image:url(../flags/1x1/mv.svg)}.flag-icon-mw{background-image:url(../flags/4x3/mw.svg)}.flag-icon-mw.flag-icon-squared{background-image:url(../flags/1x1/mw.svg)}.flag-icon-mx{background-image:url(../flags/4x3/mx.svg)}.flag-icon-mx.flag-icon-squared{background-image:url(../flags/1x1/mx.svg)}.flag-icon-my{background-image:url(../flags/4x3/my.svg)}.flag-icon-my.flag-icon-squared{background-image:url(../flags/1x1/my.svg)}.flag-icon-mz{background-image:url(../flags/4x3/mz.svg)}.flag-icon-mz.flag-icon-squared{background-image:url(../flags/1x1/mz.svg)}.flag-icon-na{background-image:url(../flags/4x3/na.svg)}.flag-icon-na.flag-icon-squared{background-image:url(../flags/1x1/na.svg)}.flag-icon-nc{background-image:url(../flags/4x3/nc.svg)}.flag-icon-nc.flag-icon-squared{background-image:url(../flags/1x1/nc.svg)}.flag-icon-ne{background-image:url(../flags/4x3/ne.svg)}.flag-icon-ne.flag-icon-squared{background-image:url(../flags/1x1/ne.svg)}.flag-icon-nf{background-image:url(../flags/4x3/nf.svg)}.flag-icon-nf.flag-icon-squared{background-image:url(../flags/1x1/nf.svg)}.flag-icon-ng{background-image:url(../flags/4x3/ng.svg)}.flag-icon-ng.flag-icon-squared{background-image:url(../flags/1x1/ng.svg)}.flag-icon-ni{background-image:url(../flags/4x3/ni.svg)}.flag-icon-ni.flag-icon-squared{background-image:url(../flags/1x1/ni.svg)}.flag-icon-nl{background-image:url(../flags/4x3/nl.svg)}.flag-icon-nl.flag-icon-squared{background-image:url(../flags/1x1/nl.svg)}.flag-icon-no{background-image:url(../flags/4x3/no.svg)}.flag-icon-no.flag-icon-squared{background-image:url(../flags/1x1/no.svg)}.flag-icon-np{background-image:url(../flags/4x3/np.svg)}.flag-icon-np.flag-icon-squared{background-image:url(../flags/1x1/np.svg)}.flag-icon-nr{background-image:url(../flags/4x3/nr.svg)}.flag-icon-nr.flag-icon-squared{background-image:url(../flags/1x1/nr.svg)}.flag-icon-nu{background-image:url(../flags/4x3/nu.svg)}.flag-icon-nu.flag-icon-squared{background-image:url(../flags/1x1/nu.svg)}.flag-icon-nz{background-image:url(../flags/4x3/nz.svg)}.flag-icon-nz.flag-icon-squared{background-image:url(../flags/1x1/nz.svg)}.flag-icon-om{background-image:url(../flags/4x3/om.svg)}.flag-icon-om.flag-icon-squared{background-image:url(../flags/1x1/om.svg)}.flag-icon-pa{background-image:url(../flags/4x3/pa.svg)}.flag-icon-pa.flag-icon-squared{background-image:url(../flags/1x1/pa.svg)}.flag-icon-pe{background-image:url(../flags/4x3/pe.svg)}.flag-icon-pe.flag-icon-squared{background-image:url(../flags/1x1/pe.svg)}.flag-icon-pf{background-image:url(../flags/4x3/pf.svg)}.flag-icon-pf.flag-icon-squared{background-image:url(../flags/1x1/pf.svg)}.flag-icon-pg{background-image:url(../flags/4x3/pg.svg)}.flag-icon-pg.flag-icon-squared{background-image:url(../flags/1x1/pg.svg)}.flag-icon-ph{background-image:url(../flags/4x3/ph.svg)}.flag-icon-ph.flag-icon-squared{background-image:url(../flags/1x1/ph.svg)}.flag-icon-pk{background-image:url(../flags/4x3/pk.svg)}.flag-icon-pk.flag-icon-squared{background-image:url(../flags/1x1/pk.svg)}.flag-icon-pl{background-image:url(../flags/4x3/pl.svg)}.flag-icon-pl.flag-icon-squared{background-image:url(../flags/1x1/pl.svg)}.flag-icon-pm{background-image:url(../flags/4x3/pm.svg)}.flag-icon-pm.flag-icon-squared{background-image:url(../flags/1x1/pm.svg)}.flag-icon-pn{background-image:url(../flags/4x3/pn.svg)}.flag-icon-pn.flag-icon-squared{background-image:url(../flags/1x1/pn.svg)}.flag-icon-pr{background-image:url(../flags/4x3/pr.svg)}.flag-icon-pr.flag-icon-squared{background-image:url(../flags/1x1/pr.svg)}.flag-icon-ps{background-image:url(../flags/4x3/ps.svg)}.flag-icon-ps.flag-icon-squared{background-image:url(../flags/1x1/ps.svg)}.flag-icon-pt{background-image:url(../flags/4x3/pt.svg)}.flag-icon-pt.flag-icon-squared{background-image:url(../flags/1x1/pt.svg)}.flag-icon-pw{background-image:url(../flags/4x3/pw.svg)}.flag-icon-pw.flag-icon-squared{background-image:url(../flags/1x1/pw.svg)}.flag-icon-py{background-image:url(../flags/4x3/py.svg)}.flag-icon-py.flag-icon-squared{background-image:url(../flags/1x1/py.svg)}.flag-icon-qa{background-image:url(../flags/4x3/qa.svg)}.flag-icon-qa.flag-icon-squared{background-image:url(../flags/1x1/qa.svg)}.flag-icon-re{background-image:url(../flags/4x3/re.svg)}.flag-icon-re.flag-icon-squared{background-image:url(../flags/1x1/re.svg)}.flag-icon-ro{background-image:url(../flags/4x3/ro.svg)}.flag-icon-ro.flag-icon-squared{background-image:url(../flags/1x1/ro.svg)}.flag-icon-rs{background-image:url(../flags/4x3/rs.svg)}.flag-icon-rs.flag-icon-squared{background-image:url(../flags/1x1/rs.svg)}.flag-icon-ru{background-image:url(../flags/4x3/ru.svg)}.flag-icon-ru.flag-icon-squared{background-image:url(../flags/1x1/ru.svg)}.flag-icon-rw{background-image:url(../flags/4x3/rw.svg)}.flag-icon-rw.flag-icon-squared{background-image:url(../flags/1x1/rw.svg)}.flag-icon-sa{background-image:url(../flags/4x3/sa.svg)}.flag-icon-sa.flag-icon-squared{background-image:url(../flags/1x1/sa.svg)}.flag-icon-sb{background-image:url(../flags/4x3/sb.svg)}.flag-icon-sb.flag-icon-squared{background-image:url(../flags/1x1/sb.svg)}.flag-icon-sc{background-image:url(../flags/4x3/sc.svg)}.flag-icon-sc.flag-icon-squared{background-image:url(../flags/1x1/sc.svg)}.flag-icon-sd{background-image:url(../flags/4x3/sd.svg)}.flag-icon-sd.flag-icon-squared{background-image:url(../flags/1x1/sd.svg)}.flag-icon-se{background-image:url(../flags/4x3/se.svg)}.flag-icon-se.flag-icon-squared{background-image:url(../flags/1x1/se.svg)}.flag-icon-sg{background-image:url(../flags/4x3/sg.svg)}.flag-icon-sg.flag-icon-squared{background-image:url(../flags/1x1/sg.svg)}.flag-icon-sh{background-image:url(../flags/4x3/sh.svg)}.flag-icon-sh.flag-icon-squared{background-image:url(../flags/1x1/sh.svg)}.flag-icon-si{background-image:url(../flags/4x3/si.svg)}.flag-icon-si.flag-icon-squared{background-image:url(../flags/1x1/si.svg)}.flag-icon-sj{background-image:url(../flags/4x3/sj.svg)}.flag-icon-sj.flag-icon-squared{background-image:url(../flags/1x1/sj.svg)}.flag-icon-sk{background-image:url(../flags/4x3/sk.svg)}.flag-icon-sk.flag-icon-squared{background-image:url(../flags/1x1/sk.svg)}.flag-icon-sl{background-image:url(../flags/4x3/sl.svg)}.flag-icon-sl.flag-icon-squared{background-image:url(../flags/1x1/sl.svg)}.flag-icon-sm{background-image:url(../flags/4x3/sm.svg)}.flag-icon-sm.flag-icon-squared{background-image:url(../flags/1x1/sm.svg)}.flag-icon-sn{background-image:url(../flags/4x3/sn.svg)}.flag-icon-sn.flag-icon-squared{background-image:url(../flags/1x1/sn.svg)}.flag-icon-so{background-image:url(../flags/4x3/so.svg)}.flag-icon-so.flag-icon-squared{background-image:url(../flags/1x1/so.svg)}.flag-icon-sr{background-image:url(../flags/4x3/sr.svg)}.flag-icon-sr.flag-icon-squared{background-image:url(../flags/1x1/sr.svg)}.flag-icon-ss{background-image:url(../flags/4x3/ss.svg)}.flag-icon-ss.flag-icon-squared{background-image:url(../flags/1x1/ss.svg)}.flag-icon-st{background-image:url(../flags/4x3/st.svg)}.flag-icon-st.flag-icon-squared{background-image:url(../flags/1x1/st.svg)}.flag-icon-sv{background-image:url(../flags/4x3/sv.svg)}.flag-icon-sv.flag-icon-squared{background-image:url(../flags/1x1/sv.svg)}.flag-icon-sx{background-image:url(../flags/4x3/sx.svg)}.flag-icon-sx.flag-icon-squared{background-image:url(../flags/1x1/sx.svg)}.flag-icon-sy{background-image:url(../flags/4x3/sy.svg)}.flag-icon-sy.flag-icon-squared{background-image:url(../flags/1x1/sy.svg)}.flag-icon-sz{background-image:url(../flags/4x3/sz.svg)}.flag-icon-sz.flag-icon-squared{background-image:url(../flags/1x1/sz.svg)}.flag-icon-tc{background-image:url(../flags/4x3/tc.svg)}.flag-icon-tc.flag-icon-squared{background-image:url(../flags/1x1/tc.svg)}.flag-icon-td{background-image:url(../flags/4x3/td.svg)}.flag-icon-td.flag-icon-squared{background-image:url(../flags/1x1/td.svg)}.flag-icon-tf{background-image:url(../flags/4x3/tf.svg)}.flag-icon-tf.flag-icon-squared{background-image:url(../flags/1x1/tf.svg)}.flag-icon-tg{background-image:url(../flags/4x3/tg.svg)}.flag-icon-tg.flag-icon-squared{background-image:url(../flags/1x1/tg.svg)}.flag-icon-th{background-image:url(../flags/4x3/th.svg)}.flag-icon-th.flag-icon-squared{background-image:url(../flags/1x1/th.svg)}.flag-icon-tj{background-image:url(../flags/4x3/tj.svg)}.flag-icon-tj.flag-icon-squared{background-image:url(../flags/1x1/tj.svg)}.flag-icon-tk{background-image:url(../flags/4x3/tk.svg)}.flag-icon-tk.flag-icon-squared{background-image:url(../flags/1x1/tk.svg)}.flag-icon-tl{background-image:url(../flags/4x3/tl.svg)}.flag-icon-tl.flag-icon-squared{background-image:url(../flags/1x1/tl.svg)}.flag-icon-tm{background-image:url(../flags/4x3/tm.svg)}.flag-icon-tm.flag-icon-squared{background-image:url(../flags/1x1/tm.svg)}.flag-icon-tn{background-image:url(../flags/4x3/tn.svg)}.flag-icon-tn.flag-icon-squared{background-image:url(../flags/1x1/tn.svg)}.flag-icon-to{background-image:url(../flags/4x3/to.svg)}.flag-icon-to.flag-icon-squared{background-image:url(../flags/1x1/to.svg)}.flag-icon-tr{background-image:url(../flags/4x3/tr.svg)}.flag-icon-tr.flag-icon-squared{background-image:url(../flags/1x1/tr.svg)}.flag-icon-tt{background-image:url(../flags/4x3/tt.svg)}.flag-icon-tt.flag-icon-squared{background-image:url(../flags/1x1/tt.svg)}.flag-icon-tv{background-image:url(../flags/4x3/tv.svg)}.flag-icon-tv.flag-icon-squared{background-image:url(../flags/1x1/tv.svg)}.flag-icon-tw{background-image:url(../flags/4x3/tw.svg)}.flag-icon-tw.flag-icon-squared{background-image:url(../flags/1x1/tw.svg)}.flag-icon-tz{background-image:url(../flags/4x3/tz.svg)}.flag-icon-tz.flag-icon-squared{background-image:url(../flags/1x1/tz.svg)}.flag-icon-ua{background-image:url(../flags/4x3/ua.svg)}.flag-icon-ua.flag-icon-squared{background-image:url(../flags/1x1/ua.svg)}.flag-icon-ug{background-image:url(../flags/4x3/ug.svg)}.flag-icon-ug.flag-icon-squared{background-image:url(../flags/1x1/ug.svg)}.flag-icon-um{background-image:url(../flags/4x3/um.svg)}.flag-icon-um.flag-icon-squared{background-image:url(../flags/1x1/um.svg)}.flag-icon-us{background-image:url(../flags/4x3/us.svg)}.flag-icon-us.flag-icon-squared{background-image:url(../flags/1x1/us.svg)}.flag-icon-uy{background-image:url(../flags/4x3/uy.svg)}.flag-icon-uy.flag-icon-squared{background-image:url(../flags/1x1/uy.svg)}.flag-icon-uz{background-image:url(../flags/4x3/uz.svg)}.flag-icon-uz.flag-icon-squared{background-image:url(../flags/1x1/uz.svg)}.flag-icon-va{background-image:url(../flags/4x3/va.svg)}.flag-icon-va.flag-icon-squared{background-image:url(../flags/1x1/va.svg)}.flag-icon-vc{background-image:url(../flags/4x3/vc.svg)}.flag-icon-vc.flag-icon-squared{background-image:url(../flags/1x1/vc.svg)}.flag-icon-ve{background-image:url(../flags/4x3/ve.svg)}.flag-icon-ve.flag-icon-squared{background-image:url(../flags/1x1/ve.svg)}.flag-icon-vg{background-image:url(../flags/4x3/vg.svg)}.flag-icon-vg.flag-icon-squared{background-image:url(../flags/1x1/vg.svg)}.flag-icon-vi{background-image:url(../flags/4x3/vi.svg)}.flag-icon-vi.flag-icon-squared{background-image:url(../flags/1x1/vi.svg)}.flag-icon-vn{background-image:url(../flags/4x3/vn.svg)}.flag-icon-vn.flag-icon-squared{background-image:url(../flags/1x1/vn.svg)}.flag-icon-vu{background-image:url(../flags/4x3/vu.svg)}.flag-icon-vu.flag-icon-squared{background-image:url(../flags/1x1/vu.svg)}.flag-icon-wf{background-image:url(../flags/4x3/wf.svg)}.flag-icon-wf.flag-icon-squared{background-image:url(../flags/1x1/wf.svg)}.flag-icon-ws{background-image:url(../flags/4x3/ws.svg)}.flag-icon-ws.flag-icon-squared{background-image:url(../flags/1x1/ws.svg)}.flag-icon-ye{background-image:url(../flags/4x3/ye.svg)}.flag-icon-ye.flag-icon-squared{background-image:url(../flags/1x1/ye.svg)}.flag-icon-yt{background-image:url(../flags/4x3/yt.svg)}.flag-icon-yt.flag-icon-squared{background-image:url(../flags/1x1/yt.svg)}.flag-icon-za{background-image:url(../flags/4x3/za.svg)}.flag-icon-za.flag-icon-squared{background-image:url(../flags/1x1/za.svg)}.flag-icon-zm{background-image:url(../flags/4x3/zm.svg)}.flag-icon-zm.flag-icon-squared{background-image:url(../flags/1x1/zm.svg)}.flag-icon-zw{background-image:url(../flags/4x3/zw.svg)}.flag-icon-zw.flag-icon-squared{background-image:url(../flags/1x1/zw.svg)}.flag-icon-es-ca{background-image:url(../flags/4x3/es-ca.svg)}.flag-icon-es-ca.flag-icon-squared{background-image:url(../flags/1x1/es-ca.svg)}.flag-icon-es-ga{background-image:url(../flags/4x3/es-ga.svg)}.flag-icon-es-ga.flag-icon-squared{background-image:url(../flags/1x1/es-ga.svg)}.flag-icon-eu{background-image:url(../flags/4x3/eu.svg)}.flag-icon-eu.flag-icon-squared{background-image:url(../flags/1x1/eu.svg)}.flag-icon-gb-eng{background-image:url(../flags/4x3/gb-eng.svg)}.flag-icon-gb-eng.flag-icon-squared{background-image:url(../flags/1x1/gb-eng.svg)}.flag-icon-gb-nir{background-image:url(../flags/4x3/gb-nir.svg)}.flag-icon-gb-nir.flag-icon-squared{background-image:url(../flags/1x1/gb-nir.svg)}.flag-icon-gb-sct{background-image:url(../flags/4x3/gb-sct.svg)}.flag-icon-gb-sct.flag-icon-squared{background-image:url(../flags/1x1/gb-sct.svg)}.flag-icon-gb-wls{background-image:url(../flags/4x3/gb-wls.svg)}.flag-icon-gb-wls.flag-icon-squared{background-image:url(../flags/1x1/gb-wls.svg)}.flag-icon-un{background-image:url(../flags/4x3/un.svg)}.flag-icon-un.flag-icon-squared{background-image:url(../flags/1x1/un.svg)}.flag-icon-xk{background-image:url(../flags/4x3/xk.svg)}.flag-icon-xk.flag-icon-squared{background-image:url(../flags/1x1/xk.svg)} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ad.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ad.svg new file mode 100644 index 0000000000..e7fc56abbc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ad.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ae.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ae.svg new file mode 100644 index 0000000000..739c5d4646 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ae.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/af.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/af.svg new file mode 100644 index 0000000000..90c34b8d5a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/af.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ag.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ag.svg new file mode 100644 index 0000000000..d0b2a83cbe --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ag.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ai.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ai.svg new file mode 100644 index 0000000000..472be200e7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ai.svg @@ -0,0 +1,763 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/al.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/al.svg new file mode 100644 index 0000000000..75995ecde6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/al.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/am.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/am.svg new file mode 100644 index 0000000000..1198be035f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/am.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ao.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ao.svg new file mode 100644 index 0000000000..a5a25bf107 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ao.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aq.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aq.svg new file mode 100644 index 0000000000..80e682abd6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ar.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ar.svg new file mode 100644 index 0000000000..1730ecac9e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ar.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/as.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/as.svg new file mode 100644 index 0000000000..b8d8162d0a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/as.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/at.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/at.svg new file mode 100644 index 0000000000..649d6efe0a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/at.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/au.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/au.svg new file mode 100644 index 0000000000..ca5d607ea0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/au.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aw.svg new file mode 100644 index 0000000000..248a08d69b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/aw.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ax.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ax.svg new file mode 100644 index 0000000000..cdeb07e273 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ax.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/az.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/az.svg new file mode 100644 index 0000000000..0119e1ab4e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/az.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ba.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ba.svg new file mode 100644 index 0000000000..5b92b0ab54 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ba.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bb.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bb.svg new file mode 100644 index 0000000000..9d627842f1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bb.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bd.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bd.svg new file mode 100644 index 0000000000..4cb38cf503 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bd.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/be.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/be.svg new file mode 100644 index 0000000000..01496c3cac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/be.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bf.svg new file mode 100644 index 0000000000..a3c7c44a4f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bg.svg new file mode 100644 index 0000000000..5abe67f642 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bh.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bh.svg new file mode 100644 index 0000000000..22fba621a9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bh.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bi.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bi.svg new file mode 100644 index 0000000000..cc11dcff2c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bi.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bj.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bj.svg new file mode 100644 index 0000000000..07c4c117ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bj.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bl.svg new file mode 100644 index 0000000000..0fa74e1c3c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bl.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bm.svg new file mode 100644 index 0000000000..a7057d24c0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bm.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bn.svg new file mode 100644 index 0000000000..2e93aea326 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bn.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bo.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bo.svg new file mode 100644 index 0000000000..52a534fe2b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bo.svg @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bq.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bq.svg new file mode 100644 index 0000000000..cc872ef1b8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bq.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/br.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/br.svg new file mode 100644 index 0000000000..8353e823fc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/br.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bs.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bs.svg new file mode 100644 index 0000000000..decdebb1ef --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bs.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bt.svg new file mode 100644 index 0000000000..3bbbfaac74 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bt.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bv.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bv.svg new file mode 100644 index 0000000000..01c9ee147d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bv.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bw.svg new file mode 100644 index 0000000000..0bc5d3d34f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bw.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/by.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/by.svg new file mode 100644 index 0000000000..73e14f7a23 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/by.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bz.svg new file mode 100644 index 0000000000..0e9a27ca6a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/bz.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ca.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ca.svg new file mode 100644 index 0000000000..6882f6da65 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ca.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cc.svg new file mode 100644 index 0000000000..dd6e20003e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cc.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cd.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cd.svg new file mode 100644 index 0000000000..5da2a96892 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cd.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cf.svg new file mode 100644 index 0000000000..e924621528 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cg.svg new file mode 100644 index 0000000000..a52ba7e211 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ch.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ch.svg new file mode 100644 index 0000000000..773cdc8a3e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ch.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ci.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ci.svg new file mode 100644 index 0000000000..bd1e3f4141 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ci.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ck.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ck.svg new file mode 100644 index 0000000000..f2df0dbe34 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ck.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cl.svg new file mode 100644 index 0000000000..b808896721 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cm.svg new file mode 100644 index 0000000000..08b710bb87 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cn.svg new file mode 100644 index 0000000000..7873c1b4fa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/co.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/co.svg new file mode 100644 index 0000000000..18d1c5f450 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/co.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cr.svg new file mode 100644 index 0000000000..a60a6dd614 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cu.svg new file mode 100644 index 0000000000..396817620d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cu.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cv.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cv.svg new file mode 100644 index 0000000000..a8311b2f83 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cv.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cw.svg new file mode 100644 index 0000000000..d7ba21865b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cw.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cx.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cx.svg new file mode 100644 index 0000000000..ef82c45363 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cx.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cy.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cy.svg new file mode 100644 index 0000000000..ba2b0f8908 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cz.svg new file mode 100644 index 0000000000..9557b6e88d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/cz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/de.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/de.svg new file mode 100644 index 0000000000..b9ea8a6134 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/de.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dj.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dj.svg new file mode 100644 index 0000000000..3f6b2e4f35 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dj.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dk.svg new file mode 100644 index 0000000000..51ff69feb0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dm.svg new file mode 100644 index 0000000000..405a4b6b96 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dm.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/do.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/do.svg new file mode 100644 index 0000000000..03d3f3547b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/do.svg @@ -0,0 +1,6745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dz.svg new file mode 100644 index 0000000000..37df0c8bba --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/dz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ec.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ec.svg new file mode 100644 index 0000000000..65fd0bad2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ec.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ee.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ee.svg new file mode 100644 index 0000000000..fbc9e33943 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ee.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eg.svg new file mode 100644 index 0000000000..2965b6afa0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eg.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eh.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eh.svg new file mode 100644 index 0000000000..4c3feba1b3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eh.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/er.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/er.svg new file mode 100644 index 0000000000..86343349e3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/er.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ca.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ca.svg new file mode 100644 index 0000000000..2a50685ddb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ca.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ga.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ga.svg new file mode 100644 index 0000000000..5c55ff855b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es-ga.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es.svg new file mode 100644 index 0000000000..d7030eb292 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/es.svg @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/et.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/et.svg new file mode 100644 index 0000000000..8b02f6b7ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/et.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eu.svg new file mode 100644 index 0000000000..b031d2d336 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/eu.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fi.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fi.svg new file mode 100644 index 0000000000..aff1304c6b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fj.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fj.svg new file mode 100644 index 0000000000..9d9c3029e1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fj.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fk.svg new file mode 100644 index 0000000000..12a34c46dc --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fk.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fm.svg new file mode 100644 index 0000000000..791fde9929 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fm.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fo.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fo.svg new file mode 100644 index 0000000000..b28915c013 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fr.svg new file mode 100644 index 0000000000..f8e3ca0d57 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/fr.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ga.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ga.svg new file mode 100644 index 0000000000..16c81b3029 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ga.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-eng.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-eng.svg new file mode 100644 index 0000000000..18026d2941 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-eng.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-nir.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-nir.svg new file mode 100644 index 0000000000..6d8a3a3974 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-nir.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-sct.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-sct.svg new file mode 100644 index 0000000000..6987b0886b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-sct.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-wls.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-wls.svg new file mode 100644 index 0000000000..3931a17954 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb-wls.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb.svg new file mode 100644 index 0000000000..ef048dc2a1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gb.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gd.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gd.svg new file mode 100644 index 0000000000..cca37ba483 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gd.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ge.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ge.svg new file mode 100644 index 0000000000..ac1d87bb49 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gf.svg new file mode 100644 index 0000000000..c00a576342 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gg.svg new file mode 100644 index 0000000000..2d06a9f86f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gh.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gh.svg new file mode 100644 index 0000000000..4b6446d645 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gi.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gi.svg new file mode 100644 index 0000000000..39f5b2770c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gi.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gl.svg new file mode 100644 index 0000000000..7a026d995f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gm.svg new file mode 100644 index 0000000000..b06ab6cb58 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gm.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gn.svg new file mode 100644 index 0000000000..8f8855da4b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gn.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gp.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gp.svg new file mode 100644 index 0000000000..0a5bdb0056 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gq.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gq.svg new file mode 100644 index 0000000000..8149406de4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gq.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gr.svg new file mode 100644 index 0000000000..4bc68fc1fa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gr.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gs.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gs.svg new file mode 100644 index 0000000000..48f68b8b67 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gs.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L + + + E + + + O + + + T + + + E + + + R + + + R + + + R + + + R + + + R + + + E + + + O + + + O + + + A + + + A + + + A + + + M + + + P + + + P + + + P + + + I + + + T + + + T + + + M + + + G + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gt.svg new file mode 100644 index 0000000000..761801ca47 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gt.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gu.svg new file mode 100644 index 0000000000..d6f5d535ca --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gu.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + G + + + U + + + A + + + M + + + + + + + + G + + + U + + + A + + + M + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gw.svg new file mode 100644 index 0000000000..064a593473 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gw.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gy.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gy.svg new file mode 100644 index 0000000000..57eb520903 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/gy.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hk.svg new file mode 100644 index 0000000000..024c070555 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hk.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hm.svg new file mode 100644 index 0000000000..7e1f7e7e64 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hm.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hn.svg new file mode 100644 index 0000000000..c7c4c4acfd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hn.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hr.svg new file mode 100644 index 0000000000..7ea00410d5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hr.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ht.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ht.svg new file mode 100644 index 0000000000..920833a66e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ht.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hu.svg new file mode 100644 index 0000000000..94bc29f134 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/hu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/id.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/id.svg new file mode 100644 index 0000000000..6d2cf0941e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/id.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ie.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ie.svg new file mode 100644 index 0000000000..60448a9ded --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ie.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/il.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/il.svg new file mode 100644 index 0000000000..6cb4b1c1f7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/il.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/im.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/im.svg new file mode 100644 index 0000000000..0f487f67d7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/im.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/in.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/in.svg new file mode 100644 index 0000000000..e6557cd01a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/in.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/io.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/io.svg new file mode 100644 index 0000000000..4d809e03e6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/io.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/iq.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/iq.svg new file mode 100644 index 0000000000..6b96774d97 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/iq.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ir.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ir.svg new file mode 100644 index 0000000000..79f66324f1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ir.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/is.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/is.svg new file mode 100644 index 0000000000..08d1e68383 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/is.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/it.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/it.svg new file mode 100644 index 0000000000..615c58fb8f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/it.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/je.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/je.svg new file mode 100644 index 0000000000..c63ccb29ec --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/je.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jm.svg new file mode 100644 index 0000000000..c261da0971 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jm.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jo.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jo.svg new file mode 100644 index 0000000000..ab1c62aaa1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jp.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jp.svg new file mode 100644 index 0000000000..dc7a64a5c7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/jp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ke.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ke.svg new file mode 100644 index 0000000000..0b82f3a6c3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ke.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kg.svg new file mode 100644 index 0000000000..71ee7b8df5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kg.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kh.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kh.svg new file mode 100644 index 0000000000..8c888f1681 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kh.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ki.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ki.svg new file mode 100644 index 0000000000..bfc5ccab46 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ki.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/km.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/km.svg new file mode 100644 index 0000000000..8f842ea8c0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/km.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kn.svg new file mode 100644 index 0000000000..4b2a2488be --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kn.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kp.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kp.svg new file mode 100644 index 0000000000..8eda6be874 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kr.svg new file mode 100644 index 0000000000..2db51b02a6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kr.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kw.svg new file mode 100644 index 0000000000..3d4047fff5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ky.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ky.svg new file mode 100644 index 0000000000..b4ae00aa86 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ky.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kz.svg new file mode 100644 index 0000000000..f17bd6e0f8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/kz.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/la.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/la.svg new file mode 100644 index 0000000000..1e7686a935 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/la.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lb.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lb.svg new file mode 100644 index 0000000000..a047b0b948 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lb.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lc.svg new file mode 100644 index 0000000000..b13b8852aa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/li.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/li.svg new file mode 100644 index 0000000000..cbed5cc84c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/li.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lk.svg new file mode 100644 index 0000000000..2b112155f6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lk.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lr.svg new file mode 100644 index 0000000000..0ae34e7282 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lr.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ls.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ls.svg new file mode 100644 index 0000000000..e71bb5bb70 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ls.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lt.svg new file mode 100644 index 0000000000..aa96cf3236 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lu.svg new file mode 100644 index 0000000000..6293671602 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lv.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lv.svg new file mode 100644 index 0000000000..5556de1a3d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/lv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ly.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ly.svg new file mode 100644 index 0000000000..fe0ed81b60 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ly.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ma.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ma.svg new file mode 100644 index 0000000000..85c99b3b9e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ma.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mc.svg new file mode 100644 index 0000000000..d38822ddac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mc.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/md.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/md.svg new file mode 100644 index 0000000000..86b2a96149 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/md.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/me.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/me.svg new file mode 100644 index 0000000000..56a19ed0a0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/me.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mf.svg new file mode 100644 index 0000000000..310afce407 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mg.svg new file mode 100644 index 0000000000..f0375cc6be --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mh.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mh.svg new file mode 100644 index 0000000000..97f34631af --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mh.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mk.svg new file mode 100644 index 0000000000..da2e9a4c40 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ml.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ml.svg new file mode 100644 index 0000000000..1e4d98900c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ml.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mm.svg new file mode 100644 index 0000000000..5076184c32 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mm.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mn.svg new file mode 100644 index 0000000000..568fda088a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mo.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mo.svg new file mode 100644 index 0000000000..83d04ea28a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mp.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mp.svg new file mode 100644 index 0000000000..54a0ede9f2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mp.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mq.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mq.svg new file mode 100644 index 0000000000..7a69fb5842 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mq.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mr.svg new file mode 100644 index 0000000000..7da23e5f4e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ms.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ms.svg new file mode 100644 index 0000000000..d7d910d020 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ms.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mt.svg new file mode 100644 index 0000000000..96acc15e73 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mt.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mu.svg new file mode 100644 index 0000000000..773d3d59ee --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mu.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mv.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mv.svg new file mode 100644 index 0000000000..aa5ed5333e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mw.svg new file mode 100644 index 0000000000..a9521a08c1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mw.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mx.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mx.svg new file mode 100644 index 0000000000..a440656807 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mx.svg @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/my.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/my.svg new file mode 100644 index 0000000000..7ebe064b74 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/my.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mz.svg new file mode 100644 index 0000000000..113a205792 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/mz.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/na.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/na.svg new file mode 100644 index 0000000000..b934fc157c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/na.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nc.svg new file mode 100644 index 0000000000..2bdf6ee5b5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nc.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ne.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ne.svg new file mode 100644 index 0000000000..e76e44c0bb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ne.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nf.svg new file mode 100644 index 0000000000..21495222f6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nf.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ng.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ng.svg new file mode 100644 index 0000000000..57d65d380a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ng.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ni.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ni.svg new file mode 100644 index 0000000000..8f68d42255 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ni.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nl.svg new file mode 100644 index 0000000000..9db233dd4b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nl.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/no.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/no.svg new file mode 100644 index 0000000000..08ea5728d2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/no.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/np.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/np.svg new file mode 100644 index 0000000000..f34ee8c638 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/np.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nr.svg new file mode 100644 index 0000000000..282d80fa14 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nr.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nu.svg new file mode 100644 index 0000000000..aced440dc1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nu.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nz.svg new file mode 100644 index 0000000000..5283a96ed4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/nz.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/om.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/om.svg new file mode 100644 index 0000000000..055d1e6986 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/om.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pa.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pa.svg new file mode 100644 index 0000000000..57965b9ea2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pa.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pe.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pe.svg new file mode 100644 index 0000000000..40b87badbb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pe.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pf.svg new file mode 100644 index 0000000000..94ff90cb2c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pf.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pg.svg new file mode 100644 index 0000000000..7397754182 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pg.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ph.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ph.svg new file mode 100644 index 0000000000..681cf23ff5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ph.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pk.svg new file mode 100644 index 0000000000..06b6022f0c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pk.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pl.svg new file mode 100644 index 0000000000..f7c12a1843 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pm.svg new file mode 100644 index 0000000000..3d4014a414 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pn.svg new file mode 100644 index 0000000000..47b0749a0a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pn.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pr.svg new file mode 100644 index 0000000000..79cf4c0991 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pr.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ps.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ps.svg new file mode 100644 index 0000000000..7c1ea3f9a6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ps.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pt.svg new file mode 100644 index 0000000000..425515054a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pt.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pw.svg new file mode 100644 index 0000000000..83bc3f71d5 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/pw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/py.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/py.svg new file mode 100644 index 0000000000..88f55e6f02 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/py.svg @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/qa.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/qa.svg new file mode 100644 index 0000000000..0bf30ea37f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/qa.svg @@ -0,0 +1,4 @@ + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/re.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/re.svg new file mode 100644 index 0000000000..027c9f32ce --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/re.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ro.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ro.svg new file mode 100644 index 0000000000..994992ab66 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ro.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rs.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rs.svg new file mode 100644 index 0000000000..3270f304c4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rs.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ru.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ru.svg new file mode 100644 index 0000000000..d643087430 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ru.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rw.svg new file mode 100644 index 0000000000..26e41f6ad7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/rw.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sa.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sa.svg new file mode 100644 index 0000000000..1cc41b80bf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sa.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sb.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sb.svg new file mode 100644 index 0000000000..f61bafd6a9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sb.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sc.svg new file mode 100644 index 0000000000..65d8943d64 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sc.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sd.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sd.svg new file mode 100644 index 0000000000..72a729708b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sd.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/se.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/se.svg new file mode 100644 index 0000000000..e824395ebb --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/se.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sg.svg new file mode 100644 index 0000000000..1444e5b6ab --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sg.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sh.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sh.svg new file mode 100644 index 0000000000..599a09d73b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sh.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/si.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/si.svg new file mode 100644 index 0000000000..6de77ef473 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/si.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sj.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sj.svg new file mode 100644 index 0000000000..f4e5829735 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sj.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sk.svg new file mode 100644 index 0000000000..fd728680b0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sk.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sl.svg new file mode 100644 index 0000000000..18c76d781b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sl.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sm.svg new file mode 100644 index 0000000000..1ed18eed6c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sm.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L + + + I + + + B + + + E + + + R + + + T + + + A + + + S + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sn.svg new file mode 100644 index 0000000000..fbbad5e026 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sn.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/so.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/so.svg new file mode 100644 index 0000000000..96d88a992a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/so.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sr.svg new file mode 100644 index 0000000000..a0ca03e367 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sr.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ss.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ss.svg new file mode 100644 index 0000000000..7e6a47ffd9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ss.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/st.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/st.svg new file mode 100644 index 0000000000..da5df2990d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/st.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sv.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sv.svg new file mode 100644 index 0000000000..65b44baeec --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sv.svg @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sx.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sx.svg new file mode 100644 index 0000000000..5d9fadfa2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sx.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sy.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sy.svg new file mode 100644 index 0000000000..904dc1f65c --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sz.svg new file mode 100644 index 0000000000..b3009e4d92 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/sz.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tc.svg new file mode 100644 index 0000000000..1029615f55 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tc.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/td.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/td.svg new file mode 100644 index 0000000000..e3e81ce2fd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/td.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tf.svg new file mode 100644 index 0000000000..2061867c96 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tg.svg new file mode 100644 index 0000000000..2c1fd98fd4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tg.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/th.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/th.svg new file mode 100644 index 0000000000..86850f5fbf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/th.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tj.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tj.svg new file mode 100644 index 0000000000..853a4a4555 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tj.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tk.svg new file mode 100644 index 0000000000..c5ff6b4cb4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tl.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tl.svg new file mode 100644 index 0000000000..ec6d44bd13 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tm.svg new file mode 100644 index 0000000000..d856424fe6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tm.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tn.svg new file mode 100644 index 0000000000..d46a1cd9aa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tn.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/to.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/to.svg new file mode 100644 index 0000000000..201d6bc39d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/to.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tr.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tr.svg new file mode 100644 index 0000000000..861d4ea580 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tr.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tt.svg new file mode 100644 index 0000000000..87e439a07d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tv.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tv.svg new file mode 100644 index 0000000000..f30c8f3710 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tv.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tw.svg new file mode 100644 index 0000000000..5f284fc612 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tw.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tz.svg new file mode 100644 index 0000000000..f993ff15d6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/tz.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ua.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ua.svg new file mode 100644 index 0000000000..18ebe0d49d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ua.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ug.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ug.svg new file mode 100644 index 0000000000..d9be945945 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ug.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/um.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/um.svg new file mode 100644 index 0000000000..25b08ce6ab --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/um.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/un.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/un.svg new file mode 100644 index 0000000000..1d50ea9250 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/un.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/us.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/us.svg new file mode 100644 index 0000000000..31f90c6c3a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/us.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uy.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uy.svg new file mode 100644 index 0000000000..0194a7cb0b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uy.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uz.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uz.svg new file mode 100644 index 0000000000..641af1b826 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/uz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/va.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/va.svg new file mode 100644 index 0000000000..7da6d3e739 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/va.svg @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vc.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vc.svg new file mode 100644 index 0000000000..ee72f781ac --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ve.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ve.svg new file mode 100644 index 0000000000..205fe848a3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ve.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vg.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vg.svg new file mode 100644 index 0000000000..9572de3480 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vg.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vi.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vi.svg new file mode 100644 index 0000000000..2740f24ffa --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vi.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vn.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vn.svg new file mode 100644 index 0000000000..6b158145ad --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vu.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vu.svg new file mode 100644 index 0000000000..397d30e60b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/vu.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/wf.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/wf.svg new file mode 100644 index 0000000000..bb726a7c2e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/wf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ws.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ws.svg new file mode 100644 index 0000000000..155ad7b558 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ws.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/xk.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/xk.svg new file mode 100644 index 0000000000..69146ca449 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/xk.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ye.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ye.svg new file mode 100644 index 0000000000..d49d2c414e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/ye.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/yt.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/yt.svg new file mode 100644 index 0000000000..7bf3837379 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/yt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/za.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/za.svg new file mode 100644 index 0000000000..9bae96fe3e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/za.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zm.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zm.svg new file mode 100644 index 0000000000..105f1076ad --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zm.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zw.svg b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zw.svg new file mode 100644 index 0000000000..ca5b7a20f9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/flag-icon/flags/1x1/zw.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/css/all.css b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/css/all.css new file mode 100644 index 0000000000..1893247a7d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/css/all.css @@ -0,0 +1,8003 @@ +/*! + * Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2023 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "Font Awesome 6 Free"); + font-weight: var(--fa-style, 900); } + +.fa, +.fa-classic, +.fa-sharp, +.fas, +.fa-solid, +.far, +.fa-regular, +.fab, +.fa-brands { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; } + +.fas, +.fa-classic, +.fa-solid, +.far, +.fa-regular { + font-family: 'Font Awesome 6 Free'; } + +.fab, +.fa-brands { + font-family: 'Font Awesome 6 Brands'; } + +.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-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; } + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; } + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; } + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; } + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; } + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: calc(var(--fa-li-width, 2em) * -1); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; } + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); } + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); } + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); } + +.fa-beat { + -webkit-animation-name: fa-beat; + animation-name: fa-beat; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-bounce { + -webkit-animation-name: fa-bounce; + animation-name: fa-bounce; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); } + +.fa-fade { + -webkit-animation-name: fa-fade; + animation-name: fa-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-beat-fade { + -webkit-animation-name: fa-beat-fade; + animation-name: fa-beat-fade; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); + animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); } + +.fa-flip { + -webkit-animation-name: fa-flip; + animation-name: fa-flip; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out); + animation-timing-function: var(--fa-animation-timing, ease-in-out); } + +.fa-shake { + -webkit-animation-name: fa-shake; + animation-name: fa-shake; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-delay: var(--fa-animation-delay, 0s); + animation-delay: var(--fa-animation-delay, 0s); + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 2s); + animation-duration: var(--fa-animation-duration, 2s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, linear); + animation-timing-function: var(--fa-animation-timing, linear); } + +.fa-spin-reverse { + --fa-animation-direction: reverse; } + +.fa-pulse, +.fa-spin-pulse { + -webkit-animation-name: fa-spin; + animation-name: fa-spin; + -webkit-animation-direction: var(--fa-animation-direction, normal); + animation-direction: var(--fa-animation-direction, normal); + -webkit-animation-duration: var(--fa-animation-duration, 1s); + animation-duration: var(--fa-animation-duration, 1s); + -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + -webkit-animation-timing-function: var(--fa-animation-timing, steps(8)); + animation-timing-function: var(--fa-animation-timing, steps(8)); } + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-bounce, + .fa-fade, + .fa-beat-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + -webkit-animation-delay: -1ms; + animation-delay: -1ms; + -webkit-animation-duration: 1ms; + animation-duration: 1ms; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; + -webkit-transition-delay: 0s; + transition-delay: 0s; + -webkit-transition-duration: 0s; + transition-duration: 0s; } } + +@-webkit-keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@keyframes fa-beat { + 0%, 90% { + -webkit-transform: scale(1); + transform: scale(1); } + 45% { + -webkit-transform: scale(var(--fa-beat-scale, 1.25)); + transform: scale(var(--fa-beat-scale, 1.25)); } } + +@-webkit-keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@keyframes fa-bounce { + 0% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 10% { + -webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); + transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); } + 30% { + -webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); + transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); } + 50% { + -webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); + transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); } + 57% { + -webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); } + 64% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } + 100% { + -webkit-transform: scale(1, 1) translateY(0); + transform: scale(1, 1) translateY(0); } } + +@-webkit-keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); } } + +@-webkit-keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@keyframes fa-beat-fade { + 0%, 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + -webkit-transform: scale(1); + transform: scale(1); } + 50% { + opacity: 1; + -webkit-transform: scale(var(--fa-beat-fade-scale, 1.125)); + transform: scale(var(--fa-beat-fade-scale, 1.125)); } } + +@-webkit-keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@keyframes fa-flip { + 50% { + -webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); + transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } } + +@-webkit-keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@keyframes fa-shake { + 0% { + -webkit-transform: rotate(-15deg); + transform: rotate(-15deg); } + 4% { + -webkit-transform: rotate(15deg); + transform: rotate(15deg); } + 8%, 24% { + -webkit-transform: rotate(-18deg); + transform: rotate(-18deg); } + 12%, 28% { + -webkit-transform: rotate(18deg); + transform: rotate(18deg); } + 16% { + -webkit-transform: rotate(-22deg); + transform: rotate(-22deg); } + 20% { + -webkit-transform: rotate(22deg); + transform: rotate(22deg); } + 32% { + -webkit-transform: rotate(-12deg); + transform: rotate(-12deg); } + 36% { + -webkit-transform: rotate(12deg); + transform: rotate(12deg); } + 40%, 100% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } } + +@-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 { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +.fa-rotate-by { + -webkit-transform: rotate(var(--fa-rotate-angle, none)); + transform: rotate(var(--fa-rotate-angle, 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%; + z-index: var(--fa-stack-z-index, auto); } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: var(--fa-inverse, #fff); } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ + +.fa-0::before { + content: "\30"; } + +.fa-1::before { + content: "\31"; } + +.fa-2::before { + content: "\32"; } + +.fa-3::before { + content: "\33"; } + +.fa-4::before { + content: "\34"; } + +.fa-5::before { + content: "\35"; } + +.fa-6::before { + content: "\36"; } + +.fa-7::before { + content: "\37"; } + +.fa-8::before { + content: "\38"; } + +.fa-9::before { + content: "\39"; } + +.fa-fill-drip::before { + content: "\f576"; } + +.fa-arrows-to-circle::before { + content: "\e4bd"; } + +.fa-circle-chevron-right::before { + content: "\f138"; } + +.fa-chevron-circle-right::before { + content: "\f138"; } + +.fa-at::before { + content: "\40"; } + +.fa-trash-can::before { + content: "\f2ed"; } + +.fa-trash-alt::before { + content: "\f2ed"; } + +.fa-text-height::before { + content: "\f034"; } + +.fa-user-xmark::before { + content: "\f235"; } + +.fa-user-times::before { + content: "\f235"; } + +.fa-stethoscope::before { + content: "\f0f1"; } + +.fa-message::before { + content: "\f27a"; } + +.fa-comment-alt::before { + content: "\f27a"; } + +.fa-info::before { + content: "\f129"; } + +.fa-down-left-and-up-right-to-center::before { + content: "\f422"; } + +.fa-compress-alt::before { + content: "\f422"; } + +.fa-explosion::before { + content: "\e4e9"; } + +.fa-file-lines::before { + content: "\f15c"; } + +.fa-file-alt::before { + content: "\f15c"; } + +.fa-file-text::before { + content: "\f15c"; } + +.fa-wave-square::before { + content: "\f83e"; } + +.fa-ring::before { + content: "\f70b"; } + +.fa-building-un::before { + content: "\e4d9"; } + +.fa-dice-three::before { + content: "\f527"; } + +.fa-calendar-days::before { + content: "\f073"; } + +.fa-calendar-alt::before { + content: "\f073"; } + +.fa-anchor-circle-check::before { + content: "\e4aa"; } + +.fa-building-circle-arrow-right::before { + content: "\e4d1"; } + +.fa-volleyball::before { + content: "\f45f"; } + +.fa-volleyball-ball::before { + content: "\f45f"; } + +.fa-arrows-up-to-line::before { + content: "\e4c2"; } + +.fa-sort-down::before { + content: "\f0dd"; } + +.fa-sort-desc::before { + content: "\f0dd"; } + +.fa-circle-minus::before { + content: "\f056"; } + +.fa-minus-circle::before { + content: "\f056"; } + +.fa-door-open::before { + content: "\f52b"; } + +.fa-right-from-bracket::before { + content: "\f2f5"; } + +.fa-sign-out-alt::before { + content: "\f2f5"; } + +.fa-atom::before { + content: "\f5d2"; } + +.fa-soap::before { + content: "\e06e"; } + +.fa-icons::before { + content: "\f86d"; } + +.fa-heart-music-camera-bolt::before { + content: "\f86d"; } + +.fa-microphone-lines-slash::before { + content: "\f539"; } + +.fa-microphone-alt-slash::before { + content: "\f539"; } + +.fa-bridge-circle-check::before { + content: "\e4c9"; } + +.fa-pump-medical::before { + content: "\e06a"; } + +.fa-fingerprint::before { + content: "\f577"; } + +.fa-hand-point-right::before { + content: "\f0a4"; } + +.fa-magnifying-glass-location::before { + content: "\f689"; } + +.fa-search-location::before { + content: "\f689"; } + +.fa-forward-step::before { + content: "\f051"; } + +.fa-step-forward::before { + content: "\f051"; } + +.fa-face-smile-beam::before { + content: "\f5b8"; } + +.fa-smile-beam::before { + content: "\f5b8"; } + +.fa-flag-checkered::before { + content: "\f11e"; } + +.fa-football::before { + content: "\f44e"; } + +.fa-football-ball::before { + content: "\f44e"; } + +.fa-school-circle-exclamation::before { + content: "\e56c"; } + +.fa-crop::before { + content: "\f125"; } + +.fa-angles-down::before { + content: "\f103"; } + +.fa-angle-double-down::before { + content: "\f103"; } + +.fa-users-rectangle::before { + content: "\e594"; } + +.fa-people-roof::before { + content: "\e537"; } + +.fa-people-line::before { + content: "\e534"; } + +.fa-beer-mug-empty::before { + content: "\f0fc"; } + +.fa-beer::before { + content: "\f0fc"; } + +.fa-diagram-predecessor::before { + content: "\e477"; } + +.fa-arrow-up-long::before { + content: "\f176"; } + +.fa-long-arrow-up::before { + content: "\f176"; } + +.fa-fire-flame-simple::before { + content: "\f46a"; } + +.fa-burn::before { + content: "\f46a"; } + +.fa-person::before { + content: "\f183"; } + +.fa-male::before { + content: "\f183"; } + +.fa-laptop::before { + content: "\f109"; } + +.fa-file-csv::before { + content: "\f6dd"; } + +.fa-menorah::before { + content: "\f676"; } + +.fa-truck-plane::before { + content: "\e58f"; } + +.fa-record-vinyl::before { + content: "\f8d9"; } + +.fa-face-grin-stars::before { + content: "\f587"; } + +.fa-grin-stars::before { + content: "\f587"; } + +.fa-bong::before { + content: "\f55c"; } + +.fa-spaghetti-monster-flying::before { + content: "\f67b"; } + +.fa-pastafarianism::before { + content: "\f67b"; } + +.fa-arrow-down-up-across-line::before { + content: "\e4af"; } + +.fa-spoon::before { + content: "\f2e5"; } + +.fa-utensil-spoon::before { + content: "\f2e5"; } + +.fa-jar-wheat::before { + content: "\e517"; } + +.fa-envelopes-bulk::before { + content: "\f674"; } + +.fa-mail-bulk::before { + content: "\f674"; } + +.fa-file-circle-exclamation::before { + content: "\e4eb"; } + +.fa-circle-h::before { + content: "\f47e"; } + +.fa-hospital-symbol::before { + content: "\f47e"; } + +.fa-pager::before { + content: "\f815"; } + +.fa-address-book::before { + content: "\f2b9"; } + +.fa-contact-book::before { + content: "\f2b9"; } + +.fa-strikethrough::before { + content: "\f0cc"; } + +.fa-k::before { + content: "\4b"; } + +.fa-landmark-flag::before { + content: "\e51c"; } + +.fa-pencil::before { + content: "\f303"; } + +.fa-pencil-alt::before { + content: "\f303"; } + +.fa-backward::before { + content: "\f04a"; } + +.fa-caret-right::before { + content: "\f0da"; } + +.fa-comments::before { + content: "\f086"; } + +.fa-paste::before { + content: "\f0ea"; } + +.fa-file-clipboard::before { + content: "\f0ea"; } + +.fa-code-pull-request::before { + content: "\e13c"; } + +.fa-clipboard-list::before { + content: "\f46d"; } + +.fa-truck-ramp-box::before { + content: "\f4de"; } + +.fa-truck-loading::before { + content: "\f4de"; } + +.fa-user-check::before { + content: "\f4fc"; } + +.fa-vial-virus::before { + content: "\e597"; } + +.fa-sheet-plastic::before { + content: "\e571"; } + +.fa-blog::before { + content: "\f781"; } + +.fa-user-ninja::before { + content: "\f504"; } + +.fa-person-arrow-up-from-line::before { + content: "\e539"; } + +.fa-scroll-torah::before { + content: "\f6a0"; } + +.fa-torah::before { + content: "\f6a0"; } + +.fa-broom-ball::before { + content: "\f458"; } + +.fa-quidditch::before { + content: "\f458"; } + +.fa-quidditch-broom-ball::before { + content: "\f458"; } + +.fa-toggle-off::before { + content: "\f204"; } + +.fa-box-archive::before { + content: "\f187"; } + +.fa-archive::before { + content: "\f187"; } + +.fa-person-drowning::before { + content: "\e545"; } + +.fa-arrow-down-9-1::before { + content: "\f886"; } + +.fa-sort-numeric-desc::before { + content: "\f886"; } + +.fa-sort-numeric-down-alt::before { + content: "\f886"; } + +.fa-face-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-grin-tongue-squint::before { + content: "\f58a"; } + +.fa-spray-can::before { + content: "\f5bd"; } + +.fa-truck-monster::before { + content: "\f63b"; } + +.fa-w::before { + content: "\57"; } + +.fa-earth-africa::before { + content: "\f57c"; } + +.fa-globe-africa::before { + content: "\f57c"; } + +.fa-rainbow::before { + content: "\f75b"; } + +.fa-circle-notch::before { + content: "\f1ce"; } + +.fa-tablet-screen-button::before { + content: "\f3fa"; } + +.fa-tablet-alt::before { + content: "\f3fa"; } + +.fa-paw::before { + content: "\f1b0"; } + +.fa-cloud::before { + content: "\f0c2"; } + +.fa-trowel-bricks::before { + content: "\e58a"; } + +.fa-face-flushed::before { + content: "\f579"; } + +.fa-flushed::before { + content: "\f579"; } + +.fa-hospital-user::before { + content: "\f80d"; } + +.fa-tent-arrow-left-right::before { + content: "\e57f"; } + +.fa-gavel::before { + content: "\f0e3"; } + +.fa-legal::before { + content: "\f0e3"; } + +.fa-binoculars::before { + content: "\f1e5"; } + +.fa-microphone-slash::before { + content: "\f131"; } + +.fa-box-tissue::before { + content: "\e05b"; } + +.fa-motorcycle::before { + content: "\f21c"; } + +.fa-bell-concierge::before { + content: "\f562"; } + +.fa-concierge-bell::before { + content: "\f562"; } + +.fa-pen-ruler::before { + content: "\f5ae"; } + +.fa-pencil-ruler::before { + content: "\f5ae"; } + +.fa-people-arrows::before { + content: "\e068"; } + +.fa-people-arrows-left-right::before { + content: "\e068"; } + +.fa-mars-and-venus-burst::before { + content: "\e523"; } + +.fa-square-caret-right::before { + content: "\f152"; } + +.fa-caret-square-right::before { + content: "\f152"; } + +.fa-scissors::before { + content: "\f0c4"; } + +.fa-cut::before { + content: "\f0c4"; } + +.fa-sun-plant-wilt::before { + content: "\e57a"; } + +.fa-toilets-portable::before { + content: "\e584"; } + +.fa-hockey-puck::before { + content: "\f453"; } + +.fa-table::before { + content: "\f0ce"; } + +.fa-magnifying-glass-arrow-right::before { + content: "\e521"; } + +.fa-tachograph-digital::before { + content: "\f566"; } + +.fa-digital-tachograph::before { + content: "\f566"; } + +.fa-users-slash::before { + content: "\e073"; } + +.fa-clover::before { + content: "\e139"; } + +.fa-reply::before { + content: "\f3e5"; } + +.fa-mail-reply::before { + content: "\f3e5"; } + +.fa-star-and-crescent::before { + content: "\f699"; } + +.fa-house-fire::before { + content: "\e50c"; } + +.fa-square-minus::before { + content: "\f146"; } + +.fa-minus-square::before { + content: "\f146"; } + +.fa-helicopter::before { + content: "\f533"; } + +.fa-compass::before { + content: "\f14e"; } + +.fa-square-caret-down::before { + content: "\f150"; } + +.fa-caret-square-down::before { + content: "\f150"; } + +.fa-file-circle-question::before { + content: "\e4ef"; } + +.fa-laptop-code::before { + content: "\f5fc"; } + +.fa-swatchbook::before { + content: "\f5c3"; } + +.fa-prescription-bottle::before { + content: "\f485"; } + +.fa-bars::before { + content: "\f0c9"; } + +.fa-navicon::before { + content: "\f0c9"; } + +.fa-people-group::before { + content: "\e533"; } + +.fa-hourglass-end::before { + content: "\f253"; } + +.fa-hourglass-3::before { + content: "\f253"; } + +.fa-heart-crack::before { + content: "\f7a9"; } + +.fa-heart-broken::before { + content: "\f7a9"; } + +.fa-square-up-right::before { + content: "\f360"; } + +.fa-external-link-square-alt::before { + content: "\f360"; } + +.fa-face-kiss-beam::before { + content: "\f597"; } + +.fa-kiss-beam::before { + content: "\f597"; } + +.fa-film::before { + content: "\f008"; } + +.fa-ruler-horizontal::before { + content: "\f547"; } + +.fa-people-robbery::before { + content: "\e536"; } + +.fa-lightbulb::before { + content: "\f0eb"; } + +.fa-caret-left::before { + content: "\f0d9"; } + +.fa-circle-exclamation::before { + content: "\f06a"; } + +.fa-exclamation-circle::before { + content: "\f06a"; } + +.fa-school-circle-xmark::before { + content: "\e56d"; } + +.fa-arrow-right-from-bracket::before { + content: "\f08b"; } + +.fa-sign-out::before { + content: "\f08b"; } + +.fa-circle-chevron-down::before { + content: "\f13a"; } + +.fa-chevron-circle-down::before { + content: "\f13a"; } + +.fa-unlock-keyhole::before { + content: "\f13e"; } + +.fa-unlock-alt::before { + content: "\f13e"; } + +.fa-cloud-showers-heavy::before { + content: "\f740"; } + +.fa-headphones-simple::before { + content: "\f58f"; } + +.fa-headphones-alt::before { + content: "\f58f"; } + +.fa-sitemap::before { + content: "\f0e8"; } + +.fa-circle-dollar-to-slot::before { + content: "\f4b9"; } + +.fa-donate::before { + content: "\f4b9"; } + +.fa-memory::before { + content: "\f538"; } + +.fa-road-spikes::before { + content: "\e568"; } + +.fa-fire-burner::before { + content: "\e4f1"; } + +.fa-flag::before { + content: "\f024"; } + +.fa-hanukiah::before { + content: "\f6e6"; } + +.fa-feather::before { + content: "\f52d"; } + +.fa-volume-low::before { + content: "\f027"; } + +.fa-volume-down::before { + content: "\f027"; } + +.fa-comment-slash::before { + content: "\f4b3"; } + +.fa-cloud-sun-rain::before { + content: "\f743"; } + +.fa-compress::before { + content: "\f066"; } + +.fa-wheat-awn::before { + content: "\e2cd"; } + +.fa-wheat-alt::before { + content: "\e2cd"; } + +.fa-ankh::before { + content: "\f644"; } + +.fa-hands-holding-child::before { + content: "\e4fa"; } + +.fa-asterisk::before { + content: "\2a"; } + +.fa-square-check::before { + content: "\f14a"; } + +.fa-check-square::before { + content: "\f14a"; } + +.fa-peseta-sign::before { + content: "\e221"; } + +.fa-heading::before { + content: "\f1dc"; } + +.fa-header::before { + content: "\f1dc"; } + +.fa-ghost::before { + content: "\f6e2"; } + +.fa-list::before { + content: "\f03a"; } + +.fa-list-squares::before { + content: "\f03a"; } + +.fa-square-phone-flip::before { + content: "\f87b"; } + +.fa-phone-square-alt::before { + content: "\f87b"; } + +.fa-cart-plus::before { + content: "\f217"; } + +.fa-gamepad::before { + content: "\f11b"; } + +.fa-circle-dot::before { + content: "\f192"; } + +.fa-dot-circle::before { + content: "\f192"; } + +.fa-face-dizzy::before { + content: "\f567"; } + +.fa-dizzy::before { + content: "\f567"; } + +.fa-egg::before { + content: "\f7fb"; } + +.fa-house-medical-circle-xmark::before { + content: "\e513"; } + +.fa-campground::before { + content: "\f6bb"; } + +.fa-folder-plus::before { + content: "\f65e"; } + +.fa-futbol::before { + content: "\f1e3"; } + +.fa-futbol-ball::before { + content: "\f1e3"; } + +.fa-soccer-ball::before { + content: "\f1e3"; } + +.fa-paintbrush::before { + content: "\f1fc"; } + +.fa-paint-brush::before { + content: "\f1fc"; } + +.fa-lock::before { + content: "\f023"; } + +.fa-gas-pump::before { + content: "\f52f"; } + +.fa-hot-tub-person::before { + content: "\f593"; } + +.fa-hot-tub::before { + content: "\f593"; } + +.fa-map-location::before { + content: "\f59f"; } + +.fa-map-marked::before { + content: "\f59f"; } + +.fa-house-flood-water::before { + content: "\e50e"; } + +.fa-tree::before { + content: "\f1bb"; } + +.fa-bridge-lock::before { + content: "\e4cc"; } + +.fa-sack-dollar::before { + content: "\f81d"; } + +.fa-pen-to-square::before { + content: "\f044"; } + +.fa-edit::before { + content: "\f044"; } + +.fa-car-side::before { + content: "\f5e4"; } + +.fa-share-nodes::before { + content: "\f1e0"; } + +.fa-share-alt::before { + content: "\f1e0"; } + +.fa-heart-circle-minus::before { + content: "\e4ff"; } + +.fa-hourglass-half::before { + content: "\f252"; } + +.fa-hourglass-2::before { + content: "\f252"; } + +.fa-microscope::before { + content: "\f610"; } + +.fa-sink::before { + content: "\e06d"; } + +.fa-bag-shopping::before { + content: "\f290"; } + +.fa-shopping-bag::before { + content: "\f290"; } + +.fa-arrow-down-z-a::before { + content: "\f881"; } + +.fa-sort-alpha-desc::before { + content: "\f881"; } + +.fa-sort-alpha-down-alt::before { + content: "\f881"; } + +.fa-mitten::before { + content: "\f7b5"; } + +.fa-person-rays::before { + content: "\e54d"; } + +.fa-users::before { + content: "\f0c0"; } + +.fa-eye-slash::before { + content: "\f070"; } + +.fa-flask-vial::before { + content: "\e4f3"; } + +.fa-hand::before { + content: "\f256"; } + +.fa-hand-paper::before { + content: "\f256"; } + +.fa-om::before { + content: "\f679"; } + +.fa-worm::before { + content: "\e599"; } + +.fa-house-circle-xmark::before { + content: "\e50b"; } + +.fa-plug::before { + content: "\f1e6"; } + +.fa-chevron-up::before { + content: "\f077"; } + +.fa-hand-spock::before { + content: "\f259"; } + +.fa-stopwatch::before { + content: "\f2f2"; } + +.fa-face-kiss::before { + content: "\f596"; } + +.fa-kiss::before { + content: "\f596"; } + +.fa-bridge-circle-xmark::before { + content: "\e4cb"; } + +.fa-face-grin-tongue::before { + content: "\f589"; } + +.fa-grin-tongue::before { + content: "\f589"; } + +.fa-chess-bishop::before { + content: "\f43a"; } + +.fa-face-grin-wink::before { + content: "\f58c"; } + +.fa-grin-wink::before { + content: "\f58c"; } + +.fa-ear-deaf::before { + content: "\f2a4"; } + +.fa-deaf::before { + content: "\f2a4"; } + +.fa-deafness::before { + content: "\f2a4"; } + +.fa-hard-of-hearing::before { + content: "\f2a4"; } + +.fa-road-circle-check::before { + content: "\e564"; } + +.fa-dice-five::before { + content: "\f523"; } + +.fa-square-rss::before { + content: "\f143"; } + +.fa-rss-square::before { + content: "\f143"; } + +.fa-land-mine-on::before { + content: "\e51b"; } + +.fa-i-cursor::before { + content: "\f246"; } + +.fa-stamp::before { + content: "\f5bf"; } + +.fa-stairs::before { + content: "\e289"; } + +.fa-i::before { + content: "\49"; } + +.fa-hryvnia-sign::before { + content: "\f6f2"; } + +.fa-hryvnia::before { + content: "\f6f2"; } + +.fa-pills::before { + content: "\f484"; } + +.fa-face-grin-wide::before { + content: "\f581"; } + +.fa-grin-alt::before { + content: "\f581"; } + +.fa-tooth::before { + content: "\f5c9"; } + +.fa-v::before { + content: "\56"; } + +.fa-bangladeshi-taka-sign::before { + content: "\e2e6"; } + +.fa-bicycle::before { + content: "\f206"; } + +.fa-staff-snake::before { + content: "\e579"; } + +.fa-rod-asclepius::before { + content: "\e579"; } + +.fa-rod-snake::before { + content: "\e579"; } + +.fa-staff-aesculapius::before { + content: "\e579"; } + +.fa-head-side-cough-slash::before { + content: "\e062"; } + +.fa-truck-medical::before { + content: "\f0f9"; } + +.fa-ambulance::before { + content: "\f0f9"; } + +.fa-wheat-awn-circle-exclamation::before { + content: "\e598"; } + +.fa-snowman::before { + content: "\f7d0"; } + +.fa-mortar-pestle::before { + content: "\f5a7"; } + +.fa-road-barrier::before { + content: "\e562"; } + +.fa-school::before { + content: "\f549"; } + +.fa-igloo::before { + content: "\f7ae"; } + +.fa-joint::before { + content: "\f595"; } + +.fa-angle-right::before { + content: "\f105"; } + +.fa-horse::before { + content: "\f6f0"; } + +.fa-q::before { + content: "\51"; } + +.fa-g::before { + content: "\47"; } + +.fa-notes-medical::before { + content: "\f481"; } + +.fa-temperature-half::before { + content: "\f2c9"; } + +.fa-temperature-2::before { + content: "\f2c9"; } + +.fa-thermometer-2::before { + content: "\f2c9"; } + +.fa-thermometer-half::before { + content: "\f2c9"; } + +.fa-dong-sign::before { + content: "\e169"; } + +.fa-capsules::before { + content: "\f46b"; } + +.fa-poo-storm::before { + content: "\f75a"; } + +.fa-poo-bolt::before { + content: "\f75a"; } + +.fa-face-frown-open::before { + content: "\f57a"; } + +.fa-frown-open::before { + content: "\f57a"; } + +.fa-hand-point-up::before { + content: "\f0a6"; } + +.fa-money-bill::before { + content: "\f0d6"; } + +.fa-bookmark::before { + content: "\f02e"; } + +.fa-align-justify::before { + content: "\f039"; } + +.fa-umbrella-beach::before { + content: "\f5ca"; } + +.fa-helmet-un::before { + content: "\e503"; } + +.fa-bullseye::before { + content: "\f140"; } + +.fa-bacon::before { + content: "\f7e5"; } + +.fa-hand-point-down::before { + content: "\f0a7"; } + +.fa-arrow-up-from-bracket::before { + content: "\e09a"; } + +.fa-folder::before { + content: "\f07b"; } + +.fa-folder-blank::before { + content: "\f07b"; } + +.fa-file-waveform::before { + content: "\f478"; } + +.fa-file-medical-alt::before { + content: "\f478"; } + +.fa-radiation::before { + content: "\f7b9"; } + +.fa-chart-simple::before { + content: "\e473"; } + +.fa-mars-stroke::before { + content: "\f229"; } + +.fa-vial::before { + content: "\f492"; } + +.fa-gauge::before { + content: "\f624"; } + +.fa-dashboard::before { + content: "\f624"; } + +.fa-gauge-med::before { + content: "\f624"; } + +.fa-tachometer-alt-average::before { + content: "\f624"; } + +.fa-wand-magic-sparkles::before { + content: "\e2ca"; } + +.fa-magic-wand-sparkles::before { + content: "\e2ca"; } + +.fa-e::before { + content: "\45"; } + +.fa-pen-clip::before { + content: "\f305"; } + +.fa-pen-alt::before { + content: "\f305"; } + +.fa-bridge-circle-exclamation::before { + content: "\e4ca"; } + +.fa-user::before { + content: "\f007"; } + +.fa-school-circle-check::before { + content: "\e56b"; } + +.fa-dumpster::before { + content: "\f793"; } + +.fa-van-shuttle::before { + content: "\f5b6"; } + +.fa-shuttle-van::before { + content: "\f5b6"; } + +.fa-building-user::before { + content: "\e4da"; } + +.fa-square-caret-left::before { + content: "\f191"; } + +.fa-caret-square-left::before { + content: "\f191"; } + +.fa-highlighter::before { + content: "\f591"; } + +.fa-key::before { + content: "\f084"; } + +.fa-bullhorn::before { + content: "\f0a1"; } + +.fa-globe::before { + content: "\f0ac"; } + +.fa-synagogue::before { + content: "\f69b"; } + +.fa-person-half-dress::before { + content: "\e548"; } + +.fa-road-bridge::before { + content: "\e563"; } + +.fa-location-arrow::before { + content: "\f124"; } + +.fa-c::before { + content: "\43"; } + +.fa-tablet-button::before { + content: "\f10a"; } + +.fa-building-lock::before { + content: "\e4d6"; } + +.fa-pizza-slice::before { + content: "\f818"; } + +.fa-money-bill-wave::before { + content: "\f53a"; } + +.fa-chart-area::before { + content: "\f1fe"; } + +.fa-area-chart::before { + content: "\f1fe"; } + +.fa-house-flag::before { + content: "\e50d"; } + +.fa-person-circle-minus::before { + content: "\e540"; } + +.fa-ban::before { + content: "\f05e"; } + +.fa-cancel::before { + content: "\f05e"; } + +.fa-camera-rotate::before { + content: "\e0d8"; } + +.fa-spray-can-sparkles::before { + content: "\f5d0"; } + +.fa-air-freshener::before { + content: "\f5d0"; } + +.fa-star::before { + content: "\f005"; } + +.fa-repeat::before { + content: "\f363"; } + +.fa-cross::before { + content: "\f654"; } + +.fa-box::before { + content: "\f466"; } + +.fa-venus-mars::before { + content: "\f228"; } + +.fa-arrow-pointer::before { + content: "\f245"; } + +.fa-mouse-pointer::before { + content: "\f245"; } + +.fa-maximize::before { + content: "\f31e"; } + +.fa-expand-arrows-alt::before { + content: "\f31e"; } + +.fa-charging-station::before { + content: "\f5e7"; } + +.fa-shapes::before { + content: "\f61f"; } + +.fa-triangle-circle-square::before { + content: "\f61f"; } + +.fa-shuffle::before { + content: "\f074"; } + +.fa-random::before { + content: "\f074"; } + +.fa-person-running::before { + content: "\f70c"; } + +.fa-running::before { + content: "\f70c"; } + +.fa-mobile-retro::before { + content: "\e527"; } + +.fa-grip-lines-vertical::before { + content: "\f7a5"; } + +.fa-spider::before { + content: "\f717"; } + +.fa-hands-bound::before { + content: "\e4f9"; } + +.fa-file-invoice-dollar::before { + content: "\f571"; } + +.fa-plane-circle-exclamation::before { + content: "\e556"; } + +.fa-x-ray::before { + content: "\f497"; } + +.fa-spell-check::before { + content: "\f891"; } + +.fa-slash::before { + content: "\f715"; } + +.fa-computer-mouse::before { + content: "\f8cc"; } + +.fa-mouse::before { + content: "\f8cc"; } + +.fa-arrow-right-to-bracket::before { + content: "\f090"; } + +.fa-sign-in::before { + content: "\f090"; } + +.fa-shop-slash::before { + content: "\e070"; } + +.fa-store-alt-slash::before { + content: "\e070"; } + +.fa-server::before { + content: "\f233"; } + +.fa-virus-covid-slash::before { + content: "\e4a9"; } + +.fa-shop-lock::before { + content: "\e4a5"; } + +.fa-hourglass-start::before { + content: "\f251"; } + +.fa-hourglass-1::before { + content: "\f251"; } + +.fa-blender-phone::before { + content: "\f6b6"; } + +.fa-building-wheat::before { + content: "\e4db"; } + +.fa-person-breastfeeding::before { + content: "\e53a"; } + +.fa-right-to-bracket::before { + content: "\f2f6"; } + +.fa-sign-in-alt::before { + content: "\f2f6"; } + +.fa-venus::before { + content: "\f221"; } + +.fa-passport::before { + content: "\f5ab"; } + +.fa-heart-pulse::before { + content: "\f21e"; } + +.fa-heartbeat::before { + content: "\f21e"; } + +.fa-people-carry-box::before { + content: "\f4ce"; } + +.fa-people-carry::before { + content: "\f4ce"; } + +.fa-temperature-high::before { + content: "\f769"; } + +.fa-microchip::before { + content: "\f2db"; } + +.fa-crown::before { + content: "\f521"; } + +.fa-weight-hanging::before { + content: "\f5cd"; } + +.fa-xmarks-lines::before { + content: "\e59a"; } + +.fa-file-prescription::before { + content: "\f572"; } + +.fa-weight-scale::before { + content: "\f496"; } + +.fa-weight::before { + content: "\f496"; } + +.fa-user-group::before { + content: "\f500"; } + +.fa-user-friends::before { + content: "\f500"; } + +.fa-arrow-up-a-z::before { + content: "\f15e"; } + +.fa-sort-alpha-up::before { + content: "\f15e"; } + +.fa-chess-knight::before { + content: "\f441"; } + +.fa-face-laugh-squint::before { + content: "\f59b"; } + +.fa-laugh-squint::before { + content: "\f59b"; } + +.fa-wheelchair::before { + content: "\f193"; } + +.fa-circle-arrow-up::before { + content: "\f0aa"; } + +.fa-arrow-circle-up::before { + content: "\f0aa"; } + +.fa-toggle-on::before { + content: "\f205"; } + +.fa-person-walking::before { + content: "\f554"; } + +.fa-walking::before { + content: "\f554"; } + +.fa-l::before { + content: "\4c"; } + +.fa-fire::before { + content: "\f06d"; } + +.fa-bed-pulse::before { + content: "\f487"; } + +.fa-procedures::before { + content: "\f487"; } + +.fa-shuttle-space::before { + content: "\f197"; } + +.fa-space-shuttle::before { + content: "\f197"; } + +.fa-face-laugh::before { + content: "\f599"; } + +.fa-laugh::before { + content: "\f599"; } + +.fa-folder-open::before { + content: "\f07c"; } + +.fa-heart-circle-plus::before { + content: "\e500"; } + +.fa-code-fork::before { + content: "\e13b"; } + +.fa-city::before { + content: "\f64f"; } + +.fa-microphone-lines::before { + content: "\f3c9"; } + +.fa-microphone-alt::before { + content: "\f3c9"; } + +.fa-pepper-hot::before { + content: "\f816"; } + +.fa-unlock::before { + content: "\f09c"; } + +.fa-colon-sign::before { + content: "\e140"; } + +.fa-headset::before { + content: "\f590"; } + +.fa-store-slash::before { + content: "\e071"; } + +.fa-road-circle-xmark::before { + content: "\e566"; } + +.fa-user-minus::before { + content: "\f503"; } + +.fa-mars-stroke-up::before { + content: "\f22a"; } + +.fa-mars-stroke-v::before { + content: "\f22a"; } + +.fa-champagne-glasses::before { + content: "\f79f"; } + +.fa-glass-cheers::before { + content: "\f79f"; } + +.fa-clipboard::before { + content: "\f328"; } + +.fa-house-circle-exclamation::before { + content: "\e50a"; } + +.fa-file-arrow-up::before { + content: "\f574"; } + +.fa-file-upload::before { + content: "\f574"; } + +.fa-wifi::before { + content: "\f1eb"; } + +.fa-wifi-3::before { + content: "\f1eb"; } + +.fa-wifi-strong::before { + content: "\f1eb"; } + +.fa-bath::before { + content: "\f2cd"; } + +.fa-bathtub::before { + content: "\f2cd"; } + +.fa-underline::before { + content: "\f0cd"; } + +.fa-user-pen::before { + content: "\f4ff"; } + +.fa-user-edit::before { + content: "\f4ff"; } + +.fa-signature::before { + content: "\f5b7"; } + +.fa-stroopwafel::before { + content: "\f551"; } + +.fa-bold::before { + content: "\f032"; } + +.fa-anchor-lock::before { + content: "\e4ad"; } + +.fa-building-ngo::before { + content: "\e4d7"; } + +.fa-manat-sign::before { + content: "\e1d5"; } + +.fa-not-equal::before { + content: "\f53e"; } + +.fa-border-top-left::before { + content: "\f853"; } + +.fa-border-style::before { + content: "\f853"; } + +.fa-map-location-dot::before { + content: "\f5a0"; } + +.fa-map-marked-alt::before { + content: "\f5a0"; } + +.fa-jedi::before { + content: "\f669"; } + +.fa-square-poll-vertical::before { + content: "\f681"; } + +.fa-poll::before { + content: "\f681"; } + +.fa-mug-hot::before { + content: "\f7b6"; } + +.fa-car-battery::before { + content: "\f5df"; } + +.fa-battery-car::before { + content: "\f5df"; } + +.fa-gift::before { + content: "\f06b"; } + +.fa-dice-two::before { + content: "\f528"; } + +.fa-chess-queen::before { + content: "\f445"; } + +.fa-glasses::before { + content: "\f530"; } + +.fa-chess-board::before { + content: "\f43c"; } + +.fa-building-circle-check::before { + content: "\e4d2"; } + +.fa-person-chalkboard::before { + content: "\e53d"; } + +.fa-mars-stroke-right::before { + content: "\f22b"; } + +.fa-mars-stroke-h::before { + content: "\f22b"; } + +.fa-hand-back-fist::before { + content: "\f255"; } + +.fa-hand-rock::before { + content: "\f255"; } + +.fa-square-caret-up::before { + content: "\f151"; } + +.fa-caret-square-up::before { + content: "\f151"; } + +.fa-cloud-showers-water::before { + content: "\e4e4"; } + +.fa-chart-bar::before { + content: "\f080"; } + +.fa-bar-chart::before { + content: "\f080"; } + +.fa-hands-bubbles::before { + content: "\e05e"; } + +.fa-hands-wash::before { + content: "\e05e"; } + +.fa-less-than-equal::before { + content: "\f537"; } + +.fa-train::before { + content: "\f238"; } + +.fa-eye-low-vision::before { + content: "\f2a8"; } + +.fa-low-vision::before { + content: "\f2a8"; } + +.fa-crow::before { + content: "\f520"; } + +.fa-sailboat::before { + content: "\e445"; } + +.fa-window-restore::before { + content: "\f2d2"; } + +.fa-square-plus::before { + content: "\f0fe"; } + +.fa-plus-square::before { + content: "\f0fe"; } + +.fa-torii-gate::before { + content: "\f6a1"; } + +.fa-frog::before { + content: "\f52e"; } + +.fa-bucket::before { + content: "\e4cf"; } + +.fa-image::before { + content: "\f03e"; } + +.fa-microphone::before { + content: "\f130"; } + +.fa-cow::before { + content: "\f6c8"; } + +.fa-caret-up::before { + content: "\f0d8"; } + +.fa-screwdriver::before { + content: "\f54a"; } + +.fa-folder-closed::before { + content: "\e185"; } + +.fa-house-tsunami::before { + content: "\e515"; } + +.fa-square-nfi::before { + content: "\e576"; } + +.fa-arrow-up-from-ground-water::before { + content: "\e4b5"; } + +.fa-martini-glass::before { + content: "\f57b"; } + +.fa-glass-martini-alt::before { + content: "\f57b"; } + +.fa-rotate-left::before { + content: "\f2ea"; } + +.fa-rotate-back::before { + content: "\f2ea"; } + +.fa-rotate-backward::before { + content: "\f2ea"; } + +.fa-undo-alt::before { + content: "\f2ea"; } + +.fa-table-columns::before { + content: "\f0db"; } + +.fa-columns::before { + content: "\f0db"; } + +.fa-lemon::before { + content: "\f094"; } + +.fa-head-side-mask::before { + content: "\e063"; } + +.fa-handshake::before { + content: "\f2b5"; } + +.fa-gem::before { + content: "\f3a5"; } + +.fa-dolly::before { + content: "\f472"; } + +.fa-dolly-box::before { + content: "\f472"; } + +.fa-smoking::before { + content: "\f48d"; } + +.fa-minimize::before { + content: "\f78c"; } + +.fa-compress-arrows-alt::before { + content: "\f78c"; } + +.fa-monument::before { + content: "\f5a6"; } + +.fa-snowplow::before { + content: "\f7d2"; } + +.fa-angles-right::before { + content: "\f101"; } + +.fa-angle-double-right::before { + content: "\f101"; } + +.fa-cannabis::before { + content: "\f55f"; } + +.fa-circle-play::before { + content: "\f144"; } + +.fa-play-circle::before { + content: "\f144"; } + +.fa-tablets::before { + content: "\f490"; } + +.fa-ethernet::before { + content: "\f796"; } + +.fa-euro-sign::before { + content: "\f153"; } + +.fa-eur::before { + content: "\f153"; } + +.fa-euro::before { + content: "\f153"; } + +.fa-chair::before { + content: "\f6c0"; } + +.fa-circle-check::before { + content: "\f058"; } + +.fa-check-circle::before { + content: "\f058"; } + +.fa-circle-stop::before { + content: "\f28d"; } + +.fa-stop-circle::before { + content: "\f28d"; } + +.fa-compass-drafting::before { + content: "\f568"; } + +.fa-drafting-compass::before { + content: "\f568"; } + +.fa-plate-wheat::before { + content: "\e55a"; } + +.fa-icicles::before { + content: "\f7ad"; } + +.fa-person-shelter::before { + content: "\e54f"; } + +.fa-neuter::before { + content: "\f22c"; } + +.fa-id-badge::before { + content: "\f2c1"; } + +.fa-marker::before { + content: "\f5a1"; } + +.fa-face-laugh-beam::before { + content: "\f59a"; } + +.fa-laugh-beam::before { + content: "\f59a"; } + +.fa-helicopter-symbol::before { + content: "\e502"; } + +.fa-universal-access::before { + content: "\f29a"; } + +.fa-circle-chevron-up::before { + content: "\f139"; } + +.fa-chevron-circle-up::before { + content: "\f139"; } + +.fa-lari-sign::before { + content: "\e1c8"; } + +.fa-volcano::before { + content: "\f770"; } + +.fa-person-walking-dashed-line-arrow-right::before { + content: "\e553"; } + +.fa-sterling-sign::before { + content: "\f154"; } + +.fa-gbp::before { + content: "\f154"; } + +.fa-pound-sign::before { + content: "\f154"; } + +.fa-viruses::before { + content: "\e076"; } + +.fa-square-person-confined::before { + content: "\e577"; } + +.fa-user-tie::before { + content: "\f508"; } + +.fa-arrow-down-long::before { + content: "\f175"; } + +.fa-long-arrow-down::before { + content: "\f175"; } + +.fa-tent-arrow-down-to-line::before { + content: "\e57e"; } + +.fa-certificate::before { + content: "\f0a3"; } + +.fa-reply-all::before { + content: "\f122"; } + +.fa-mail-reply-all::before { + content: "\f122"; } + +.fa-suitcase::before { + content: "\f0f2"; } + +.fa-person-skating::before { + content: "\f7c5"; } + +.fa-skating::before { + content: "\f7c5"; } + +.fa-filter-circle-dollar::before { + content: "\f662"; } + +.fa-funnel-dollar::before { + content: "\f662"; } + +.fa-camera-retro::before { + content: "\f083"; } + +.fa-circle-arrow-down::before { + content: "\f0ab"; } + +.fa-arrow-circle-down::before { + content: "\f0ab"; } + +.fa-file-import::before { + content: "\f56f"; } + +.fa-arrow-right-to-file::before { + content: "\f56f"; } + +.fa-square-arrow-up-right::before { + content: "\f14c"; } + +.fa-external-link-square::before { + content: "\f14c"; } + +.fa-box-open::before { + content: "\f49e"; } + +.fa-scroll::before { + content: "\f70e"; } + +.fa-spa::before { + content: "\f5bb"; } + +.fa-location-pin-lock::before { + content: "\e51f"; } + +.fa-pause::before { + content: "\f04c"; } + +.fa-hill-avalanche::before { + content: "\e507"; } + +.fa-temperature-empty::before { + content: "\f2cb"; } + +.fa-temperature-0::before { + content: "\f2cb"; } + +.fa-thermometer-0::before { + content: "\f2cb"; } + +.fa-thermometer-empty::before { + content: "\f2cb"; } + +.fa-bomb::before { + content: "\f1e2"; } + +.fa-registered::before { + content: "\f25d"; } + +.fa-address-card::before { + content: "\f2bb"; } + +.fa-contact-card::before { + content: "\f2bb"; } + +.fa-vcard::before { + content: "\f2bb"; } + +.fa-scale-unbalanced-flip::before { + content: "\f516"; } + +.fa-balance-scale-right::before { + content: "\f516"; } + +.fa-subscript::before { + content: "\f12c"; } + +.fa-diamond-turn-right::before { + content: "\f5eb"; } + +.fa-directions::before { + content: "\f5eb"; } + +.fa-burst::before { + content: "\e4dc"; } + +.fa-house-laptop::before { + content: "\e066"; } + +.fa-laptop-house::before { + content: "\e066"; } + +.fa-face-tired::before { + content: "\f5c8"; } + +.fa-tired::before { + content: "\f5c8"; } + +.fa-money-bills::before { + content: "\e1f3"; } + +.fa-smog::before { + content: "\f75f"; } + +.fa-crutch::before { + content: "\f7f7"; } + +.fa-cloud-arrow-up::before { + content: "\f0ee"; } + +.fa-cloud-upload::before { + content: "\f0ee"; } + +.fa-cloud-upload-alt::before { + content: "\f0ee"; } + +.fa-palette::before { + content: "\f53f"; } + +.fa-arrows-turn-right::before { + content: "\e4c0"; } + +.fa-vest::before { + content: "\e085"; } + +.fa-ferry::before { + content: "\e4ea"; } + +.fa-arrows-down-to-people::before { + content: "\e4b9"; } + +.fa-seedling::before { + content: "\f4d8"; } + +.fa-sprout::before { + content: "\f4d8"; } + +.fa-left-right::before { + content: "\f337"; } + +.fa-arrows-alt-h::before { + content: "\f337"; } + +.fa-boxes-packing::before { + content: "\e4c7"; } + +.fa-circle-arrow-left::before { + content: "\f0a8"; } + +.fa-arrow-circle-left::before { + content: "\f0a8"; } + +.fa-group-arrows-rotate::before { + content: "\e4f6"; } + +.fa-bowl-food::before { + content: "\e4c6"; } + +.fa-candy-cane::before { + content: "\f786"; } + +.fa-arrow-down-wide-short::before { + content: "\f160"; } + +.fa-sort-amount-asc::before { + content: "\f160"; } + +.fa-sort-amount-down::before { + content: "\f160"; } + +.fa-cloud-bolt::before { + content: "\f76c"; } + +.fa-thunderstorm::before { + content: "\f76c"; } + +.fa-text-slash::before { + content: "\f87d"; } + +.fa-remove-format::before { + content: "\f87d"; } + +.fa-face-smile-wink::before { + content: "\f4da"; } + +.fa-smile-wink::before { + content: "\f4da"; } + +.fa-file-word::before { + content: "\f1c2"; } + +.fa-file-powerpoint::before { + content: "\f1c4"; } + +.fa-arrows-left-right::before { + content: "\f07e"; } + +.fa-arrows-h::before { + content: "\f07e"; } + +.fa-house-lock::before { + content: "\e510"; } + +.fa-cloud-arrow-down::before { + content: "\f0ed"; } + +.fa-cloud-download::before { + content: "\f0ed"; } + +.fa-cloud-download-alt::before { + content: "\f0ed"; } + +.fa-children::before { + content: "\e4e1"; } + +.fa-chalkboard::before { + content: "\f51b"; } + +.fa-blackboard::before { + content: "\f51b"; } + +.fa-user-large-slash::before { + content: "\f4fa"; } + +.fa-user-alt-slash::before { + content: "\f4fa"; } + +.fa-envelope-open::before { + content: "\f2b6"; } + +.fa-handshake-simple-slash::before { + content: "\e05f"; } + +.fa-handshake-alt-slash::before { + content: "\e05f"; } + +.fa-mattress-pillow::before { + content: "\e525"; } + +.fa-guarani-sign::before { + content: "\e19a"; } + +.fa-arrows-rotate::before { + content: "\f021"; } + +.fa-refresh::before { + content: "\f021"; } + +.fa-sync::before { + content: "\f021"; } + +.fa-fire-extinguisher::before { + content: "\f134"; } + +.fa-cruzeiro-sign::before { + content: "\e152"; } + +.fa-greater-than-equal::before { + content: "\f532"; } + +.fa-shield-halved::before { + content: "\f3ed"; } + +.fa-shield-alt::before { + content: "\f3ed"; } + +.fa-book-atlas::before { + content: "\f558"; } + +.fa-atlas::before { + content: "\f558"; } + +.fa-virus::before { + content: "\e074"; } + +.fa-envelope-circle-check::before { + content: "\e4e8"; } + +.fa-layer-group::before { + content: "\f5fd"; } + +.fa-arrows-to-dot::before { + content: "\e4be"; } + +.fa-archway::before { + content: "\f557"; } + +.fa-heart-circle-check::before { + content: "\e4fd"; } + +.fa-house-chimney-crack::before { + content: "\f6f1"; } + +.fa-house-damage::before { + content: "\f6f1"; } + +.fa-file-zipper::before { + content: "\f1c6"; } + +.fa-file-archive::before { + content: "\f1c6"; } + +.fa-square::before { + content: "\f0c8"; } + +.fa-martini-glass-empty::before { + content: "\f000"; } + +.fa-glass-martini::before { + content: "\f000"; } + +.fa-couch::before { + content: "\f4b8"; } + +.fa-cedi-sign::before { + content: "\e0df"; } + +.fa-italic::before { + content: "\f033"; } + +.fa-church::before { + content: "\f51d"; } + +.fa-comments-dollar::before { + content: "\f653"; } + +.fa-democrat::before { + content: "\f747"; } + +.fa-z::before { + content: "\5a"; } + +.fa-person-skiing::before { + content: "\f7c9"; } + +.fa-skiing::before { + content: "\f7c9"; } + +.fa-road-lock::before { + content: "\e567"; } + +.fa-a::before { + content: "\41"; } + +.fa-temperature-arrow-down::before { + content: "\e03f"; } + +.fa-temperature-down::before { + content: "\e03f"; } + +.fa-feather-pointed::before { + content: "\f56b"; } + +.fa-feather-alt::before { + content: "\f56b"; } + +.fa-p::before { + content: "\50"; } + +.fa-snowflake::before { + content: "\f2dc"; } + +.fa-newspaper::before { + content: "\f1ea"; } + +.fa-rectangle-ad::before { + content: "\f641"; } + +.fa-ad::before { + content: "\f641"; } + +.fa-circle-arrow-right::before { + content: "\f0a9"; } + +.fa-arrow-circle-right::before { + content: "\f0a9"; } + +.fa-filter-circle-xmark::before { + content: "\e17b"; } + +.fa-locust::before { + content: "\e520"; } + +.fa-sort::before { + content: "\f0dc"; } + +.fa-unsorted::before { + content: "\f0dc"; } + +.fa-list-ol::before { + content: "\f0cb"; } + +.fa-list-1-2::before { + content: "\f0cb"; } + +.fa-list-numeric::before { + content: "\f0cb"; } + +.fa-person-dress-burst::before { + content: "\e544"; } + +.fa-money-check-dollar::before { + content: "\f53d"; } + +.fa-money-check-alt::before { + content: "\f53d"; } + +.fa-vector-square::before { + content: "\f5cb"; } + +.fa-bread-slice::before { + content: "\f7ec"; } + +.fa-language::before { + content: "\f1ab"; } + +.fa-face-kiss-wink-heart::before { + content: "\f598"; } + +.fa-kiss-wink-heart::before { + content: "\f598"; } + +.fa-filter::before { + content: "\f0b0"; } + +.fa-question::before { + content: "\3f"; } + +.fa-file-signature::before { + content: "\f573"; } + +.fa-up-down-left-right::before { + content: "\f0b2"; } + +.fa-arrows-alt::before { + content: "\f0b2"; } + +.fa-house-chimney-user::before { + content: "\e065"; } + +.fa-hand-holding-heart::before { + content: "\f4be"; } + +.fa-puzzle-piece::before { + content: "\f12e"; } + +.fa-money-check::before { + content: "\f53c"; } + +.fa-star-half-stroke::before { + content: "\f5c0"; } + +.fa-star-half-alt::before { + content: "\f5c0"; } + +.fa-code::before { + content: "\f121"; } + +.fa-whiskey-glass::before { + content: "\f7a0"; } + +.fa-glass-whiskey::before { + content: "\f7a0"; } + +.fa-building-circle-exclamation::before { + content: "\e4d3"; } + +.fa-magnifying-glass-chart::before { + content: "\e522"; } + +.fa-arrow-up-right-from-square::before { + content: "\f08e"; } + +.fa-external-link::before { + content: "\f08e"; } + +.fa-cubes-stacked::before { + content: "\e4e6"; } + +.fa-won-sign::before { + content: "\f159"; } + +.fa-krw::before { + content: "\f159"; } + +.fa-won::before { + content: "\f159"; } + +.fa-virus-covid::before { + content: "\e4a8"; } + +.fa-austral-sign::before { + content: "\e0a9"; } + +.fa-f::before { + content: "\46"; } + +.fa-leaf::before { + content: "\f06c"; } + +.fa-road::before { + content: "\f018"; } + +.fa-taxi::before { + content: "\f1ba"; } + +.fa-cab::before { + content: "\f1ba"; } + +.fa-person-circle-plus::before { + content: "\e541"; } + +.fa-chart-pie::before { + content: "\f200"; } + +.fa-pie-chart::before { + content: "\f200"; } + +.fa-bolt-lightning::before { + content: "\e0b7"; } + +.fa-sack-xmark::before { + content: "\e56a"; } + +.fa-file-excel::before { + content: "\f1c3"; } + +.fa-file-contract::before { + content: "\f56c"; } + +.fa-fish-fins::before { + content: "\e4f2"; } + +.fa-building-flag::before { + content: "\e4d5"; } + +.fa-face-grin-beam::before { + content: "\f582"; } + +.fa-grin-beam::before { + content: "\f582"; } + +.fa-object-ungroup::before { + content: "\f248"; } + +.fa-poop::before { + content: "\f619"; } + +.fa-location-pin::before { + content: "\f041"; } + +.fa-map-marker::before { + content: "\f041"; } + +.fa-kaaba::before { + content: "\f66b"; } + +.fa-toilet-paper::before { + content: "\f71e"; } + +.fa-helmet-safety::before { + content: "\f807"; } + +.fa-hard-hat::before { + content: "\f807"; } + +.fa-hat-hard::before { + content: "\f807"; } + +.fa-eject::before { + content: "\f052"; } + +.fa-circle-right::before { + content: "\f35a"; } + +.fa-arrow-alt-circle-right::before { + content: "\f35a"; } + +.fa-plane-circle-check::before { + content: "\e555"; } + +.fa-face-rolling-eyes::before { + content: "\f5a5"; } + +.fa-meh-rolling-eyes::before { + content: "\f5a5"; } + +.fa-object-group::before { + content: "\f247"; } + +.fa-chart-line::before { + content: "\f201"; } + +.fa-line-chart::before { + content: "\f201"; } + +.fa-mask-ventilator::before { + content: "\e524"; } + +.fa-arrow-right::before { + content: "\f061"; } + +.fa-signs-post::before { + content: "\f277"; } + +.fa-map-signs::before { + content: "\f277"; } + +.fa-cash-register::before { + content: "\f788"; } + +.fa-person-circle-question::before { + content: "\e542"; } + +.fa-h::before { + content: "\48"; } + +.fa-tarp::before { + content: "\e57b"; } + +.fa-screwdriver-wrench::before { + content: "\f7d9"; } + +.fa-tools::before { + content: "\f7d9"; } + +.fa-arrows-to-eye::before { + content: "\e4bf"; } + +.fa-plug-circle-bolt::before { + content: "\e55b"; } + +.fa-heart::before { + content: "\f004"; } + +.fa-mars-and-venus::before { + content: "\f224"; } + +.fa-house-user::before { + content: "\e1b0"; } + +.fa-home-user::before { + content: "\e1b0"; } + +.fa-dumpster-fire::before { + content: "\f794"; } + +.fa-house-crack::before { + content: "\e3b1"; } + +.fa-martini-glass-citrus::before { + content: "\f561"; } + +.fa-cocktail::before { + content: "\f561"; } + +.fa-face-surprise::before { + content: "\f5c2"; } + +.fa-surprise::before { + content: "\f5c2"; } + +.fa-bottle-water::before { + content: "\e4c5"; } + +.fa-circle-pause::before { + content: "\f28b"; } + +.fa-pause-circle::before { + content: "\f28b"; } + +.fa-toilet-paper-slash::before { + content: "\e072"; } + +.fa-apple-whole::before { + content: "\f5d1"; } + +.fa-apple-alt::before { + content: "\f5d1"; } + +.fa-kitchen-set::before { + content: "\e51a"; } + +.fa-r::before { + content: "\52"; } + +.fa-temperature-quarter::before { + content: "\f2ca"; } + +.fa-temperature-1::before { + content: "\f2ca"; } + +.fa-thermometer-1::before { + content: "\f2ca"; } + +.fa-thermometer-quarter::before { + content: "\f2ca"; } + +.fa-cube::before { + content: "\f1b2"; } + +.fa-bitcoin-sign::before { + content: "\e0b4"; } + +.fa-shield-dog::before { + content: "\e573"; } + +.fa-solar-panel::before { + content: "\f5ba"; } + +.fa-lock-open::before { + content: "\f3c1"; } + +.fa-elevator::before { + content: "\e16d"; } + +.fa-money-bill-transfer::before { + content: "\e528"; } + +.fa-money-bill-trend-up::before { + content: "\e529"; } + +.fa-house-flood-water-circle-arrow-right::before { + content: "\e50f"; } + +.fa-square-poll-horizontal::before { + content: "\f682"; } + +.fa-poll-h::before { + content: "\f682"; } + +.fa-circle::before { + content: "\f111"; } + +.fa-backward-fast::before { + content: "\f049"; } + +.fa-fast-backward::before { + content: "\f049"; } + +.fa-recycle::before { + content: "\f1b8"; } + +.fa-user-astronaut::before { + content: "\f4fb"; } + +.fa-plane-slash::before { + content: "\e069"; } + +.fa-trademark::before { + content: "\f25c"; } + +.fa-basketball::before { + content: "\f434"; } + +.fa-basketball-ball::before { + content: "\f434"; } + +.fa-satellite-dish::before { + content: "\f7c0"; } + +.fa-circle-up::before { + content: "\f35b"; } + +.fa-arrow-alt-circle-up::before { + content: "\f35b"; } + +.fa-mobile-screen-button::before { + content: "\f3cd"; } + +.fa-mobile-alt::before { + content: "\f3cd"; } + +.fa-volume-high::before { + content: "\f028"; } + +.fa-volume-up::before { + content: "\f028"; } + +.fa-users-rays::before { + content: "\e593"; } + +.fa-wallet::before { + content: "\f555"; } + +.fa-clipboard-check::before { + content: "\f46c"; } + +.fa-file-audio::before { + content: "\f1c7"; } + +.fa-burger::before { + content: "\f805"; } + +.fa-hamburger::before { + content: "\f805"; } + +.fa-wrench::before { + content: "\f0ad"; } + +.fa-bugs::before { + content: "\e4d0"; } + +.fa-rupee-sign::before { + content: "\f156"; } + +.fa-rupee::before { + content: "\f156"; } + +.fa-file-image::before { + content: "\f1c5"; } + +.fa-circle-question::before { + content: "\f059"; } + +.fa-question-circle::before { + content: "\f059"; } + +.fa-plane-departure::before { + content: "\f5b0"; } + +.fa-handshake-slash::before { + content: "\e060"; } + +.fa-book-bookmark::before { + content: "\e0bb"; } + +.fa-code-branch::before { + content: "\f126"; } + +.fa-hat-cowboy::before { + content: "\f8c0"; } + +.fa-bridge::before { + content: "\e4c8"; } + +.fa-phone-flip::before { + content: "\f879"; } + +.fa-phone-alt::before { + content: "\f879"; } + +.fa-truck-front::before { + content: "\e2b7"; } + +.fa-cat::before { + content: "\f6be"; } + +.fa-anchor-circle-exclamation::before { + content: "\e4ab"; } + +.fa-truck-field::before { + content: "\e58d"; } + +.fa-route::before { + content: "\f4d7"; } + +.fa-clipboard-question::before { + content: "\e4e3"; } + +.fa-panorama::before { + content: "\e209"; } + +.fa-comment-medical::before { + content: "\f7f5"; } + +.fa-teeth-open::before { + content: "\f62f"; } + +.fa-file-circle-minus::before { + content: "\e4ed"; } + +.fa-tags::before { + content: "\f02c"; } + +.fa-wine-glass::before { + content: "\f4e3"; } + +.fa-forward-fast::before { + content: "\f050"; } + +.fa-fast-forward::before { + content: "\f050"; } + +.fa-face-meh-blank::before { + content: "\f5a4"; } + +.fa-meh-blank::before { + content: "\f5a4"; } + +.fa-square-parking::before { + content: "\f540"; } + +.fa-parking::before { + content: "\f540"; } + +.fa-house-signal::before { + content: "\e012"; } + +.fa-bars-progress::before { + content: "\f828"; } + +.fa-tasks-alt::before { + content: "\f828"; } + +.fa-faucet-drip::before { + content: "\e006"; } + +.fa-cart-flatbed::before { + content: "\f474"; } + +.fa-dolly-flatbed::before { + content: "\f474"; } + +.fa-ban-smoking::before { + content: "\f54d"; } + +.fa-smoking-ban::before { + content: "\f54d"; } + +.fa-terminal::before { + content: "\f120"; } + +.fa-mobile-button::before { + content: "\f10b"; } + +.fa-house-medical-flag::before { + content: "\e514"; } + +.fa-basket-shopping::before { + content: "\f291"; } + +.fa-shopping-basket::before { + content: "\f291"; } + +.fa-tape::before { + content: "\f4db"; } + +.fa-bus-simple::before { + content: "\f55e"; } + +.fa-bus-alt::before { + content: "\f55e"; } + +.fa-eye::before { + content: "\f06e"; } + +.fa-face-sad-cry::before { + content: "\f5b3"; } + +.fa-sad-cry::before { + content: "\f5b3"; } + +.fa-audio-description::before { + content: "\f29e"; } + +.fa-person-military-to-person::before { + content: "\e54c"; } + +.fa-file-shield::before { + content: "\e4f0"; } + +.fa-user-slash::before { + content: "\f506"; } + +.fa-pen::before { + content: "\f304"; } + +.fa-tower-observation::before { + content: "\e586"; } + +.fa-file-code::before { + content: "\f1c9"; } + +.fa-signal::before { + content: "\f012"; } + +.fa-signal-5::before { + content: "\f012"; } + +.fa-signal-perfect::before { + content: "\f012"; } + +.fa-bus::before { + content: "\f207"; } + +.fa-heart-circle-xmark::before { + content: "\e501"; } + +.fa-house-chimney::before { + content: "\e3af"; } + +.fa-home-lg::before { + content: "\e3af"; } + +.fa-window-maximize::before { + content: "\f2d0"; } + +.fa-face-frown::before { + content: "\f119"; } + +.fa-frown::before { + content: "\f119"; } + +.fa-prescription::before { + content: "\f5b1"; } + +.fa-shop::before { + content: "\f54f"; } + +.fa-store-alt::before { + content: "\f54f"; } + +.fa-floppy-disk::before { + content: "\f0c7"; } + +.fa-save::before { + content: "\f0c7"; } + +.fa-vihara::before { + content: "\f6a7"; } + +.fa-scale-unbalanced::before { + content: "\f515"; } + +.fa-balance-scale-left::before { + content: "\f515"; } + +.fa-sort-up::before { + content: "\f0de"; } + +.fa-sort-asc::before { + content: "\f0de"; } + +.fa-comment-dots::before { + content: "\f4ad"; } + +.fa-commenting::before { + content: "\f4ad"; } + +.fa-plant-wilt::before { + content: "\e5aa"; } + +.fa-diamond::before { + content: "\f219"; } + +.fa-face-grin-squint::before { + content: "\f585"; } + +.fa-grin-squint::before { + content: "\f585"; } + +.fa-hand-holding-dollar::before { + content: "\f4c0"; } + +.fa-hand-holding-usd::before { + content: "\f4c0"; } + +.fa-bacterium::before { + content: "\e05a"; } + +.fa-hand-pointer::before { + content: "\f25a"; } + +.fa-drum-steelpan::before { + content: "\f56a"; } + +.fa-hand-scissors::before { + content: "\f257"; } + +.fa-hands-praying::before { + content: "\f684"; } + +.fa-praying-hands::before { + content: "\f684"; } + +.fa-arrow-rotate-right::before { + content: "\f01e"; } + +.fa-arrow-right-rotate::before { + content: "\f01e"; } + +.fa-arrow-rotate-forward::before { + content: "\f01e"; } + +.fa-redo::before { + content: "\f01e"; } + +.fa-biohazard::before { + content: "\f780"; } + +.fa-location-crosshairs::before { + content: "\f601"; } + +.fa-location::before { + content: "\f601"; } + +.fa-mars-double::before { + content: "\f227"; } + +.fa-child-dress::before { + content: "\e59c"; } + +.fa-users-between-lines::before { + content: "\e591"; } + +.fa-lungs-virus::before { + content: "\e067"; } + +.fa-face-grin-tears::before { + content: "\f588"; } + +.fa-grin-tears::before { + content: "\f588"; } + +.fa-phone::before { + content: "\f095"; } + +.fa-calendar-xmark::before { + content: "\f273"; } + +.fa-calendar-times::before { + content: "\f273"; } + +.fa-child-reaching::before { + content: "\e59d"; } + +.fa-head-side-virus::before { + content: "\e064"; } + +.fa-user-gear::before { + content: "\f4fe"; } + +.fa-user-cog::before { + content: "\f4fe"; } + +.fa-arrow-up-1-9::before { + content: "\f163"; } + +.fa-sort-numeric-up::before { + content: "\f163"; } + +.fa-door-closed::before { + content: "\f52a"; } + +.fa-shield-virus::before { + content: "\e06c"; } + +.fa-dice-six::before { + content: "\f526"; } + +.fa-mosquito-net::before { + content: "\e52c"; } + +.fa-bridge-water::before { + content: "\e4ce"; } + +.fa-person-booth::before { + content: "\f756"; } + +.fa-text-width::before { + content: "\f035"; } + +.fa-hat-wizard::before { + content: "\f6e8"; } + +.fa-pen-fancy::before { + content: "\f5ac"; } + +.fa-person-digging::before { + content: "\f85e"; } + +.fa-digging::before { + content: "\f85e"; } + +.fa-trash::before { + content: "\f1f8"; } + +.fa-gauge-simple::before { + content: "\f629"; } + +.fa-gauge-simple-med::before { + content: "\f629"; } + +.fa-tachometer-average::before { + content: "\f629"; } + +.fa-book-medical::before { + content: "\f7e6"; } + +.fa-poo::before { + content: "\f2fe"; } + +.fa-quote-right::before { + content: "\f10e"; } + +.fa-quote-right-alt::before { + content: "\f10e"; } + +.fa-shirt::before { + content: "\f553"; } + +.fa-t-shirt::before { + content: "\f553"; } + +.fa-tshirt::before { + content: "\f553"; } + +.fa-cubes::before { + content: "\f1b3"; } + +.fa-divide::before { + content: "\f529"; } + +.fa-tenge-sign::before { + content: "\f7d7"; } + +.fa-tenge::before { + content: "\f7d7"; } + +.fa-headphones::before { + content: "\f025"; } + +.fa-hands-holding::before { + content: "\f4c2"; } + +.fa-hands-clapping::before { + content: "\e1a8"; } + +.fa-republican::before { + content: "\f75e"; } + +.fa-arrow-left::before { + content: "\f060"; } + +.fa-person-circle-xmark::before { + content: "\e543"; } + +.fa-ruler::before { + content: "\f545"; } + +.fa-align-left::before { + content: "\f036"; } + +.fa-dice-d6::before { + content: "\f6d1"; } + +.fa-restroom::before { + content: "\f7bd"; } + +.fa-j::before { + content: "\4a"; } + +.fa-users-viewfinder::before { + content: "\e595"; } + +.fa-file-video::before { + content: "\f1c8"; } + +.fa-up-right-from-square::before { + content: "\f35d"; } + +.fa-external-link-alt::before { + content: "\f35d"; } + +.fa-table-cells::before { + content: "\f00a"; } + +.fa-th::before { + content: "\f00a"; } + +.fa-file-pdf::before { + content: "\f1c1"; } + +.fa-book-bible::before { + content: "\f647"; } + +.fa-bible::before { + content: "\f647"; } + +.fa-o::before { + content: "\4f"; } + +.fa-suitcase-medical::before { + content: "\f0fa"; } + +.fa-medkit::before { + content: "\f0fa"; } + +.fa-user-secret::before { + content: "\f21b"; } + +.fa-otter::before { + content: "\f700"; } + +.fa-person-dress::before { + content: "\f182"; } + +.fa-female::before { + content: "\f182"; } + +.fa-comment-dollar::before { + content: "\f651"; } + +.fa-business-time::before { + content: "\f64a"; } + +.fa-briefcase-clock::before { + content: "\f64a"; } + +.fa-table-cells-large::before { + content: "\f009"; } + +.fa-th-large::before { + content: "\f009"; } + +.fa-book-tanakh::before { + content: "\f827"; } + +.fa-tanakh::before { + content: "\f827"; } + +.fa-phone-volume::before { + content: "\f2a0"; } + +.fa-volume-control-phone::before { + content: "\f2a0"; } + +.fa-hat-cowboy-side::before { + content: "\f8c1"; } + +.fa-clipboard-user::before { + content: "\f7f3"; } + +.fa-child::before { + content: "\f1ae"; } + +.fa-lira-sign::before { + content: "\f195"; } + +.fa-satellite::before { + content: "\f7bf"; } + +.fa-plane-lock::before { + content: "\e558"; } + +.fa-tag::before { + content: "\f02b"; } + +.fa-comment::before { + content: "\f075"; } + +.fa-cake-candles::before { + content: "\f1fd"; } + +.fa-birthday-cake::before { + content: "\f1fd"; } + +.fa-cake::before { + content: "\f1fd"; } + +.fa-envelope::before { + content: "\f0e0"; } + +.fa-angles-up::before { + content: "\f102"; } + +.fa-angle-double-up::before { + content: "\f102"; } + +.fa-paperclip::before { + content: "\f0c6"; } + +.fa-arrow-right-to-city::before { + content: "\e4b3"; } + +.fa-ribbon::before { + content: "\f4d6"; } + +.fa-lungs::before { + content: "\f604"; } + +.fa-arrow-up-9-1::before { + content: "\f887"; } + +.fa-sort-numeric-up-alt::before { + content: "\f887"; } + +.fa-litecoin-sign::before { + content: "\e1d3"; } + +.fa-border-none::before { + content: "\f850"; } + +.fa-circle-nodes::before { + content: "\e4e2"; } + +.fa-parachute-box::before { + content: "\f4cd"; } + +.fa-indent::before { + content: "\f03c"; } + +.fa-truck-field-un::before { + content: "\e58e"; } + +.fa-hourglass::before { + content: "\f254"; } + +.fa-hourglass-empty::before { + content: "\f254"; } + +.fa-mountain::before { + content: "\f6fc"; } + +.fa-user-doctor::before { + content: "\f0f0"; } + +.fa-user-md::before { + content: "\f0f0"; } + +.fa-circle-info::before { + content: "\f05a"; } + +.fa-info-circle::before { + content: "\f05a"; } + +.fa-cloud-meatball::before { + content: "\f73b"; } + +.fa-camera::before { + content: "\f030"; } + +.fa-camera-alt::before { + content: "\f030"; } + +.fa-square-virus::before { + content: "\e578"; } + +.fa-meteor::before { + content: "\f753"; } + +.fa-car-on::before { + content: "\e4dd"; } + +.fa-sleigh::before { + content: "\f7cc"; } + +.fa-arrow-down-1-9::before { + content: "\f162"; } + +.fa-sort-numeric-asc::before { + content: "\f162"; } + +.fa-sort-numeric-down::before { + content: "\f162"; } + +.fa-hand-holding-droplet::before { + content: "\f4c1"; } + +.fa-hand-holding-water::before { + content: "\f4c1"; } + +.fa-water::before { + content: "\f773"; } + +.fa-calendar-check::before { + content: "\f274"; } + +.fa-braille::before { + content: "\f2a1"; } + +.fa-prescription-bottle-medical::before { + content: "\f486"; } + +.fa-prescription-bottle-alt::before { + content: "\f486"; } + +.fa-landmark::before { + content: "\f66f"; } + +.fa-truck::before { + content: "\f0d1"; } + +.fa-crosshairs::before { + content: "\f05b"; } + +.fa-person-cane::before { + content: "\e53c"; } + +.fa-tent::before { + content: "\e57d"; } + +.fa-vest-patches::before { + content: "\e086"; } + +.fa-check-double::before { + content: "\f560"; } + +.fa-arrow-down-a-z::before { + content: "\f15d"; } + +.fa-sort-alpha-asc::before { + content: "\f15d"; } + +.fa-sort-alpha-down::before { + content: "\f15d"; } + +.fa-money-bill-wheat::before { + content: "\e52a"; } + +.fa-cookie::before { + content: "\f563"; } + +.fa-arrow-rotate-left::before { + content: "\f0e2"; } + +.fa-arrow-left-rotate::before { + content: "\f0e2"; } + +.fa-arrow-rotate-back::before { + content: "\f0e2"; } + +.fa-arrow-rotate-backward::before { + content: "\f0e2"; } + +.fa-undo::before { + content: "\f0e2"; } + +.fa-hard-drive::before { + content: "\f0a0"; } + +.fa-hdd::before { + content: "\f0a0"; } + +.fa-face-grin-squint-tears::before { + content: "\f586"; } + +.fa-grin-squint-tears::before { + content: "\f586"; } + +.fa-dumbbell::before { + content: "\f44b"; } + +.fa-rectangle-list::before { + content: "\f022"; } + +.fa-list-alt::before { + content: "\f022"; } + +.fa-tarp-droplet::before { + content: "\e57c"; } + +.fa-house-medical-circle-check::before { + content: "\e511"; } + +.fa-person-skiing-nordic::before { + content: "\f7ca"; } + +.fa-skiing-nordic::before { + content: "\f7ca"; } + +.fa-calendar-plus::before { + content: "\f271"; } + +.fa-plane-arrival::before { + content: "\f5af"; } + +.fa-circle-left::before { + content: "\f359"; } + +.fa-arrow-alt-circle-left::before { + content: "\f359"; } + +.fa-train-subway::before { + content: "\f239"; } + +.fa-subway::before { + content: "\f239"; } + +.fa-chart-gantt::before { + content: "\e0e4"; } + +.fa-indian-rupee-sign::before { + content: "\e1bc"; } + +.fa-indian-rupee::before { + content: "\e1bc"; } + +.fa-inr::before { + content: "\e1bc"; } + +.fa-crop-simple::before { + content: "\f565"; } + +.fa-crop-alt::before { + content: "\f565"; } + +.fa-money-bill-1::before { + content: "\f3d1"; } + +.fa-money-bill-alt::before { + content: "\f3d1"; } + +.fa-left-long::before { + content: "\f30a"; } + +.fa-long-arrow-alt-left::before { + content: "\f30a"; } + +.fa-dna::before { + content: "\f471"; } + +.fa-virus-slash::before { + content: "\e075"; } + +.fa-minus::before { + content: "\f068"; } + +.fa-subtract::before { + content: "\f068"; } + +.fa-chess::before { + content: "\f439"; } + +.fa-arrow-left-long::before { + content: "\f177"; } + +.fa-long-arrow-left::before { + content: "\f177"; } + +.fa-plug-circle-check::before { + content: "\e55c"; } + +.fa-street-view::before { + content: "\f21d"; } + +.fa-franc-sign::before { + content: "\e18f"; } + +.fa-volume-off::before { + content: "\f026"; } + +.fa-hands-asl-interpreting::before { + content: "\f2a3"; } + +.fa-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-asl-interpreting::before { + content: "\f2a3"; } + +.fa-hands-american-sign-language-interpreting::before { + content: "\f2a3"; } + +.fa-gear::before { + content: "\f013"; } + +.fa-cog::before { + content: "\f013"; } + +.fa-droplet-slash::before { + content: "\f5c7"; } + +.fa-tint-slash::before { + content: "\f5c7"; } + +.fa-mosque::before { + content: "\f678"; } + +.fa-mosquito::before { + content: "\e52b"; } + +.fa-star-of-david::before { + content: "\f69a"; } + +.fa-person-military-rifle::before { + content: "\e54b"; } + +.fa-cart-shopping::before { + content: "\f07a"; } + +.fa-shopping-cart::before { + content: "\f07a"; } + +.fa-vials::before { + content: "\f493"; } + +.fa-plug-circle-plus::before { + content: "\e55f"; } + +.fa-place-of-worship::before { + content: "\f67f"; } + +.fa-grip-vertical::before { + content: "\f58e"; } + +.fa-arrow-turn-up::before { + content: "\f148"; } + +.fa-level-up::before { + content: "\f148"; } + +.fa-u::before { + content: "\55"; } + +.fa-square-root-variable::before { + content: "\f698"; } + +.fa-square-root-alt::before { + content: "\f698"; } + +.fa-clock::before { + content: "\f017"; } + +.fa-clock-four::before { + content: "\f017"; } + +.fa-backward-step::before { + content: "\f048"; } + +.fa-step-backward::before { + content: "\f048"; } + +.fa-pallet::before { + content: "\f482"; } + +.fa-faucet::before { + content: "\e005"; } + +.fa-baseball-bat-ball::before { + content: "\f432"; } + +.fa-s::before { + content: "\53"; } + +.fa-timeline::before { + content: "\e29c"; } + +.fa-keyboard::before { + content: "\f11c"; } + +.fa-caret-down::before { + content: "\f0d7"; } + +.fa-house-chimney-medical::before { + content: "\f7f2"; } + +.fa-clinic-medical::before { + content: "\f7f2"; } + +.fa-temperature-three-quarters::before { + content: "\f2c8"; } + +.fa-temperature-3::before { + content: "\f2c8"; } + +.fa-thermometer-3::before { + content: "\f2c8"; } + +.fa-thermometer-three-quarters::before { + content: "\f2c8"; } + +.fa-mobile-screen::before { + content: "\f3cf"; } + +.fa-mobile-android-alt::before { + content: "\f3cf"; } + +.fa-plane-up::before { + content: "\e22d"; } + +.fa-piggy-bank::before { + content: "\f4d3"; } + +.fa-battery-half::before { + content: "\f242"; } + +.fa-battery-3::before { + content: "\f242"; } + +.fa-mountain-city::before { + content: "\e52e"; } + +.fa-coins::before { + content: "\f51e"; } + +.fa-khanda::before { + content: "\f66d"; } + +.fa-sliders::before { + content: "\f1de"; } + +.fa-sliders-h::before { + content: "\f1de"; } + +.fa-folder-tree::before { + content: "\f802"; } + +.fa-network-wired::before { + content: "\f6ff"; } + +.fa-map-pin::before { + content: "\f276"; } + +.fa-hamsa::before { + content: "\f665"; } + +.fa-cent-sign::before { + content: "\e3f5"; } + +.fa-flask::before { + content: "\f0c3"; } + +.fa-person-pregnant::before { + content: "\e31e"; } + +.fa-wand-sparkles::before { + content: "\f72b"; } + +.fa-ellipsis-vertical::before { + content: "\f142"; } + +.fa-ellipsis-v::before { + content: "\f142"; } + +.fa-ticket::before { + content: "\f145"; } + +.fa-power-off::before { + content: "\f011"; } + +.fa-right-long::before { + content: "\f30b"; } + +.fa-long-arrow-alt-right::before { + content: "\f30b"; } + +.fa-flag-usa::before { + content: "\f74d"; } + +.fa-laptop-file::before { + content: "\e51d"; } + +.fa-tty::before { + content: "\f1e4"; } + +.fa-teletype::before { + content: "\f1e4"; } + +.fa-diagram-next::before { + content: "\e476"; } + +.fa-person-rifle::before { + content: "\e54e"; } + +.fa-house-medical-circle-exclamation::before { + content: "\e512"; } + +.fa-closed-captioning::before { + content: "\f20a"; } + +.fa-person-hiking::before { + content: "\f6ec"; } + +.fa-hiking::before { + content: "\f6ec"; } + +.fa-venus-double::before { + content: "\f226"; } + +.fa-images::before { + content: "\f302"; } + +.fa-calculator::before { + content: "\f1ec"; } + +.fa-people-pulling::before { + content: "\e535"; } + +.fa-n::before { + content: "\4e"; } + +.fa-cable-car::before { + content: "\f7da"; } + +.fa-tram::before { + content: "\f7da"; } + +.fa-cloud-rain::before { + content: "\f73d"; } + +.fa-building-circle-xmark::before { + content: "\e4d4"; } + +.fa-ship::before { + content: "\f21a"; } + +.fa-arrows-down-to-line::before { + content: "\e4b8"; } + +.fa-download::before { + content: "\f019"; } + +.fa-face-grin::before { + content: "\f580"; } + +.fa-grin::before { + content: "\f580"; } + +.fa-delete-left::before { + content: "\f55a"; } + +.fa-backspace::before { + content: "\f55a"; } + +.fa-eye-dropper::before { + content: "\f1fb"; } + +.fa-eye-dropper-empty::before { + content: "\f1fb"; } + +.fa-eyedropper::before { + content: "\f1fb"; } + +.fa-file-circle-check::before { + content: "\e5a0"; } + +.fa-forward::before { + content: "\f04e"; } + +.fa-mobile::before { + content: "\f3ce"; } + +.fa-mobile-android::before { + content: "\f3ce"; } + +.fa-mobile-phone::before { + content: "\f3ce"; } + +.fa-face-meh::before { + content: "\f11a"; } + +.fa-meh::before { + content: "\f11a"; } + +.fa-align-center::before { + content: "\f037"; } + +.fa-book-skull::before { + content: "\f6b7"; } + +.fa-book-dead::before { + content: "\f6b7"; } + +.fa-id-card::before { + content: "\f2c2"; } + +.fa-drivers-license::before { + content: "\f2c2"; } + +.fa-outdent::before { + content: "\f03b"; } + +.fa-dedent::before { + content: "\f03b"; } + +.fa-heart-circle-exclamation::before { + content: "\e4fe"; } + +.fa-house::before { + content: "\f015"; } + +.fa-home::before { + content: "\f015"; } + +.fa-home-alt::before { + content: "\f015"; } + +.fa-home-lg-alt::before { + content: "\f015"; } + +.fa-calendar-week::before { + content: "\f784"; } + +.fa-laptop-medical::before { + content: "\f812"; } + +.fa-b::before { + content: "\42"; } + +.fa-file-medical::before { + content: "\f477"; } + +.fa-dice-one::before { + content: "\f525"; } + +.fa-kiwi-bird::before { + content: "\f535"; } + +.fa-arrow-right-arrow-left::before { + content: "\f0ec"; } + +.fa-exchange::before { + content: "\f0ec"; } + +.fa-rotate-right::before { + content: "\f2f9"; } + +.fa-redo-alt::before { + content: "\f2f9"; } + +.fa-rotate-forward::before { + content: "\f2f9"; } + +.fa-utensils::before { + content: "\f2e7"; } + +.fa-cutlery::before { + content: "\f2e7"; } + +.fa-arrow-up-wide-short::before { + content: "\f161"; } + +.fa-sort-amount-up::before { + content: "\f161"; } + +.fa-mill-sign::before { + content: "\e1ed"; } + +.fa-bowl-rice::before { + content: "\e2eb"; } + +.fa-skull::before { + content: "\f54c"; } + +.fa-tower-broadcast::before { + content: "\f519"; } + +.fa-broadcast-tower::before { + content: "\f519"; } + +.fa-truck-pickup::before { + content: "\f63c"; } + +.fa-up-long::before { + content: "\f30c"; } + +.fa-long-arrow-alt-up::before { + content: "\f30c"; } + +.fa-stop::before { + content: "\f04d"; } + +.fa-code-merge::before { + content: "\f387"; } + +.fa-upload::before { + content: "\f093"; } + +.fa-hurricane::before { + content: "\f751"; } + +.fa-mound::before { + content: "\e52d"; } + +.fa-toilet-portable::before { + content: "\e583"; } + +.fa-compact-disc::before { + content: "\f51f"; } + +.fa-file-arrow-down::before { + content: "\f56d"; } + +.fa-file-download::before { + content: "\f56d"; } + +.fa-caravan::before { + content: "\f8ff"; } + +.fa-shield-cat::before { + content: "\e572"; } + +.fa-bolt::before { + content: "\f0e7"; } + +.fa-zap::before { + content: "\f0e7"; } + +.fa-glass-water::before { + content: "\e4f4"; } + +.fa-oil-well::before { + content: "\e532"; } + +.fa-vault::before { + content: "\e2c5"; } + +.fa-mars::before { + content: "\f222"; } + +.fa-toilet::before { + content: "\f7d8"; } + +.fa-plane-circle-xmark::before { + content: "\e557"; } + +.fa-yen-sign::before { + content: "\f157"; } + +.fa-cny::before { + content: "\f157"; } + +.fa-jpy::before { + content: "\f157"; } + +.fa-rmb::before { + content: "\f157"; } + +.fa-yen::before { + content: "\f157"; } + +.fa-ruble-sign::before { + content: "\f158"; } + +.fa-rouble::before { + content: "\f158"; } + +.fa-rub::before { + content: "\f158"; } + +.fa-ruble::before { + content: "\f158"; } + +.fa-sun::before { + content: "\f185"; } + +.fa-guitar::before { + content: "\f7a6"; } + +.fa-face-laugh-wink::before { + content: "\f59c"; } + +.fa-laugh-wink::before { + content: "\f59c"; } + +.fa-horse-head::before { + content: "\f7ab"; } + +.fa-bore-hole::before { + content: "\e4c3"; } + +.fa-industry::before { + content: "\f275"; } + +.fa-circle-down::before { + content: "\f358"; } + +.fa-arrow-alt-circle-down::before { + content: "\f358"; } + +.fa-arrows-turn-to-dots::before { + content: "\e4c1"; } + +.fa-florin-sign::before { + content: "\e184"; } + +.fa-arrow-down-short-wide::before { + content: "\f884"; } + +.fa-sort-amount-desc::before { + content: "\f884"; } + +.fa-sort-amount-down-alt::before { + content: "\f884"; } + +.fa-less-than::before { + content: "\3c"; } + +.fa-angle-down::before { + content: "\f107"; } + +.fa-car-tunnel::before { + content: "\e4de"; } + +.fa-head-side-cough::before { + content: "\e061"; } + +.fa-grip-lines::before { + content: "\f7a4"; } + +.fa-thumbs-down::before { + content: "\f165"; } + +.fa-user-lock::before { + content: "\f502"; } + +.fa-arrow-right-long::before { + content: "\f178"; } + +.fa-long-arrow-right::before { + content: "\f178"; } + +.fa-anchor-circle-xmark::before { + content: "\e4ac"; } + +.fa-ellipsis::before { + content: "\f141"; } + +.fa-ellipsis-h::before { + content: "\f141"; } + +.fa-chess-pawn::before { + content: "\f443"; } + +.fa-kit-medical::before { + content: "\f479"; } + +.fa-first-aid::before { + content: "\f479"; } + +.fa-person-through-window::before { + content: "\e5a9"; } + +.fa-toolbox::before { + content: "\f552"; } + +.fa-hands-holding-circle::before { + content: "\e4fb"; } + +.fa-bug::before { + content: "\f188"; } + +.fa-credit-card::before { + content: "\f09d"; } + +.fa-credit-card-alt::before { + content: "\f09d"; } + +.fa-car::before { + content: "\f1b9"; } + +.fa-automobile::before { + content: "\f1b9"; } + +.fa-hand-holding-hand::before { + content: "\e4f7"; } + +.fa-book-open-reader::before { + content: "\f5da"; } + +.fa-book-reader::before { + content: "\f5da"; } + +.fa-mountain-sun::before { + content: "\e52f"; } + +.fa-arrows-left-right-to-line::before { + content: "\e4ba"; } + +.fa-dice-d20::before { + content: "\f6cf"; } + +.fa-truck-droplet::before { + content: "\e58c"; } + +.fa-file-circle-xmark::before { + content: "\e5a1"; } + +.fa-temperature-arrow-up::before { + content: "\e040"; } + +.fa-temperature-up::before { + content: "\e040"; } + +.fa-medal::before { + content: "\f5a2"; } + +.fa-bed::before { + content: "\f236"; } + +.fa-square-h::before { + content: "\f0fd"; } + +.fa-h-square::before { + content: "\f0fd"; } + +.fa-podcast::before { + content: "\f2ce"; } + +.fa-temperature-full::before { + content: "\f2c7"; } + +.fa-temperature-4::before { + content: "\f2c7"; } + +.fa-thermometer-4::before { + content: "\f2c7"; } + +.fa-thermometer-full::before { + content: "\f2c7"; } + +.fa-bell::before { + content: "\f0f3"; } + +.fa-superscript::before { + content: "\f12b"; } + +.fa-plug-circle-xmark::before { + content: "\e560"; } + +.fa-star-of-life::before { + content: "\f621"; } + +.fa-phone-slash::before { + content: "\f3dd"; } + +.fa-paint-roller::before { + content: "\f5aa"; } + +.fa-handshake-angle::before { + content: "\f4c4"; } + +.fa-hands-helping::before { + content: "\f4c4"; } + +.fa-location-dot::before { + content: "\f3c5"; } + +.fa-map-marker-alt::before { + content: "\f3c5"; } + +.fa-file::before { + content: "\f15b"; } + +.fa-greater-than::before { + content: "\3e"; } + +.fa-person-swimming::before { + content: "\f5c4"; } + +.fa-swimmer::before { + content: "\f5c4"; } + +.fa-arrow-down::before { + content: "\f063"; } + +.fa-droplet::before { + content: "\f043"; } + +.fa-tint::before { + content: "\f043"; } + +.fa-eraser::before { + content: "\f12d"; } + +.fa-earth-americas::before { + content: "\f57d"; } + +.fa-earth::before { + content: "\f57d"; } + +.fa-earth-america::before { + content: "\f57d"; } + +.fa-globe-americas::before { + content: "\f57d"; } + +.fa-person-burst::before { + content: "\e53b"; } + +.fa-dove::before { + content: "\f4ba"; } + +.fa-battery-empty::before { + content: "\f244"; } + +.fa-battery-0::before { + content: "\f244"; } + +.fa-socks::before { + content: "\f696"; } + +.fa-inbox::before { + content: "\f01c"; } + +.fa-section::before { + content: "\e447"; } + +.fa-gauge-high::before { + content: "\f625"; } + +.fa-tachometer-alt::before { + content: "\f625"; } + +.fa-tachometer-alt-fast::before { + content: "\f625"; } + +.fa-envelope-open-text::before { + content: "\f658"; } + +.fa-hospital::before { + content: "\f0f8"; } + +.fa-hospital-alt::before { + content: "\f0f8"; } + +.fa-hospital-wide::before { + content: "\f0f8"; } + +.fa-wine-bottle::before { + content: "\f72f"; } + +.fa-chess-rook::before { + content: "\f447"; } + +.fa-bars-staggered::before { + content: "\f550"; } + +.fa-reorder::before { + content: "\f550"; } + +.fa-stream::before { + content: "\f550"; } + +.fa-dharmachakra::before { + content: "\f655"; } + +.fa-hotdog::before { + content: "\f80f"; } + +.fa-person-walking-with-cane::before { + content: "\f29d"; } + +.fa-blind::before { + content: "\f29d"; } + +.fa-drum::before { + content: "\f569"; } + +.fa-ice-cream::before { + content: "\f810"; } + +.fa-heart-circle-bolt::before { + content: "\e4fc"; } + +.fa-fax::before { + content: "\f1ac"; } + +.fa-paragraph::before { + content: "\f1dd"; } + +.fa-check-to-slot::before { + content: "\f772"; } + +.fa-vote-yea::before { + content: "\f772"; } + +.fa-star-half::before { + content: "\f089"; } + +.fa-boxes-stacked::before { + content: "\f468"; } + +.fa-boxes::before { + content: "\f468"; } + +.fa-boxes-alt::before { + content: "\f468"; } + +.fa-link::before { + content: "\f0c1"; } + +.fa-chain::before { + content: "\f0c1"; } + +.fa-ear-listen::before { + content: "\f2a2"; } + +.fa-assistive-listening-systems::before { + content: "\f2a2"; } + +.fa-tree-city::before { + content: "\e587"; } + +.fa-play::before { + content: "\f04b"; } + +.fa-font::before { + content: "\f031"; } + +.fa-rupiah-sign::before { + content: "\e23d"; } + +.fa-magnifying-glass::before { + content: "\f002"; } + +.fa-search::before { + content: "\f002"; } + +.fa-table-tennis-paddle-ball::before { + content: "\f45d"; } + +.fa-ping-pong-paddle-ball::before { + content: "\f45d"; } + +.fa-table-tennis::before { + content: "\f45d"; } + +.fa-person-dots-from-line::before { + content: "\f470"; } + +.fa-diagnoses::before { + content: "\f470"; } + +.fa-trash-can-arrow-up::before { + content: "\f82a"; } + +.fa-trash-restore-alt::before { + content: "\f82a"; } + +.fa-naira-sign::before { + content: "\e1f6"; } + +.fa-cart-arrow-down::before { + content: "\f218"; } + +.fa-walkie-talkie::before { + content: "\f8ef"; } + +.fa-file-pen::before { + content: "\f31c"; } + +.fa-file-edit::before { + content: "\f31c"; } + +.fa-receipt::before { + content: "\f543"; } + +.fa-square-pen::before { + content: "\f14b"; } + +.fa-pen-square::before { + content: "\f14b"; } + +.fa-pencil-square::before { + content: "\f14b"; } + +.fa-suitcase-rolling::before { + content: "\f5c1"; } + +.fa-person-circle-exclamation::before { + content: "\e53f"; } + +.fa-chevron-down::before { + content: "\f078"; } + +.fa-battery-full::before { + content: "\f240"; } + +.fa-battery::before { + content: "\f240"; } + +.fa-battery-5::before { + content: "\f240"; } + +.fa-skull-crossbones::before { + content: "\f714"; } + +.fa-code-compare::before { + content: "\e13a"; } + +.fa-list-ul::before { + content: "\f0ca"; } + +.fa-list-dots::before { + content: "\f0ca"; } + +.fa-school-lock::before { + content: "\e56f"; } + +.fa-tower-cell::before { + content: "\e585"; } + +.fa-down-long::before { + content: "\f309"; } + +.fa-long-arrow-alt-down::before { + content: "\f309"; } + +.fa-ranking-star::before { + content: "\e561"; } + +.fa-chess-king::before { + content: "\f43f"; } + +.fa-person-harassing::before { + content: "\e549"; } + +.fa-brazilian-real-sign::before { + content: "\e46c"; } + +.fa-landmark-dome::before { + content: "\f752"; } + +.fa-landmark-alt::before { + content: "\f752"; } + +.fa-arrow-up::before { + content: "\f062"; } + +.fa-tv::before { + content: "\f26c"; } + +.fa-television::before { + content: "\f26c"; } + +.fa-tv-alt::before { + content: "\f26c"; } + +.fa-shrimp::before { + content: "\e448"; } + +.fa-list-check::before { + content: "\f0ae"; } + +.fa-tasks::before { + content: "\f0ae"; } + +.fa-jug-detergent::before { + content: "\e519"; } + +.fa-circle-user::before { + content: "\f2bd"; } + +.fa-user-circle::before { + content: "\f2bd"; } + +.fa-user-shield::before { + content: "\f505"; } + +.fa-wind::before { + content: "\f72e"; } + +.fa-car-burst::before { + content: "\f5e1"; } + +.fa-car-crash::before { + content: "\f5e1"; } + +.fa-y::before { + content: "\59"; } + +.fa-person-snowboarding::before { + content: "\f7ce"; } + +.fa-snowboarding::before { + content: "\f7ce"; } + +.fa-truck-fast::before { + content: "\f48b"; } + +.fa-shipping-fast::before { + content: "\f48b"; } + +.fa-fish::before { + content: "\f578"; } + +.fa-user-graduate::before { + content: "\f501"; } + +.fa-circle-half-stroke::before { + content: "\f042"; } + +.fa-adjust::before { + content: "\f042"; } + +.fa-clapperboard::before { + content: "\e131"; } + +.fa-circle-radiation::before { + content: "\f7ba"; } + +.fa-radiation-alt::before { + content: "\f7ba"; } + +.fa-baseball::before { + content: "\f433"; } + +.fa-baseball-ball::before { + content: "\f433"; } + +.fa-jet-fighter-up::before { + content: "\e518"; } + +.fa-diagram-project::before { + content: "\f542"; } + +.fa-project-diagram::before { + content: "\f542"; } + +.fa-copy::before { + content: "\f0c5"; } + +.fa-volume-xmark::before { + content: "\f6a9"; } + +.fa-volume-mute::before { + content: "\f6a9"; } + +.fa-volume-times::before { + content: "\f6a9"; } + +.fa-hand-sparkles::before { + content: "\e05d"; } + +.fa-grip::before { + content: "\f58d"; } + +.fa-grip-horizontal::before { + content: "\f58d"; } + +.fa-share-from-square::before { + content: "\f14d"; } + +.fa-share-square::before { + content: "\f14d"; } + +.fa-child-combatant::before { + content: "\e4e0"; } + +.fa-child-rifle::before { + content: "\e4e0"; } + +.fa-gun::before { + content: "\e19b"; } + +.fa-square-phone::before { + content: "\f098"; } + +.fa-phone-square::before { + content: "\f098"; } + +.fa-plus::before { + content: "\2b"; } + +.fa-add::before { + content: "\2b"; } + +.fa-expand::before { + content: "\f065"; } + +.fa-computer::before { + content: "\e4e5"; } + +.fa-xmark::before { + content: "\f00d"; } + +.fa-close::before { + content: "\f00d"; } + +.fa-multiply::before { + content: "\f00d"; } + +.fa-remove::before { + content: "\f00d"; } + +.fa-times::before { + content: "\f00d"; } + +.fa-arrows-up-down-left-right::before { + content: "\f047"; } + +.fa-arrows::before { + content: "\f047"; } + +.fa-chalkboard-user::before { + content: "\f51c"; } + +.fa-chalkboard-teacher::before { + content: "\f51c"; } + +.fa-peso-sign::before { + content: "\e222"; } + +.fa-building-shield::before { + content: "\e4d8"; } + +.fa-baby::before { + content: "\f77c"; } + +.fa-users-line::before { + content: "\e592"; } + +.fa-quote-left::before { + content: "\f10d"; } + +.fa-quote-left-alt::before { + content: "\f10d"; } + +.fa-tractor::before { + content: "\f722"; } + +.fa-trash-arrow-up::before { + content: "\f829"; } + +.fa-trash-restore::before { + content: "\f829"; } + +.fa-arrow-down-up-lock::before { + content: "\e4b0"; } + +.fa-lines-leaning::before { + content: "\e51e"; } + +.fa-ruler-combined::before { + content: "\f546"; } + +.fa-copyright::before { + content: "\f1f9"; } + +.fa-equals::before { + content: "\3d"; } + +.fa-blender::before { + content: "\f517"; } + +.fa-teeth::before { + content: "\f62e"; } + +.fa-shekel-sign::before { + content: "\f20b"; } + +.fa-ils::before { + content: "\f20b"; } + +.fa-shekel::before { + content: "\f20b"; } + +.fa-sheqel::before { + content: "\f20b"; } + +.fa-sheqel-sign::before { + content: "\f20b"; } + +.fa-map::before { + content: "\f279"; } + +.fa-rocket::before { + content: "\f135"; } + +.fa-photo-film::before { + content: "\f87c"; } + +.fa-photo-video::before { + content: "\f87c"; } + +.fa-folder-minus::before { + content: "\f65d"; } + +.fa-store::before { + content: "\f54e"; } + +.fa-arrow-trend-up::before { + content: "\e098"; } + +.fa-plug-circle-minus::before { + content: "\e55e"; } + +.fa-sign-hanging::before { + content: "\f4d9"; } + +.fa-sign::before { + content: "\f4d9"; } + +.fa-bezier-curve::before { + content: "\f55b"; } + +.fa-bell-slash::before { + content: "\f1f6"; } + +.fa-tablet::before { + content: "\f3fb"; } + +.fa-tablet-android::before { + content: "\f3fb"; } + +.fa-school-flag::before { + content: "\e56e"; } + +.fa-fill::before { + content: "\f575"; } + +.fa-angle-up::before { + content: "\f106"; } + +.fa-drumstick-bite::before { + content: "\f6d7"; } + +.fa-holly-berry::before { + content: "\f7aa"; } + +.fa-chevron-left::before { + content: "\f053"; } + +.fa-bacteria::before { + content: "\e059"; } + +.fa-hand-lizard::before { + content: "\f258"; } + +.fa-notdef::before { + content: "\e1fe"; } + +.fa-disease::before { + content: "\f7fa"; } + +.fa-briefcase-medical::before { + content: "\f469"; } + +.fa-genderless::before { + content: "\f22d"; } + +.fa-chevron-right::before { + content: "\f054"; } + +.fa-retweet::before { + content: "\f079"; } + +.fa-car-rear::before { + content: "\f5de"; } + +.fa-car-alt::before { + content: "\f5de"; } + +.fa-pump-soap::before { + content: "\e06b"; } + +.fa-video-slash::before { + content: "\f4e2"; } + +.fa-battery-quarter::before { + content: "\f243"; } + +.fa-battery-2::before { + content: "\f243"; } + +.fa-radio::before { + content: "\f8d7"; } + +.fa-baby-carriage::before { + content: "\f77d"; } + +.fa-carriage-baby::before { + content: "\f77d"; } + +.fa-traffic-light::before { + content: "\f637"; } + +.fa-thermometer::before { + content: "\f491"; } + +.fa-vr-cardboard::before { + content: "\f729"; } + +.fa-hand-middle-finger::before { + content: "\f806"; } + +.fa-percent::before { + content: "\25"; } + +.fa-percentage::before { + content: "\25"; } + +.fa-truck-moving::before { + content: "\f4df"; } + +.fa-glass-water-droplet::before { + content: "\e4f5"; } + +.fa-display::before { + content: "\e163"; } + +.fa-face-smile::before { + content: "\f118"; } + +.fa-smile::before { + content: "\f118"; } + +.fa-thumbtack::before { + content: "\f08d"; } + +.fa-thumb-tack::before { + content: "\f08d"; } + +.fa-trophy::before { + content: "\f091"; } + +.fa-person-praying::before { + content: "\f683"; } + +.fa-pray::before { + content: "\f683"; } + +.fa-hammer::before { + content: "\f6e3"; } + +.fa-hand-peace::before { + content: "\f25b"; } + +.fa-rotate::before { + content: "\f2f1"; } + +.fa-sync-alt::before { + content: "\f2f1"; } + +.fa-spinner::before { + content: "\f110"; } + +.fa-robot::before { + content: "\f544"; } + +.fa-peace::before { + content: "\f67c"; } + +.fa-gears::before { + content: "\f085"; } + +.fa-cogs::before { + content: "\f085"; } + +.fa-warehouse::before { + content: "\f494"; } + +.fa-arrow-up-right-dots::before { + content: "\e4b7"; } + +.fa-splotch::before { + content: "\f5bc"; } + +.fa-face-grin-hearts::before { + content: "\f584"; } + +.fa-grin-hearts::before { + content: "\f584"; } + +.fa-dice-four::before { + content: "\f524"; } + +.fa-sim-card::before { + content: "\f7c4"; } + +.fa-transgender::before { + content: "\f225"; } + +.fa-transgender-alt::before { + content: "\f225"; } + +.fa-mercury::before { + content: "\f223"; } + +.fa-arrow-turn-down::before { + content: "\f149"; } + +.fa-level-down::before { + content: "\f149"; } + +.fa-person-falling-burst::before { + content: "\e547"; } + +.fa-award::before { + content: "\f559"; } + +.fa-ticket-simple::before { + content: "\f3ff"; } + +.fa-ticket-alt::before { + content: "\f3ff"; } + +.fa-building::before { + content: "\f1ad"; } + +.fa-angles-left::before { + content: "\f100"; } + +.fa-angle-double-left::before { + content: "\f100"; } + +.fa-qrcode::before { + content: "\f029"; } + +.fa-clock-rotate-left::before { + content: "\f1da"; } + +.fa-history::before { + content: "\f1da"; } + +.fa-face-grin-beam-sweat::before { + content: "\f583"; } + +.fa-grin-beam-sweat::before { + content: "\f583"; } + +.fa-file-export::before { + content: "\f56e"; } + +.fa-arrow-right-from-file::before { + content: "\f56e"; } + +.fa-shield::before { + content: "\f132"; } + +.fa-shield-blank::before { + content: "\f132"; } + +.fa-arrow-up-short-wide::before { + content: "\f885"; } + +.fa-sort-amount-up-alt::before { + content: "\f885"; } + +.fa-house-medical::before { + content: "\e3b2"; } + +.fa-golf-ball-tee::before { + content: "\f450"; } + +.fa-golf-ball::before { + content: "\f450"; } + +.fa-circle-chevron-left::before { + content: "\f137"; } + +.fa-chevron-circle-left::before { + content: "\f137"; } + +.fa-house-chimney-window::before { + content: "\e00d"; } + +.fa-pen-nib::before { + content: "\f5ad"; } + +.fa-tent-arrow-turn-left::before { + content: "\e580"; } + +.fa-tents::before { + content: "\e582"; } + +.fa-wand-magic::before { + content: "\f0d0"; } + +.fa-magic::before { + content: "\f0d0"; } + +.fa-dog::before { + content: "\f6d3"; } + +.fa-carrot::before { + content: "\f787"; } + +.fa-moon::before { + content: "\f186"; } + +.fa-wine-glass-empty::before { + content: "\f5ce"; } + +.fa-wine-glass-alt::before { + content: "\f5ce"; } + +.fa-cheese::before { + content: "\f7ef"; } + +.fa-yin-yang::before { + content: "\f6ad"; } + +.fa-music::before { + content: "\f001"; } + +.fa-code-commit::before { + content: "\f386"; } + +.fa-temperature-low::before { + content: "\f76b"; } + +.fa-person-biking::before { + content: "\f84a"; } + +.fa-biking::before { + content: "\f84a"; } + +.fa-broom::before { + content: "\f51a"; } + +.fa-shield-heart::before { + content: "\e574"; } + +.fa-gopuram::before { + content: "\f664"; } + +.fa-earth-oceania::before { + content: "\e47b"; } + +.fa-globe-oceania::before { + content: "\e47b"; } + +.fa-square-xmark::before { + content: "\f2d3"; } + +.fa-times-square::before { + content: "\f2d3"; } + +.fa-xmark-square::before { + content: "\f2d3"; } + +.fa-hashtag::before { + content: "\23"; } + +.fa-up-right-and-down-left-from-center::before { + content: "\f424"; } + +.fa-expand-alt::before { + content: "\f424"; } + +.fa-oil-can::before { + content: "\f613"; } + +.fa-t::before { + content: "\54"; } + +.fa-hippo::before { + content: "\f6ed"; } + +.fa-chart-column::before { + content: "\e0e3"; } + +.fa-infinity::before { + content: "\f534"; } + +.fa-vial-circle-check::before { + content: "\e596"; } + +.fa-person-arrow-down-to-line::before { + content: "\e538"; } + +.fa-voicemail::before { + content: "\f897"; } + +.fa-fan::before { + content: "\f863"; } + +.fa-person-walking-luggage::before { + content: "\e554"; } + +.fa-up-down::before { + content: "\f338"; } + +.fa-arrows-alt-v::before { + content: "\f338"; } + +.fa-cloud-moon-rain::before { + content: "\f73c"; } + +.fa-calendar::before { + content: "\f133"; } + +.fa-trailer::before { + content: "\e041"; } + +.fa-bahai::before { + content: "\f666"; } + +.fa-haykal::before { + content: "\f666"; } + +.fa-sd-card::before { + content: "\f7c2"; } + +.fa-dragon::before { + content: "\f6d5"; } + +.fa-shoe-prints::before { + content: "\f54b"; } + +.fa-circle-plus::before { + content: "\f055"; } + +.fa-plus-circle::before { + content: "\f055"; } + +.fa-face-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-grin-tongue-wink::before { + content: "\f58b"; } + +.fa-hand-holding::before { + content: "\f4bd"; } + +.fa-plug-circle-exclamation::before { + content: "\e55d"; } + +.fa-link-slash::before { + content: "\f127"; } + +.fa-chain-broken::before { + content: "\f127"; } + +.fa-chain-slash::before { + content: "\f127"; } + +.fa-unlink::before { + content: "\f127"; } + +.fa-clone::before { + content: "\f24d"; } + +.fa-person-walking-arrow-loop-left::before { + content: "\e551"; } + +.fa-arrow-up-z-a::before { + content: "\f882"; } + +.fa-sort-alpha-up-alt::before { + content: "\f882"; } + +.fa-fire-flame-curved::before { + content: "\f7e4"; } + +.fa-fire-alt::before { + content: "\f7e4"; } + +.fa-tornado::before { + content: "\f76f"; } + +.fa-file-circle-plus::before { + content: "\e494"; } + +.fa-book-quran::before { + content: "\f687"; } + +.fa-quran::before { + content: "\f687"; } + +.fa-anchor::before { + content: "\f13d"; } + +.fa-border-all::before { + content: "\f84c"; } + +.fa-face-angry::before { + content: "\f556"; } + +.fa-angry::before { + content: "\f556"; } + +.fa-cookie-bite::before { + content: "\f564"; } + +.fa-arrow-trend-down::before { + content: "\e097"; } + +.fa-rss::before { + content: "\f09e"; } + +.fa-feed::before { + content: "\f09e"; } + +.fa-draw-polygon::before { + content: "\f5ee"; } + +.fa-scale-balanced::before { + content: "\f24e"; } + +.fa-balance-scale::before { + content: "\f24e"; } + +.fa-gauge-simple-high::before { + content: "\f62a"; } + +.fa-tachometer::before { + content: "\f62a"; } + +.fa-tachometer-fast::before { + content: "\f62a"; } + +.fa-shower::before { + content: "\f2cc"; } + +.fa-desktop::before { + content: "\f390"; } + +.fa-desktop-alt::before { + content: "\f390"; } + +.fa-m::before { + content: "\4d"; } + +.fa-table-list::before { + content: "\f00b"; } + +.fa-th-list::before { + content: "\f00b"; } + +.fa-comment-sms::before { + content: "\f7cd"; } + +.fa-sms::before { + content: "\f7cd"; } + +.fa-book::before { + content: "\f02d"; } + +.fa-user-plus::before { + content: "\f234"; } + +.fa-check::before { + content: "\f00c"; } + +.fa-battery-three-quarters::before { + content: "\f241"; } + +.fa-battery-4::before { + content: "\f241"; } + +.fa-house-circle-check::before { + content: "\e509"; } + +.fa-angle-left::before { + content: "\f104"; } + +.fa-diagram-successor::before { + content: "\e47a"; } + +.fa-truck-arrow-right::before { + content: "\e58b"; } + +.fa-arrows-split-up-and-left::before { + content: "\e4bc"; } + +.fa-hand-fist::before { + content: "\f6de"; } + +.fa-fist-raised::before { + content: "\f6de"; } + +.fa-cloud-moon::before { + content: "\f6c3"; } + +.fa-briefcase::before { + content: "\f0b1"; } + +.fa-person-falling::before { + content: "\e546"; } + +.fa-image-portrait::before { + content: "\f3e0"; } + +.fa-portrait::before { + content: "\f3e0"; } + +.fa-user-tag::before { + content: "\f507"; } + +.fa-rug::before { + content: "\e569"; } + +.fa-earth-europe::before { + content: "\f7a2"; } + +.fa-globe-europe::before { + content: "\f7a2"; } + +.fa-cart-flatbed-suitcase::before { + content: "\f59d"; } + +.fa-luggage-cart::before { + content: "\f59d"; } + +.fa-rectangle-xmark::before { + content: "\f410"; } + +.fa-rectangle-times::before { + content: "\f410"; } + +.fa-times-rectangle::before { + content: "\f410"; } + +.fa-window-close::before { + content: "\f410"; } + +.fa-baht-sign::before { + content: "\e0ac"; } + +.fa-book-open::before { + content: "\f518"; } + +.fa-book-journal-whills::before { + content: "\f66a"; } + +.fa-journal-whills::before { + content: "\f66a"; } + +.fa-handcuffs::before { + content: "\e4f8"; } + +.fa-triangle-exclamation::before { + content: "\f071"; } + +.fa-exclamation-triangle::before { + content: "\f071"; } + +.fa-warning::before { + content: "\f071"; } + +.fa-database::before { + content: "\f1c0"; } + +.fa-share::before { + content: "\f064"; } + +.fa-mail-forward::before { + content: "\f064"; } + +.fa-bottle-droplet::before { + content: "\e4c4"; } + +.fa-mask-face::before { + content: "\e1d7"; } + +.fa-hill-rockslide::before { + content: "\e508"; } + +.fa-right-left::before { + content: "\f362"; } + +.fa-exchange-alt::before { + content: "\f362"; } + +.fa-paper-plane::before { + content: "\f1d8"; } + +.fa-road-circle-exclamation::before { + content: "\e565"; } + +.fa-dungeon::before { + content: "\f6d9"; } + +.fa-align-right::before { + content: "\f038"; } + +.fa-money-bill-1-wave::before { + content: "\f53b"; } + +.fa-money-bill-wave-alt::before { + content: "\f53b"; } + +.fa-life-ring::before { + content: "\f1cd"; } + +.fa-hands::before { + content: "\f2a7"; } + +.fa-sign-language::before { + content: "\f2a7"; } + +.fa-signing::before { + content: "\f2a7"; } + +.fa-calendar-day::before { + content: "\f783"; } + +.fa-water-ladder::before { + content: "\f5c5"; } + +.fa-ladder-water::before { + content: "\f5c5"; } + +.fa-swimming-pool::before { + content: "\f5c5"; } + +.fa-arrows-up-down::before { + content: "\f07d"; } + +.fa-arrows-v::before { + content: "\f07d"; } + +.fa-face-grimace::before { + content: "\f57f"; } + +.fa-grimace::before { + content: "\f57f"; } + +.fa-wheelchair-move::before { + content: "\e2ce"; } + +.fa-wheelchair-alt::before { + content: "\e2ce"; } + +.fa-turn-down::before { + content: "\f3be"; } + +.fa-level-down-alt::before { + content: "\f3be"; } + +.fa-person-walking-arrow-right::before { + content: "\e552"; } + +.fa-square-envelope::before { + content: "\f199"; } + +.fa-envelope-square::before { + content: "\f199"; } + +.fa-dice::before { + content: "\f522"; } + +.fa-bowling-ball::before { + content: "\f436"; } + +.fa-brain::before { + content: "\f5dc"; } + +.fa-bandage::before { + content: "\f462"; } + +.fa-band-aid::before { + content: "\f462"; } + +.fa-calendar-minus::before { + content: "\f272"; } + +.fa-circle-xmark::before { + content: "\f057"; } + +.fa-times-circle::before { + content: "\f057"; } + +.fa-xmark-circle::before { + content: "\f057"; } + +.fa-gifts::before { + content: "\f79c"; } + +.fa-hotel::before { + content: "\f594"; } + +.fa-earth-asia::before { + content: "\f57e"; } + +.fa-globe-asia::before { + content: "\f57e"; } + +.fa-id-card-clip::before { + content: "\f47f"; } + +.fa-id-card-alt::before { + content: "\f47f"; } + +.fa-magnifying-glass-plus::before { + content: "\f00e"; } + +.fa-search-plus::before { + content: "\f00e"; } + +.fa-thumbs-up::before { + content: "\f164"; } + +.fa-user-clock::before { + content: "\f4fd"; } + +.fa-hand-dots::before { + content: "\f461"; } + +.fa-allergies::before { + content: "\f461"; } + +.fa-file-invoice::before { + content: "\f570"; } + +.fa-window-minimize::before { + content: "\f2d1"; } + +.fa-mug-saucer::before { + content: "\f0f4"; } + +.fa-coffee::before { + content: "\f0f4"; } + +.fa-brush::before { + content: "\f55d"; } + +.fa-mask::before { + content: "\f6fa"; } + +.fa-magnifying-glass-minus::before { + content: "\f010"; } + +.fa-search-minus::before { + content: "\f010"; } + +.fa-ruler-vertical::before { + content: "\f548"; } + +.fa-user-large::before { + content: "\f406"; } + +.fa-user-alt::before { + content: "\f406"; } + +.fa-train-tram::before { + content: "\e5b4"; } + +.fa-user-nurse::before { + content: "\f82f"; } + +.fa-syringe::before { + content: "\f48e"; } + +.fa-cloud-sun::before { + content: "\f6c4"; } + +.fa-stopwatch-20::before { + content: "\e06f"; } + +.fa-square-full::before { + content: "\f45c"; } + +.fa-magnet::before { + content: "\f076"; } + +.fa-jar::before { + content: "\e516"; } + +.fa-note-sticky::before { + content: "\f249"; } + +.fa-sticky-note::before { + content: "\f249"; } + +.fa-bug-slash::before { + content: "\e490"; } + +.fa-arrow-up-from-water-pump::before { + content: "\e4b6"; } + +.fa-bone::before { + content: "\f5d7"; } + +.fa-user-injured::before { + content: "\f728"; } + +.fa-face-sad-tear::before { + content: "\f5b4"; } + +.fa-sad-tear::before { + content: "\f5b4"; } + +.fa-plane::before { + content: "\f072"; } + +.fa-tent-arrows-down::before { + content: "\e581"; } + +.fa-exclamation::before { + content: "\21"; } + +.fa-arrows-spin::before { + content: "\e4bb"; } + +.fa-print::before { + content: "\f02f"; } + +.fa-turkish-lira-sign::before { + content: "\e2bb"; } + +.fa-try::before { + content: "\e2bb"; } + +.fa-turkish-lira::before { + content: "\e2bb"; } + +.fa-dollar-sign::before { + content: "\24"; } + +.fa-dollar::before { + content: "\24"; } + +.fa-usd::before { + content: "\24"; } + +.fa-x::before { + content: "\58"; } + +.fa-magnifying-glass-dollar::before { + content: "\f688"; } + +.fa-search-dollar::before { + content: "\f688"; } + +.fa-users-gear::before { + content: "\f509"; } + +.fa-users-cog::before { + content: "\f509"; } + +.fa-person-military-pointing::before { + content: "\e54a"; } + +.fa-building-columns::before { + content: "\f19c"; } + +.fa-bank::before { + content: "\f19c"; } + +.fa-institution::before { + content: "\f19c"; } + +.fa-museum::before { + content: "\f19c"; } + +.fa-university::before { + content: "\f19c"; } + +.fa-umbrella::before { + content: "\f0e9"; } + +.fa-trowel::before { + content: "\e589"; } + +.fa-d::before { + content: "\44"; } + +.fa-stapler::before { + content: "\e5af"; } + +.fa-masks-theater::before { + content: "\f630"; } + +.fa-theater-masks::before { + content: "\f630"; } + +.fa-kip-sign::before { + content: "\e1c4"; } + +.fa-hand-point-left::before { + content: "\f0a5"; } + +.fa-handshake-simple::before { + content: "\f4c6"; } + +.fa-handshake-alt::before { + content: "\f4c6"; } + +.fa-jet-fighter::before { + content: "\f0fb"; } + +.fa-fighter-jet::before { + content: "\f0fb"; } + +.fa-square-share-nodes::before { + content: "\f1e1"; } + +.fa-share-alt-square::before { + content: "\f1e1"; } + +.fa-barcode::before { + content: "\f02a"; } + +.fa-plus-minus::before { + content: "\e43c"; } + +.fa-video::before { + content: "\f03d"; } + +.fa-video-camera::before { + content: "\f03d"; } + +.fa-graduation-cap::before { + content: "\f19d"; } + +.fa-mortar-board::before { + content: "\f19d"; } + +.fa-hand-holding-medical::before { + content: "\e05c"; } + +.fa-person-circle-check::before { + content: "\e53e"; } + +.fa-turn-up::before { + content: "\f3bf"; } + +.fa-level-up-alt::before { + content: "\f3bf"; } + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; } +:root, :host { + --fa-style-family-brands: 'Font Awesome 6 Brands'; + --fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; } + +@font-face { + font-family: 'Font Awesome 6 Brands'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +.fab, +.fa-brands { + font-weight: 400; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-shopware:before { + content: "\f5b5"; } + +.fa-creative-commons-nc:before { + content: "\f4e8"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-redhat:before { + content: "\f7bc"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-cloudflare:before { + content: "\e07d"; } + +.fa-ups:before { + content: "\f7e0"; } + +.fa-pixiv:before { + content: "\e640"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-stackpath:before { + content: "\f842"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-guilded:before { + content: "\e07e"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-square-js:before { + content: "\f3b9"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-orcid:before { + content: "\f8d2"; } + +.fa-java:before { + content: "\f4e4"; } + +.fa-invision:before { + content: "\f7b0"; } + +.fa-creative-commons-pd-alt:before { + content: "\f4ed"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-creative-commons-by:before { + content: "\f4e7"; } + +.fa-unity:before { + content: "\e049"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-mailchimp:before { + content: "\f59e"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-square-reddit:before { + content: "\f1a2"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-square-font-awesome:before { + content: "\e5ad"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-brave:before { + content: "\e63c"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-square-instagram:before { + content: "\e055"; } + +.fa-instagram-square:before { + content: "\e055"; } + +.fa-battle-net:before { + content: "\f835"; } + +.fa-the-red-yeti:before { + content: "\f69d"; } + +.fa-square-hacker-news:before { + content: "\f3af"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-threads:before { + content: "\e618"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-square-snapchat:before { + content: "\f2ad"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-artstation:before { + content: "\f77a"; } + +.fa-markdown:before { + content: "\f60f"; } + +.fa-sourcetree:before { + content: "\f7d3"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-diaspora:before { + content: "\f791"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-phoenix-squadron:before { + content: "\f511"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-creative-commons-sa:before { + content: "\f4ef"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-square-font-awesome-stroke:before { + content: "\f35c"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-atlassian:before { + content: "\f77b"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-nimblr:before { + content: "\f5a8"; } + +.fa-chromecast:before { + content: "\f838"; } + +.fa-evernote:before { + content: "\f839"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-creative-commons-sampling:before { + content: "\f4f0"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-watchman-monitoring:before { + content: "\e087"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-git-alt:before { + content: "\f841"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-rev:before { + content: "\f5b2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-wizards-of-the-coast:before { + content: "\f730"; } + +.fa-square-viadeo:before { + content: "\f2aa"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-centos:before { + content: "\f789"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-opensuse:before { + content: "\e62b"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-square-dribbble:before { + content: "\f397"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-node:before { + content: "\f419"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-debian:before { + content: "\e60b"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-instalod:before { + content: "\e081"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-square-twitter:before { + content: "\f081"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-r-project:before { + content: "\f4f7"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-golang:before { + content: "\e40f"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-uncharted:before { + content: "\e084"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-square-youtube:before { + content: "\f431"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-wpressr:before { + content: "\f3e4"; } + +.fa-rendact:before { + content: "\f3e4"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-galactic-republic:before { + content: "\f50c"; } + +.fa-nfc-directional:before { + content: "\e530"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-fedora:before { + content: "\f798"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-meta:before { + content: "\e49b"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-square-letterboxd:before { + content: "\e62e"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-creative-commons-zero:before { + content: "\f4f3"; } + +.fa-hips:before { + content: "\f452"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-confluence:before { + content: "\f78d"; } + +.fa-shoelace:before { + content: "\e60c"; } + +.fa-mdb:before { + content: "\f8ca"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-ebay:before { + content: "\f4f4"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-unsplash:before { + content: "\e07c"; } + +.fa-yarn:before { + content: "\f7e3"; } + +.fa-square-steam:before { + content: "\f1b7"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-500px:before { + content: "\f26e"; } + +.fa-square-vimeo:before { + content: "\f194"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-flag:before { + content: "\f2b4"; } + +.fa-font-awesome-logo-full:before { + content: "\f2b4"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-hive:before { + content: "\e07f"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-keybase:before { + content: "\f4f5"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-padlet:before { + content: "\e4a0"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-square-github:before { + content: "\f092"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-fedex:before { + content: "\f797"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-shopify:before { + content: "\e057"; } + +.fa-neos:before { + content: "\f612"; } + +.fa-square-threads:before { + content: "\e619"; } + +.fa-hackerrank:before { + content: "\f5f7"; } + +.fa-researchgate:before { + content: "\f4f8"; } + +.fa-swift:before { + content: "\f8e1"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-google-scholar:before { + content: "\e63b"; } + +.fa-square-gitlab:before { + content: "\e5ae"; } + +.fa-gitlab-square:before { + content: "\e5ae"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-square-odnoklassniki:before { + content: "\f264"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-sith:before { + content: "\f512"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-hashnode:before { + content: "\e499"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-squarespace:before { + content: "\f5be"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-creative-commons-share:before { + content: "\f4f2"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-itch-io:before { + content: "\f83a"; } + +.fa-umbraco:before { + content: "\f8e8"; } + +.fa-galactic-senate:before { + content: "\f50d"; } + +.fa-ubuntu:before { + content: "\f7df"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-dhl:before { + content: "\f790"; } + +.fa-square-pinterest:before { + content: "\f0d3"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-creative-commons-pd:before { + content: "\f4ec"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-quinscape:before { + content: "\f459"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-signal-messenger:before { + content: "\e663"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-dailymotion:before { + content: "\e052"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-bootstrap:before { + content: "\f836"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-nfc-symbol:before { + content: "\e531"; } + +.fa-mintbit:before { + content: "\e62f"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-speaker-deck:before { + content: "\f83c"; } + +.fa-creative-commons-nc-eu:before { + content: "\f4e9"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-ello:before { + content: "\f5f1"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-brave-reverse:before { + content: "\e63d"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-square-google-plus:before { + content: "\f0d4"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-mandalorian:before { + content: "\f50f"; } + +.fa-first-order-alt:before { + content: "\f50a"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-d-and-d-beyond:before { + content: "\f6ca"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-fulcrum:before { + content: "\f50b"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-square-xing:before { + content: "\f169"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-usps:before { + content: "\f7e1"; } + +.fa-megaport:before { + content: "\f5a3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-letterboxd:before { + content: "\e62d"; } + +.fa-symfony:before { + content: "\f83d"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-think-peaks:before { + content: "\f731"; } + +.fa-bilibili:before { + content: "\e3d9"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-x-twitter:before { + content: "\e61b"; } + +.fa-cotton-bureau:before { + content: "\f89e"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-42-group:before { + content: "\e080"; } + +.fa-innosoft:before { + content: "\e080"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-square-pied-piper:before { + content: "\e01e"; } + +.fa-pied-piper-square:before { + content: "\e01e"; } + +.fa-creative-commons-nd:before { + content: "\f4eb"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-tiktok:before { + content: "\e07b"; } + +.fa-square-facebook:before { + content: "\f082"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-mixer:before { + content: "\e056"; } + +.fa-square-lastfm:before { + content: "\f203"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-mendeley:before { + content: "\f7b3"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-figma:before { + content: "\f799"; } + +.fa-creative-commons-remix:before { + content: "\f4ee"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-cmplid:before { + content: "\e360"; } + +.fa-upwork:before { + content: "\e641"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-jedi-order:before { + content: "\f50e"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-trade-federation:before { + content: "\f513"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-deezer:before { + content: "\e077"; } + +.fa-raspberry-pi:before { + content: "\f7bb"; } + +.fa-jira:before { + content: "\f7b1"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-screenpal:before { + content: "\e570"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-microblog:before { + content: "\e01a"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-pied-piper-hat:before { + content: "\f4e5"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-readme:before { + content: "\f4d5"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-wirsindhandwerk:before { + content: "\e2d0"; } + +.fa-wsh:before { + content: "\e2d0"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-salesforce:before { + content: "\f83b"; } + +.fa-octopus-deploy:before { + content: "\e082"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-waze:before { + content: "\f83f"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ab"; } + +.fa-fantasy-flight-games:before { + content: "\f6dc"; } + +.fa-rust:before { + content: "\e07a"; } + +.fa-wix:before { + content: "\f5cf"; } + +.fa-square-behance:before { + content: "\f1b5"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-webflow:before { + content: "\e65c"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-kaggle:before { + content: "\f5fa"; } + +.fa-space-awesome:before { + content: "\e5ac"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-square-git:before { + content: "\f1d2"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-square-tumblr:before { + content: "\f174"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-creative-commons-nc-jp:before { + content: "\f4ea"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-perbyte:before { + content: "\e083"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-weebly:before { + content: "\f5cc"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-themeco:before { + content: "\f5c6"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-bots:before { + content: "\e340"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-hornbill:before { + content: "\f592"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-ideal:before { + content: "\e013"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-dev:before { + content: "\f6cc"; } + +.fa-sketch:before { + content: "\f7c6"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-php:before { + content: "\f457"; } + +.fa-alipay:before { + content: "\f642"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-firefox-browser:before { + content: "\e007"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-suse:before { + content: "\f7d6"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-buffer:before { + content: "\f837"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-yammer:before { + content: "\f840"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-stubber:before { + content: "\e5c7"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f2c6"; } + +.fa-old-republic:before { + content: "\f510"; } + +.fa-odysee:before { + content: "\e5c6"; } + +.fa-square-whatsapp:before { + content: "\f40c"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-edge-legacy:before { + content: "\e078"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f198"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-square-x-twitter:before { + content: "\e61a"; } + +.fa-reacteurope:before { + content: "\f75d"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f23a"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-flipboard:before { + content: "\f44d"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-critical-role:before { + content: "\f6c9"; } + +.fa-sitrox:before { + content: "\e44a"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-mastodon:before { + content: "\f4f6"; } + +.fa-airbnb:before { + content: "\f834"; } + +.fa-wolf-pack-battalion:before { + content: "\f514"; } + +.fa-buy-n-large:before { + content: "\f8a6"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-creative-commons-sampling-plus:before { + content: "\f4f1"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-canadian-maple-leaf:before { + content: "\f785"; } + +.fa-teamspeak:before { + content: "\f4f9"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-wodu:before { + content: "\e088"; } + +.fa-google-pay:before { + content: "\e079"; } + +.fa-intercom:before { + content: "\f7af"; } + +.fa-zhihu:before { + content: "\f63f"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-pix:before { + content: "\e43a"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } +:root, :host { + --fa-style-family-classic: 'Font Awesome 6 Free'; + --fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } + +.far, +.fa-regular { + font-weight: 400; } +:root, :host { + --fa-style-family-classic: 'Font Awesome 6 Free'; + --fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; } + +@font-face { + font-family: 'Font Awesome 6 Free'; + font-style: normal; + font-weight: 900; + font-display: block; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +.fas, +.fa-solid { + font-weight: 900; } +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-display: block; + font-weight: 400; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-display: block; + font-weight: 900; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-display: block; + font-weight: 400; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } + +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } + +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); + unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; } + +@font-face { + font-family: 'FontAwesome'; + font-display: block; + src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype"); + unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.ttf b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000..5efb1d4f96 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000000..36fbda7d33 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.ttf b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000000..838b4e2cfe Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000..b6cabbacb6 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.ttf b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000..ec24749db9 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000000..824d518eb4 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.ttf b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000000..b175aa8ece Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.ttf differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.woff2 b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000000..e09b5a5500 Binary files /dev/null and b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor/wwwroot/libs/fontawesome/webfonts/fa-v4compatibility.woff2 differ diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpWasmCultureMenuItemUrlProvider.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpWasmCultureMenuItemUrlProvider.cs new file mode 100644 index 0000000000..9a1077e1b0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpWasmCultureMenuItemUrlProvider.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.Client; +using Volo.Abp.DependencyInjection; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; + +/// +/// Prepends the culture route prefix to menu item URLs in Blazor WebAssembly when route-based culture is enabled. +/// +public class AbpWasmCultureMenuItemUrlProvider : IMenuItemUrlProvider, ITransientDependency +{ + protected ICachedApplicationConfigurationClient ConfigurationClient { get; } + protected IMenuItemCulturePrefixHelper MenuItemCulturePrefixHelper { get; } + + public AbpWasmCultureMenuItemUrlProvider( + ICachedApplicationConfigurationClient configurationClient, + IMenuItemCulturePrefixHelper menuItemCulturePrefixHelper) + { + ConfigurationClient = configurationClient; + MenuItemCulturePrefixHelper = menuItemCulturePrefixHelper; + } + + public virtual async Task HandleAsync(MenuItemUrlProviderContext context) + { + if (!OperatingSystem.IsBrowser()) + { + return; + } + + var config = await ConfigurationClient.GetAsync(); + if (!config.Localization.UseRouteBasedCulture) + { + return; + } + + var culture = GetCulture(config); + if (string.IsNullOrEmpty(culture)) + { + return; + } + + await MenuItemCulturePrefixHelper.PrependCulturePrefixAsync(context.Menu, "/" + culture); + } + + protected virtual string? GetCulture(Mvc.ApplicationConfigurations.ApplicationConfigurationDto config) + { + var currentCulture = CultureInfo.CurrentCulture.Name; + var languages = config.Localization.Languages; + if (languages.Count == 0) + { + return null; + } + + var isKnownCulture = languages + .Any(l => string.Equals(l.CultureName, currentCulture, StringComparison.OrdinalIgnoreCase)); + + return isKnownCulture ? currentCulture : null; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/CultureAwareAuthenticationBase.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/CultureAwareAuthenticationBase.cs new file mode 100644 index 0000000000..26a25b7f0e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/CultureAwareAuthenticationBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; + +/// +/// Shared base for WASM theme Authentication pages. +/// Provides a helper so the culture-aware +/// home URL construction is not duplicated across theme packages. +/// +public abstract class CultureAwareAuthenticationBase : AbpComponentBase +{ + [Inject] + protected NavigationManager Navigation { get; set; } = default!; + + [Parameter] + public string? Culture { get; set; } + + protected virtual string GetCultureAwareHomeUrl() + { + return string.IsNullOrEmpty(Culture) + ? Navigation.BaseUri + : Navigation.BaseUri + Culture + "/"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/CultureAwareRedirectToLoginHelper.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/CultureAwareRedirectToLoginHelper.cs new file mode 100644 index 0000000000..02c2b749bf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/CultureAwareRedirectToLoginHelper.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Components.Web; +using Volo.Abp.AspNetCore.Components.WebAssembly; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; + +/// +/// Provides the shared culture-aware login redirect logic for all WASM theme +/// WebAssemblyRedirectToLogin components. Each theme must keep +/// @inherits RedirectToLogin for ABP service-replacement assignability, +/// so a common component base class is not feasible; this static helper +/// centralises the logic instead. +/// +public static class CultureAwareRedirectToLoginHelper +{ + public static async Task RedirectAsync( + NavigationManager navigation, + string loginUrl, + IRouteBasedCultureUrlHelper cultureUrlHelper, + IOptions webOptions) + { + var cultureLoginUrl = await cultureUrlHelper.PrependCulturePrefixAsync(loginUrl); + if (webOptions.Value.IsBlazorWebApp) + { + navigation.NavigateTo(cultureLoginUrl, forceLoad: true); + } + else + { + navigation.NavigateToLogin(cultureLoginUrl); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj index b3fc2633a7..76b63c76c7 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj @@ -9,6 +9,10 @@ Nullable
+ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpAspNetCoreComponentsWebAssemblyModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpAspNetCoreComponentsWebAssemblyModule.cs index b4ea3cb50c..958278226f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpAspNetCoreComponentsWebAssemblyModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpAspNetCoreComponentsWebAssemblyModule.cs @@ -48,7 +48,8 @@ public class AbpAspNetCoreComponentsWebAssemblyModule : AbpModule context.Services.AddHttpClient(); context.Services .GetHostBuilder().Logging - .AddProvider(new AbpExceptionHandlingLoggerProvider(context.Services)); + .AddProvider(new AbpExceptionHandlingLoggerProvider(context.Services)) + .AddFilter(typeof(UserExceptionInformer).FullName, LogLevel.None); if (!context.Services.ExecutePreConfiguredActions().IsBlazorWebApp) { diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/IRouteBasedCultureNavigationHelper.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/IRouteBasedCultureNavigationHelper.cs new file mode 100644 index 0000000000..b2b34e1a65 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/IRouteBasedCultureNavigationHelper.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Volo.Abp.Localization; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly; + +public interface IRouteBasedCultureNavigationHelper +{ + Task NavigateToNewCultureAsync( + NavigationManager navigationManager, + LanguageInfo newLanguage, + IEnumerable allLanguages); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/IRouteBasedCultureUrlHelper.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/IRouteBasedCultureUrlHelper.cs new file mode 100644 index 0000000000..f56aeaf693 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/IRouteBasedCultureUrlHelper.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly; + +public interface IRouteBasedCultureUrlHelper +{ + /// + /// Prepends the current culture to when route-based culture is enabled + /// and the current culture is a known application language. + /// Returns the original unchanged when the feature is disabled or the + /// culture is not recognised. + /// + Task PrependCulturePrefixAsync(string url); +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/RouteBasedCultureNavigationHelper.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/RouteBasedCultureNavigationHelper.cs new file mode 100644 index 0000000000..6a16f7430a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/RouteBasedCultureNavigationHelper.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly; + +public class RouteBasedCultureNavigationHelper : IRouteBasedCultureNavigationHelper, ITransientDependency +{ + public virtual Task NavigateToNewCultureAsync( + NavigationManager navigationManager, + LanguageInfo newLanguage, + IEnumerable allLanguages) + { + var relativePath = navigationManager.ToBaseRelativePath(navigationManager.Uri); + + // Separate the path from any query string or fragment so the culture segment + // is correctly identified even for URLs like "tr?x=1" (no slash after culture). + var suffixIndex = relativePath.IndexOfAny(['?', '#']); + var pathPart = suffixIndex >= 0 ? relativePath.Substring(0, suffixIndex) : relativePath; + var suffix = suffixIndex >= 0 ? relativePath.Substring(suffixIndex) : string.Empty; + + var slashIndex = pathPart.IndexOf('/'); + var firstSegment = GetFirstPathSegment(relativePath); + var pathRemainder = slashIndex >= 0 ? pathPart.Substring(slashIndex) : string.Empty; + + // No-op: the current URL already shows the target culture — no navigation needed. + if (string.Equals(firstSegment, newLanguage.CultureName, StringComparison.OrdinalIgnoreCase)) + { + return Task.CompletedTask; + } + + var newRelativePath = allLanguages.Any(l => string.Equals(l.CultureName, firstSegment, StringComparison.OrdinalIgnoreCase)) + ? newLanguage.CultureName + pathRemainder + suffix + : newLanguage.CultureName + "/" + pathPart + suffix; + + navigationManager.NavigateTo(navigationManager.ToAbsoluteUri(newRelativePath).ToString(), forceLoad: true); + return Task.CompletedTask; + } + + /// + /// Returns the first path segment of , + /// stripping any query string or fragment before splitting on '/'. + /// For example: "zh-Hans/account?x=1" → "zh-Hans", "tr/home#top" → "tr". + /// + protected virtual string GetFirstPathSegment(string baseRelativePath) + { + var suffixIndex = baseRelativePath.IndexOfAny(['?', '#']); + var pathPart = suffixIndex >= 0 ? baseRelativePath.Substring(0, suffixIndex) : baseRelativePath; + var slashIndex = pathPart.IndexOf('/'); + return slashIndex >= 0 ? pathPart.Substring(0, slashIndex) : pathPart; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/RouteBasedCultureUrlHelper.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/RouteBasedCultureUrlHelper.cs new file mode 100644 index 0000000000..0c03e56b5b --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/RouteBasedCultureUrlHelper.cs @@ -0,0 +1,90 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc.Client; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly; + +public class RouteBasedCultureUrlHelper : IRouteBasedCultureUrlHelper, ITransientDependency +{ + private readonly ICachedApplicationConfigurationClient _configurationClient; + + public RouteBasedCultureUrlHelper(ICachedApplicationConfigurationClient configurationClient) + { + _configurationClient = configurationClient; + } + + public virtual async Task PrependCulturePrefixAsync(string url) + { + if (string.IsNullOrEmpty(url)) + { + return url; + } + + // Skip absolute URLs with a web scheme and protocol-relative URLs. + // Intentionally avoids Uri.TryCreate here: on Unix, root-relative paths such as + // "/account/login" are parsed as absolute file:// URIs, which would incorrectly + // skip them before the culture prefix could be applied. + if (url.StartsWith("//", StringComparison.Ordinal) || + url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return url; + } + + var config = await _configurationClient.GetAsync(); + if (config?.Localization.UseRouteBasedCulture != true) + { + return url; + } + + var currentCulture = CultureInfo.CurrentCulture.Name; + var isKnownCulture = config.Localization.Languages + .Any(l => string.Equals(l.CultureName, currentCulture, StringComparison.OrdinalIgnoreCase)); + + if (!isKnownCulture) + { + return url; + } + + // Idempotency guard: if the URL already carries the culture prefix, return it unchanged. + // Strip the leading scheme prefix (~/ or /) before checking the first path segment. + var pathForSegmentCheck = url.StartsWith("~/", StringComparison.Ordinal) ? url.Substring(2) + : url.StartsWith("/", StringComparison.Ordinal) ? url.Substring(1) + : url; + + if (string.Equals(GetFirstPathSegment(pathForSegmentCheck), + currentCulture, StringComparison.OrdinalIgnoreCase)) + { + return url; + } + + if (url.StartsWith("~/", StringComparison.Ordinal)) + { + return "~/" + currentCulture + "/" + url.Substring(2); + } + + if (url.StartsWith("/", StringComparison.Ordinal)) + { + return "/" + currentCulture + url; + } + + // Bare relative path (e.g. "authentication/login") + return currentCulture + "/" + url; + } + + /// + /// Returns the first path segment of , + /// stripping any query string or fragment before splitting on '/'. + /// For example: "zh-Hans/account?x=1" → "zh-Hans", "tr/home#top" → "tr". + /// + protected virtual string GetFirstPathSegment(string baseRelativePath) + { + var suffixIndex = baseRelativePath.IndexOfAny(['?', '#']); + var pathPart = suffixIndex >= 0 ? baseRelativePath.Substring(0, suffixIndex) : baseRelativePath; + var slashIndex = pathPart.IndexOf('/'); + return slashIndex >= 0 ? pathPart.Substring(0, slashIndex) : pathPart; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs index 5ae2a8ed2d..1f3bbe96a3 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs @@ -34,6 +34,8 @@ public class ApplicationLocalizationConfigurationDto public Dictionary> LanguageFilesMap { get; set; } + public bool UseRouteBasedCulture { get; set; } + public ApplicationLocalizationConfigurationDto() { Values = new Dictionary>(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index ad8d8c1c28..9da7e7454f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.RequestLocalization; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Localization; @@ -212,6 +214,45 @@ public class AbpAspNetCoreMvcModule : AbpModule { preConfigureActions.Configure(options); }); + + ConfigureRouteBasedCulture(context); + } + + protected virtual void ConfigureRouteBasedCulture(ServiceConfigurationContext context) + { + context.Services.Configure(options => + { + options.ConstraintMap["culture"] = typeof(AbpCultureRouteConstraint); + }); + + context.Services + .AddOptions() + .PostConfigure>((routerOptions, abpLocOptions) => + { + if (abpLocOptions.Value.UseRouteBasedCulture) + { + routerOptions.EndpointConfigureActions.Insert(0, endpointContext => + { + endpointContext.Endpoints.MapControllerRoute( + "AbpCultureRoute", + AbpCultureRoutePagesConvention.CultureRouteTemplate + "/{controller=Home}/{action=Index}/{id?}"); + }); + } + }); + + context.Services + .AddOptions() + .PostConfigure>((pagesOptions, abpLocOptions) => + { + if (abpLocOptions.Value.UseRouteBasedCulture && + !pagesOptions.Conventions.OfType().Any()) + { + pagesOptions.Conventions.Add(new AbpCultureRoutePagesConvention()); + } + }); + + context.Services.TryAddSingleton(); + context.Services.Replace(ServiceDescriptor.Singleton()); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs index 8c93400929..e7979584d8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs @@ -59,13 +59,20 @@ public class AbpRemoteServiceApiDescriptionProvider : IApiDescriptionProvider, I protected virtual IEnumerable GetApiResponseTypes() { - foreach (var apiResponse in _options.SupportedResponseTypes) + foreach (var template in _options.SupportedResponseTypes) { - apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(apiResponse.Type!); + var apiResponse = new ApiResponseType + { + Type = template.Type, + StatusCode = template.StatusCode, + Description = template.Description, + IsDefaultResponse = template.IsDefaultResponse, + ModelMetadata = _modelMetadataProvider.GetMetadataForType(template.Type!) + }; foreach (var responseTypeMetadataProvider in _mvcOptions.OutputFormatters.OfType()) { - var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(null!, apiResponse.Type!); + var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(null!, template.Type!); if (formatterSupportedContentTypes == null) { continue; @@ -80,8 +87,8 @@ public class AbpRemoteServiceApiDescriptionProvider : IApiDescriptionProvider, I }); } } - } - return _options.SupportedResponseTypes; + yield return apiResponse; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index a59e2d8dd6..beae2d9e0f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.RequestLocalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; @@ -27,6 +28,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApplicationConfigurationAppService { private readonly AbpLocalizationOptions _localizationOptions; + private readonly AbpRequestLocalizationOptions _requestLocalizationOptions; private readonly AbpMultiTenancyOptions _multiTenancyOptions; private readonly IServiceProvider _serviceProvider; private readonly IAbpAuthorizationPolicyProvider _abpAuthorizationPolicyProvider; @@ -46,6 +48,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp public AbpApplicationConfigurationAppService( IOptions localizationOptions, + IOptions requestLocalizationOptions, IOptions multiTenancyOptions, IServiceProvider serviceProvider, IAbpAuthorizationPolicyProvider abpAuthorizationPolicyProvider, @@ -79,6 +82,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp _cachedObjectExtensionsDtoService = cachedObjectExtensionsDtoService; _options = options.Value; _localizationOptions = localizationOptions.Value; + _requestLocalizationOptions = requestLocalizationOptions.Value; _multiTenancyOptions = multiTenancyOptions.Value; } @@ -257,6 +261,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp localizationConfig.LanguagesMap = _localizationOptions.LanguagesMap; localizationConfig.LanguageFilesMap = _localizationOptions.LanguageFilesMap; + localizationConfig.UseRouteBasedCulture = _requestLocalizationOptions.UseRouteBasedCulture; return localizationConfig; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpAspNetCoreMvcQueryStringCultureReplacement.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpAspNetCoreMvcQueryStringCultureReplacement.cs index 688392f3f1..14a9ba5c51 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpAspNetCoreMvcQueryStringCultureReplacement.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpAspNetCoreMvcQueryStringCultureReplacement.cs @@ -1,6 +1,10 @@ -using System; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.WebUtilities; using Volo.Abp.DependencyInjection; namespace Volo.Abp.AspNetCore.Mvc.Localization; @@ -9,25 +13,71 @@ public class AbpAspNetCoreMvcQueryStringCultureReplacement : IQueryStringCulture { public virtual Task ReplaceAsync(QueryStringCultureReplacementContext context) { - if (!string.IsNullOrWhiteSpace(context.ReturnUrl)) + if (string.IsNullOrWhiteSpace(context.ReturnUrl)) { - if (context.ReturnUrl.Contains("culture=", StringComparison.OrdinalIgnoreCase) && - context.ReturnUrl.Contains("ui-Culture=", StringComparison.OrdinalIgnoreCase)) - { - context.ReturnUrl = Regex.Replace( - context.ReturnUrl, - "culture=[A-Za-z-]+", - $"culture={context.RequestCulture.Culture}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - context.ReturnUrl = Regex.Replace( - context.ReturnUrl, - "ui-culture=[A-Za-z-]+", - $"ui-culture={context.RequestCulture.UICulture}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - } + return Task.CompletedTask; } + var currentCulture = context.CurrentCulture + ?? context.HttpContext.GetRouteValue("culture")?.ToString(); + + if (!string.IsNullOrEmpty(currentCulture)) + { + var escapedCulture = Regex.Escape(currentCulture); + // Replace only the first occurrence so that paths like /en/products/en/details + // only have the leading culture segment replaced, while tenant-prefixed paths + // like /tenant-a/en/... are also handled correctly. + var pattern = $"/{escapedCulture}(?=/|$|\\?|#)"; + context.ReturnUrl = new Regex(pattern, RegexOptions.IgnoreCase) + .Replace(context.ReturnUrl, "/" + context.RequestCulture.Culture.Name, 1); + } + + context.ReturnUrl = ReplaceQueryStringCulture(context.ReturnUrl, context); + return Task.CompletedTask; } + + /// + /// Replaces culture and ui-culture query parameters in + /// with the values from . Each parameter is handled independently — + /// the presence of one does not require the other. Uses a proper query parser instead of + /// regex to avoid false-positive matches inside other parameter values. + /// + protected virtual string ReplaceQueryStringCulture(string url, QueryStringCultureReplacementContext context) + { + var fragmentIndex = url.IndexOf('#'); + var fragment = fragmentIndex >= 0 ? url.Substring(fragmentIndex) : string.Empty; + var urlWithoutFragment = fragmentIndex >= 0 ? url.Substring(0, fragmentIndex) : url; + + var queryIndex = urlWithoutFragment.IndexOf('?'); + if (queryIndex < 0) + { + return url; + } + + var path = urlWithoutFragment.Substring(0, queryIndex); + var queryString = urlWithoutFragment.Substring(queryIndex); + var query = QueryHelpers.ParseQuery(queryString); + + if (!query.ContainsKey("culture") && !query.ContainsKey("ui-culture")) + { + return url; + } + + if (query.ContainsKey("culture")) + { + query["culture"] = context.RequestCulture.Culture.Name; + } + + if (query.ContainsKey("ui-culture")) + { + query["ui-culture"] = context.RequestCulture.UICulture.Name; + } + + var rebuiltUrl = QueryHelpers.AddQueryString( + path, + query.SelectMany(kvp => kvp.Value.Select(v => KeyValuePair.Create(kvp.Key, v)))); + + return rebuiltUrl + fragment; + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureAwareUrlHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureAwareUrlHelper.cs new file mode 100644 index 0000000000..35c8dedeef --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureAwareUrlHelper.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +/// +/// Wraps an to automatically inject the culture route value +/// into all URL generation calls. +/// +public class AbpCultureAwareUrlHelper : IUrlHelper +{ + protected IUrlHelper Inner { get; } + protected string Culture { get; } + + public AbpCultureAwareUrlHelper(IUrlHelper inner, string culture) + { + Inner = inner; + Culture = culture; + } + + public ActionContext ActionContext => Inner.ActionContext; + + public virtual string? Action(UrlActionContext actionContext) + { + var values = new RouteValueDictionary(actionContext.Values); + values.TryAdd("culture", Culture); + + return Inner.Action(new UrlActionContext + { + Action = actionContext.Action, + Controller = actionContext.Controller, + Values = values, + Protocol = actionContext.Protocol, + Host = actionContext.Host, + Fragment = actionContext.Fragment, + }); + } + + public virtual string? Content(string? contentPath) + { + return Inner.Content(contentPath); + } + + public virtual bool IsLocalUrl(string? url) + { + return Inner.IsLocalUrl(url); + } + + public virtual string? Link(string? routeName, object? values) + { + var rvd = new RouteValueDictionary(values); + rvd.TryAdd("culture", Culture); + return Inner.Link(routeName, rvd); + } + + public virtual string? RouteUrl(UrlRouteContext routeContext) + { + var values = new RouteValueDictionary(routeContext.Values); + values.TryAdd("culture", Culture); + + return Inner.RouteUrl(new UrlRouteContext + { + RouteName = routeContext.RouteName, + Values = values, + Protocol = routeContext.Protocol, + Host = routeContext.Host, + Fragment = routeContext.Fragment, + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider.cs new file mode 100644 index 0000000000..1b91ce72e3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RequestLocalization; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +/// +/// Prepends the culture route prefix to menu item URLs when route-based culture is enabled. +/// +public class AbpCultureMenuItemUrlProvider : IMenuItemUrlProvider, ITransientDependency +{ + protected IHttpContextAccessor HttpContextAccessor { get; } + protected IOptions LocalizationOptions { get; } + protected IOptions AbpLocalizationOptions { get; } + protected IMenuItemCulturePrefixHelper MenuItemCulturePrefixHelper { get; } + + public AbpCultureMenuItemUrlProvider( + IHttpContextAccessor httpContextAccessor, + IOptions localizationOptions, + IOptions abpLocalizationOptions, + IMenuItemCulturePrefixHelper menuItemCulturePrefixHelper) + { + HttpContextAccessor = httpContextAccessor; + LocalizationOptions = localizationOptions; + AbpLocalizationOptions = abpLocalizationOptions; + MenuItemCulturePrefixHelper = menuItemCulturePrefixHelper; + } + + public virtual async Task HandleAsync(MenuItemUrlProviderContext context) + { + if (!LocalizationOptions.Value.UseRouteBasedCulture) + { + return; + } + + var culture = GetCulture(); + if (string.IsNullOrEmpty(culture)) + { + return; + } + + await MenuItemCulturePrefixHelper.PrependCulturePrefixAsync(context.Menu, "/" + culture); + } + + protected virtual string? GetCulture() + { + var httpContext = HttpContextAccessor.HttpContext; + if (httpContext != null) + { + return AbpRequestCultureCookieHelper.GetRouteCulture(httpContext); + } + + // No HttpContext: fallback to CurrentCulture. + var currentCulture = CultureInfo.CurrentCulture.Name; + var isKnownCulture = AbpLocalizationOptions.Value.Languages + .Any(l => string.Equals(l.CultureName, currentCulture, StringComparison.OrdinalIgnoreCase)); + + return isKnownCulture ? currentCulture : null; + } + +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRouteConstraint.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRouteConstraint.cs new file mode 100644 index 0000000000..fe12bac3ff --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRouteConstraint.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.Localization; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +/// +/// A route constraint that only matches culture values configured in +/// . +/// +public class AbpCultureRouteConstraint : IRouteConstraint +{ + public virtual bool Match(HttpContext? httpContext, IRouter? route, string routeKey, + RouteValueDictionary values, RouteDirection routeDirection) + { + if (!values.TryGetValue(routeKey, out var value) || value is not string cultureValue) + { + return false; + } + + var languages = httpContext?.RequestServices + .GetService>()?.Value.Languages; + + if (languages == null || languages.Count == 0) + { + // During URL generation, HttpContext or services may not be available. + return routeDirection == RouteDirection.UrlGeneration; + } + + return languages.Any(l => + string.Equals(l.CultureName, cultureValue, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRoutePagesConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRoutePagesConvention.cs new file mode 100644 index 0000000000..9410562f33 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRoutePagesConvention.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +/// +/// Adds a {culture}-prefixed route selector to every Razor Page. +/// Automatically registered when UseRouteBasedCulture is true. +/// +public class AbpCultureRoutePagesConvention : IPageRouteModelConvention +{ + /// + /// Route parameter template using the custom "culture" route constraint registered in + /// . The constraint only matches culture values + /// configured in . + /// + internal const string CultureRouteTemplate = "{culture:culture}"; + + public virtual void Apply(PageRouteModel model) + { + var selectorsToAdd = new List(); + + foreach (var selector in model.Selectors.ToList()) + { + var originalTemplate = selector.AttributeRouteModel?.Template?.TrimStart('/'); + if (originalTemplate == null) + { + continue; + } + + selectorsToAdd.Add(new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel + { + Template = AttributeRouteModel.CombineTemplates(CultureRouteTemplate, originalTemplate), + Order = -1 + } + }); + } + + foreach (var selector in selectorsToAdd) + { + model.Selectors.Add(selector); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRouteUrlHelperFactory.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRouteUrlHelperFactory.cs new file mode 100644 index 0000000000..7be4456677 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureRouteUrlHelperFactory.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.RequestLocalization; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +/// +/// Wraps the default to automatically inject the culture +/// route value into all URL generation calls when the current request has a {culture} route value. +/// Only activates when is true. +/// +public class AbpCultureRouteUrlHelperFactory : IUrlHelperFactory +{ + protected UrlHelperFactory Inner { get; } + protected IOptions LocalizationOptions { get; } + + public AbpCultureRouteUrlHelperFactory( + UrlHelperFactory inner, + IOptions localizationOptions) + { + Inner = inner; + LocalizationOptions = localizationOptions; + } + + public virtual IUrlHelper GetUrlHelper(ActionContext context) + { + var urlHelper = Inner.GetUrlHelper(context); + + if (!LocalizationOptions.Value.UseRouteBasedCulture) + { + return urlHelper; + } + + if (context.RouteData.Values.TryGetValue("culture", out var culture) && + culture != null) + { + return CreateCultureAwareUrlHelper(urlHelper, culture.ToString()!); + } + + return urlHelper; + } + + protected virtual AbpCultureAwareUrlHelper CreateCultureAwareUrlHelper(IUrlHelper urlHelper, string culture) + { + return new AbpCultureAwareUrlHelper(urlHelper, culture); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs index 73c93851ff..dc5dd3ccf6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs @@ -1,7 +1,8 @@ -using Microsoft.AspNetCore.Localization; -using Microsoft.AspNetCore.Mvc; using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RequestLocalization; using Volo.Abp.Auditing; using Volo.Abp.Localization; @@ -42,7 +43,12 @@ public class AbpLanguagesController : AbpController HttpContext.Items[AbpRequestLocalizationMiddleware.HttpContextItemName] = true; - var context = new QueryStringCultureReplacementContext(HttpContext, new RequestCulture(culture, uiCulture), returnUrl); + var context = new QueryStringCultureReplacementContext( + HttpContext, + new RequestCulture(culture, uiCulture), + returnUrl, + GetCurrentCultureFromRequestCookie()); + await QueryStringCultureReplacement.ReplaceAsync(context); if (!string.IsNullOrWhiteSpace(context.ReturnUrl)) @@ -53,6 +59,18 @@ public class AbpLanguagesController : AbpController return Redirect("~/"); } + protected virtual string? GetCurrentCultureFromRequestCookie() + { + var cookieValue = HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; + if (cookieValue == null) + { + return null; + } + + var result = CookieRequestCultureProvider.ParseCookieValue(cookieValue); + return result?.Cultures.FirstOrDefault().Value; + } + protected virtual string GetRedirectUrl(string returnUrl) { if (returnUrl.IsNullOrEmpty()) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/QueryStringCultureReplacementContext.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/QueryStringCultureReplacementContext.cs index b70c24fb95..15866f54bc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/QueryStringCultureReplacementContext.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/QueryStringCultureReplacementContext.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; namespace Volo.Abp.AspNetCore.Mvc.Localization; @@ -11,10 +11,17 @@ public class QueryStringCultureReplacementContext public string ReturnUrl { get; set; } - public QueryStringCultureReplacementContext(HttpContext httpContext, RequestCulture requestCulture, string returnUrl) + public string? CurrentCulture { get; } + + public QueryStringCultureReplacementContext( + HttpContext httpContext, + RequestCulture requestCulture, + string returnUrl, + string? currentCulture = null) { HttpContext = httpContext; RequestCulture = requestCulture; ReturnUrl = returnUrl; + CurrentCulture = currentCulture; } } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestCultureCookieHelper.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestCultureCookieHelper.cs index 9bb5b61d28..552c3bfc84 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestCultureCookieHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestCultureCookieHelper.cs @@ -1,11 +1,41 @@ using System; +using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.RequestLocalization; public static class AbpRequestCultureCookieHelper { + public const string HasRouteCultureCookieName = "Abp.HasRouteCulture"; + + /// + /// Gets the current route culture from the request. First checks route values, + /// then falls back to the HasRouteCulture cookie (set during Blazor SSR) with CurrentCulture. + /// Returns null if the request has no route-based culture. + /// + public static string? GetRouteCulture(HttpContext? httpContext) + { + if (httpContext == null) + { + return null; + } + + var routeCulture = httpContext.GetRouteValue("culture")?.ToString(); + if (!string.IsNullOrEmpty(routeCulture)) + { + return routeCulture; + } + + if (httpContext.Request.Cookies.ContainsKey(HasRouteCultureCookieName)) + { + return CultureInfo.CurrentCulture.Name; + } + + return null; + } + public static void SetCultureCookie( HttpContext httpContext, RequestCulture requestCulture) @@ -20,4 +50,21 @@ public static class AbpRequestCultureCookieHelper } ); } + + public static void SetHasRouteCultureCookie(HttpContext httpContext, bool hasRouteCulture) + { + if (hasRouteCulture) + { + httpContext.Response.Cookies.Append( + HasRouteCultureCookieName, "1", + new CookieOptions + { + IsEssential = true + }); + } + else + { + httpContext.Response.Cookies.Delete(HasRouteCultureCookieName); + } + } } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs index cb531762ba..4683c3151d 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs @@ -1,7 +1,9 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Localization.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Middleware; @@ -39,13 +41,28 @@ public class AbpRequestLocalizationMiddleware : AbpMiddlewareBase, ITransientDep if (context.Items[HttpContextItemName] == null) { var requestCultureFeature = context.Features.Get(); - if (requestCultureFeature?.Provider is QueryStringRequestCultureProvider) + if (requestCultureFeature?.Provider is QueryStringRequestCultureProvider + or RouteDataRequestCultureProvider) { AbpRequestCultureCookieHelper.SetCultureCookie( context, requestCultureFeature.RequestCulture ); } + + // Only manage HasRouteCulture cookie for Blazor component page requests. + // This cookie is used by AbpCultureMenuItemUrlProvider to determine if the + // initial SSR page had a culture prefix, since the Blazor interactive circuit + // (/_blazor) does not carry the original route values. + // Note: ComponentTypeMetadata is an internal ASP.NET Core type + // (Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata). + // We match by full type name to avoid false positives from other assemblies. + var endpoint = context.GetEndpoint(); + if (endpoint?.Metadata.Any(m => m.GetType().FullName == "Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata") == true) + { + AbpRequestCultureCookieHelper.SetHasRouteCultureCookie( + context, requestCultureFeature?.Provider is RouteDataRequestCultureProvider); + } } return Task.CompletedTask; diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationOptions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationOptions.cs index bb83ce9a53..55394b7d17 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationOptions.cs @@ -9,6 +9,12 @@ public class AbpRequestLocalizationOptions { public List> RequestLocalizationOptionConfigurators { get; } + /// + /// Enables culture detection from route data (e.g. /{culture}/page). + /// Default value: false. + /// + public bool UseRouteBasedCulture { get; set; } + public AbpRequestLocalizationOptions() { RequestLocalizationOptionConfigurators = new List>(); diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs index cf9f128852..3cd2e88993 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Localization.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; @@ -70,9 +71,18 @@ public class DefaultAbpRequestLocalizationOptionsProvider : .ToArray() }; - foreach (var configurator in serviceScope.ServiceProvider - .GetRequiredService>() - .Value.RequestLocalizationOptionConfigurators) + var abpRequestLocalizationOptions = serviceScope.ServiceProvider + .GetRequiredService>() + .Value; + + if (abpRequestLocalizationOptions.UseRouteBasedCulture) + { + options.RequestCultureProviders.InsertAfter( + p => p is QueryStringRequestCultureProvider, + new RouteDataRequestCultureProvider()); + } + + foreach (var configurator in abpRequestLocalizationOptions.RequestLocalizationOptionConfigurators) { await configurator(serviceScope.ServiceProvider, options); } diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs index fe0a0e09f9..3d6cd86941 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs @@ -7,10 +7,16 @@ namespace Volo.Abp.BlobStoring; public class BlobContainerConfiguration { + private Type? _providerType; + /// /// The provider to be used to store BLOBs of this container. /// - public Type? ProviderType { get; set; } + public Type? ProviderType + { + get => _providerType ?? _fallbackConfiguration?.ProviderType; + set => _providerType = value; + } /// /// Indicates whether this container is multi-tenant or not. @@ -37,6 +43,20 @@ public class BlobContainerConfiguration _properties = new Dictionary(); } + /// + /// Returns the naming normalizers in effect for this container, inheriting from the fallback + /// configuration only when this container has none and does not override . + /// + public IEnumerable GetEffectiveNamingNormalizers() + { + if (NamingNormalizers.Count == 0 && _providerType == null && _fallbackConfiguration != null) + { + return _fallbackConfiguration.GetEffectiveNamingNormalizers(); + } + + return NamingNormalizers; + } + public T? GetConfigurationOrDefault(string name, T? defaultValue = default) { return (T?)GetConfigurationOrNull(name, defaultValue); diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs index 397b48ca0c..cd1ae931b5 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs @@ -19,15 +19,15 @@ public class BlobNormalizeNamingService : IBlobNormalizeNamingService, ITransien string? containerName, string? blobName) { - - if (!configuration.NamingNormalizers.Any()) + var normalizerTypes = configuration.GetEffectiveNamingNormalizers(); + if (!normalizerTypes.Any()) { return new BlobNormalizeNaming(containerName, blobName); } using (var scope = ServiceProvider.CreateScope()) { - foreach (var normalizerType in configuration.NamingNormalizers) + foreach (var normalizerType in normalizerTypes) { var normalizer = scope.ServiceProvider .GetRequiredService(normalizerType) @@ -43,21 +43,11 @@ public class BlobNormalizeNamingService : IBlobNormalizeNamingService, ITransien public string NormalizeContainerName(BlobContainerConfiguration configuration, string containerName) { - if (!configuration.NamingNormalizers.Any()) - { - return containerName; - } - return NormalizeNaming(configuration, containerName, null).ContainerName!; } public string NormalizeBlobName(BlobContainerConfiguration configuration, string blobName) { - if (!configuration.NamingNormalizers.Any()) - { - return blobName; - } - return NormalizeNaming(configuration, null, blobName).BlobName!; } } 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 a594613b44..257e12d0dc 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 @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -240,14 +241,14 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency using (var response = await client.PostAsync(url, stringContent, _cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)))) { - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + await EnsureAbpIoSuccessfulResponseAsync(response); var result = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(result).Version; } } - catch (Exception ex) + catch (Exception ex) when (ex is not CliUsageException) { - Console.WriteLine("Error occured while getting the latest version from {0} : {1}", url, ex.Message); + Console.WriteLine("Error occurred while getting the latest version from {0} : {1}", url, ex.Message); return null; } } @@ -273,17 +274,39 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency using (var response = await client.PostAsync(url, stringContent, _cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)))) { - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + await EnsureAbpIoSuccessfulResponseAsync(response); var result = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(result).Version; } } - catch (Exception) + catch (Exception ex) when (ex is not CliUsageException) { return null; } } + private async Task EnsureAbpIoSuccessfulResponseAsync(HttpResponseMessage responseMessage) + { + if (responseMessage is { StatusCode: HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden }) + { + var message = $"Remote server returns '{(int)responseMessage.StatusCode}-{responseMessage.ReasonPhrase}'. "; + + var serverError = await RemoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage); + if (!string.IsNullOrWhiteSpace(serverError)) + { + message += serverError + " "; + } + + message += $"Authentication or license check failed while accessing {CliUrls.WwwAbpIo}. " + + "Please make sure you are logged in with `abp login ` and your ABP commercial license is active and covers the requested version. " + + $"You can check your license at {CliUrls.WwwAbpIo}my-organizations"; + + throw new CliUsageException(message); + } + + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + } + private async Task IsVersionExists(string templateName, string version) { var url = $"{CliUrls.WwwAbpIo}api/download/all-versions?includePreReleases=true"; @@ -313,6 +336,8 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency private async Task DownloadSourceCodeContentAsync(SourceCodeDownloadInputDto input) { var url = $"{CliUrls.WwwAbpIo}api/download/{input.Type}/"; + var isAbpIoDownload = input.TemplateSource.IsNullOrWhiteSpace(); + var downloadUrl = isAbpIoDownload ? url : input.TemplateSource; HttpResponseMessage responseMessage = null; @@ -320,7 +345,7 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency { var client = _cliHttpClientFactory.CreateClient(timeout: TimeSpan.FromMinutes(5)); - if (input.TemplateSource.IsNullOrWhiteSpace()) + if (isAbpIoDownload) { responseMessage = await client.PostAsync( url, @@ -334,24 +359,38 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency _cliHttpClientFactory.GetCancellationToken()); } - await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); - var resultAsBytes = await responseMessage.Content.ReadAsByteArrayAsync(); - responseMessage.Dispose(); + if (isAbpIoDownload) + { + await EnsureAbpIoSuccessfulResponseAsync(responseMessage); + } + else + { + await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage); + } - return resultAsBytes; + return await responseMessage.Content.ReadAsByteArrayAsync(); } catch (Exception ex) { - if(ex is UserFriendlyException) + if (ex is CliUsageException) + { + throw; + } + + if (ex is UserFriendlyException) { Logger.LogWarning(ex.Message); throw; } - Console.WriteLine("Error occured while downloading source-code from {0} : {1}{2}{3}", url, + Console.WriteLine("Error occurred while downloading source-code from {0} : {1}{2}{3}", downloadUrl, responseMessage?.ToString(), Environment.NewLine, ex.Message); throw; } + finally + { + responseMessage?.Dispose(); + } } private static bool IsNetworkSource(string source) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs index fffdb6291f..2cf1e5f6ab 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler.cs @@ -4,10 +4,11 @@ using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; using Volo.Abp.Json; +using NewtonsoftJsonException = Newtonsoft.Json.JsonException; +using SystemJsonException = System.Text.Json.JsonException; namespace Volo.Abp.Cli.ProjectBuilding; @@ -49,12 +50,11 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr RemoteServiceErrorResponse errorResult; try { - errorResult = _jsonSerializer.Deserialize - ( + errorResult = _jsonSerializer.Deserialize( await responseMessage.Content.ReadAsStringAsync() ); } - catch (JsonReaderException) + catch (Exception ex) when (IsJsonException(ex)) { return null; } @@ -98,4 +98,9 @@ public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITr return sbError.ToString(); } + + private static bool IsJsonException(Exception ex) + { + return ex is SystemJsonException or NewtonsoftJsonException; + } } diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs index e1899f7efc..bf52211a39 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Logging/AbpLoggerExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Text.Json; using Volo.Abp.ExceptionHandling; using Volo.Abp.Logging; @@ -92,12 +93,37 @@ public static class AbpLoggerExtensions exceptionData.AppendLine("---------- Exception Data ----------"); foreach (var key in exception.Data.Keys) { - exceptionData.AppendLine($"{key} = {exception.Data[key]}"); + exceptionData.AppendLine($"{key} = {FormatDataValue(exception.Data[key])}"); } logger.LogWithLevel(logLevel, exceptionData.ToString()); } + private const int MaxDataValueLength = 4096; + + private static string FormatDataValue(object? value) + { + if (value == null) + { + return string.Empty; + } + + var type = value.GetType(); + if (value is string || type.IsPrimitive || value is decimal || value is DateTime || value is DateTimeOffset || value is Guid || type.IsEnum) + { + return value.ToString()!; + } + + try + { + return JsonSerializer.Serialize(value).TruncateWithPostfix(MaxDataValueLength, "...(truncated)")!; + } + catch + { + return value.ToString()!; + } + } + private static void LogSelfLogging(ILogger logger, Exception exception) { var loggingExceptions = new List(); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs index 3d01c3c48d..ffc02d3207 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerManager.cs @@ -46,7 +46,7 @@ public class SimpleStateCheckerManager : ISimpleStateCheckerManager !x)) diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs index 737bcb3c2f..9c0f1fed13 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs @@ -30,6 +30,11 @@ namespace Volo.Abp.Application; )] public class AbpDddApplicationModule : AbpModule { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + AbpDynamicSortingGuard.Install(); + } + public override void ConfigureServices(ServiceConfigurationContext context) { Configure(options => diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs new file mode 100644 index 0000000000..63caa66af5 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using Volo.Abp.Validation; + +[assembly: InternalsVisibleTo("Volo.Abp.Ddd.Application.Tests")] + +namespace Volo.Abp.Application.Services; + +/// +/// Framework infrastructure. Hooks so +/// every OrderBy / ThenBy expression built from a user-supplied sorting string is +/// constrained to plain property or field access. Methods, comparisons, ternaries +/// and constants in the sort key are rejected with . +/// +internal static class AbpDynamicSortingGuard +{ + private static readonly object InstallLock = new(); + private static Func? _activeOptimizer; + + public static void Install() + { + lock (InstallLock) + { + var current = ExtensibilityPoint.QueryOptimizer; + if (_activeOptimizer != null && ReferenceEquals(current, _activeOptimizer)) + { + return; + } + + var previous = current; + _activeOptimizer = expression => + { + new OrderByMethodVisitor().Visit(expression); + return previous != null ? previous(expression) : expression; + }; + ExtensibilityPoint.QueryOptimizer = _activeOptimizer; + } + } + + internal static void Reset() + { + lock (InstallLock) + { + if (ReferenceEquals(ExtensibilityPoint.QueryOptimizer, _activeOptimizer)) + { + ExtensibilityPoint.QueryOptimizer = null; + } + _activeOptimizer = null; + } + } + + private sealed class OrderByMethodVisitor : ExpressionVisitor + { + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(Queryable) && + IsOrderByMethod(node.Method.Name) && + node.Arguments.Count >= 2 && + node.Arguments[1] is UnaryExpression { Operand: LambdaExpression lambda }) + { + new PropertyOnlySelectorVisitor().Visit(lambda.Body); + } + + return base.VisitMethodCall(node); + } + + private static bool IsOrderByMethod(string name) + { + return name == nameof(Queryable.OrderBy) + || name == nameof(Queryable.OrderByDescending) + || name == nameof(Queryable.ThenBy) + || name == nameof(Queryable.ThenByDescending); + } + } + + private sealed class PropertyOnlySelectorVisitor : ExpressionVisitor + { + private const string Message = "Sorting expression is not supported."; + + protected override Expression VisitMethodCall(MethodCallExpression node) + => throw new AbpValidationException(Message); + + protected override Expression VisitBinary(BinaryExpression node) + => throw new AbpValidationException(Message); + + protected override Expression VisitConditional(ConditionalExpression node) + => throw new AbpValidationException(Message); + + protected override Expression VisitConstant(ConstantExpression node) + => throw new AbpValidationException(Message); + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index 6e0a5008a3..107008deea 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -50,7 +50,9 @@ public class ClientProxyBase : ITransientDependency protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue? arguments = null) { - await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments)); + using (await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments))) + { + } } protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue? arguments = null) @@ -120,18 +122,21 @@ public class ClientProxyBase : ITransientDependency responseContent.Headers?.ContentLength); } - var stringContent = await responseContent.ReadAsStringAsync(); - if (typeof(T) == typeof(string)) + using (responseContent) { - return (T)(object)stringContent; - } + var stringContent = await responseContent.ReadAsStringAsync(); + if (typeof(T) == typeof(string)) + { + return (T)(object)stringContent; + } - if (stringContent.IsNullOrWhiteSpace()) - { - return default!; - } + if (stringContent.IsNullOrWhiteSpace()) + { + return default!; + } - return JsonSerializer.Deserialize(stringContent); + return JsonSerializer.Deserialize(stringContent); + } } protected virtual async Task RequestAsync(ClientProxyRequestContext requestContext) @@ -184,7 +189,15 @@ public class ClientProxyBase : ITransientDependency if (!response.IsSuccessStatusCode) { - await ThrowExceptionForResponseAsync(response); + try + { + await ThrowExceptionForResponseAsync(response); + } + catch + { + response.Dispose(); + throw; + } } return response.Content; diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs index 0f5a9e3642..a0b60ce006 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -59,7 +59,9 @@ public class DynamicHttpProxyInterceptor : AbpInterceptor, ITransientD if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) { - await InterceptorClientProxy.CallRequestAsync(context); + using (await InterceptorClientProxy.CallRequestAsync(context)) + { + } } else { diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs new file mode 100644 index 0000000000..1c7e029807 --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpCompressOptions.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.Imaging; + +public class SkiaSharpCompressOptions +{ + public int Quality { get; set; } + + public SkiaSharpCompressOptions() + { + Quality = 75; + } +} diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs new file mode 100644 index 0000000000..c3117d0461 --- /dev/null +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageCompressorContributor.cs @@ -0,0 +1,152 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using SkiaSharp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; + +namespace Volo.Abp.Imaging; + +public class SkiaSharpImageCompressorContributor : IImageCompressorContributor, ITransientDependency +{ + protected SkiaSharpCompressOptions Options { get; } + + public SkiaSharpImageCompressorContributor(IOptions options) + { + Options = options.Value; + } + + public virtual async Task> TryCompressAsync( + Stream stream, + string? mimeType = null, + CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) + { + return new ImageCompressResult(stream, ImageProcessState.Unsupported); + } + + var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream, cancellationToken); + var originalLength = memoryBitmapStream.Length; + + try + { + using var codec = SKCodec.Create(memorySkCodecStream); + if (codec == null || !CanEncodeFormat(codec.EncodedFormat)) + { + return new ImageCompressResult(stream, ImageProcessState.Unsupported); + } + + using var bitmap = SKBitmap.Decode(memoryBitmapStream); + if (bitmap == null) + { + return new ImageCompressResult(stream, ImageProcessState.Unsupported); + } + + using var image = SKImage.FromBitmap(bitmap); + using var encoded = image.Encode(codec.EncodedFormat, Options.Quality); + + var output = new MemoryStream(); + try + { + encoded.SaveTo(output); + output.Position = 0; + + if (output.Length < originalLength) + { + return new ImageCompressResult(output, ImageProcessState.Done); + } + + output.Dispose(); + return new ImageCompressResult(stream, ImageProcessState.Canceled); + } + catch + { + output.Dispose(); + throw; + } + } + finally + { + memoryBitmapStream.Dispose(); + memorySkCodecStream.Dispose(); + } + } + + public virtual async Task> TryCompressAsync( + byte[] bytes, + string? mimeType = null, + CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(mimeType) && !CanCompress(mimeType)) + { + return new ImageCompressResult(bytes, ImageProcessState.Unsupported); + } + + using var ms = new MemoryStream(bytes); + var result = await TryCompressAsync(ms, mimeType, cancellationToken); + + if (result.State != ImageProcessState.Done) + { + return new ImageCompressResult(bytes, result.State); + } + + var newBytes = await result.Result.GetAllBytesAsync(cancellationToken); + result.Result.Dispose(); + return new ImageCompressResult(newBytes, result.State); + } + + protected virtual bool CanCompress(string? mimeType) + { + return mimeType switch { + MimeTypes.Image.Jpeg => true, + MimeTypes.Image.Png => true, + MimeTypes.Image.Webp => true, + _ => false + }; + } + + protected virtual bool CanEncodeFormat(SKEncodedImageFormat format) + { + return format switch + { + SKEncodedImageFormat.Jpeg => true, + SKEncodedImageFormat.Png => true, + SKEncodedImageFormat.Webp => true, + _ => false + }; + } + + protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream, CancellationToken cancellationToken) + { + var streamPosition = stream.CanSeek ? stream.Position : 0; + + var memoryBitmapStream = new MemoryStream(); + var memorySkCodecStream = new MemoryStream(); + + try + { + await stream.CopyToAsync(memoryBitmapStream, cancellationToken); + + if (stream.CanSeek) + { + stream.Position = streamPosition; + } + + memoryBitmapStream.Position = 0; + await memoryBitmapStream.CopyToAsync(memorySkCodecStream, cancellationToken); + + memoryBitmapStream.Position = 0; + memorySkCodecStream.Position = 0; + + return (memoryBitmapStream, memorySkCodecStream); + } + catch + { + memoryBitmapStream.Dispose(); + memorySkCodecStream.Dispose(); + throw; + } + } +} diff --git a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs index 7a5db23620..dd7da3251a 100644 --- a/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs +++ b/framework/src/Volo.Abp.Imaging.SkiaSharp/Volo/Abp/Imaging/SkiaSharpImageResizerContributor.cs @@ -47,34 +47,76 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans return new ImageResizeResult(stream, ImageProcessState.Unsupported); } - var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream); - - using var original = SKBitmap.Decode(memoryBitmapStream); - using var resized = original.Resize(new SKImageInfo((int)resizeArgs.Width, (int)resizeArgs.Height), Options.SKSamplingOptions); - using var image = SKImage.FromBitmap(resized); - using var codec = SKCodec.Create(memorySkCodecStream); - var memoryStream = new MemoryStream(); - using var skData = image.Encode(codec.EncodedFormat, Options.Quality); - skData.SaveTo(memoryStream); - return new ImageResizeResult(memoryStream, ImageProcessState.Done); + var (memoryBitmapStream, memorySkCodecStream) = await CreateMemoryStream(stream, cancellationToken); + + try + { + using var codec = SKCodec.Create(memorySkCodecStream); + if (codec == null || !CanEncodeFormat(codec.EncodedFormat)) + { + return new ImageResizeResult(stream, ImageProcessState.Unsupported); + } + + using var original = SKBitmap.Decode(memoryBitmapStream); + if (original == null) + { + return new ImageResizeResult(stream, ImageProcessState.Unsupported); + } + + using var resized = ApplyResize(original, resizeArgs); + using var image = SKImage.FromBitmap(resized); + + var memoryStream = new MemoryStream(); + try + { + using var skData = image.Encode(codec.EncodedFormat, Options.Quality); + skData.SaveTo(memoryStream); + memoryStream.Position = 0; + return new ImageResizeResult(memoryStream, ImageProcessState.Done); + } + catch + { + memoryStream.Dispose(); + throw; + } + } + finally + { + memoryBitmapStream.Dispose(); + memorySkCodecStream.Dispose(); + } } - protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream) + protected virtual async Task<(MemoryStream, MemoryStream)> CreateMemoryStream(Stream stream, CancellationToken cancellationToken = default) { - var streamPosition = stream.Position; + var streamPosition = stream.CanSeek ? stream.Position : 0; var memoryBitmapStream = new MemoryStream(); var memorySkCodecStream = new MemoryStream(); - await stream.CopyToAsync(memoryBitmapStream); - stream.Position = streamPosition; - await stream.CopyToAsync(memorySkCodecStream); - stream.Position = streamPosition; + try + { + await stream.CopyToAsync(memoryBitmapStream, cancellationToken); + + if (stream.CanSeek) + { + stream.Position = streamPosition; + } + + memoryBitmapStream.Position = 0; + await memoryBitmapStream.CopyToAsync(memorySkCodecStream, cancellationToken); - memoryBitmapStream.Position = 0; - memorySkCodecStream.Position = 0; + memoryBitmapStream.Position = 0; + memorySkCodecStream.Position = 0; - return (memoryBitmapStream, memorySkCodecStream); + return (memoryBitmapStream, memorySkCodecStream); + } + catch + { + memoryBitmapStream.Dispose(); + memorySkCodecStream.Dispose(); + throw; + } } protected virtual bool CanResize(string? mimeType) @@ -86,4 +128,150 @@ public class SkiaSharpImageResizerContributor : IImageResizerContributor, ITrans _ => false }; } + + protected virtual bool CanEncodeFormat(SKEncodedImageFormat format) + { + return format switch + { + SKEncodedImageFormat.Jpeg => true, + SKEncodedImageFormat.Png => true, + SKEncodedImageFormat.Webp => true, + _ => false + }; + } + + protected virtual SKBitmap ApplyResize(SKBitmap source, ImageResizeArgs resizeArgs) + { + var targetWidth = (int)resizeArgs.Width; + var targetHeight = (int)resizeArgs.Height; + + if (targetWidth <= 0 && targetHeight <= 0) + { + return source.Copy(); + } + + if (targetWidth <= 0) + { + targetWidth = Math.Max(1, (int)Math.Round((double)source.Width * targetHeight / source.Height)); + } + else if (targetHeight <= 0) + { + targetHeight = Math.Max(1, (int)Math.Round((double)source.Height * targetWidth / source.Width)); + } + + var mode = resizeArgs.Mode == ImageResizeMode.Default ? ImageResizeMode.Stretch : resizeArgs.Mode; + + switch (mode) + { + case ImageResizeMode.None: + case ImageResizeMode.Stretch: + return source.Resize(new SKImageInfo(targetWidth, targetHeight), Options.SKSamplingOptions); + + case ImageResizeMode.Max: + { + var scale = Math.Min((double)targetWidth / source.Width, (double)targetHeight / source.Height); + var newW = Math.Max(1, (int)Math.Round(source.Width * scale)); + var newH = Math.Max(1, (int)Math.Round(source.Height * scale)); + return source.Resize(new SKImageInfo(newW, newH), Options.SKSamplingOptions); + } + + case ImageResizeMode.Min: + { + var scale = Math.Max((double)targetWidth / source.Width, (double)targetHeight / source.Height); + var newW = Math.Max(1, (int)Math.Round(source.Width * scale)); + var newH = Math.Max(1, (int)Math.Round(source.Height * scale)); + return source.Resize(new SKImageInfo(newW, newH), Options.SKSamplingOptions); + } + + case ImageResizeMode.Crop: + { + var scale = Math.Max((double)targetWidth / source.Width, (double)targetHeight / source.Height); + var intermediateW = Math.Max(1, (int)Math.Round(source.Width * scale)); + var intermediateH = Math.Max(1, (int)Math.Round(source.Height * scale)); + using var intermediate = source.Resize(new SKImageInfo(intermediateW, intermediateH), Options.SKSamplingOptions); + + var bitmap = new SKBitmap(targetWidth, targetHeight); + try + { + using var canvas = new SKCanvas(bitmap); + var srcX = (intermediateW - targetWidth) / 2; + var srcY = (intermediateH - targetHeight) / 2; + canvas.DrawBitmap( + intermediate, + new SKRect(srcX, srcY, srcX + targetWidth, srcY + targetHeight), + new SKRect(0, 0, targetWidth, targetHeight)); + return bitmap; + } + catch + { + bitmap.Dispose(); + throw; + } + } + + case ImageResizeMode.Pad: + { + var scale = Math.Min((double)targetWidth / source.Width, (double)targetHeight / source.Height); + var intermediateW = Math.Max(1, (int)Math.Round(source.Width * scale)); + var intermediateH = Math.Max(1, (int)Math.Round(source.Height * scale)); + using var intermediate = source.Resize(new SKImageInfo(intermediateW, intermediateH), Options.SKSamplingOptions); + + var bitmap = new SKBitmap(targetWidth, targetHeight); + try + { + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Transparent); + var dstX = (targetWidth - intermediateW) / 2; + var dstY = (targetHeight - intermediateH) / 2; + canvas.DrawBitmap(intermediate, new SKPoint(dstX, dstY)); + return bitmap; + } + catch + { + bitmap.Dispose(); + throw; + } + } + + case ImageResizeMode.BoxPad: + { + SKBitmap? scaled = null; + try + { + var working = source; + if (source.Width > targetWidth || source.Height > targetHeight) + { + var scale = Math.Min((double)targetWidth / source.Width, (double)targetHeight / source.Height); + var newW = Math.Max(1, (int)Math.Round(source.Width * scale)); + var newH = Math.Max(1, (int)Math.Round(source.Height * scale)); + scaled = source.Resize(new SKImageInfo(newW, newH), Options.SKSamplingOptions); + working = scaled; + } + + var bitmap = new SKBitmap(targetWidth, targetHeight); + try + { + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Transparent); + var dstX = (targetWidth - working.Width) / 2; + var dstY = (targetHeight - working.Height) / 2; + canvas.DrawBitmap(working, new SKPoint(dstX, dstY)); + return bitmap; + } + catch + { + bitmap.Dispose(); + throw; + } + } + finally + { + scaled?.Dispose(); + } + } + + default: + throw new NotSupportedException("Resize mode " + resizeArgs.Mode + " is not supported!"); + } + } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs index f065f0e695..d39c06a1b4 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs @@ -60,7 +60,6 @@ public static class JsonLocalizationDictionaryBuilder } var dictionary = new Dictionary(); - var dublicateNames = new List(); foreach (var item in FlattenTexts(jsonFile.Texts)) { @@ -68,20 +67,10 @@ public static class JsonLocalizationDictionaryBuilder { throw new AbpException("The key is empty in given json string."); } - if (dictionary.GetOrDefault(item.Key) != null) - { - dublicateNames.Add(item.Key); - } + dictionary[item.Key] = new LocalizedString(item.Key, item.Value.NormalizeLineEndings()); } - if (dublicateNames.Count > 0) - { - throw new AbpException( - "A dictionary can not contain same key twice. There are some duplicated names: " + - dublicateNames.JoinAsString(", ")); - } - return new StaticLocalizationDictionary(cultureCode, dictionary); } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs index d151dc78df..41fd38a430 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -19,7 +20,6 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza private Dictionary? _dictionaries; private bool _subscribedForChanges; private readonly object _syncObj = new object(); - private LocalizationResourceBase _resource = default!; protected VirtualFileLocalizationResourceContributorBase(string virtualPath) { @@ -28,7 +28,6 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza public virtual void Initialize(LocalizationResourceInitializationContext context) { - _resource = context.Resource; _virtualFileProvider = context.ServiceProvider.GetRequiredService(); } @@ -88,15 +87,12 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza private Dictionary CreateDictionaries() { - var dictionaries = new Dictionary(); + var rawDictionaries = new Dictionary>(); - foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath)) + foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath) + .Where(f => !f.IsDirectory && CanParseFile(f)) + .OrderBy(f => f.Name, StringComparer.Ordinal)) { - if (file.IsDirectory || !CanParseFile(file)) - { - continue; - } - var dictionary = CreateDictionaryFromFile(file); if (dictionary == null) @@ -104,15 +100,19 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza continue; } - if (dictionaries.ContainsKey(dictionary.CultureName)) + if (!rawDictionaries.TryGetValue(dictionary.CultureName, out var raw)) { - throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()} dictionary has a culture name '{dictionary.CultureName}' which is already defined! Localization resource: {_resource.ResourceName}"); + raw = new Dictionary(); + rawDictionaries[dictionary.CultureName] = raw; } - dictionaries[dictionary.CultureName] = dictionary; + dictionary.Fill(raw); } - return dictionaries; + return rawDictionaries.ToDictionary( + kvp => kvp.Key, + kvp => (ILocalizationDictionary)new StaticLocalizationDictionary(kvp.Key, kvp.Value) + ); } protected abstract bool CanParseFile(IFileInfo file); diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 4fd72ded4b..32dfa93807 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -46,7 +46,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider { mapper.BeforeMap((TSource)source); var destination = mapper.Map((TSource)source); - TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, GetExtraProperties(destination)); + TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary()); mapper.AfterMap((TSource)source, destination); return destination; } @@ -56,7 +56,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider { reverseMapper.BeforeReverseMap((TSource)source); var destination = reverseMapper.ReverseMap((TSource)source); - TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, GetExtraProperties(destination)); + TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary()); reverseMapper.AfterReverseMap((TSource)source, destination); return destination; } diff --git a/framework/src/Volo.Abp.MudBlazorUI/AbpMudBlazorMessageLocalizerHelper.cs b/framework/src/Volo.Abp.MudBlazorUI/AbpMudBlazorMessageLocalizerHelper.cs new file mode 100644 index 0000000000..007db40ca6 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/AbpMudBlazorMessageLocalizerHelper.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.MudBlazorUI; + +public class AbpMudBlazorMessageLocalizerHelper +{ + private readonly IStringLocalizer _localizer; + + public AbpMudBlazorMessageLocalizerHelper(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public string Localize(string message, params object[] arguments) + { + return _localizer[message, arguments]; + } + + public string? LocalizeOrNull(string? message, params object[] arguments) + { + if (message.IsNullOrEmpty()) + { + return null; + } + + return _localizer[message!, arguments]; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/AbpMudBlazorUIModule.cs b/framework/src/Volo.Abp.MudBlazorUI/AbpMudBlazorUIModule.cs new file mode 100644 index 0000000000..74c425c214 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/AbpMudBlazorUIModule.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using MudBlazor; +using MudBlazor.Services; +using Volo.Abp.Application; +using Volo.Abp.AspNetCore.Components.Web; +using Volo.Abp.Authorization; +using Volo.Abp.Features; +using Volo.Abp.GlobalFeatures; +using Volo.Abp.Modularity; + +namespace Volo.Abp.MudBlazorUI; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebModule), + typeof(AbpDddApplicationContractsModule), + typeof(AbpAuthorizationModule), + typeof(AbpGlobalFeaturesModule), + typeof(AbpFeaturesModule) +)] +public class AbpMudBlazorUIModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + ConfigureMudBlazor(context); + } + + private void ConfigureMudBlazor(ServiceConfigurationContext context) + { + context.Services.AddMudServices(config => + { + config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomEnd; + config.SnackbarConfiguration.PreventDuplicates = false; + config.SnackbarConfiguration.NewestOnTop = true; + config.SnackbarConfiguration.ShowCloseIcon = true; + config.SnackbarConfiguration.VisibleStateDuration = 5000; + config.SnackbarConfiguration.HideTransitionDuration = 500; + config.SnackbarConfiguration.ShowTransitionDuration = 500; + config.SnackbarConfiguration.SnackbarVariant = Variant.Filled; + }); + + context.Services.AddSingleton(typeof(AbpMudBlazorMessageLocalizerHelper<>)); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/AbpMudCrudPageBase.cs b/framework/src/Volo.Abp.MudBlazorUI/AbpMudCrudPageBase.cs new file mode 100644 index 0000000000..9bc201bdca --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/AbpMudCrudPageBase.cs @@ -0,0 +1,753 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; +using Volo.Abp.Authorization; +using Volo.Abp.Localization; +using Volo.Abp.MudBlazorUI.Components; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.Modularity; + +namespace Volo.Abp.MudBlazorUI; + +public abstract class AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey> + : AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey, + PagedAndSortedResultRequestDto> + where TAppService : ICrudAppService< + TEntityDto, + TKey> + where TEntityDto : class, IEntityDto, new() +{ +} + +public abstract class AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey, + TGetListInput> + : AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey, + TGetListInput, + TEntityDto> + where TAppService : ICrudAppService< + TEntityDto, + TKey, + TGetListInput> + where TEntityDto : class, IEntityDto, new() + where TGetListInput : new() +{ +} + +public abstract class AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey, + TGetListInput, + TCreateInput> + : AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey, + TGetListInput, + TCreateInput, + TCreateInput> + where TAppService : ICrudAppService< + TEntityDto, + TKey, + TGetListInput, + TCreateInput> + where TEntityDto : IEntityDto + where TCreateInput : class, new() + where TGetListInput : new() +{ +} + +public abstract class AbpMudCrudPageBase< + TAppService, + TEntityDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput> + : AbpMudCrudPageBase< + TAppService, + TEntityDto, + TEntityDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput> + where TAppService : ICrudAppService< + TEntityDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput> + where TEntityDto : IEntityDto + where TCreateInput : class, new() + where TUpdateInput : class, new() + where TGetListInput : new() +{ +} + +public abstract class AbpMudCrudPageBase< + TAppService, + TGetOutputDto, + TGetListOutputDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput> + : AbpMudCrudPageBase< + TAppService, + TGetOutputDto, + TGetListOutputDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput, + TGetListOutputDto, + TCreateInput, + TUpdateInput> + where TAppService : ICrudAppService< + TGetOutputDto, + TGetListOutputDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput> + where TGetOutputDto : IEntityDto + where TGetListOutputDto : IEntityDto + where TCreateInput : class, new() + where TUpdateInput : class, new() + where TGetListInput : new() +{ +} + +public abstract class AbpMudCrudPageBase< + TAppService, + TGetOutputDto, + TGetListOutputDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput, + TListViewModel, + TCreateViewModel, + TUpdateViewModel> + : AbpComponentBase + where TAppService : ICrudAppService< + TGetOutputDto, + TGetListOutputDto, + TKey, + TGetListInput, + TCreateInput, + TUpdateInput> + where TGetOutputDto : IEntityDto + where TGetListOutputDto : IEntityDto + where TCreateInput : class + where TUpdateInput : class + where TGetListInput : new() + where TListViewModel : IEntityDto + where TCreateViewModel : class, new() + where TUpdateViewModel : class, new() +{ + [Inject] protected TAppService AppService { get; set; } = default!; + [Inject] protected IStringLocalizer UiLocalizer { get; set; } = default!; + [Inject] public IAbpEnumLocalizer AbpEnumLocalizer { get; set; } = default!; + [Inject] protected IDialogService DialogService { get; set; } = default!; + [Inject] protected ISnackbar Snackbar { get; set; } = default!; + [Inject] protected ExtensionPropertyPolicyChecker ExtensionPropertyPolicyChecker { get; set; } = default!; + + protected virtual int PageSize { get; set; } = LimitedResultRequestDto.DefaultMaxResultCount; + + protected int CurrentPage = 1; + protected string CurrentSorting = default!; + protected TGetListInput GetListInput = new TGetListInput(); + protected TCreateViewModel NewEntity; + protected TKey EditingEntityId = default!; + protected TUpdateViewModel EditingEntity; + protected MudForm? CreateFormRef; + protected MudForm? EditFormRef; + protected List BreadcrumbItems = new List(2); + protected MudDataGridEntityActionsColumn EntityActionsColumn = default!; + protected EntityActionDictionary EntityActions { get; set; } + protected TableColumnDictionary TableColumns { get; set; } + + protected string? CreatePolicyName { get; set; } + protected string? UpdatePolicyName { get; set; } + protected string? DeletePolicyName { get; set; } + + public bool HasCreatePermission { get; set; } + public bool HasUpdatePermission { get; set; } + public bool HasDeletePermission { get; set; } + + protected MudDialog? _createDialog; + protected MudDialog? _editDialog; + + protected bool IsCreating { get; set; } + protected bool IsUpdating { get; set; } + + protected virtual DialogOptions CreateDialogOptions => new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + CloseOnEscapeKey = true, + BackdropClick = false + }; + + protected virtual DialogOptions EditDialogOptions => new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + CloseOnEscapeKey = true, + BackdropClick = false + }; + + protected AbpMudExtensibleDataGrid _dataGrid = default!; + + protected AbpMudCrudPageBase() + { + NewEntity = new TCreateViewModel(); + EditingEntity = new TUpdateViewModel(); + TableColumns = new TableColumnDictionary(); + EntityActions = new EntityActionDictionary(); + } + + protected override async Task OnInitializedAsync() + { + await TrySetPermissionsAsync(); + await TrySetEntityActionsAsync(); + await TrySetTableColumnsAsync(); + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await SetToolbarItemsAsync(); + await SetBreadcrumbItemsAsync(); + } + await base.OnAfterRenderAsync(firstRender); + } + + private async Task TrySetPermissionsAsync() + { + if (IsDisposed) + { + return; + } + + await SetPermissionsAsync(); + } + + protected virtual async Task SetPermissionsAsync() + { + if (CreatePolicyName != null) + { + HasCreatePermission = await AuthorizationService.IsGrantedAsync(CreatePolicyName); + } + + if (UpdatePolicyName != null) + { + HasUpdatePermission = await AuthorizationService.IsGrantedAsync(UpdatePolicyName); + } + + if (DeletePolicyName != null) + { + HasDeletePermission = await AuthorizationService.IsGrantedAsync(DeletePolicyName); + } + } + + private IReadOnlyList MapToListViewModel(IReadOnlyList dtos) + { + if (typeof(TGetListOutputDto) == typeof(TListViewModel)) + { + return dtos.As>(); + } + + return ObjectMapper.Map, List>(dtos); + } + + protected virtual string MapSortFieldToPropertyName(string sortBy, List columns) + { + if (string.IsNullOrEmpty(sortBy)) + { + return sortBy; + } + + return sortBy; + } + + protected virtual Task UpdateGetListInputAsync() + { + if (GetListInput is ISortedResultRequest sortedResultRequestInput) + { + sortedResultRequestInput.Sorting = CurrentSorting; + } + + if (GetListInput is IPagedResultRequest pagedResultRequestInput) + { + pagedResultRequestInput.SkipCount = (CurrentPage - 1) * PageSize; + } + + if (GetListInput is ILimitedResultRequest limitedResultRequestInput) + { + limitedResultRequestInput.MaxResultCount = PageSize; + } + + return Task.CompletedTask; + } + + protected virtual async Task SearchEntitiesAsync() + { + var currentPage = CurrentPage; + CurrentPage = 1; + if (currentPage == 1) + { + await _dataGrid.ReloadServerDataAsync(); + } + else + { + await InvokeAsync(StateHasChanged); + } + } + + protected virtual async Task> OnDataGridReadAsync(GridState state, CancellationToken cancellationToken = default) + { + CurrentSorting = state.SortDefinitions + .Select(s => _dataGrid.ResolveSortPropertyName(s) + (s.Descending ? " DESC" : "")) + .JoinAsString(","); + CurrentPage = state.Page + 1; + if (state.PageSize > 0) + { + PageSize = state.PageSize; + } + + try + { + await UpdateGetListInputAsync(); + var result = await AppService.GetListAsync(GetListInput); + var items = MapToListViewModel(result.Items); + + await InvokeAsync(StateHasChanged); + + return new GridData + { + Items = items, + TotalItems = (int)result.TotalCount + }; + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + return new GridData + { + Items = Array.Empty(), + TotalItems = 0 + }; + } + } + + protected virtual async Task OpenCreateDialogAsync() + { + try + { + await CheckCreatePolicyAsync(); + + NewEntity = new TCreateViewModel(); + + if (_createDialog != null) + { + await InvokeAsync(() => _createDialog.ShowAsync()); + } + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual Task CloseCreateDialogAsync() + { + NewEntity = new TCreateViewModel(); + if (_createDialog != null) + { + return InvokeAsync(() => _createDialog.CloseAsync()); + } + return Task.CompletedTask; + } + + protected virtual async Task OpenEditDialogAsync(TListViewModel entity) + { + try + { + await CheckUpdatePolicyAsync(); + + var entityDto = await AppService.GetAsync(entity.Id); + + EditingEntityId = entity.Id; + EditingEntity = MapToEditingEntity(entityDto); + + if (_editDialog != null) + { + await InvokeAsync(() => _editDialog.ShowAsync()); + } + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual TUpdateViewModel MapToEditingEntity(TGetOutputDto entityDto) + { + return ObjectMapper.Map(entityDto); + } + + protected virtual TCreateInput MapToCreateInput(TCreateViewModel createViewModel) + { + if (typeof(TCreateInput) == typeof(TCreateViewModel)) + { + return createViewModel.As(); + } + + return ObjectMapper.Map(createViewModel); + } + + protected virtual TUpdateInput MapToUpdateInput(TUpdateViewModel updateViewModel) + { + if (typeof(TUpdateInput) == typeof(TUpdateViewModel)) + { + return updateViewModel.As(); + } + + return ObjectMapper.Map(updateViewModel); + } + + protected virtual Task CloseEditDialogAsync() + { + if (_editDialog != null) + { + return InvokeAsync(() => _editDialog.CloseAsync()); + } + return Task.CompletedTask; + } + + protected virtual async Task CreateEntityAsync() + { + if (IsCreating) + { + return; + } + + try + { + IsCreating = true; + await InvokeAsync(StateHasChanged); + + var isValid = true; + if (CreateFormRef != null) + { + await CreateFormRef.ValidateAsync(); + isValid = CreateFormRef.IsValid; + } + + if (isValid) + { + await OnCreatingEntityAsync(); + + await CheckCreatePolicyAsync(); + var createInput = MapToCreateInput(NewEntity); + await AppService.CreateAsync(createInput); + + await OnCreatedEntityAsync(); + } + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + finally + { + IsCreating = false; + await InvokeAsync(StateHasChanged); + } + } + + protected virtual Task OnCreatingEntityAsync() + { + return Task.CompletedTask; + } + + protected virtual async Task OnCreatedEntityAsync() + { + await _dataGrid.ReloadServerDataAsync(); + await CloseCreateDialogAsync(); + Snackbar.Add(GetCreateMessage(), Severity.Success); + } + + protected virtual string GetCreateMessage() + { + return UiLocalizer["CreatedSuccessfully"]; + } + + protected virtual async Task UpdateEntityAsync() + { + if (IsUpdating) + { + return; + } + + try + { + IsUpdating = true; + await InvokeAsync(StateHasChanged); + + var isValid = true; + if (EditFormRef != null) + { + await EditFormRef.ValidateAsync(); + isValid = EditFormRef.IsValid; + } + + if (isValid) + { + await OnUpdatingEntityAsync(); + + await CheckUpdatePolicyAsync(); + var updateInput = MapToUpdateInput(EditingEntity); + await AppService.UpdateAsync(EditingEntityId, updateInput); + + await OnUpdatedEntityAsync(); + } + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + finally + { + IsUpdating = false; + await InvokeAsync(StateHasChanged); + } + } + + protected virtual Task OnUpdatingEntityAsync() + { + return Task.CompletedTask; + } + + protected virtual async Task OnUpdatedEntityAsync() + { + await _dataGrid.ReloadServerDataAsync(); + await CloseEditDialogAsync(); + Snackbar.Add(GetUpdateMessage(), Severity.Success); + } + + protected virtual string GetUpdateMessage() + { + return UiLocalizer["SavedSuccessfully"]; + } + + protected virtual async Task DeleteEntityAsync(TListViewModel entity) + { + try + { + await CheckDeletePolicyAsync(); + + var confirmed = await ConfirmDeleteAsync(entity); + if (!confirmed) + { + return; + } + + await OnDeletingEntityAsync(); + await AppService.DeleteAsync(entity.Id); + await OnDeletedEntityAsync(); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual Task OnDeletingEntityAsync() + { + return Task.CompletedTask; + } + + protected virtual async Task OnDeletedEntityAsync() + { + if (_dataGrid != null) + { + await InvokeAsync(() => _dataGrid.ReloadServerDataAsync()); + } + Snackbar.Add(GetDeleteMessage(), Severity.Success); + } + + protected virtual string GetDeleteMessage() + { + return UiLocalizer["DeletedSuccessfully"]; + } + + protected virtual string GetDeleteConfirmationMessage(TListViewModel entity) + { + return UiLocalizer["ItemWillBeDeletedMessage"]; + } + + protected virtual async Task ConfirmDeleteAsync(TListViewModel entity) + { + var result = await DialogService.ShowMessageBoxAsync( + UiLocalizer["AreYouSure"], + GetDeleteConfirmationMessage(entity), + yesText: UiLocalizer["Yes"], + cancelText: UiLocalizer["Cancel"]); + + return result == true; + } + + protected virtual async Task CheckCreatePolicyAsync() + { + await CheckPolicyAsync(CreatePolicyName); + } + + protected virtual async Task CheckUpdatePolicyAsync() + { + await CheckPolicyAsync(UpdatePolicyName); + } + + protected virtual async Task CheckDeletePolicyAsync() + { + await CheckPolicyAsync(DeletePolicyName); + } + + /// + /// Calls IAuthorizationService.CheckAsync for the given . + /// Throws if given policy was not granted for the current user. + /// + /// Does nothing if is null or empty. + /// + /// A policy name to check + protected virtual async Task CheckPolicyAsync(string? policyName) + { + if (string.IsNullOrEmpty(policyName)) + { + return; + } + + await AuthorizationService.CheckAsync(policyName); + } + + protected virtual ValueTask SetBreadcrumbItemsAsync() + { + return ValueTask.CompletedTask; + } + + private async ValueTask TrySetEntityActionsAsync() + { + if (IsDisposed) + { + return; + } + + await SetEntityActionsAsync(); + } + + protected virtual ValueTask SetEntityActionsAsync() + { + return ValueTask.CompletedTask; + } + + private async ValueTask TrySetTableColumnsAsync() + { + if (IsDisposed) + { + return; + } + + await SetTableColumnsAsync(); + } + + protected virtual ValueTask SetTableColumnsAsync() + { + return ValueTask.CompletedTask; + } + + protected virtual ValueTask SetToolbarItemsAsync() + { + return ValueTask.CompletedTask; + } + + protected virtual async Task> GetExtensionTableColumnsAsync(string moduleName, string entityType) + { + var tableColumns = new List(); + var properties = ModuleExtensionConfigurationHelper.GetPropertyConfigurations(moduleName, entityType).ToList(); + foreach (var propertyInfo in properties) + { + if (!await ExtensionPropertyPolicyChecker.CheckPolicyAsync(propertyInfo.Policy)) + { + continue; + } + + if (propertyInfo.IsAvailableToClients && propertyInfo.UI.OnTable.IsVisible) + { + if (propertyInfo.Name.EndsWith("_Text")) + { + var lookupPropertyName = propertyInfo.Name.RemovePostFix("_Text"); + var lookupPropertyDefinition = properties.SingleOrDefault(t => t.Name == lookupPropertyName)!; + tableColumns.Add(new TableColumn + { + Title = lookupPropertyDefinition.GetLocalizedDisplayName(StringLocalizerFactory), + Data = $"ExtraProperties[{propertyInfo.Name}]", + PropertyName = propertyInfo.Name + }); + } + else + { + var column = new TableColumn + { + Title = propertyInfo.GetLocalizedDisplayName(StringLocalizerFactory), + Data = $"ExtraProperties[{propertyInfo.Name}]", + PropertyName = propertyInfo.Name + }; + + if (propertyInfo.IsDate() || propertyInfo.IsDateTime()) + { + column.DisplayFormat = propertyInfo.GetDateEditInputFormatOrNull(); + } + + if (propertyInfo.Type.IsEnum) + { + column.ValueConverter = (val) => + AbpEnumLocalizer.GetString(propertyInfo.Type, val.As().ExtraProperties[propertyInfo.Name]!, new IStringLocalizer?[] { StringLocalizerFactory.CreateDefaultOrNull() }); + } + + tableColumns.Add(column); + } + } + } + + return tableColumns; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudExtensibleDataGrid.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudExtensibleDataGrid.razor new file mode 100644 index 0000000000..2bf9b47935 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudExtensibleDataGrid.razor @@ -0,0 +1,133 @@ +@typeparam TItem +@using MudBlazor +@using Volo.Abp.Data + + + + + + + + @UiLocalizer["NoDataAvailableInDatatable"] + + + + @if (Columns != null) + { + @foreach (var column in Columns) + { + if (column.Actions.Any()) + { + + + @{ + var effectiveActions = column.Actions + .Where(a => a.Visible?.Invoke(context.Item!) != false) + .ToList(); + var entityActionType = effectiveActions.Count == 1 ? MudActionType.Button : MudActionType.Dropdown; + } + + @foreach (var action in column.Actions) + { + if (action.ConfirmationMessage != null) + { + + + } + else + { + + + } + } + + + + } + else + { + @if (column.Component != null) + { + + } + else + { + var hasExtensionPropertyData = !string.IsNullOrWhiteSpace(column.Data) && ExtensionPropertiesRegex.IsMatch(column.Data!); + if (!hasExtensionPropertyData) + { + @if (column.ValueConverter == null) + { + + + @if (ShowPager) + { + + } + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudExtensibleDataGrid.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudExtensibleDataGrid.razor.cs new file mode 100644 index 0000000000..1c57c40a80 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AbpMudExtensibleDataGrid.razor.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; +using Volo.Abp.Data; +using Volo.Abp.Timing; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class AbpMudExtensibleDataGrid : ComponentBase +{ + protected const string DataFieldAttributeName = "Data"; + + private MudDataGrid? _dataGrid; + + protected Regex ExtensionPropertiesRegex = new Regex(@"ExtraProperties\[(.*?)\]"); + + /// + /// Maps SortBy func references to their corresponding property names. + /// Used to resolve property names from MudBlazor SortDefinition.SortFunc. + /// + private readonly Dictionary, string> _sortFuncToPropertyMap = new(); + + [Parameter] public Func, CancellationToken, Task>>? ServerData { get; set; } + + [Parameter] public bool Loading { get; set; } + + [Parameter] public bool ShowPager { get; set; } = true; + + [Parameter] public int PageSize { get; set; } = 10; + + [Parameter] public IEnumerable Columns { get; set; } = default!; + + [Parameter] public int CurrentPage { get; set; } = 1; + + [Parameter] public string? Class { get; set; } + + [Parameter] public string ActionColumnWidth { get; set; } = "150px"; + + protected int ZeroBasedCurrentPage => CurrentPage > 0 ? CurrentPage - 1 : 0; + + [Inject] + public IStringLocalizerFactory StringLocalizerFactory { get; set; } = default!; + + [Inject] + public IStringLocalizer UiLocalizer { get; set; } = default!; + + [Inject] + public IClock Clock { get; set; } = default!; + + public virtual async Task ReloadServerDataAsync() + { + if (_dataGrid != null && ServerData != null) + { + await InvokeAsync(() => _dataGrid.ReloadServerData()); + } + } + + /// + /// Resolves the property name for a given SortDefinition by matching its SortFunc reference. + /// When MudBlazor uses Func for SortBy on TemplateColumn, it returns an internal GUID identifier + /// instead of the property name. This method looks up the original property name from the + /// registered sort function mapping. + /// + /// The property name if found in the mapping, otherwise falls back to SortDefinition.SortBy. + public virtual string ResolveSortPropertyName(SortDefinition sortDefinition) + { + if (sortDefinition.SortFunc != null && _sortFuncToPropertyMap.TryGetValue(sortDefinition.SortFunc, out var propertyName)) + { + return propertyName; + } + + return sortDefinition.SortBy; + } + + protected virtual RenderFragment RenderCustomTableColumnComponent(Type type, object data) + { + return (builder) => + { + builder.OpenComponent(0, type); + builder.AddAttribute(1, DataFieldAttributeName, data); + builder.CloseComponent(); + }; + } + + protected virtual string GetConvertedFieldValue(TItem? item, TableColumn columnDefinition) + { + if (item == null) + { + return string.Empty; + } + + var convertedValue = columnDefinition.ValueConverter!.Invoke(item!); + if (!columnDefinition.DisplayFormat.IsNullOrEmpty()) + { + return string.Format(columnDefinition.DisplayFormatProvider, columnDefinition.DisplayFormat!, + convertedValue); + } + + return convertedValue; + } + + protected virtual string GetConvertedFieldValue(object? value, TableColumn columnDefinition) + { + if (value is DateTime dateTime) + { + var converted = Clock.ConvertToUserTime(dateTime); + if (!columnDefinition.DisplayFormat.IsNullOrEmpty()) + { + return string.Format(columnDefinition.DisplayFormatProvider, columnDefinition.DisplayFormat!, converted); + } + + return converted.ToString(columnDefinition.DisplayFormatProvider as CultureInfo ?? CultureInfo.CurrentCulture); + } + + if (value is DateTimeOffset dateTimeOffset) + { + var converted = Clock.ConvertToUserTime(dateTimeOffset); + if (!columnDefinition.DisplayFormat.IsNullOrEmpty()) + { + return string.Format(columnDefinition.DisplayFormatProvider, columnDefinition.DisplayFormat!, converted); + } + + return converted.ToString(columnDefinition.DisplayFormatProvider as CultureInfo ?? CultureInfo.CurrentCulture); + } + + if (value == null) + { + return string.Empty; + } + + if (!columnDefinition.DisplayFormat.IsNullOrEmpty()) + { + return string.Format(columnDefinition.DisplayFormatProvider, columnDefinition.DisplayFormat!, value); + } + + return value.ToString() ?? string.Empty; + } + + protected virtual object GetPropertyValue(TItem item, string propertyPath) + { + ArgumentNullException.ThrowIfNull(item); + + var properties = propertyPath.Split('.'); + object value = item; + + foreach (var prop in properties) + { + var propertyInfo = value.GetType().GetProperty(prop, BindingFlags.Public | BindingFlags.Instance) + ?? throw new ArgumentException($"Property '{prop}' not found on type '{value.GetType().Name}'", nameof(propertyPath)); + + var propertyValue = propertyInfo.GetValue(value); + + // Allow null property values and treat them as empty for display/sorting purposes + if (propertyValue == null) + { + return string.Empty; + } + + value = propertyValue; + } + + if (value is DateTime dateTime) + { + return Clock.ConvertToUserTime(dateTime); + } + + if (value is DateTimeOffset dateTimeOffset) + { + return Clock.ConvertToUserTime(dateTimeOffset); + } + + return value; + } + + protected virtual string GetColumnStyle(TableColumn column) + { + if (!string.IsNullOrEmpty(column.Width)) + { + return $"width: {column.Width}"; + } + + return string.Empty; + } + + protected virtual Func? GetSortByFunc(TableColumn column) + { + if (!column.Sortable) + { + return null; + } + + // Determine the property path for sorting + string? propertyPath = null; + if (!string.IsNullOrEmpty(column.PropertyName)) + { + propertyPath = column.PropertyName; + } + else if (!string.IsNullOrEmpty(column.Data) && !ExtensionPropertiesRegex.IsMatch(column.Data)) + { + propertyPath = column.Data; + } + + if (string.IsNullOrEmpty(propertyPath)) + { + return null; + } + + var properties = propertyPath.Split('.'); + + // Build a lambda expression that directly accesses the property + var parameter = System.Linq.Expressions.Expression.Parameter(typeof(TItem), "x"); + System.Linq.Expressions.Expression expression = parameter; + + Func? sortFunc = null; + foreach (var prop in properties) + { + var propertyInfo = expression.Type.GetProperty(prop, BindingFlags.Public | BindingFlags.Instance); + if (propertyInfo == null) + { + // Fallback to GetPropertyValue if property not found + sortFunc = item => GetPropertyValue(item, propertyPath); + break; + } + expression = System.Linq.Expressions.Expression.Property(expression, propertyInfo); + } + + if (sortFunc == null) + { + var convertExpression = System.Linq.Expressions.Expression.Convert(expression, typeof(object)); + var lambda = System.Linq.Expressions.Expression.Lambda>(convertExpression, parameter); + sortFunc = lambda.Compile(); + } + + // Register the mapping so we can resolve property name from SortDefinition.SortFunc later + _sortFuncToPropertyMap[sortFunc] = propertyPath; + + return sortFunc; + } + + protected virtual Func? GetExtensionPropertySortFunc(TableColumn column) + { + if (!column.Sortable || string.IsNullOrWhiteSpace(column.Data)) + { + return null; + } + + var match = ExtensionPropertiesRegex.Match(column.Data); + if (!match.Success) + { + return null; + } + + var propertyName = match.Groups[1].Value; + Func sortFunc = item => + { + var entity = item as IHasExtraProperties; + return entity?.GetProperty(propertyName) ?? string.Empty; + }; + + // Register the mapping so we can resolve property name from SortDefinition.SortFunc later + _sortFuncToPropertyMap[sortFunc] = propertyName; + + return sortFunc; + } + + protected virtual Color GetColor(object? color) + { + if (color == null) + { + return Color.Primary; + } + + // Handle if it's already a MudBlazor Color + if (color is Color mudColor) + { + return mudColor; + } + + // Handle string color names + if (color is string colorString) + { + return colorString.ToLowerInvariant() switch + { + "primary" => Color.Primary, + "secondary" => Color.Secondary, + "success" => Color.Success, + "warning" => Color.Warning, + "error" or "danger" => Color.Error, + "info" => Color.Info, + "dark" => Color.Dark, + _ => Color.Primary + }; + } + + return Color.Primary; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/AlertWrapper.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/AlertWrapper.cs new file mode 100644 index 0000000000..9466ced609 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/AlertWrapper.cs @@ -0,0 +1,9 @@ +using Volo.Abp.AspNetCore.Components.Alerts; + +namespace Volo.Abp.MudBlazorUI.Components; + +internal class AlertWrapper +{ + public AlertMessage AlertMessage { get; set; } = default!; + public bool IsVisible { get; set; } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudDataGridEntityActionsColumn.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/MudDataGridEntityActionsColumn.razor new file mode 100644 index 0000000000..4b0eb0efd9 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudDataGridEntityActionsColumn.razor @@ -0,0 +1,6 @@ +@typeparam TItem +@using MudBlazor + + + @ChildContent + diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudDataGridEntityActionsColumn.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/MudDataGridEntityActionsColumn.razor.cs new file mode 100644 index 0000000000..cedf0a74bf --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudDataGridEntityActionsColumn.razor.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class MudDataGridEntityActionsColumn : ComponentBase +{ + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string Width { get; set; } = "150px"; + + [Parameter] + public bool Displayable { get; set; } = true; + + [Inject] + public IStringLocalizer UiLocalizer { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await SetDefaultValuesAsync(); + } + + protected virtual ValueTask SetDefaultValuesAsync() + { + Title ??= UiLocalizer["Actions"]; + return ValueTask.CompletedTask; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor new file mode 100644 index 0000000000..c1ae598b89 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor @@ -0,0 +1,29 @@ +@typeparam TItem +@using MudBlazor + +@if (ParentActions != null && Visible && HasPermission) +{ + if (ParentActions.Type == MudActionType.Dropdown) + { + if (Primary == false) + { + + @if (!string.IsNullOrEmpty(Icon)) + { + + } + @Text + + } + } + else + { + + @if (!string.IsNullOrEmpty(Icon)) + { + + } + @Text + + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor.cs new file mode 100644 index 0000000000..07ba3a3bed --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityAction.razor.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class MudEntityAction : ComponentBase +{ + [Parameter] + public bool Visible { get; set; } = true; + + [Parameter] + public bool Disabled { get; set; } = false; + + internal bool HasPermission { get; set; } = true; + + [Parameter] + public string Text { get; set; } = default!; + + [Parameter] + public bool Primary { get; set; } + + [Parameter] + public EventCallback Clicked { get; set; } + + [Parameter] + [Obsolete("Use Visible to hide actions based on permissions. Check the permission yourself. It is more performant. This option might be removed in future versions.")] + public string? RequiredPolicy { get; set; } + + [Parameter] + public Color Color { get; set; } = Color.Primary; + + [Parameter] + public Func? ConfirmationMessage { get; set; } + + [Parameter] + public string? Icon { get; set; } + + [CascadingParameter(Name = "ParentActions")] + public IMudEntityActions? ParentActions { get; set; } = default!; + + [Inject] + protected IAuthorizationService AuthorizationService { get; set; } = default!; + + [Inject] + protected IUiMessageService UiMessageService { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await SetDefaultValuesAsync(); + +#pragma warning disable CS0618 + if (!RequiredPolicy.IsNullOrEmpty()) + { + HasPermission = await AuthorizationService.IsGrantedAsync(RequiredPolicy!); + } +#pragma warning restore CS0618 + + ParentActions?.AddAction(this); + } + + protected internal virtual async Task ActionClickedAsync() + { + if (ConfirmationMessage != null) + { + if (await UiMessageService.Confirm(ConfirmationMessage())) + { + await InvokeAsync(() => Clicked.InvokeAsync()); + } + } + else + { + await Clicked.InvokeAsync(); + } + } + + protected virtual ValueTask SetDefaultValuesAsync() + { + Color = Color.Primary; + return ValueTask.CompletedTask; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor new file mode 100644 index 0000000000..e60654fdf1 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor @@ -0,0 +1,24 @@ +@typeparam TItem +@using MudBlazor +@using Volo.Abp.MudBlazorUI.Components + +@if (Type == MudActionType.Dropdown || DisabledOrNoActions()) +{ + + + @ChildContent + + +} +else +{ + + + @ChildContent + + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor.cs new file mode 100644 index 0000000000..35b09f83f3 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/MudEntityActions.razor.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Localization.Resources.AbpUi; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; + +namespace Volo.Abp.MudBlazorUI.Components; + +/// +/// Non-generic interface for MudEntityActions to allow cascading parameter matching +/// regardless of the generic type argument. +/// +public interface IMudEntityActions +{ + MudActionType Type { get; } + void AddAction(object action); +} + +public partial class MudEntityActions : ComponentBase, IMudEntityActions +{ + protected readonly List> Actions = new List>(); + protected bool HasPrimaryAction => Actions.Any(t => t.Primary); + protected MudEntityAction? PrimaryAction => Actions.FirstOrDefault(t => t.Primary); + + [Parameter] + public Color ToggleColor { get; set; } = Color.Primary; + + [Parameter] + public string? ToggleText { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } = default!; + + [Parameter] + public MudDataGridEntityActionsColumn EntityActionsColumn { get; set; } = default!; + + [Parameter] + public MudActionType Type { get; set; } = MudActionType.Dropdown; + + [Parameter] + public bool Disabled { get; set; } = false; + + [CascadingParameter] + public MudDataGridEntityActionsColumn? ParentEntityActionsColumn { get; set; } + + [Inject] + public IStringLocalizer UiLocalizer { get; set; } = default!; + + internal void AddAction(MudEntityAction action) + { + Actions.Add(action); + } + + void IMudEntityActions.AddAction(object action) + { + if (action is MudEntityAction typedAction) + { + AddAction(typedAction); + } + } + + protected virtual bool DisabledOrNoActions() + { + return Disabled || (Actions.Any() && Actions.All(t => !t.Visible || !t.HasPermission)); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + ToggleText = UiLocalizer["Actions"]; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + if (ParentEntityActionsColumn != null) + { + ParentEntityActionsColumn.Displayable = Actions.Any(t => t.Visible && t.HasPermission); + } + + await InvokeAsync(StateHasChanged); + } + + await base.OnAfterRenderAsync(firstRender); + } +} + +public enum MudActionType +{ + Dropdown, + Button +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/ExtensionPropertyModalType.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/ExtensionPropertyModalType.cs new file mode 100644 index 0000000000..0a9c881010 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/ExtensionPropertyModalType.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public enum ExtensionPropertyModalType : byte +{ + CreateModal = 0, + EditModal = 1 +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudCheckExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudCheckExtensionProperty.razor new file mode 100644 index 0000000000..1fe1d6352a --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudCheckExtensionProperty.razor @@ -0,0 +1,14 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudCheckExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudCheckExtensionProperty.razor.cs new file mode 100644 index 0000000000..7e098005b7 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudCheckExtensionProperty.razor.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Data; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudCheckExtensionProperty + where TEntity : IHasExtraProperties +{ + protected bool Value + { + get => Entity.GetProperty(PropertyInfo.Name); + set => Entity.SetProperty(PropertyInfo.Name, value, validate: false); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeExtensionProperty.razor new file mode 100644 index 0000000000..142ebcc181 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeExtensionProperty.razor @@ -0,0 +1,32 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@using Volo.Abp.ObjectExtending +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + @if (PropertyInfo.IsDate()) + { + + } + else + { + + + + + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeExtensionProperty.razor.cs new file mode 100644 index 0000000000..00de58baa5 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeExtensionProperty.razor.cs @@ -0,0 +1,60 @@ +using System; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudDateTimeExtensionProperty + where TEntity : IHasExtraProperties +{ + protected DateTime? Value + { + get => PropertyInfo.GetInputValueOrDefault(Entity.GetProperty(PropertyInfo.Name)); + set => Entity.SetProperty(PropertyInfo.Name, value, false); + } + + protected DateTime? DateValue + { + get => Value?.Date; + set + { + if (value.HasValue) + { + var time = Value?.TimeOfDay ?? TimeSpan.Zero; + Value = value.Value.Date + time; + } + else + { + Value = null; + } + } + } + + protected TimeSpan? TimeValue + { + get => Value?.TimeOfDay; + set + { + if (Value.HasValue && value.HasValue) + { + Value = Value.Value.Date + value.Value; + } + } + } + + protected string GetDateFormat() + { + var dataFormatString = PropertyInfo.GetDataFormatStringOrNull(); + if (!string.IsNullOrEmpty(dataFormatString)) + { + return dataFormatString.Replace("{0:", "").Replace("}", ""); + } + + return PropertyInfo.IsDate() ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm"; + } + + protected string GetTimeLabel() + { + return "Time"; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeOffsetExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeOffsetExtensionProperty.razor new file mode 100644 index 0000000000..ef1025f914 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeOffsetExtensionProperty.razor @@ -0,0 +1,32 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@using Volo.Abp.ObjectExtending +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + @if (PropertyInfo.IsDate()) + { + + } + else + { + + + + + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeOffsetExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeOffsetExtensionProperty.razor.cs new file mode 100644 index 0000000000..ee2a965004 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudDateTimeOffsetExtensionProperty.razor.cs @@ -0,0 +1,93 @@ +using System; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudDateTimeOffsetExtensionProperty + where TEntity : IHasExtraProperties +{ + protected DateTimeOffset? Value + { + get + { + var raw = Entity.GetProperty(PropertyInfo.Name); + return raw switch + { + null => null, + DateTimeOffset dto => dto, + DateTime dt => dt.Kind switch + { + DateTimeKind.Utc => new DateTimeOffset(dt, TimeSpan.Zero), + DateTimeKind.Local => new DateTimeOffset(dt), + _ => new DateTimeOffset(DateTime.SpecifyKind(dt, DateTimeKind.Utc), TimeSpan.Zero) + }, + _ => null + }; + } + set => Entity.SetProperty(PropertyInfo.Name, value, false); + } + + protected DateTime? DateOnlyValue + { + get => Value?.DateTime.Date; + set + { + if (value.HasValue) + { + Value = new DateTimeOffset(value.Value); + } + else + { + Value = null; + } + } + } + + protected DateTime? DateValue + { + get => Value?.DateTime.Date; + set + { + if (value.HasValue) + { + var time = Value?.DateTime.TimeOfDay ?? TimeSpan.Zero; + var offset = Value?.Offset ?? TimeSpan.Zero; + Value = new DateTimeOffset(value.Value.Date + time, offset); + } + else + { + Value = null; + } + } + } + + protected TimeSpan? TimeValue + { + get => Value?.DateTime.TimeOfDay; + set + { + if (Value.HasValue && value.HasValue) + { + var offset = Value.Value.Offset; + Value = new DateTimeOffset(Value.Value.DateTime.Date + value.Value, offset); + } + } + } + + protected string GetDateFormat() + { + var dataFormatString = PropertyInfo.GetDataFormatStringOrNull(); + if (!string.IsNullOrEmpty(dataFormatString)) + { + return dataFormatString.Replace("{0:", "").Replace("}", ""); + } + + return PropertyInfo.IsDate() ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm"; + } + + protected string GetTimeLabel() + { + return "Time"; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionProperties.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionProperties.razor new file mode 100644 index 0000000000..8208f28e8c --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionProperties.razor @@ -0,0 +1,32 @@ +@typeparam TEntityType +@typeparam TResourceType +@using Volo.Abp.Data + +@{ + Entity.SetDefaultsForExtraProperties(); + + foreach (var propertyInfo in Properties) + { + if (!propertyInfo.Name.EndsWith("_Text")) + { + if (propertyInfo.IsEnum()) + { + + } + else if (!propertyInfo.Lookup.Url.IsNullOrEmpty()) + { + + } + else + { + var inputType = propertyInfo.GetInputType(); + __builder.OpenComponent(0, inputType.MakeGenericType(new[] { typeof(TEntityType), typeof(TResourceType) })); + __builder.AddAttribute(1, "PropertyInfo", propertyInfo); + __builder.AddAttribute(2, "Entity", Entity); + __builder.AddAttribute(3, "LH", LH); + __builder.AddAttribute(4, "ModalType", ModalType); + __builder.CloseComponent(); + } + } + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionProperties.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionProperties.razor.cs new file mode 100644 index 0000000000..6ff16ea051 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionProperties.razor.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudExtensionProperties : ComponentBase + where TEntityType : IHasExtraProperties +{ + [Inject] + public IStringLocalizerFactory StringLocalizerFactory { get; set; } = default!; + + [Parameter] + public AbpMudBlazorMessageLocalizerHelper LH { get; set; } = default!; + + [Parameter] + public TEntityType Entity { get; set; } = default!; + + [Inject] + public IServiceProvider ServiceProvider { get; set; } = default!; + + [Parameter] + public ExtensionPropertyModalType? ModalType { get; set; } + + public ImmutableList Properties { get; set; } = ImmutableList.Empty; + + protected override async Task OnInitializedAsync() + { + Properties = + (await ObjectExtensionManager.Instance.GetPropertiesAndCheckPolicyAsync(ServiceProvider)) + .Where(p => ModalType == ExtensionPropertyModalType.CreateModal + ? p.UI.CreateModal.IsVisible + : p.UI.EditModal.IsVisible) + .ToImmutableList(); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionPropertyComponentBase.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionPropertyComponentBase.cs new file mode 100644 index 0000000000..36aa652b7d --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudExtensionPropertyComponentBase.cs @@ -0,0 +1,64 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Volo.Abp.Data; +using Volo.Abp.Localization; +using Volo.Abp.MudBlazorUI.Validation; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public abstract class MudExtensionPropertyComponentBase : OwningComponentBase + where TEntity : IHasExtraProperties +{ + [Inject] + public IStringLocalizerFactory StringLocalizerFactory { get; set; } = default!; + + [Inject] + public IAbpEnumLocalizer AbpEnumLocalizer { get; set; } = default!; + + [Parameter] + public TEntity Entity { get; set; } = default!; + + [Parameter] + public ObjectExtensionPropertyInfo PropertyInfo { get; set; } = default!; + + [Parameter] + public AbpMudBlazorMessageLocalizerHelper LH { get; set; } = default!; + + [Parameter] + public ExtensionPropertyModalType? ModalType { get; set; } + + protected virtual string? Validate(object? value) + { + var validationAttributes = PropertyInfo.GetValidationAttributes(); + var validationContext = new ValidationContext(Entity) + { + DisplayName = PropertyInfo.Name, + MemberName = PropertyInfo.Name + }; + + foreach (var validationAttribute in validationAttributes) + { + var result = validationAttribute.GetValidationResult(value, validationContext); + if (result == ValidationResult.Success || result == null) + { + continue; + } + + var errorMessage = result.ErrorMessage; + if (LH != null && errorMessage != null) + { + var formattedErrorMessage = MudValidationAttributeHelper.GetDefaultErrorMessage(validationAttribute); + var errorMessageString = MudValidationAttributeHelper.RevertErrorMessagePlaceholders(formattedErrorMessage); + errorMessage = LH.Localize(errorMessageString); + } + + return errorMessage; + } + + return null; + } + + protected bool IsReadonlyField => ModalType is ExtensionPropertyModalType.EditModal && PropertyInfo.UI.EditModal.IsReadOnly; +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudLookupExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudLookupExtensionProperty.razor new file mode 100644 index 0000000000..812049a795 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudLookupExtensionProperty.razor @@ -0,0 +1,20 @@ +#nullable enable +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudLookupExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudLookupExtensionProperty.razor.cs new file mode 100644 index 0000000000..7fbb7d08c5 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudLookupExtensionProperty.razor.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components.Web.Extensibility; +using Volo.Abp.Data; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudLookupExtensionProperty + where TEntity : IHasExtraProperties +{ + protected List> LookupItems = new(); + + [Inject] + public ILookupApiRequestService LookupApiService { get; set; } = default!; + + public string TextPropertyName => PropertyInfo.Name + "_Text"; + + private MudSelectItem? _selectedItem; + + public MudSelectItem SelectedItem { + get => _selectedItem!; + set => _selectedItem = value; + } + + public object? SelectedValue + { + get => Entity.GetProperty(PropertyInfo.Name); + set + { + Entity.SetProperty(PropertyInfo.Name, value, false); + UpdateLookupTextProperty(value); + } + } + + protected override void OnParametersSet() + { + var value = Entity.GetProperty(PropertyInfo.Name); + var text = Entity.GetProperty(TextPropertyName); + if (value != null && text != null) + { + SelectedItem = new MudSelectItem + { + Text = text.ToString()!, + Value = value + }; + LookupItems = new List> { SelectedItem }; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender) + { + LookupItems = await GetLookupItemsAsync(string.Empty); + await InvokeAsync(StateHasChanged); + } + } + + protected virtual void UpdateLookupTextProperty(object? value) + { + var selectedItemText = LookupItems.SingleOrDefault(t => t.Value?.Equals(value) == true)?.Text; + Entity.SetProperty(TextPropertyName, selectedItemText); + } + + protected virtual async Task>> GetLookupItemsAsync(string filter) + { + var selectItems = new List>(); + + var url = PropertyInfo.Lookup.Url; + if (!string.IsNullOrEmpty(filter)) + { + url += $"?{PropertyInfo.Lookup.FilterParamName}={filter.Trim()}"; + } + + var response = await LookupApiService.SendAsync(url); + + var document = JsonDocument.Parse(response); + var itemsArrayProp = document.RootElement.GetProperty(PropertyInfo.Lookup.ResultListPropertyName); + foreach (var item in itemsArrayProp.EnumerateArray()) + { + selectItems.Add(new MudSelectItem + { + Text = item.GetProperty(PropertyInfo.Lookup.DisplayPropertyName).GetString()!, + Value = JsonSerializer.Deserialize(item.GetProperty(PropertyInfo.Lookup.ValuePropertyName).GetRawText(), PropertyInfo.Type)! + }); + } + + return selectItems; + } + + protected virtual Task SelectedItemChanged(MudSelectItem item) + { + SelectedItem = item; + SelectedValue = item?.Value; + return Task.CompletedTask; + } + + protected virtual async Task?>> SearchAsync(string? value, CancellationToken token) + { + LookupItems = await GetLookupItemsAsync(value ?? string.Empty); + return LookupItems!; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudSelectExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudSelectExtensionProperty.razor new file mode 100644 index 0000000000..902691d5f2 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudSelectExtensionProperty.razor @@ -0,0 +1,18 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + + @foreach (var item in SelectItems) + { + @item.Text + } + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudSelectExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudSelectExtensionProperty.razor.cs new file mode 100644 index 0000000000..ee9f5b2bb7 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudSelectExtensionProperty.razor.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Localization; +using Volo.Abp.Data; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudSelectExtensionProperty + where TEntity : IHasExtraProperties +{ + protected List> SelectItems = new(); + + public int? SelectedValue + { + get => Entity.GetProperty(PropertyInfo.Name, Nullable.GetUnderlyingType(PropertyInfo.Type!) != null ? null : 0); + set => Entity.SetProperty(PropertyInfo.Name, value, false); + } + + protected virtual List> GetSelectItemsFromEnum() + { + var selectItems = new List>(); + + var isNullableType = Nullable.GetUnderlyingType(PropertyInfo.Type!) != null; + var enumType = isNullableType + ? Nullable.GetUnderlyingType(PropertyInfo.Type)! + : PropertyInfo.Type; + + if (isNullableType) + { + selectItems.Add(new MudSelectItem()); + } + + foreach (var enumValue in enumType.GetEnumValues()) + { + selectItems.Add(new MudSelectItem + { + Value = (int)enumValue, + Text = AbpEnumLocalizer.GetString(enumType, enumValue, new[] { StringLocalizerFactory.CreateDefaultOrNull() }) + }); + } + + return selectItems; + } + + protected override void OnParametersSet() + { + SelectItems = GetSelectItemsFromEnum(); + StateHasChanged(); + + if (!Entity.HasProperty(PropertyInfo.Name)) + { + var isNullableType = Nullable.GetUnderlyingType(PropertyInfo.Type!) != null; + if (!isNullableType) + { + SelectedValue = (int)PropertyInfo.Type.GetEnumValues().GetValue(0)!; + } + } + } +} + +public class MudSelectItem +{ + public string Text { get; set; } = default!; + public TValue Value { get; set; } = default!; +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextAreaExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextAreaExtensionProperty.razor new file mode 100644 index 0000000000..4d26db8deb --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextAreaExtensionProperty.razor @@ -0,0 +1,16 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextAreaExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextAreaExtensionProperty.razor.cs new file mode 100644 index 0000000000..84c8017c74 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextAreaExtensionProperty.razor.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Data; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudTextAreaExtensionProperty + where TEntity : IHasExtraProperties +{ + protected string? Value + { + get => PropertyInfo.GetTextInputValueOrNull(Entity.GetProperty(PropertyInfo.Name)); + set => Entity.SetProperty(PropertyInfo.Name, value, validate: false); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextExtensionProperty.razor new file mode 100644 index 0000000000..aaa7326a85 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextExtensionProperty.razor @@ -0,0 +1,16 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextExtensionProperty.razor.cs new file mode 100644 index 0000000000..f100372b37 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTextExtensionProperty.razor.cs @@ -0,0 +1,101 @@ +using System.ComponentModel.DataAnnotations; +using System.Linq; +using MudBlazor; +using Volo.Abp.Data; +using Volo.Abp.Reflection; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudTextExtensionProperty + where TEntity : IHasExtraProperties +{ + protected string? Value + { + get => PropertyInfo.GetTextInputValueOrNull(Entity.GetProperty(PropertyInfo.Name)); + set => Entity.SetProperty(PropertyInfo.Name, value, validate: false); + } + + protected InputType GetInputType() + { + foreach (var attribute in PropertyInfo.Attributes) + { + if (attribute is DataTypeAttribute dataTypeAttribute) + { + return dataTypeAttribute.DataType switch + { + DataType.Password => InputType.Password, + DataType.EmailAddress => InputType.Email, + DataType.Url => InputType.Url, + DataType.PhoneNumber => InputType.Telephone, + _ => InputType.Text + }; + } + + if (attribute is EmailAddressAttribute) + { + return InputType.Email; + } + + if (attribute is UrlAttribute) + { + return InputType.Url; + } + + if (attribute is PhoneAttribute) + { + return InputType.Telephone; + } + } + + return InputType.Text; + } + + protected InputMode GetInputMode() + { + foreach (var attribute in PropertyInfo.Attributes) + { + if (attribute is DataTypeAttribute dataTypeAttribute) + { + return dataTypeAttribute.DataType switch + { + DataType.EmailAddress => InputMode.email, + DataType.Url => InputMode.url, + DataType.PhoneNumber => InputMode.tel, + _ => GetInputModeFromType() + }; + } + + if (attribute is EmailAddressAttribute) + { + return InputMode.email; + } + + if (attribute is UrlAttribute) + { + return InputMode.url; + } + + if (attribute is PhoneAttribute) + { + return InputMode.tel; + } + } + + return GetInputModeFromType(); + } + + private InputMode GetInputModeFromType() + { + if (TypeHelper.IsFloatingType(PropertyInfo.Type)) + { + return InputMode.@decimal; + } + + if (MudBlazorUiObjectExtensionPropertyInfoExtensions.NumberTypes.Contains(PropertyInfo.Type)) + { + return InputMode.numeric; + } + + return InputMode.text; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTimeExtensionProperty.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTimeExtensionProperty.razor new file mode 100644 index 0000000000..2c4c4d79b4 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTimeExtensionProperty.razor @@ -0,0 +1,13 @@ +@typeparam TEntity +@typeparam TResourceType +@using Volo.Abp.Localization +@inherits MudExtensionPropertyComponentBase + +@if (PropertyInfo != null && Entity != null) +{ + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTimeExtensionProperty.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTimeExtensionProperty.razor.cs new file mode 100644 index 0000000000..be83d980a9 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/ObjectExtending/MudTimeExtensionProperty.razor.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Data; + +namespace Volo.Abp.MudBlazorUI.Components.ObjectExtending; + +public partial class MudTimeExtensionProperty + where TEntity : IHasExtraProperties +{ + protected TimeSpan? Value + { + get => PropertyInfo.GetInputValueOrDefault(Entity.GetProperty(PropertyInfo.Name)); + set => Entity.SetProperty(PropertyInfo.Name, value, false); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/PageAlert.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/PageAlert.razor new file mode 100644 index 0000000000..81798d124f --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/PageAlert.razor @@ -0,0 +1,18 @@ +@using MudBlazor + +@foreach (var alert in Alerts) +{ + if (alert.IsVisible) + { + + @if (!string.IsNullOrEmpty(alert.AlertMessage.Title)) + { + @alert.AlertMessage.Title + } + @alert.AlertMessage.Text + + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/PageAlert.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/PageAlert.razor.cs new file mode 100644 index 0000000000..bec9130877 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/PageAlert.razor.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Alerts; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class PageAlert : ComponentBase, IDisposable +{ + private List Alerts = new List(); + + [Inject] + protected IAlertManager AlertManager { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + protected override void OnInitialized() + { + base.OnInitialized(); + NavigationManager.LocationChanged += NavigationManager_LocationChanged; + AlertManager.Alerts.CollectionChanged += Alerts_CollectionChanged; + + Alerts.AddRange(AlertManager.Alerts.Select(t => new AlertWrapper + { + AlertMessage = t, + IsVisible = true + })); + } + + //Since Blazor WASM doesn't support scoped dependency, we need to clear alerts on each location changed event. + private void NavigationManager_LocationChanged(object? sender, LocationChangedEventArgs e) + { + AlertManager.Alerts.Clear(); + Alerts.Clear(); + } + + private void Alerts_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (var item in e.NewItems!) + { + Alerts.Add(new AlertWrapper + { + AlertMessage = (AlertMessage)item, + IsVisible = true + }); + } + } + InvokeAsync(StateHasChanged); + } + + protected virtual Severity GetAlertSeverity(AlertType alertType) + { + return alertType switch + { + AlertType.Info => Severity.Info, + AlertType.Success => Severity.Success, + AlertType.Warning => Severity.Warning, + AlertType.Danger => Severity.Error, + _ => Severity.Normal, + }; + } + + public void Dispose() + { + NavigationManager.LocationChanged -= NavigationManager_LocationChanged; + AlertManager.Alerts.CollectionChanged -= Alerts_CollectionChanged; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/UiMessageAlert.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/UiMessageAlert.razor new file mode 100644 index 0000000000..15233cd198 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/UiMessageAlert.razor @@ -0,0 +1,68 @@ +@using MudBlazor + + + + @if (!string.IsNullOrEmpty(Title)) + { + @Title + } + + +
+ @if (ShowMessageIcon) + { + + } + + @if (!IsMessageHtmlMarkup) + { + @Message + } + else + { + @((MarkupString)Message) + } + +
+
+ +
+ @if (IsConfirmation) + { + + @if (Options?.CancelButtonIcon != null) + { + + } + @CancelButtonText + + + @if (Options?.ConfirmButtonIcon != null) + { + + } + @ConfirmButtonText + + } + else + { + + @if (Options?.OkButtonIcon != null) + { + + } + @OkButtonText + + } +
+
+
diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/UiMessageAlert.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/UiMessageAlert.razor.cs new file mode 100644 index 0000000000..58ed2fae1b --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/UiMessageAlert.razor.cs @@ -0,0 +1,155 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class UiMessageAlert : ComponentBase, IDisposable +{ + private bool _visible; + private readonly DialogOptions _dialogOptions = new() + { + CloseOnEscapeKey = false, + BackdropClick = false, + MaxWidth = MaxWidth.Small, + FullWidth = true + }; + + protected virtual bool IsConfirmation + => MessageType == UiMessageType.Confirmation; + + protected virtual bool CenterMessage + => Options?.CenterMessage ?? true; + + protected virtual bool ShowMessageIcon + => Options?.ShowMessageIcon ?? true; + + protected virtual bool IsMessageHtmlMarkup + => Options?.IsMessageHtmlMarkup ?? false; + + protected virtual string MessageIcon => MessageType switch + { + UiMessageType.Info => Icons.Material.Filled.Info, + UiMessageType.Success => Icons.Material.Filled.CheckCircle, + UiMessageType.Warning => Icons.Material.Filled.Warning, + UiMessageType.Error => Icons.Material.Filled.Error, + UiMessageType.Confirmation => Icons.Material.Filled.Help, + _ => Icons.Material.Filled.Info, + }; + + protected virtual Color MessageIconColor => MessageType switch + { + UiMessageType.Info => Color.Info, + UiMessageType.Success => Color.Success, + UiMessageType.Warning => Color.Warning, + UiMessageType.Error => Color.Error, + UiMessageType.Confirmation => Color.Default, + _ => Color.Default, + }; + + protected virtual string OkButtonText + => Options?.OkButtonText ?? "OK"; + + protected virtual string ConfirmButtonText + => Options?.ConfirmButtonText ?? "Confirm"; + + protected virtual string CancelButtonText + => Options?.CancelButtonText ?? "Cancel"; + + [Parameter] public UiMessageType MessageType { get; set; } + + [Parameter] public string? Title { get; set; } + + [Parameter] public string Message { get; set; } = default!; + + [Parameter] public TaskCompletionSource? Callback { get; set; } + + [Parameter] public UiMessageOptions? Options { get; set; } + + [Parameter] public EventCallback Okayed { get; set; } + + [Parameter] public EventCallback Confirmed { get; set; } + + [Parameter] public EventCallback Canceled { get; set; } + + [Inject] protected MudBlazorUiMessageService? UiMessageService { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (UiMessageService != null) + { + UiMessageService.MessageReceived += OnMessageReceived; + } + } + + private async void OnMessageReceived(object? sender, UiMessageEventArgs e) + { + MessageType = e.MessageType; + Message = e.Message; + Title = e.Title; + Options = e.Options; + Callback = e.Callback; + + await ShowMessageAlert(); + } + + protected virtual async Task ShowMessageAlert() + { + await InvokeAsync(() => + { + _visible = true; + StateHasChanged(); + }); + } + + protected virtual async Task HideMessageAlert() + { + await InvokeAsync(() => + { + _visible = false; + StateHasChanged(); + }); + } + + public void Dispose() + { + if (UiMessageService != null) + { + UiMessageService.MessageReceived -= OnMessageReceived; + } + } + + protected async Task OnOkClicked() + { + await HideMessageAlert(); + await Okayed.InvokeAsync(null); + } + + protected async Task OnConfirmClicked() + { + await HideMessageAlert(); + + if (IsConfirmation && Callback != null) + { + Callback.SetResult(true); + } + + await Confirmed.InvokeAsync(null); + } + + protected async Task OnCancelClicked() + { + await HideMessageAlert(); + + if (IsConfirmation && Callback != null) + { + Callback.SetResult(false); + } + + await Canceled.InvokeAsync(null); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/UiNotificationAlert.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/UiNotificationAlert.razor new file mode 100644 index 0000000000..fdf1b7cbe4 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/UiNotificationAlert.razor @@ -0,0 +1 @@ +@using MudBlazor diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/UiNotificationAlert.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/UiNotificationAlert.razor.cs new file mode 100644 index 0000000000..ac5c3a8c0d --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/UiNotificationAlert.razor.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Notifications; +using Volo.Abp.Localization; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class UiNotificationAlert : ComponentBase, IDisposable +{ + [Parameter] public UiNotificationType NotificationType { get; set; } + + [Parameter] public string Message { get; set; } = default!; + + [Parameter] public string? Title { get; set; } + + [Parameter] public UiNotificationOptions? Options { get; set; } + + [Parameter] public EventCallback Okayed { get; set; } + + [Parameter] public EventCallback Closed { get; set; } + + [Inject] protected MudBlazorUiNotificationService? UiNotificationService { get; set; } + + [Inject] protected ISnackbar Snackbar { get; set; } = default!; + + [Inject] protected IStringLocalizerFactory StringLocalizerFactory { get; set; } = default!; + + protected virtual Severity GetSeverity(UiNotificationType notificationType) + { + return notificationType switch + { + UiNotificationType.Info => Severity.Info, + UiNotificationType.Success => Severity.Success, + UiNotificationType.Warning => Severity.Warning, + UiNotificationType.Error => Severity.Error, + _ => Severity.Normal, + }; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (UiNotificationService != null) + { + UiNotificationService.NotificationReceived += OnNotificationReceived; + } + } + + protected virtual async void OnNotificationReceived(object? sender, UiNotificationEventArgs e) + { + NotificationType = e.NotificationType; + Message = e.Message; + Title = e.Title; + Options = e.Options; + + var message = !string.IsNullOrEmpty(Title) + ? $"{Title}
{Message}" + : Message; + + await InvokeAsync(() => + { + Snackbar.Add(message, GetSeverity(e.NotificationType), config => + { + config.RequireInteraction = false; + config.ShowCloseIcon = true; + config.CloseAfterNavigation = true; + }); + }); + } + + public virtual void Dispose() + { + if (UiNotificationService != null) + { + UiNotificationService.NotificationReceived -= OnNotificationReceived; + } + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/UiPageProgress.razor b/framework/src/Volo.Abp.MudBlazorUI/Components/UiPageProgress.razor new file mode 100644 index 0000000000..d60e4734be --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/UiPageProgress.razor @@ -0,0 +1,10 @@ +@using MudBlazor + +@if (Visible) +{ + +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Components/UiPageProgress.razor.cs b/framework/src/Volo.Abp.MudBlazorUI/Components/UiPageProgress.razor.cs new file mode 100644 index 0000000000..e85349b25a --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Components/UiPageProgress.razor.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Progression; + +namespace Volo.Abp.MudBlazorUI.Components; + +public partial class UiPageProgress : ComponentBase, IDisposable +{ + protected int? Percentage { get; set; } + + protected bool Visible { get; set; } + + protected Color Color { get; set; } = Color.Primary; + + [Inject] protected IUiPageProgressService? UiPageProgressService { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (UiPageProgressService != null) + { + UiPageProgressService.ProgressChanged += OnProgressChanged; + } + } + + private async void OnProgressChanged(object? sender, UiPageProgressEventArgs e) + { + Percentage = e.Percentage; + Visible = e.Percentage == null || (e.Percentage >= 0 && e.Percentage <= 100); + Color = GetColor(e.Options.Type); + + await InvokeAsync(StateHasChanged); + } + + public virtual void Dispose() + { + if (UiPageProgressService != null) + { + UiPageProgressService.ProgressChanged -= OnProgressChanged; + } + } + + protected virtual Color GetColor(UiPageProgressType pageProgressType) + { + return pageProgressType switch + { + UiPageProgressType.Info => Color.Info, + UiPageProgressType.Success => Color.Success, + UiPageProgressType.Warning => Color.Warning, + UiPageProgressType.Error => Color.Error, + _ => Color.Primary, + }; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/FodyWeavers.xml b/framework/src/Volo.Abp.MudBlazorUI/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.MudBlazorUI/FodyWeavers.xsd b/framework/src/Volo.Abp.MudBlazorUI/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/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.MudBlazorUI/MudBlazorExtensionPropertyPolicyChecker.cs b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorExtensionPropertyPolicyChecker.cs new file mode 100644 index 0000000000..bd021b593d --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorExtensionPropertyPolicyChecker.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; +using Volo.Abp.GlobalFeatures; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.MudBlazorUI; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(ExtensionPropertyPolicyChecker))] +public class MudBlazorExtensionPropertyPolicyChecker : ExtensionPropertyPolicyChecker +{ + protected IFeatureChecker FeatureChecker { get; } + protected IPermissionChecker PermissionChecker { get; } + + public MudBlazorExtensionPropertyPolicyChecker(IFeatureChecker featureChecker, IPermissionChecker permissionChecker) + { + FeatureChecker = featureChecker; + PermissionChecker = permissionChecker; + } + + protected override Task CheckGlobalFeaturesAsync(string featureName) + { + return Task.FromResult(GlobalFeatureManager.Instance.IsEnabled(featureName)); + } + + protected async override Task CheckFeaturesAsync(string featureName) + { + return await FeatureChecker.IsEnabledAsync(featureName); + } + + protected async override Task CheckPermissionsAsync(string permissionName) + { + return await PermissionChecker.IsGrantedAsync(permissionName); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiMessageService.cs b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiMessageService.cs new file mode 100644 index 0000000000..4d329feaec --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiMessageService.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Localization.Resources.AbpUi; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.MudBlazorUI; + +[Dependency(ReplaceServices = true)] +public class MudBlazorUiMessageService : IUiMessageService, IScopedDependency +{ + /// + /// An event raised after the message is received. Used to notify the message dialog. + /// + public event EventHandler? MessageReceived; + + private readonly IStringLocalizer _localizer; + + public ILogger Logger { get; set; } + + public MudBlazorUiMessageService(IStringLocalizer localizer) + { + _localizer = localizer; + Logger = NullLogger.Instance; + } + + public Task Info(string message, string? title = null, Action? options = null) + { + var uiMessageOptions = CreateDefaultOptions(); + options?.Invoke(uiMessageOptions); + + MessageReceived?.Invoke(this, new UiMessageEventArgs(UiMessageType.Info, message, title, uiMessageOptions)); + + return Task.CompletedTask; + } + + public Task Success(string message, string? title = null, Action? options = null) + { + var uiMessageOptions = CreateDefaultOptions(); + options?.Invoke(uiMessageOptions); + + MessageReceived?.Invoke(this, new UiMessageEventArgs(UiMessageType.Success, message, title, uiMessageOptions)); + + return Task.CompletedTask; + } + + public Task Warn(string message, string? title = null, Action? options = null) + { + var uiMessageOptions = CreateDefaultOptions(); + options?.Invoke(uiMessageOptions); + + MessageReceived?.Invoke(this, new UiMessageEventArgs(UiMessageType.Warning, message, title, uiMessageOptions)); + + return Task.CompletedTask; + } + + public Task Error(string message, string? title = null, Action? options = null) + { + var uiMessageOptions = CreateDefaultOptions(); + options?.Invoke(uiMessageOptions); + + MessageReceived?.Invoke(this, new UiMessageEventArgs(UiMessageType.Error, message, title, uiMessageOptions)); + + return Task.CompletedTask; + } + + public Task Confirm(string message, string? title = null, Action? options = null) + { + var uiMessageOptions = CreateDefaultOptions(); + options?.Invoke(uiMessageOptions); + + var callback = new TaskCompletionSource(); + + MessageReceived?.Invoke(this, new UiMessageEventArgs(UiMessageType.Confirmation, message, title, uiMessageOptions, callback)); + + return callback.Task; + } + + protected virtual UiMessageOptions CreateDefaultOptions() + { + return new UiMessageOptions + { + CenterMessage = true, + ShowMessageIcon = true, + OkButtonText = _localizer["Ok"], + CancelButtonText = _localizer["Cancel"], + ConfirmButtonText = _localizer["Yes"], + }; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiNotificationService.cs b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiNotificationService.cs new file mode 100644 index 0000000000..e8fd9b493a --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiNotificationService.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Components.Notifications; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.MudBlazorUI; + +[Dependency(ReplaceServices = true)] +public class MudBlazorUiNotificationService : IUiNotificationService, IScopedDependency +{ + /// + /// An event raised after the notification is received. + /// + public event EventHandler? NotificationReceived; + + public Task Info(string message, string? title = null, Action? options = null) + { + var uiNotificationOptions = CreateDefaultOptions(); + options?.Invoke(uiNotificationOptions); + + NotificationReceived?.Invoke(this, new UiNotificationEventArgs(UiNotificationType.Info, message, title, uiNotificationOptions)); + + return Task.CompletedTask; + } + + public Task Success(string message, string? title = null, Action? options = null) + { + var uiNotificationOptions = CreateDefaultOptions(); + options?.Invoke(uiNotificationOptions); + + NotificationReceived?.Invoke(this, new UiNotificationEventArgs(UiNotificationType.Success, message, title, uiNotificationOptions)); + + return Task.CompletedTask; + } + + public Task Warn(string message, string? title = null, Action? options = null) + { + var uiNotificationOptions = CreateDefaultOptions(); + options?.Invoke(uiNotificationOptions); + + NotificationReceived?.Invoke(this, new UiNotificationEventArgs(UiNotificationType.Warning, message, title, uiNotificationOptions)); + + return Task.CompletedTask; + } + + public Task Error(string message, string? title = null, Action? options = null) + { + var uiNotificationOptions = CreateDefaultOptions(); + options?.Invoke(uiNotificationOptions); + + NotificationReceived?.Invoke(this, new UiNotificationEventArgs(UiNotificationType.Error, message, title, uiNotificationOptions)); + + return Task.CompletedTask; + } + + protected virtual UiNotificationOptions CreateDefaultOptions() + { + return new UiNotificationOptions(); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiObjectExtensionPropertyInfoExtensions.cs new file mode 100644 index 0000000000..db55e67058 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiObjectExtensionPropertyInfoExtensions.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Volo.Abp.MudBlazorUI.Components.ObjectExtending; +using Volo.Abp.ObjectExtending; +using Volo.Abp.Reflection; + +namespace Volo.Abp.MudBlazorUI; + +public static class MudBlazorUiObjectExtensionPropertyInfoExtensions +{ + private static readonly Type[] DateTimeTypes = + { + typeof(DateTime), + typeof(DateTime?), + typeof(DateTimeOffset), + typeof(DateTimeOffset?) + }; + + internal static readonly FrozenSet NumberTypes = new HashSet + { + typeof(int), + typeof(long), + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(uint), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + typeof(int?), + typeof(long?), + typeof(byte?), + typeof(sbyte?), + typeof(short?), + typeof(ushort?), + typeof(uint?), + typeof(ulong?), + typeof(float?), + typeof(double?), + typeof(decimal?) + }.ToFrozenSet(); + + private static readonly FrozenSet TextEditSupportedAttributeTypes = new HashSet + { + typeof(EmailAddressAttribute), + typeof(UrlAttribute), + typeof(PhoneAttribute) + }.ToFrozenSet(); + + public static bool IsDate(this IBasicObjectExtensionPropertyInfo property) + { + return DateTimeTypes.Contains(property.Type) && + property.GetDataTypeOrNull() == DataType.Date; + } + + public static bool IsDateTime(this IBasicObjectExtensionPropertyInfo property) + { + return DateTimeTypes.Contains(property.Type) && + !property.IsDate(); + } + + public static DataType? GetDataTypeOrNull(this IBasicObjectExtensionPropertyInfo property) + { + return property + .Attributes + .OfType() + .FirstOrDefault()?.DataType; + } + + public static string? GetDateEditInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) + { + var dataFormatString = property.GetDataFormatStringOrNull(); + if (dataFormatString != null) + { + return dataFormatString; + } + + if (property.IsDate()) + { + return "{0:yyyy-MM-dd}"; + } + + if (property.IsDateTime()) + { + return "{0:yyyy-MM-ddTHH:mm}"; + } + + return null; + } + + public static string? GetDataFormatStringOrNull(this IBasicObjectExtensionPropertyInfo property) + { + return property + .Attributes + .OfType() + .FirstOrDefault()?.DataFormatString; + } + + public static string? GetTextInputValueOrNull(this IBasicObjectExtensionPropertyInfo property, object? value) + { + if (value == null) + { + return null; + } + + if (TypeHelper.IsFloatingType(property.Type)) + { + return value.ToString()?.Replace(',', '.'); + } + + return value.ToString(); + } + + public static T? GetInputValueOrDefault(this IBasicObjectExtensionPropertyInfo property, object? value) + { + if (value == null) + { + return default; + } + + return (T)value; + } + + public static Type GetInputType(this ObjectExtensionPropertyInfo propertyInfo) + { + foreach (var attribute in propertyInfo.Attributes) + { + var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute, propertyInfo.Type); + if (inputTypeByAttribute != null) + { + return inputTypeByAttribute; + } + } + return GetInputTypeFromTypeOrNull(propertyInfo.Type) + ?? typeof(MudTextExtensionProperty<,>); //default + } + + public static bool IsEnum(this ObjectExtensionPropertyInfo propertyInfo) + { + return propertyInfo.Type.IsEnum || TypeHelper.IsNullableEnum(propertyInfo.Type); + } + + private static Type? GetInputTypeFromAttributeOrNull(Attribute attribute, Type propertyType) + { + var hasTextEditSupport = TextEditSupportedAttributeTypes.Any(t => t == attribute.GetType()); + + if (hasTextEditSupport) + { + return typeof(MudTextExtensionProperty<,>); + } + + if (attribute is DataTypeAttribute dataTypeAttribute) + { + switch (dataTypeAttribute.DataType) + { + case DataType.Password: + case DataType.EmailAddress: + case DataType.Url: + case DataType.PhoneNumber: + return typeof(MudTextExtensionProperty<,>); + case DataType.Date: + case DataType.DateTime: + return propertyType == typeof(DateTimeOffset) || propertyType == typeof(DateTimeOffset?) + ? typeof(MudDateTimeOffsetExtensionProperty<,>) + : typeof(MudDateTimeExtensionProperty<,>); + case DataType.Time: + return typeof(MudTimeExtensionProperty<,>); + case DataType.MultilineText: + return typeof(MudTextAreaExtensionProperty<,>); + } + } + + return null; + } + + private static Type? GetInputTypeFromTypeOrNull(Type type) + { + if (type == typeof(bool)) + { + return typeof(MudCheckExtensionProperty<,>); + } + + if (type == typeof(DateTime) || type == typeof(DateTime?)) + { + return typeof(MudDateTimeExtensionProperty<,>); + } + + if (type == typeof(DateTimeOffset) || type == typeof(DateTimeOffset?)) + { + return typeof(MudDateTimeOffsetExtensionProperty<,>); + } + + if (NumberTypes.Contains(type)) + { + return typeof(MudTextExtensionProperty<,>); + } + + return null; + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiPageProgressService.cs b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiPageProgressService.cs new file mode 100644 index 0000000000..9a9c11e717 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/MudBlazorUiPageProgressService.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Components.Progression; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.MudBlazorUI; + +[Dependency(ReplaceServices = true)] +public class MudBlazorUiPageProgressService : IUiPageProgressService, IScopedDependency, IDisposable +{ + /// + /// An event raised after the progress is changed. + /// + public event EventHandler? ProgressChanged; + + protected virtual int HideDelayMs => 250; + + private int _activeCount; + private bool _visible; + private UiPageProgressOptions _lastOptions; + private readonly Timer _hideTimer; + + public MudBlazorUiPageProgressService() + { + _lastOptions = CreateDefaultOptions(); + _hideTimer = new Timer(_ => + { + _visible = false; + ProgressChanged?.Invoke(this, new UiPageProgressEventArgs(-1, _lastOptions)); + }); + } + + public Task Go(int? percentage, Action? options = null) + { + var opt = CreateDefaultOptions(); + options?.Invoke(opt); + _lastOptions = opt; + + if (percentage == -1) + { + _activeCount--; + if (_activeCount <= 0) + { + _activeCount = 0; + _hideTimer.Change(HideDelayMs, Timeout.Infinite); + } + } + else if (percentage == null) + { + _activeCount++; + _hideTimer.Change(Timeout.Infinite, Timeout.Infinite); + if (!_visible) + { + _visible = true; + ProgressChanged?.Invoke(this, new UiPageProgressEventArgs(null, opt)); + } + } + else + { + ProgressChanged?.Invoke(this, new UiPageProgressEventArgs(percentage, opt)); + } + + return Task.CompletedTask; + } + + protected virtual UiPageProgressOptions CreateDefaultOptions() + { + return new UiPageProgressOptions(); + } + + public void Dispose() => _hideTimer.Dispose(); +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Validation/MudValidationAttributeHelper.cs b/framework/src/Volo.Abp.MudBlazorUI/Validation/MudValidationAttributeHelper.cs new file mode 100644 index 0000000000..40dde703df --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Validation/MudValidationAttributeHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Volo.Abp.MudBlazorUI.Validation; + +public static class MudValidationAttributeHelper +{ + internal static readonly PropertyInfo ValidationAttributeErrorMessageStringProperty = typeof(ValidationAttribute) + .GetProperty("ErrorMessageString", BindingFlags.Instance | BindingFlags.NonPublic)!; + + internal static readonly PropertyInfo ValidationAttributeCustomErrorMessageSetProperty = typeof(ValidationAttribute) + .GetProperty("CustomErrorMessageSet", BindingFlags.Instance | BindingFlags.NonPublic)!; + + private const string PlaceholderPrefix = "__Abp_"; + + public static string SetErrorMessagePlaceholders(string? errorMessage) + { + if (string.IsNullOrEmpty(errorMessage)) + { + return string.Empty; + } + + return Regex.Replace(errorMessage, @"\{(\d+)\}", m => PlaceholderPrefix + m.Groups[1].Value); + } + + public static string RevertErrorMessagePlaceholders(string? errorMessage) + { + if (string.IsNullOrEmpty(errorMessage)) + { + return string.Empty; + } + + return Regex.Replace(errorMessage, PlaceholderPrefix + @"(\d+)", m => "{" + m.Groups[1].Value + "}"); + } + + public static string GetDefaultErrorMessage(ValidationAttribute validationAttribute) + { + if (validationAttribute is StringLengthAttribute stringLengthAttribute && stringLengthAttribute.MinimumLength != 0) + { + var customErrorMessageSet = ValidationAttributeCustomErrorMessageSetProperty.GetValue(validationAttribute) as bool?; + if (customErrorMessageSet != true) + { + return SetErrorMessagePlaceholders("The field {0} must be a string with a minimum length of {2} and a maximum length of {1}."); + } + } + + var errorMessageString = ValidationAttributeErrorMessageStringProperty.GetValue(validationAttribute) as string; + return SetErrorMessagePlaceholders(errorMessageString); + } +} diff --git a/framework/src/Volo.Abp.MudBlazorUI/Volo.Abp.MudBlazorUI.csproj b/framework/src/Volo.Abp.MudBlazorUI/Volo.Abp.MudBlazorUI.csproj new file mode 100644 index 0000000000..16ed2f6423 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/Volo.Abp.MudBlazorUI.csproj @@ -0,0 +1,24 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.MudBlazorUI/_Imports.razor b/framework/src/Volo.Abp.MudBlazorUI/_Imports.razor new file mode 100644 index 0000000000..5a0411ed78 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/_Imports.razor @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Components.Web +@using MudBlazor diff --git a/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-popover-patch.js b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-popover-patch.js new file mode 100644 index 0000000000..c083e1277a --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-popover-patch.js @@ -0,0 +1,16 @@ +(function () { + 'use strict'; + + var popover = window.mudPopover; + if (!popover || typeof popover.createObservers !== 'function') { + return; + } + + var original = popover.createObservers; + popover.createObservers = function (id) { + if (!this.map[id]) { + return; + } + return original.call(this, id); + }; +})(); diff --git a/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js new file mode 100644 index 0000000000..1a17a4420f --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/abp-mud-ripple-patch.js @@ -0,0 +1,63 @@ +(function () { + 'use strict'; + + // Workaround for MudBlazor 9.x ripple cleanup not always running in Blazor SSR / + // WebApp / InteractiveAuto render modes. mud's own Ripple.js relies on the + // pointerdown / pointerup pair to schedule `.mud-ripple-effect-expanding` removal + // (see Q/J/K in MudBlazor.min.js). When Blazor re-renders the click target before + // pointerup fires, mud loses its `_mudRipples` Map and the ripple span is never + // cleaned up - stale spans accumulate as gray splotches on tabs / nav-links / + // buttons, looking as if several items are "selected". + // + // Tracking upstream at https://github.com/MudBlazor/MudBlazor/issues/12128 (open + // since 2025-11, reproduces on 9.1+ with Blazor Server). Until that ships we + // observe DOM mutations and schedule the same fade-then-remove sequence mud's + // own Q() would. If mud's cleanup runs first the span is already gone and the + // setTimeout callbacks are no-ops. + + var CLEANUP_DELAY = 600; + var FADE_DELAY = 400; + + function scheduleCleanup(ripple) { + setTimeout(function () { + if (!ripple.parentNode) { + return; + } + if (!ripple.classList.contains('mud-ripple-effect-fading')) { + ripple.classList.add('mud-ripple-effect-fading'); + } + setTimeout(function () { + if (ripple.parentNode) { + ripple.remove(); + } + }, FADE_DELAY); + }, CLEANUP_DELAY); + } + + function startObserving() { + new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var added = mutations[i].addedNodes; + for (var j = 0; j < added.length; j++) { + var n = added[j]; + if (n.nodeType !== 1 || !n.classList) { + continue; + } + // mud appends the ripple span with className `mud-ripple-effect` + // first, and only adds `-expanding` on the next tick. Match by + // the base class so the MutationObserver `childList` callback + // catches the node on the very first mutation. + if (n.classList.contains('mud-ripple-effect')) { + scheduleCleanup(n); + } + } + } + }).observe(document.body, { subtree: true, childList: true }); + } + + if (document.body) { + startObserving(); + } else { + document.addEventListener('DOMContentLoaded', startObserving); + } +})(); diff --git a/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css new file mode 100644 index 0000000000..5fa2c86435 --- /dev/null +++ b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css @@ -0,0 +1,112 @@ +/* ABP MudBlazor UI Custom Styles */ + +/* Entity Actions Styling */ +.abp-entity-actions .mud-menu-activator { + min-width: auto; +} + +/* Data Grid Customizations */ +.abp-mud-data-grid .mud-table-container { + overflow-x: auto; +} + +/* Extension Property Form Spacing */ +.abp-extension-property { + margin-bottom: 16px; +} + +/* Dialog Customizations */ +.abp-mud-dialog .mud-dialog-title { + padding: 16px 24px; +} + +.abp-mud-dialog .mud-dialog-content { + padding: 0 24px 16px; +} + +.abp-mud-dialog .mud-dialog-actions { + padding: 8px 24px 16px; +} + +/* Message Alert Icon Sizing */ +.abp-message-alert-icon { + font-size: 3rem; +} + +/* Page Progress Bar */ +.abp-page-progress { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9999; +} + +/* CRUD Page Layout */ +.abp-crud-page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.abp-crud-page-header-title { + margin: 0; +} + +.abp-crud-page-header-actions { + display: flex; + gap: 8px; +} +/* BlockUI spinner inside mud-dialog: shrink to fit short modal bodies (e.g. file upload modal). + abp.css defines a 150px x 150px spinner via .abp-block-area-busy:before, which gets clipped + by mud-dialog's overflow when the modal content is short. */ +.mud-dialog .abp-block-area.abp-block-area-busy:before { + width: 48px; + height: 48px; + top: calc(50% - 24px); + left: calc(50% - 24px); +} + +.mud-dialog .abp-block-area.abp-block-area-busy:after { + font-size: 12px; +} + + +/* DataGrid header: prevent CJK characters from breaking one-per-line when column is narrow. + v9 column-header is a flex container with flex item width:100%; without nowrap, CJK + (no word boundary) collapses to min-content = single character width, stacking each + header character vertically. */ +.mud-data-grid .mud-table-cell .column-header .sortable-column-header, +.mud-data-grid .mud-table-cell .column-header > span:not(.column-options), +.mud-data-grid .mud-table-head .mud-table-cell { + white-space: nowrap; +} + + +/* Picker label (date / time / color picker): when label is long it overflows into the + toggle icon on the right and visually overlaps the calendar/clock icon. + Reserve ~40px on the right for the end-adornment toggle icon so the label truncates with ellipsis. */ +.mud-picker .mud-input-label.mud-input-label-inputcontrol { + max-width: calc(100% - 40px); +} + + +/* Input label: increase line-height slightly so descenders (g, p, q, y) in the floated + label text are not clipped by the input baseline immediately below. */ +.mud-input-control .mud-input-label-inputcontrol { + line-height: 1.4rem; +} + + +/* Vertical MudTabs (Position="Left"/"Right"): add horizontal gap between the tabbar + and the panels container so the active tab indicator does not visually touch the + panel content. v9 default puts them flush against each other. */ +.mud-tabs.mud-tabs-vertical > .mud-tabs-panels { + padding-inline-start: 16px; +} + +.mud-tabs.mud-tabs-vertical-reverse > .mud-tabs-panels { + padding-inline-end: 16px; +} + diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index 03d6029b69..933e1bf005 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -61,29 +61,45 @@ public class ChannelPool : IChannelPool, ISingletonDependency } } - poolItem.Acquire(); - if (poolItem.Channel.IsClosed) { - await poolItem.DisposeAsync(); - Channels.TryRemove(channelName, out _); - - using (await Semaphore.LockAsync()) + ChannelPoolItem? staleItem = null; + try { - if (Channels.TryGetValue(channelName, out var existingChannelPoolItem3)) + using (await Semaphore.LockAsync()) { - poolItem = existingChannelPoolItem3; + if (Channels.TryGetValue(channelName, out var currentChannelPoolItem) && + ReferenceEquals(currentChannelPoolItem, poolItem)) + { + staleItem = poolItem; + Channels.TryRemove(channelName, out _); + + poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); + Channels.TryAdd(channelName, poolItem); + } + else if (currentChannelPoolItem != null) + { + poolItem = currentChannelPoolItem; + } + else + { + poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); + Channels.TryAdd(channelName, poolItem); + } } - else + } + finally + { + if (staleItem != null) { - poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); - Channels.TryAdd(channelName, poolItem); + staleItem.WaitIfInUse(TotalDisposeWaitDuration); + await staleItem.DisposeAsync(); } } - - poolItem.Acquire(); } + poolItem.Acquire(); + return new ChannelAccessor( poolItem.Channel, channelName, diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs index e100825011..e9f74595c8 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs @@ -9,6 +9,13 @@ public interface ITemplateRenderingEngine { string Name { get; } + /// + /// True when this engine restricts templates to a DSL without direct .NET + /// interop (e.g. Scriban). False when templates compile to fully-trusted + /// .NET code (e.g. Razor). + /// + bool IsSandboxed { get; } + /// /// Renders a text template. /// diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs index 62c8dee43c..7e5e10d86c 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs @@ -8,6 +8,8 @@ public abstract class TemplateRenderingEngineBase : ITemplateRenderingEngine { public abstract string Name { get; } + public virtual bool IsSandboxed => false; + protected readonly ITemplateDefinitionManager TemplateDefinitionManager; protected readonly ITemplateContentProvider TemplateContentProvider; protected readonly IStringLocalizerFactory StringLocalizerFactory; diff --git a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs index 485ca44121..b6c120fe9e 100644 --- a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs @@ -17,6 +17,8 @@ public class RazorTemplateRenderingEngine : TemplateRenderingEngineBase, ITransi public const string EngineName = "Razor"; public override string Name => EngineName; + public override bool IsSandboxed => false; + protected readonly IServiceScopeFactory ServiceScopeFactory; public RazorTemplateRenderingEngine( diff --git a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs index 7a5ac79592..9b7b3393a6 100644 --- a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Localization; @@ -14,6 +15,8 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran public const string EngineName = "Scriban"; public override string Name => EngineName; + public override bool IsSandboxed => true; + public ScribanTemplateRenderingEngine( ITemplateDefinitionManager templateDefinitionManager, ITemplateContentProvider templateContentProvider, @@ -118,7 +121,10 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran Dictionary globalContext, object? model = null) { - var context = new TemplateContext(); + var context = new TemplateContext + { + MemberFilter = IsMemberAllowed + }; var scriptObject = new ScriptObject(); @@ -140,4 +146,12 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran return context; } + + /// + /// Scriban member filter: only public properties on imported objects are exposed. + /// + protected virtual bool IsMemberAllowed(MemberInfo member) + { + return member is PropertyInfo; + } } diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IMenuItemCulturePrefixHelper.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IMenuItemCulturePrefixHelper.cs new file mode 100644 index 0000000000..93d9992b3f --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IMenuItemCulturePrefixHelper.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.UI.Navigation; + +public interface IMenuItemCulturePrefixHelper +{ + Task PrependCulturePrefixAsync(IHasMenuItems menuWithItems, string prefix); +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IMenuItemUrlProvider.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IMenuItemUrlProvider.cs new file mode 100644 index 0000000000..faae8c2367 --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IMenuItemUrlProvider.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.UI.Navigation; + +/// +/// Provides a way to modify menu item URLs after the menu is fully configured. +/// Implementations can transform URLs based on the current request context +/// (e.g. adding a culture prefix for URL-based localization). +/// +public interface IMenuItemUrlProvider +{ + Task HandleAsync(MenuItemUrlProviderContext context); +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuItemCulturePrefixHelper.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuItemCulturePrefixHelper.cs new file mode 100644 index 0000000000..242cf9f679 --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuItemCulturePrefixHelper.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.UI.Navigation; + +public class MenuItemCulturePrefixHelper : IMenuItemCulturePrefixHelper, ITransientDependency +{ + public virtual Task PrependCulturePrefixAsync(IHasMenuItems menuWithItems, string prefix) + { + PrependCulturePrefix(menuWithItems, prefix); + return Task.CompletedTask; + } + + protected virtual void PrependCulturePrefix(IHasMenuItems menuWithItems, string prefix) + { + foreach (var item in menuWithItems.Items) + { + if (item.Url != null) + { + if (item.Url.StartsWith("~/")) + { + item.Url = "~" + prefix + item.Url.Substring(1); + } + else if (item.Url.StartsWith("/")) + { + item.Url = prefix + item.Url; + } + } + + PrependCulturePrefix(item, prefix); + } + } +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuItemUrlProviderContext.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuItemUrlProviderContext.cs new file mode 100644 index 0000000000..55f9d178da --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuItemUrlProviderContext.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.UI.Navigation; + +public class MenuItemUrlProviderContext +{ + public ApplicationMenu Menu { get; } + + public MenuItemUrlProviderContext(ApplicationMenu menu) + { + Menu = menu; + } +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs index 711d2ed4de..49cbfaae03 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs @@ -94,6 +94,12 @@ public class MenuManager : IMenuManager, ITransientDependency await CheckPermissionsAsync(scope.ServiceProvider, menu); } + + var urlProviderContext = new MenuItemUrlProviderContext(menu); + foreach (var urlProvider in scope.ServiceProvider.GetServices()) + { + await urlProvider.HandleAsync(urlProviderContext); + } } NormalizeMenu(menu); diff --git a/framework/src/Volo.Abp.VirtualFileSystem/Volo/Abp/VirtualFileSystem/VirtualFileSetListExtensions.cs b/framework/src/Volo.Abp.VirtualFileSystem/Volo/Abp/VirtualFileSystem/VirtualFileSetListExtensions.cs index e25721f00f..be8a0cfb88 100644 --- a/framework/src/Volo.Abp.VirtualFileSystem/Volo/Abp/VirtualFileSystem/VirtualFileSetListExtensions.cs +++ b/framework/src/Volo.Abp.VirtualFileSystem/Volo/Abp/VirtualFileSystem/VirtualFileSetListExtensions.cs @@ -65,6 +65,14 @@ public static class VirtualFileSetListExtensions public static void ReplaceEmbeddedByPhysical( [NotNull] this VirtualFileSetList fileSets, [NotNull] string physicalPath) + { + ReplaceEmbeddedByPhysical(fileSets, physicalPath, ExclusionFilters.Sensitive); + } + + public static void ReplaceEmbeddedByPhysical( + [NotNull] this VirtualFileSetList fileSets, + [NotNull] string physicalPath, + ExclusionFilters exclusionFilters) { Check.NotNull(fileSets, nameof(fileSets)); Check.NotNullOrWhiteSpace(physicalPath, nameof(physicalPath)); @@ -83,7 +91,7 @@ public static class VirtualFileSetListExtensions thisPath = Path.Combine(thisPath, embeddedVirtualFileSet.BaseFolder!); } - fileSets[i] = new PhysicalVirtualFileSetInfo(new PhysicalFileProvider(thisPath), thisPath); + fileSets[i] = new PhysicalVirtualFileSetInfo(new PhysicalFileProvider(thisPath, exclusionFilters), thisPath); } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Components.Web.Theming.Tests/Volo/Abp/AspNetCore/Components/Web/ExceptionHandling/AbpExceptionHandlingLogger_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Components.Web.Theming.Tests/Volo/Abp/AspNetCore/Components/Web/ExceptionHandling/AbpExceptionHandlingLogger_Tests.cs new file mode 100644 index 0000000000..b010483f28 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Components.Web.Theming.Tests/Volo/Abp/AspNetCore/Components/Web/ExceptionHandling/AbpExceptionHandlingLogger_Tests.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Shouldly; +using Volo.Abp.AspNetCore.Components.ExceptionHandling; +using Xunit; + +namespace Volo.Abp.AspNetCore.Components.Web.ExceptionHandling; + +public class AbpExceptionHandlingLogger_Tests +{ + [Fact] + public void Should_Not_Recurse_When_Informer_Writes_To_Same_Logger_Pipeline() + { + var services = new ServiceCollection(); + var informer = new RecordingUserExceptionInformer(); + services.AddSingleton(informer); + + var provider = new AbpExceptionHandlingLoggerProvider(services); + services.AddLogging(builder => builder + .AddProvider(provider) + .AddFilter(typeof(UserExceptionInformer).FullName, LogLevel.None)); + + var accessor = services.AddObjectAccessor(); + var sp = services.BuildServiceProvider(); + accessor.Value = sp; + + informer.Logger = sp.GetRequiredService() + .CreateLogger(typeof(UserExceptionInformer).FullName!); + + var entryLogger = sp.GetRequiredService>(); + entryLogger.LogError(new InvalidOperationException("boom"), "entry"); + + informer.InformCalls.ShouldBe(1); + } + + private sealed class RecordingUserExceptionInformer : IUserExceptionInformer + { + private const int RecursionCircuitBreaker = 50; + + public int InformCalls; + public ILogger? Logger; + + public void Inform(UserExceptionInformerContext context) + { + InformCalls++; + if (InformCalls > RecursionCircuitBreaker) + { + return; + } + + Logger?.LogError(context.Exception, context.Exception.Message); + } + + public Task InformAsync(UserExceptionInformerContext context) + { + Inform(context); + return Task.CompletedTask; + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj index 8a929dd38a..496dba12ed 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs index 9154608680..4339104b30 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs @@ -11,6 +11,7 @@ using Volo.Abp.AspNetCore.Mvc.GlobalFeatures; using Volo.Abp.AspNetCore.Mvc.Libs; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.Localization.Resource; +using Microsoft.AspNetCore.RequestLocalization; using Volo.Abp.AspNetCore.Security.Claims; using Volo.Abp.AspNetCore.TestBase; using Volo.Abp.Authorization; @@ -121,6 +122,11 @@ public class AbpAspNetCoreMvcTestModule : AbpModule options.Languages.Add(new LanguageInfo("el", "el", "Ελληνικά")); }); + Configure(options => + { + options.UseRouteBasedCulture = true; + }); + Configure(options => { options.RootDirectory = "/Volo/Abp/AspNetCore/Mvc"; diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider_Tests.cs new file mode 100644 index 0000000000..9a062e0fc3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider_Tests.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Options; +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public class AbpRemoteServiceApiDescriptionProvider_Tests +{ + private static (AbpRemoteServiceApiDescriptionProvider provider, + AbpRemoteServiceApiDescriptionProviderOptions options) + CreateProvider() + { + var options = new AbpRemoteServiceApiDescriptionProviderOptions(); + options.SupportedResponseTypes.Add(new ApiResponseType + { + Type = typeof(string), + StatusCode = 400 + }); + options.SupportedResponseTypes.Add(new ApiResponseType + { + Type = typeof(string), + StatusCode = 500 + }); + + var mvcOptions = new MvcOptions(); + mvcOptions.OutputFormatters.Add(new FakeOutputFormatter()); + + var provider = new AbpRemoteServiceApiDescriptionProvider( + new EmptyModelMetadataProvider(), + new OptionsWrapper(mvcOptions), + new OptionsWrapper(options)); + + return (provider, options); + } + + private static ApiDescriptionProviderContext CreateContext() + { + var actionDescriptor = new ControllerActionDescriptor + { + ControllerName = "Fake", + ActionName = "Get", + ControllerTypeInfo = typeof(FakeRemoteController).GetTypeInfo(), + MethodInfo = typeof(FakeRemoteController).GetMethod(nameof(FakeRemoteController.Get))!, + AttributeRouteInfo = new AttributeRouteInfo { Template = "api/fake" } + }; + + var description = new ApiDescription + { + ActionDescriptor = actionDescriptor, + HttpMethod = "GET", + RelativePath = "api/fake" + }; + + return new ApiDescriptionProviderContext(new[] { actionDescriptor }) + { + Results = { description } + }; + } + + [Fact] + public void Should_Not_Mutate_Shared_Options_On_Repeated_Calls() + { + var (provider, options) = CreateProvider(); + + provider.OnProvidersExecuting(CreateContext()); + provider.OnProvidersExecuting(CreateContext()); + provider.OnProvidersExecuting(CreateContext()); + + foreach (var template in options.SupportedResponseTypes) + { + template.ApiResponseFormats.ShouldBeEmpty(); + } + } + + [Fact] + public void Should_Add_Response_Types_With_Preserved_Template_Fields_And_Formats() + { + var options = new AbpRemoteServiceApiDescriptionProviderOptions(); + options.SupportedResponseTypes.Add(new ApiResponseType + { + Type = typeof(string), + StatusCode = 418, + Description = "I'm a teapot", + IsDefaultResponse = true + }); + + var mvcOptions = new MvcOptions(); + mvcOptions.OutputFormatters.Add(new FakeOutputFormatter()); + + var provider = new AbpRemoteServiceApiDescriptionProvider( + new EmptyModelMetadataProvider(), + new OptionsWrapper(mvcOptions), + new OptionsWrapper(options)); + + var context = CreateContext(); + provider.OnProvidersExecuting(context); + + var result = context.Results.Single(); + var added = result.SupportedResponseTypes.ShouldHaveSingleItem(); + added.StatusCode.ShouldBe(418); + added.Type.ShouldBe(typeof(string)); + added.Description.ShouldBe("I'm a teapot"); + added.IsDefaultResponse.ShouldBeTrue(); + added.ModelMetadata.ShouldNotBeNull(); + added.ApiResponseFormats.Select(x => x.MediaType) + .ShouldBe(new[] { "application/json", "text/json" }); + } + + [Fact] + public void Should_Not_Throw_Under_Concurrent_Calls() + { + var (provider, _) = CreateProvider(); + + var exception = Record.Exception(() => + Parallel.For(0, 8, _ => + { + for (var i = 0; i < 500; i++) + { + provider.OnProvidersExecuting(CreateContext()); + } + })); + + exception.ShouldBeNull(); + } + + private sealed class FakeRemoteController : IRemoteService + { + public string Get() => string.Empty; + } + + private sealed class FakeOutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider + { + public bool CanWriteResult(OutputFormatterCanWriteContext context) => true; + + public Task WriteAsync(OutputFormatterWriteContext context) => Task.CompletedTask; + + public IReadOnlyList GetSupportedContentTypes(string? contentType, Type objectType) + => new[] { "application/json", "text/json" }; + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureAwareUrlHelper_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureAwareUrlHelper_Tests.cs new file mode 100644 index 0000000000..d761d85bf8 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureAwareUrlHelper_Tests.cs @@ -0,0 +1,137 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.RequestLocalization; +using Microsoft.AspNetCore.Routing; +using NSubstitute; +using Shouldly; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class AbpCultureAwareUrlHelper_Tests +{ + [Fact] + public void Action_Should_Inject_Culture() + { + var inner = Substitute.For(); + inner.Action(Arg.Any()).Returns(callInfo => + { + var ctx = callInfo.Arg(); + var values = new RouteValueDictionary(ctx.Values); + return values.ContainsKey("culture") ? $"/{values["culture"]}/{ctx.Controller}/{ctx.Action}" : $"/{ctx.Controller}/{ctx.Action}"; + }); + + var helper = new AbpCultureAwareUrlHelper(inner, "zh-Hans"); + var result = helper.Action(new UrlActionContext { Controller = "Home", Action = "Index" }); + + result.ShouldContain("zh-Hans"); + } + + [Fact] + public void Action_Should_Not_Override_Explicit_Culture() + { + var inner = Substitute.For(); + inner.Action(Arg.Any()).Returns(callInfo => + { + var ctx = callInfo.Arg(); + var values = new RouteValueDictionary(ctx.Values); + return $"/{values["culture"]}/Home/Index"; + }); + + var helper = new AbpCultureAwareUrlHelper(inner, "zh-Hans"); + var result = helper.Action(new UrlActionContext + { + Controller = "Home", + Action = "Index", + Values = new { culture = "en" } + }); + + // Explicit "en" should not be overridden by "zh-Hans" + result.ShouldBe("/en/Home/Index"); + } + + [Fact] + public void RouteUrl_Should_Inject_Culture() + { + var inner = Substitute.For(); + inner.RouteUrl(Arg.Any()).Returns(callInfo => + { + var ctx = callInfo.Arg(); + var values = new RouteValueDictionary(ctx.Values); + return values.ContainsKey("culture") ? $"/{values["culture"]}/page" : "/page"; + }); + + var helper = new AbpCultureAwareUrlHelper(inner, "tr"); + var result = helper.RouteUrl(new UrlRouteContext()); + + result.ShouldBe("/tr/page"); + } + + [Fact] + public void Content_Should_Pass_Through() + { + var inner = Substitute.For(); + inner.Content("~/test").Returns("/test"); + + var helper = new AbpCultureAwareUrlHelper(inner, "en"); + helper.Content("~/test").ShouldBe("/test"); + } + + [Fact] + public void IsLocalUrl_Should_Pass_Through() + { + var inner = Substitute.For(); + inner.IsLocalUrl("/test").Returns(true); + + var helper = new AbpCultureAwareUrlHelper(inner, "en"); + helper.IsLocalUrl("/test").ShouldBeTrue(); + } + + [Fact] + public void Factory_Should_Return_CultureAwareHelper_When_Culture_In_Route() + { + var factory = CreateFactory(useRouteBasedCulture: true); + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["culture"] = "tr"; + var actionContext = new ActionContext(httpContext, new RouteData(httpContext.Request.RouteValues), new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()); + + var urlHelper = factory.GetUrlHelper(actionContext); + + urlHelper.ShouldBeOfType(); + } + + [Fact] + public void Factory_Should_Return_Default_Helper_When_No_Culture() + { + var factory = CreateFactory(useRouteBasedCulture: true); + var httpContext = new DefaultHttpContext(); + var actionContext = new ActionContext(httpContext, new RouteData(), new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()); + + var urlHelper = factory.GetUrlHelper(actionContext); + + urlHelper.ShouldNotBeOfType(); + } + + [Fact] + public void Factory_Should_Return_Default_Helper_When_RouteBasedCulture_Disabled() + { + var factory = CreateFactory(useRouteBasedCulture: false); + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["culture"] = "tr"; + var actionContext = new ActionContext(httpContext, new RouteData(httpContext.Request.RouteValues), new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor()); + + // Even with culture in route, should not wrap when the feature is disabled + var urlHelper = factory.GetUrlHelper(actionContext); + + urlHelper.ShouldNotBeOfType(); + } + + private static AbpCultureRouteUrlHelperFactory CreateFactory(bool useRouteBasedCulture) + { + return new AbpCultureRouteUrlHelperFactory( + new UrlHelperFactory(), + Microsoft.Extensions.Options.Options.Create(new AbpRequestLocalizationOptions { UseRouteBasedCulture = useRouteBasedCulture })); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider_Tests.cs new file mode 100644 index 0000000000..bd77480bc4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpCultureMenuItemUrlProvider_Tests.cs @@ -0,0 +1,280 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RequestLocalization; +using Microsoft.AspNetCore.Routing; +using Shouldly; +using MsOptions = Microsoft.Extensions.Options.Options; +using Volo.Abp.Localization; +using Volo.Abp.UI.Navigation; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class AbpCultureMenuItemUrlProvider_Tests +{ + [Fact] + public async Task Should_Not_Modify_Urls_When_RouteBasedCulture_Is_Disabled() + { + var provider = CreateProvider(useRouteBasedCulture: false, cultureName: "zh-Hans"); + var menu = CreateMenuWithItems("/home", "/about"); + + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + + menu.Items[0].Url.ShouldBe("/home"); + menu.Items[1].Url.ShouldBe("/about"); + } + + [Fact] + public async Task Should_Prepend_Culture_Prefix_When_Route_Has_Culture() + { + var provider = CreateProvider(useRouteBasedCulture: true, cultureName: "zh-Hans"); + var menu = CreateMenuWithItems("/home", "/about"); + + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + + menu.Items[0].Url.ShouldBe("/zh-Hans/home"); + menu.Items[1].Url.ShouldBe("/zh-Hans/about"); + } + + [Fact] + public async Task Should_Not_Add_Prefix_For_Mvc_Request_Without_Culture() + { + // MVC request to /about (no culture, no HasRouteCulture cookie). + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["controller"] = "Home"; + httpContext.Request.RouteValues["action"] = "About"; + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var localizationOptions = MsOptions.Create( + new AbpRequestLocalizationOptions { UseRouteBasedCulture = true }); + var abpLocOptions = new AbpLocalizationOptions(); + abpLocOptions.Languages.Add(new LanguageInfo("en")); + abpLocOptions.Languages.Add(new LanguageInfo("zh-Hans")); + var provider = new AbpCultureMenuItemUrlProvider( + httpContextAccessor, localizationOptions, MsOptions.Create(abpLocOptions), new MenuItemCulturePrefixHelper()); + + var menu = CreateMenuWithItems("/home", "/about"); + + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo("zh-Hans"); + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + + menu.Items[0].Url.ShouldBe("/home"); + menu.Items[1].Url.ShouldBe("/about"); + } + + [Fact] + public async Task Should_Fallback_To_CurrentCulture_In_Blazor_Circuit() + { + // Blazor Server interactive circuit: HttpContext exists (SignalR) but has + // no route culture. Cookie was set during SSR indicating route culture was used. + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Cookie"] = $"{AbpRequestCultureCookieHelper.HasRouteCultureCookieName}=1"; + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var localizationOptions = MsOptions.Create( + new AbpRequestLocalizationOptions { UseRouteBasedCulture = true }); + var abpLocOptions = new AbpLocalizationOptions(); + abpLocOptions.Languages.Add(new LanguageInfo("en")); + abpLocOptions.Languages.Add(new LanguageInfo("zh-Hans")); + var provider = new AbpCultureMenuItemUrlProvider( + httpContextAccessor, localizationOptions, MsOptions.Create(abpLocOptions), new MenuItemCulturePrefixHelper()); + + var menu = CreateMenuWithItems("/home", "/about"); + + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo("zh-Hans"); + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + + menu.Items[0].Url.ShouldBe("/zh-Hans/home"); + menu.Items[1].Url.ShouldBe("/zh-Hans/about"); + } + + [Fact] + public async Task Should_Use_CurrentCulture_Fallback_When_No_HttpContext() + { + // Simulates Blazor interactive circuit: no HttpContext, but CurrentCulture is set. + // CurrentCulture (not CurrentUICulture) is used because {culture} route segments + // represent the culture, not the UI culture. + var provider = CreateProviderWithoutHttpContext( + useRouteBasedCulture: true, + knownLanguages: new[] { "en", "zh-Hans", "tr" }); + + var menu = CreateMenuWithItems("/home", "/about"); + + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo("zh-Hans"); + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + + menu.Items[0].Url.ShouldBe("/zh-Hans/home"); + menu.Items[1].Url.ShouldBe("/zh-Hans/about"); + } + + [Fact] + public async Task Should_Not_Modify_Urls_When_No_HttpContext_And_Unknown_Culture() + { + // Blazor interactive circuit with a culture that is not in the known languages list + var provider = CreateProviderWithoutHttpContext( + useRouteBasedCulture: true, + knownLanguages: new[] { "en", "tr" }); + + var menu = CreateMenuWithItems("/home", "/about"); + + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo("fr"); + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + + menu.Items[0].Url.ShouldBe("/home"); + menu.Items[1].Url.ShouldBe("/about"); + } + + [Fact] + public async Task Should_Prepend_Prefix_Recursively_For_Nested_Items() + { + var provider = CreateProvider(useRouteBasedCulture: true, cultureName: "tr"); + + var menu = new ApplicationMenu("TestMenu"); + var parent = new ApplicationMenuItem("Parent", "Parent", url: "/parent"); + var child = new ApplicationMenuItem("Child", "Child", url: "/child"); + var grandChild = new ApplicationMenuItem("GrandChild", "GrandChild", url: "/grandchild"); + child.AddItem(grandChild); + parent.AddItem(child); + menu.AddItem(parent); + + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + + parent.Url.ShouldBe("/tr/parent"); + child.Url.ShouldBe("/tr/child"); + grandChild.Url.ShouldBe("/tr/grandchild"); + } + + [Fact] + public async Task Should_Handle_Tilde_Slash_Urls() + { + // ~/identity/users is the pattern used by ABP module menu contributors (e.g. Identity) + var provider = CreateProvider(useRouteBasedCulture: true, cultureName: "zh-Hans"); + + var menu = new ApplicationMenu("TestMenu"); + menu.AddItem(new ApplicationMenuItem("Users", "Users", url: "~/identity/users")); + menu.AddItem(new ApplicationMenuItem("Roles", "Roles", url: "~/identity/roles")); + + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + + // ~/identity/users → ~/zh-Hans/identity/users + // Blazor theme strips "~/" via TrimStart('/', '~') → "zh-Hans/identity/users" + // With resolves to /zh-Hans/identity/users + menu.Items[0].Url.ShouldBe("~/zh-Hans/identity/users"); + menu.Items[1].Url.ShouldBe("~/zh-Hans/identity/roles"); + } + + [Fact] + public async Task Should_Not_Modify_External_Urls() + { + var provider = CreateProvider(useRouteBasedCulture: true, cultureName: "zh-Hans"); + + var menu = new ApplicationMenu("TestMenu"); + menu.AddItem(new ApplicationMenuItem("External", "External", url: "https://example.com/page")); + menu.AddItem(new ApplicationMenuItem("Relative", "Relative", url: "page")); + menu.AddItem(new ApplicationMenuItem("Local", "Local", url: "/local")); + + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + + // External and relative URLs should not be modified + menu.Items[0].Url.ShouldBe("https://example.com/page"); + menu.Items[1].Url.ShouldBe("page"); + // Local URL should be prefixed + menu.Items[2].Url.ShouldBe("/zh-Hans/local"); + } + + [Fact] + public async Task Should_Not_Throw_When_Url_Is_Null() + { + var provider = CreateProvider(useRouteBasedCulture: true, cultureName: "tr"); + + var menu = new ApplicationMenu("TestMenu"); + menu.AddItem(new ApplicationMenuItem("NoUrl", "No URL", url: null)); + menu.AddItem(new ApplicationMenuItem("WithUrl", "With URL", url: "/page")); + + await provider.HandleAsync(new MenuItemUrlProviderContext(menu)); + + // Null URL should remain null + menu.Items[0].Url.ShouldBeNull(); + // Normal URL should be prefixed + menu.Items[1].Url.ShouldBe("/tr/page"); + } + + private static AbpCultureMenuItemUrlProvider CreateProvider( + bool useRouteBasedCulture, + string? cultureName) + { + var httpContext = new DefaultHttpContext(); + if (cultureName != null) + { + httpContext.Request.RouteValues["culture"] = cultureName; + } + + var httpContextAccessor = new HttpContextAccessor { HttpContext = httpContext }; + var localizationOptions = MsOptions.Create( + new AbpRequestLocalizationOptions { UseRouteBasedCulture = useRouteBasedCulture }); + var abpLocalizationOptions = MsOptions.Create(new AbpLocalizationOptions()); + + return new AbpCultureMenuItemUrlProvider( + httpContextAccessor, localizationOptions, abpLocalizationOptions, new MenuItemCulturePrefixHelper()); + } + + private static AbpCultureMenuItemUrlProvider CreateProviderWithoutHttpContext( + bool useRouteBasedCulture, + string[] knownLanguages) + { + var httpContextAccessor = new HttpContextAccessor { HttpContext = null }; + var localizationOptions = MsOptions.Create( + new AbpRequestLocalizationOptions { UseRouteBasedCulture = useRouteBasedCulture }); + var abpLocOptions = new AbpLocalizationOptions(); + foreach (var lang in knownLanguages) + { + abpLocOptions.Languages.Add(new LanguageInfo(lang)); + } + + return new AbpCultureMenuItemUrlProvider( + httpContextAccessor, localizationOptions, MsOptions.Create(abpLocOptions), new MenuItemCulturePrefixHelper()); + } + + private static ApplicationMenu CreateMenuWithItems(params string[] urls) + { + var menu = new ApplicationMenu("TestMenu"); + for (var i = 0; i < urls.Length; i++) + { + menu.AddItem(new ApplicationMenuItem($"Item{i}", $"Item {i}", url: urls[i])); + } + return menu; + } + +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController_Tests.cs new file mode 100644 index 0000000000..491577c43e --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController_Tests.cs @@ -0,0 +1,123 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Localization; +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class AbpLanguagesController_Tests : AspNetCoreMvcTestBase +{ + private const string SwitchUrl = "/Abp/Languages/Switch"; + + [Fact] + public async Task Should_Replace_Route_Culture_In_ReturnUrl_When_Cookie_Is_Set() + { + var response = await SendSwitchRequestAsync( + targetCulture: "zh-Hans", + returnUrl: "/en/Home/About", + currentCultureCookie: "en"); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldBe("/zh-Hans/Home/About"); + } + + [Fact] + public async Task Should_Replace_Route_Culture_When_Switching_Back() + { + var response = await SendSwitchRequestAsync( + targetCulture: "en", + returnUrl: "/zh-Hans/About", + currentCultureCookie: "zh-Hans"); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldBe("/en/About"); + } + + [Fact] + public async Task Should_Replace_Region_Culture_In_ReturnUrl() + { + var response = await SendSwitchRequestAsync( + targetCulture: "zh-Hans", + returnUrl: "/en-US/products", + currentCultureCookie: "en-US"); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldBe("/zh-Hans/products"); + } + + [Fact] + public async Task Should_Not_Replace_When_No_Cookie() + { + // No cookie — GetCurrentCultureFromRequestCookie returns null, no route replacement + var response = await SendSwitchRequestAsync( + targetCulture: "zh-Hans", + returnUrl: "/en/Home/About", + currentCultureCookie: null); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldBe("/en/Home/About"); + } + + [Fact] + public async Task Should_Redirect_To_Root_When_ReturnUrl_Is_Empty() + { + var response = await SendSwitchRequestAsync( + targetCulture: "zh-Hans", + returnUrl: "", + currentCultureCookie: "en"); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldStartWith("/"); + } + + [Fact] + public async Task Should_Not_Replace_Culture_Inside_Longer_Segment_Via_Http() + { + // "en" must not corrupt "/enterprise/products" + var response = await SendSwitchRequestAsync( + targetCulture: "zh-Hans", + returnUrl: "/enterprise/products", + currentCultureCookie: "en"); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldBe("/enterprise/products"); + } + + [Fact] + public async Task Should_Replace_Culture_After_Tenant_Segment() + { + // Multi-tenant URL: /tenant-a/zh-Hans/About → /tenant-a/en/About + var response = await SendSwitchRequestAsync( + targetCulture: "en", + returnUrl: "/tenant-a/zh-Hans/About", + currentCultureCookie: "zh-Hans"); + + response.StatusCode.ShouldBe(HttpStatusCode.Found); + response.Headers.Location?.ToString().ShouldBe("/tenant-a/en/About"); + } + + private async Task SendSwitchRequestAsync( + string targetCulture, + string returnUrl, + string? currentCultureCookie) + { + var url = $"{SwitchUrl}?culture={Uri.EscapeDataString(targetCulture)}" + + $"&uiCulture={Uri.EscapeDataString(targetCulture)}" + + $"&returnUrl={Uri.EscapeDataString(returnUrl)}"; + + var request = new HttpRequestMessage(HttpMethod.Get, url); + + if (currentCultureCookie != null) + { + var cookieValue = CookieRequestCultureProvider.MakeCookieValue( + new RequestCulture(currentCultureCookie, currentCultureCookie)); + request.Headers.Add("Cookie", + $"{CookieRequestCultureProvider.DefaultCookieName}={Uri.EscapeDataString(cookieValue)}"); + } + + return await Client.SendAsync(request); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/LanguageSwitchRouteCultureReplacement_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/LanguageSwitchRouteCultureReplacement_Tests.cs new file mode 100644 index 0000000000..1c5fd4cb22 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/LanguageSwitchRouteCultureReplacement_Tests.cs @@ -0,0 +1,259 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Shouldly; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class LanguageSwitchRouteCultureReplacement_Tests +{ + private readonly AbpAspNetCoreMvcQueryStringCultureReplacement _replacement = new(); + + [Fact] + public async Task Should_Replace_Route_Prefix() + { + var context = CreateContext("tr", "en", "/tr/products"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en/products"); + } + + [Fact] + public async Task Should_Replace_Region_Culture() + { + var context = CreateContext("en-US", "zh-Hans", "/en-US/about"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/zh-Hans/about"); + } + + [Fact] + public async Task Should_Replace_Culture_Only_Url() + { + var context = CreateContext("tr", "en", "/tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en"); + } + + [Fact] + public async Task Should_Replace_Culture_With_Query_String() + { + var context = CreateContext("tr", "en", "/tr?returnUrl=/home"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en?returnUrl=/home"); + } + + [Fact] + public async Task Should_Replace_Culture_After_Tenant() + { + var context = CreateContext("zh-Hans", "en", "/tenant-a/zh-Hans/About"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/tenant-a/en/About"); + } + + [Fact] + public async Task Should_Replace_Culture_Only_After_Tenant() + { + var context = CreateContext("tr", "en", "/tenant-a/tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/tenant-a/en"); + } + + [Fact] + public async Task Should_Replace_Via_RouteData_When_No_CurrentCulture() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["culture"] = "tr"; + var context = new QueryStringCultureReplacementContext( + httpContext, new RequestCulture("en"), "/tr/products"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en/products"); + } + + [Fact] + public async Task Should_Not_Replace_When_No_Culture_Source() + { + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en"), "/volosoft/products"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/volosoft/products"); + } + + [Fact] + public async Task Should_Not_Replace_Culture_Inside_Longer_Segment() + { + // "en" must not match inside "enterprise" + var context = CreateContext("en", "tr", "/enterprise/products"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/enterprise/products"); + } + + [Fact] + public async Task Should_Not_Replace_Culture_When_Culture_Is_Segment_Prefix() + { + // "fr" appears at the start of "fr-zone" but is not a complete segment + var context = CreateContext("fr", "en", "/fr-zone/about"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/fr-zone/about"); + } + + [Fact] + public async Task Should_Replace_Culture_Before_Fragment() + { + var context = CreateContext("en", "tr", "/en#section"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/tr#section"); + } + + [Fact] + public async Task Should_Replace_Culture_Before_Fragment_With_Path() + { + var context = CreateContext("en", "tr", "/en/about#top"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/tr/about#top"); + } + + [Fact] + public async Task Should_Replace_Query_String_Culture() + { + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en", "en"), "/home?culture=tr&ui-culture=tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/home?culture=en&ui-culture=en"); + } + + [Fact] + public async Task Should_Replace_Both_Route_And_Query_String() + { + var context = CreateContext("tr", "en", "/tr/home?culture=tr&ui-culture=tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en/home?culture=en&ui-culture=en"); + } + + [Fact] + public async Task Should_Handle_Null_ReturnUrl() + { + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en"), null!); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBeNull(); + } + + [Fact] + public async Task Should_Handle_Empty_ReturnUrl() + { + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en"), ""); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe(""); + } + + [Fact] + public async Task Should_Not_Replace_When_CurrentCulture_Not_In_ReturnUrl() + { + // currentCulture is "fr" but returnUrl has no "/fr" segment + var context = CreateContext("fr", "en", "/about"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/about"); + } + + [Fact] + public async Task Should_Handle_Same_Culture_Switch() + { + // Switching to the same culture — no change + var context = CreateContext("en", "en", "/en/about"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en/about"); + } + + [Fact] + public async Task Should_Replace_Case_Insensitive() + { + var context = CreateContext("zh-hans", "en", "/zh-Hans/about"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/en/about"); + } + + [Fact] + public async Task Should_Handle_Whitespace_ReturnUrl() + { + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en"), " "); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe(" "); + } + + [Fact] + public async Task Should_Prefer_CurrentCulture_Over_RouteData() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues["culture"] = "fr"; + var context = new QueryStringCultureReplacementContext( + httpContext, new RequestCulture("en"), "/tr/about", currentCulture: "tr"); + await _replacement.ReplaceAsync(context); + // Should use "tr" from CurrentCulture, not "fr" from RouteData + context.ReturnUrl.ShouldBe("/en/about"); + } + + [Fact] + public async Task Should_Only_Replace_Query_String_When_No_Route_Culture() + { + // No currentCulture, no RouteData — only query string replacement + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en", "en"), "/?culture=tr&ui-culture=tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/?culture=en&ui-culture=en"); + } + + [Fact] + public async Task Should_Replace_Culture_When_Only_Culture_Param_Present() + { + // culture= and ui-culture= are now handled independently + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en"), "/?culture=tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/?culture=en"); + } + + [Fact] + public async Task Should_Replace_UiCulture_When_Only_UiCulture_Param_Present() + { + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), new RequestCulture("en"), "/?ui-culture=tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/?ui-culture=en"); + } + + [Fact] + public async Task Should_Support_Numeric_Region_Culture_Tag() + { + // es-419 (Latin America Spanish) contains a digit — previously the regex + // [A-Za-z-]+ would not match it, leaving the query string unreplaced. + var context = new QueryStringCultureReplacementContext( + new DefaultHttpContext(), + new RequestCulture("es-419", "es-419"), + "/home?culture=tr&ui-culture=tr"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/home?culture=es-419&ui-culture=es-419"); + } + + [Fact] + public async Task Should_Replace_Only_First_Culture_Occurrence_In_Path() + { + // /en/products/en/details — the second "/en" is part of the path content, + // not a culture prefix, and must not be replaced. + var context = CreateContext("en", "tr", "/en/products/en/details"); + await _replacement.ReplaceAsync(context); + context.ReturnUrl.ShouldBe("/tr/products/en/details"); + } + + private static QueryStringCultureReplacementContext CreateContext( + string currentCulture, string targetCulture, string returnUrl) + { + return new QueryStringCultureReplacementContext( + new DefaultHttpContext(), + new RequestCulture(targetCulture), + returnUrl, + currentCulture); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureApiRouting_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureApiRouting_Tests.cs new file mode 100644 index 0000000000..84f16e487c --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureApiRouting_Tests.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.AspNetCore.App; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class RouteBasedCultureApiRouting_Tests : AspNetCoreMvcTestBase +{ + [Fact] + public async Task Api_Route_Should_Not_Be_Intercepted_By_Culture_Route() + { + var response = await GetResponseAsync("api/json-result-test/json-result-action"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Content.Headers.ContentType!.MediaType.ShouldBe("application/json"); + } + + [Fact] + public async Task Controller_Should_Work_With_Culture_Prefix() + { + var result = await GetResponseAsStringAsync( + "/tr" + GetUrl(nameof(SimpleController.Index))); + result.ShouldBe("Index-Result"); + } + + [Fact] + public async Task Controller_Should_Work_Without_Culture_Prefix() + { + var result = await GetResponseAsStringAsync( + GetUrl(nameof(SimpleController.Index))); + result.ShouldBe("Index-Result"); + } + + [Fact] + public async Task RazorPage_Should_Work_With_Culture_Prefix() + { + var response = await GetResponseAsync("/tr/Auditing/AuditTestPage"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task RazorPage_Should_Work_Without_Culture_Prefix() + { + var response = await GetResponseAsync("/Auditing/AuditTestPage"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureNavigationHelper_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureNavigationHelper_Tests.cs new file mode 100644 index 0000000000..08706b3179 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureNavigationHelper_Tests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Shouldly; +using Volo.Abp.AspNetCore.Components.WebAssembly; +using Volo.Abp.Localization; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class RouteBasedCultureNavigationHelper_Tests +{ + private static readonly IEnumerable AllLanguages = new[] + { + new LanguageInfo("en"), + new LanguageInfo("tr"), + new LanguageInfo("zh-Hans"), + }; + + private readonly RouteBasedCultureNavigationHelper _helper = new(); + + [Fact] + public async Task Should_Replace_Culture_In_Simple_Path() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr/home"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/en/home"); + } + + [Fact] + public async Task Should_Replace_Culture_When_No_Path_After_Culture() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/en"); + } + + [Fact] + public async Task Should_Replace_Culture_When_Query_String_Follows_Culture_Directly() + { + // Regression: "tr?x=1" was being treated as a single segment "tr?x=1" + // instead of culture="tr" + suffix="?x=1". + var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr?x=1"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/en?x=1"); + } + + [Fact] + public async Task Should_Replace_Culture_When_Fragment_Follows_Culture_Directly() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr#section"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("en"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/en#section"); + } + + [Fact] + public async Task Should_Replace_Culture_Preserving_Path_Query_And_Fragment() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr/about?ref=main#top"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("zh-Hans"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/zh-Hans/about?ref=main#top"); + } + + [Fact] + public async Task Should_Prepend_Culture_When_No_Existing_Culture_Prefix() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/identity/users"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("zh-Hans"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/zh-Hans/identity/users"); + } + + [Fact] + public async Task Should_Prepend_Culture_When_At_Root() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("tr"), AllLanguages); + nav.LastNavigatedUri.ShouldBe("https://example.com/tr/"); + } + + [Fact] + public async Task Should_Not_Navigate_When_Target_Culture_Matches_Current() + { + var nav = new TestNavigationManager("https://example.com/", "https://example.com/tr/home"); + await _helper.NavigateToNewCultureAsync(nav, new LanguageInfo("tr"), AllLanguages); + // Already on /tr/home — no navigation should occur + nav.LastNavigatedUri.ShouldBeNull(); + } + + private sealed class TestNavigationManager : NavigationManager + { + public string? LastNavigatedUri { get; private set; } + + public TestNavigationManager(string baseUri, string uri) + { + Initialize(baseUri, uri); + } + + protected override void NavigateToCore(string uri, bool forceLoad) + { + LastNavigatedUri = uri; + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureUrlHelper_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureUrlHelper_Tests.cs new file mode 100644 index 0000000000..b2e8ca129a --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Localization/RouteBasedCultureUrlHelper_Tests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using NSubstitute; +using Shouldly; +using Volo.Abp.AspNetCore.Components.WebAssembly; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.AspNetCore.Mvc.Client; +using Volo.Abp.Localization; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +public class RouteBasedCultureUrlHelper_Tests +{ + private readonly ICachedApplicationConfigurationClient _configClient; + private readonly RouteBasedCultureUrlHelper _helper; + private readonly ApplicationConfigurationDto _config; + + public RouteBasedCultureUrlHelper_Tests() + { + _config = new ApplicationConfigurationDto + { + Localization = new ApplicationLocalizationConfigurationDto + { + UseRouteBasedCulture = true, + Languages = new List + { + new LanguageInfo("en"), + new LanguageInfo("zh-Hans"), + new LanguageInfo("tr"), + new LanguageInfo("es-419"), + } + } + }; + + _configClient = Substitute.For(); + _configClient.GetAsync().Returns(_config); + + _helper = new RouteBasedCultureUrlHelper(_configClient); + } + + [Theory] + [InlineData("https://auth-server.example.com/connect/authorize")] + [InlineData("http://example.com/login")] + public async Task Should_Not_Modify_Absolute_Urls(string url) + { + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync(url); + result.ShouldBe(url); + } + + [Fact] + public async Task Should_Not_Modify_Protocol_Relative_Url() + { + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync("//cdn.example.com/asset.js"); + result.ShouldBe("//cdn.example.com/asset.js"); + } + + [Fact] + public async Task Should_Prepend_Culture_To_Root_Relative_Url() + { + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync("/account/manage-profile"); + result.ShouldBe("/zh-Hans/account/manage-profile"); + } + + [Fact] + public async Task Should_Prepend_Culture_To_Tilde_Slash_Url() + { + using var _ = CultureScope("tr"); + var result = await _helper.PrependCulturePrefixAsync("~/account/manage-profile"); + result.ShouldBe("~/tr/account/manage-profile"); + } + + [Fact] + public async Task Should_Prepend_Culture_To_Bare_Relative_Url() + { + // Default auth URLs like "authentication/login" have no leading slash. + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync("authentication/login"); + result.ShouldBe("zh-Hans/authentication/login"); + } + + [Fact] + public async Task Should_Not_Modify_Url_When_Feature_Disabled() + { + _config.Localization.UseRouteBasedCulture = false; + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync("/home"); + result.ShouldBe("/home"); + } + + [Fact] + public async Task Should_Not_Modify_Url_When_Culture_Not_In_Language_List() + { + using var _ = CultureScope("fr"); + var result = await _helper.PrependCulturePrefixAsync("/home"); + result.ShouldBe("/home"); + } + + [Fact] + public async Task Should_Return_Empty_String_Unchanged() + { + var result = await _helper.PrependCulturePrefixAsync(string.Empty); + result.ShouldBe(string.Empty); + } + + [Fact] + public async Task Should_Support_Numeric_Region_Culture_Tag() + { + using var _ = CultureScope("es-419"); + var result = await _helper.PrependCulturePrefixAsync("/home"); + result.ShouldBe("/es-419/home"); + } + + [Fact] + public async Task Should_Be_Idempotent_On_Root_Relative_Url() + { + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync("/zh-Hans/account/manage-profile"); + result.ShouldBe("/zh-Hans/account/manage-profile"); + } + + [Fact] + public async Task Should_Be_Idempotent_On_Tilde_Slash_Url() + { + using var _ = CultureScope("tr"); + var result = await _helper.PrependCulturePrefixAsync("~/tr/account/manage-profile"); + result.ShouldBe("~/tr/account/manage-profile"); + } + + [Fact] + public async Task Should_Be_Idempotent_On_Bare_Relative_Url() + { + using var _ = CultureScope("zh-Hans"); + var result = await _helper.PrependCulturePrefixAsync("zh-Hans/authentication/login"); + result.ShouldBe("zh-Hans/authentication/login"); + } + + private static IDisposable CultureScope(string cultureName) + { + var previous = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = new CultureInfo(cultureName); + return new DelegateDisposable(() => CultureInfo.CurrentCulture = previous); + } + + private sealed class DelegateDisposable : IDisposable + { + private readonly System.Action _onDispose; + public DelegateDisposable(System.Action onDispose) => _onDispose = onDispose; + public void Dispose() => _onDispose(); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Localization/RouteBasedCultureTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Localization/RouteBasedCultureTestModule.cs new file mode 100644 index 0000000000..31d6dc2bb6 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Localization/RouteBasedCultureTestModule.cs @@ -0,0 +1,53 @@ +using System.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RequestLocalization; +using Microsoft.AspNetCore.Routing; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Localization; + +[DependsOn(typeof(AbpAspNetCoreTestModule))] +public class RouteBasedCultureTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.UseRouteBasedCulture = true; + }); + + Configure(options => + { + options.Languages.Add(new LanguageInfo("en", "en", "English")); + options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe")); + }); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + app.UseRouting(); + app.UseAbpRequestLocalization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapGet("{culture}/culture", async ctx => + { + await ctx.Response.WriteAsync(CultureInfo.CurrentCulture.Name); + }); + + endpoints.MapGet("culture", async ctx => + { + await ctx.Response.WriteAsync(CultureInfo.CurrentCulture.Name); + }); + + endpoints.MapGet("api/data", async ctx => + { + await ctx.Response.WriteAsync(CultureInfo.CurrentCulture.Name); + }); + }); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Localization/RouteBasedCulture_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Localization/RouteBasedCulture_Tests.cs new file mode 100644 index 0000000000..ac3be73275 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Localization/RouteBasedCulture_Tests.cs @@ -0,0 +1,75 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Xunit; + +namespace Volo.Abp.AspNetCore.Localization; + +public class RouteBasedCulture_Tests : IAsyncLifetime +{ + private WebApplication _app; + private HttpClient _client; + + public async Task InitializeAsync() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + builder.Host.UseAutofac(); + await builder.AddApplicationAsync(); + _app = builder.Build(); + await _app.InitializeApplicationAsync(); + await _app.StartAsync(); + _client = ((IHost)_app).GetTestClient(); + } + + public async Task DisposeAsync() + { + _client?.Dispose(); + if (_app != null) + { + await _app.StopAsync(); + await _app.DisposeAsync(); + } + } + + [Fact] + public async Task RouteBasedCulture_SetsCultureCorrectly() + { + var response = await _client!.GetStringAsync("/tr/culture"); + response.ShouldBe("tr"); + } + + [Fact] + public async Task RouteBasedCulture_SetsCookieOnResponse() + { + var response = await _client!.GetAsync("/tr/culture"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + + response.Headers.Contains("Set-Cookie").ShouldBeTrue(); + var cookieValue = string.Join(";", response.Headers.GetValues("Set-Cookie")); + cookieValue.ShouldContain(CookieRequestCultureProvider.DefaultCookieName); + cookieValue.ShouldContain("tr"); + } + + [Fact] + public async Task RouteBasedCulture_InvalidCultureCodeFallsThrough() + { + // "xyz1234" is not a valid culture - should fall through to the default culture "en" + var response = await _client!.GetStringAsync("/xyz1234/culture"); + response.ShouldBe("en"); + } + + [Fact] + public async Task RouteBasedCulture_ApiRoutesNotAffected() + { + // /api/data has no {culture} prefix route - falls through to the default culture "en" + var response = await _client!.GetStringAsync("/api/data"); + response.ShouldBe("en"); + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs index 0035cfbe36..48c4c6948f 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs @@ -29,10 +29,22 @@ public class AbpBlobStoringOptions_Tests : AbpBlobStoringTestBase } [Fact] - public void Should_Fallback_To_Default_Configuration_If_Not_Specialized() + public void Should_Fallback_To_Default_ProviderType_When_Not_Explicitly_Configured() { var config = _configurationProvider.Get(); config.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); + config.IsMultiTenant.ShouldBeFalse(); config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault"); } + + [Fact] + public void Should_Resolve_Fallback_Chain_Through_Configuration_Provider() + { + var testContainer1Config = _configurationProvider.Get(); + testContainer1Config.IsMultiTenant.ShouldBeTrue(); + + var testContainer3Config = _configurationProvider.Get(); + testContainer3Config.IsMultiTenant.ShouldBeFalse(); + testContainer3Config.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); + } } diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs index a6746a9480..93523a8c88 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs @@ -36,6 +36,10 @@ public class AbpBlobStoringTestModule : AbpModule { container.SetConfiguration("TestConfig2", "TestValue2"); container.ProviderType = typeof(FakeBlobProvider2); + }) + .Configure(container => + { + container.IsMultiTenant = false; }); }); } diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs new file mode 100644 index 0000000000..f47faa1f25 --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Shouldly; +using Volo.Abp.BlobStoring.Fakes; +using Xunit; + +namespace Volo.Abp.BlobStoring; + +public class BlobContainerConfiguration_Tests +{ + [Fact] + public void Should_Override_Default_Container_ProviderType_When_Named_Container_Has_Its_Own() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.ProviderType = typeof(FakeBlobProvider1); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + namedConfig.ProviderType = typeof(FakeBlobProvider2); + + namedConfig.ProviderType.ShouldBe(typeof(FakeBlobProvider2)); + defaultConfig.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); + } + + [Fact] + public void Should_Inherit_Default_Container_NamingNormalizers_When_Provider_Also_Inherited() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.ProviderType = typeof(FakeBlobProvider1); + defaultConfig.NamingNormalizers.Add(); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + + namedConfig.GetEffectiveNamingNormalizers().ShouldContain(typeof(FakeNamingNormalizer)); + namedConfig.NamingNormalizers.ShouldBeEmpty(); + } + + [Fact] + public void Should_Not_Inherit_NamingNormalizers_When_Named_Container_Has_Its_Own_ProviderType() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.ProviderType = typeof(FakeBlobProvider1); + defaultConfig.NamingNormalizers.Add(); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + namedConfig.ProviderType = typeof(FakeBlobProvider2); + + namedConfig.GetEffectiveNamingNormalizers().ShouldBeEmpty(); + } + + [Fact] + public void Should_Override_Default_Container_NamingNormalizers_When_Named_Container_Has_Its_Own() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.NamingNormalizers.Add(); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + namedConfig.NamingNormalizers.Add(); + + var effective = namedConfig.GetEffectiveNamingNormalizers().ToList(); + effective.ShouldContain(typeof(AnotherFakeNamingNormalizer)); + effective.ShouldNotContain(typeof(FakeNamingNormalizer)); + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs new file mode 100644 index 0000000000..60e53b5c2a --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.BlobStoring.Fakes; + +public class AnotherFakeNamingNormalizer : IBlobNamingNormalizer +{ + public string NormalizeContainerName(string containerName) + { + return containerName; + } + + public string NormalizeBlobName(string blobName) + { + return blobName; + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs new file mode 100644 index 0000000000..a5a32a0d6d --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.BlobStoring.Fakes; + +public class FakeNamingNormalizer : IBlobNamingNormalizer +{ + public string NormalizeContainerName(string containerName) + { + return containerName; + } + + public string NormalizeBlobName(string blobName) + { + return blobName; + } +} diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs new file mode 100644 index 0000000000..32e60e9605 --- /dev/null +++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectBuilding/RemoteServiceExceptionHandler_Tests.cs @@ -0,0 +1,193 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Cli.ProjectBuilding; +using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; +using Xunit; + +namespace Volo.Abp.Cli.ProjectBuilding; + +public class RemoteServiceExceptionHandler_Tests +{ + private readonly RemoteServiceExceptionHandler _handler; + + public RemoteServiceExceptionHandler_Tests() + { + var jsonSerializer = new AbpSystemTextJsonSerializer( + Microsoft.Extensions.Options.Options.Create(new AbpSystemTextJsonSerializerOptions()) + ); + _handler = new RemoteServiceExceptionHandler(jsonSerializer); + } + + [Fact] + public async Task EnsureSuccessfulHttpResponseAsync_Should_Not_Throw_On_Success() + { + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}") + }; + + await _handler.EnsureSuccessfulHttpResponseAsync(response); + } + + [Fact] + public async Task EnsureSuccessfulHttpResponseAsync_Should_Not_Throw_When_Response_Is_Null() + { + await _handler.EnsureSuccessfulHttpResponseAsync(null); + } + + [Fact] + public async Task Should_Wrap_Html_Body_Without_Json_Parse_Exception() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + ReasonPhrase = "Forbidden", + Content = new StringContent("Forbidden", System.Text.Encoding.UTF8, "text/html") + }; + + var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); + + exception.Message.ShouldContain("403-Forbidden"); + exception.Message.ShouldNotContain("invalid start of a value"); + } + + [Fact] + public async Task Should_Surface_Server_Error_Message_When_Body_Is_Valid_Json() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + ReasonPhrase = "Forbidden", + Content = new StringContent( + "{\"error\":{\"code\":\"LicenseExpired\",\"message\":\"Your ABP license has expired.\"}}", + System.Text.Encoding.UTF8, + "application/json") + }; + + var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); + + exception.Message.ShouldContain("403-Forbidden"); + exception.Message.ShouldContain("LicenseExpired"); + exception.Message.ShouldContain("Your ABP license has expired."); + } + + [Fact] + public async Task Should_Surface_Server_Error_Message_For_5xx_With_Json_Body() + { + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) + { + ReasonPhrase = "Internal Server Error", + Content = new StringContent( + "{\"error\":{\"code\":\"InternalError\",\"message\":\"Database connection failed\"}}", + System.Text.Encoding.UTF8, + "application/json") + }; + + var exception = await Should.ThrowAsync(() => _handler.EnsureSuccessfulHttpResponseAsync(response)); + + exception.Message.ShouldContain("500-Internal Server Error"); + exception.Message.ShouldContain("InternalError"); + exception.Message.ShouldContain("Database connection failed"); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Propagate_OperationCanceledException() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new CanceledStringContent() + }; + + await Should.ThrowAsync( + () => _handler.GetAbpRemoteServiceErrorAsync(response) + ); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Html_Body() + { + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent("", System.Text.Encoding.UTF8, "text/html") + }; + + var result = await _handler.GetAbpRemoteServiceErrorAsync(response); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Return_Null_For_Newtonsoft_JsonException() + { + var handler = new RemoteServiceExceptionHandler( + new ThrowingJsonSerializer(new Newtonsoft.Json.JsonException("Invalid JSON")) + ); + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent("{}") + }; + + var result = await handler.GetAbpRemoteServiceErrorAsync(response); + + result.ShouldBeNull(); + } + + [Fact] + public async Task GetAbpRemoteServiceErrorAsync_Should_Propagate_Non_Json_Exceptions() + { + var handler = new RemoteServiceExceptionHandler( + new ThrowingJsonSerializer(new InvalidOperationException("Unexpected serializer failure")) + ); + var response = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + Content = new StringContent("{}") + }; + + var exception = await Should.ThrowAsync( + () => handler.GetAbpRemoteServiceErrorAsync(response) + ); + + exception.Message.ShouldBe("Unexpected serializer failure"); + } + + private class CanceledStringContent : HttpContent + { + protected override Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext context) + { + throw new OperationCanceledException(); + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + } + + private class ThrowingJsonSerializer : IJsonSerializer + { + private readonly Exception _exception; + + public ThrowingJsonSerializer(Exception exception) + { + _exception = exception; + } + + public string Serialize(object obj, bool camelCase = true, bool indented = false) + { + throw new NotImplementedException(); + } + + public T Deserialize(string jsonString, bool camelCase = true) + { + throw _exception; + } + + public object Deserialize(Type type, string jsonString, bool camelCase = true) + { + throw _exception; + } + } +} diff --git a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs new file mode 100644 index 0000000000..dbaabd3a63 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/Logging/AbpLoggerExtensions_Tests.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Shouldly; +using Xunit; + +namespace Microsoft.Extensions.Logging; + +public class AbpLoggerExtensions_Tests +{ + [Fact] + public void LogException_Should_Format_String_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Name"] = "John"; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("Name = John"); + } + + [Fact] + public void LogException_Should_Format_Primitive_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Count"] = 42; + exception.Data["IsActive"] = true; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("Count = 42"); + logger.LastLoggedMessage.ShouldContain("IsActive = True"); + } + + [Fact] + public void LogException_Should_Format_Complex_Object_As_Json() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Details"] = new Dictionary + { + { "RuleName", "FixedWindow" }, + { "Limit", 10 } + }; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("\"RuleName\":\"FixedWindow\""); + logger.LastLoggedMessage.ShouldContain("\"Limit\":10"); + } + + [Fact] + public void LogException_Should_Format_List_Of_Complex_Objects_As_Json() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["RuleDetails"] = new List> + { + new() { { "RuleName", "FixedWindow" }, { "Limit", 10 } }, + new() { { "RuleName", "SlidingWindow" }, { "Limit", 20 } } + }; + + logger.LogException(exception); + + var message = logger.LastLoggedMessage; + message.ShouldNotContain("System.Collections.Generic.List"); + message.ShouldContain("FixedWindow"); + message.ShouldContain("SlidingWindow"); + } + + [Fact] + public void LogException_Should_Format_Null_Data_As_Empty() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["NullKey"] = null; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("NullKey = "); + } + + [Fact] + public void LogException_Should_Format_Enum_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Level"] = LogLevel.Warning; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain("Level = Warning"); + } + + [Fact] + public void LogException_Should_Format_DateTime_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var now = new DateTime(2025, 1, 15, 10, 30, 0); + exception.Data["Timestamp"] = now; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain($"Timestamp = {now.ToString(CultureInfo.CurrentCulture)}"); + } + + [Fact] + public void LogException_Should_Format_Guid_Data() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var id = Guid.NewGuid(); + exception.Data["Id"] = id; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldContain($"Id = {id}"); + } + + [Fact] + public void LogException_Should_Format_Anonymous_Object_As_Json() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + exception.Data["Info"] = new { Name = "Test", Value = 123 }; + + logger.LogException(exception); + + var message = logger.LastLoggedMessage; + message.ShouldContain("\"Name\":\"Test\""); + message.ShouldContain("\"Value\":123"); + } + + [Fact] + public void LogException_Should_Fallback_To_ToString_For_Non_Serializable_Object() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var selfRef = new SelfReferencingObject { Name = "Loop" }; + selfRef.Self = selfRef; + exception.Data["BadObject"] = selfRef; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldNotBeNull(); + logger.LastLoggedMessage.ShouldContain("BadObject = SelfRef:Loop"); + logger.LastLoggedMessage.ShouldNotContain("\"Name\""); + } + + [Fact] + public void LogException_Should_Truncate_Large_Json_Output() + { + var logger = new FakeLogger(); + var exception = new Exception("test"); + var largeList = new List>(); + for (var i = 0; i < 500; i++) + { + largeList.Add(new Dictionary + { + { "Key", new string('x', 100) } + }); + } + exception.Data["LargeData"] = largeList; + + logger.LogException(exception); + + logger.LastLoggedMessage.ShouldNotBeNull(); + logger.LastLoggedMessage.ShouldContain("...(truncated)"); + } + + private class SelfReferencingObject + { + public string Name { get; set; } = default!; + public override string ToString() => $"SelfRef:{Name}"; + public SelfReferencingObject? Self { get; set; } + } + + private class FakeLogger : ILogger + { + public string? LastLoggedMessage { get; private set; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + LastLoggedMessage = formatter(state, exception); + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public IDisposable BeginScope(TState state) where TState : notnull => NullDisposable.Instance; + + private class NullDisposable : IDisposable + { + public static readonly NullDisposable Instance = new(); + public void Dispose() { } + } + } +} diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_AndCombineResults_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_AndCombineResults_Tests.cs new file mode 100644 index 0000000000..6fdc54b940 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/SimpleStateChecking/SimpleStateChecker_AndCombineResults_Tests.cs @@ -0,0 +1,101 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Xunit; + +namespace Volo.Abp.SimpleStateChecking; + +public class SimpleStateChecker_AndCombineResults_Tests : SimpleStateCheckerTestBase +{ + [Fact] + public async Task False_Result_From_One_Batch_Checker_Should_Not_Be_Overwritten_By_Later_True_Checker() + { + // Regression test for the bug fixed in SimpleStateCheckerManager.IsEnabledAsync: + // when a state had multiple batch checkers the manager used to OVERWRITE + // result[x.Key] on each checker, so a later "true" silently masked an earlier "false". + // The fix AND-combines results. + // + // To make sure we actually hit the second batch checker (and not the early-return + // `result.Values.All(x => !x)` short-circuit), we use two states: + // - state1: [falseChecker, trueChecker] - the target case + // - state2: [trueChecker] - keeps result.Values not all-false + // so the loop continues to the next checker + + var falseChecker = new AlwaysFalseBatchStateChecker(); + var trueChecker = new AlwaysTrueBatchStateChecker(); + + var state1 = new MyStateEntity(); + state1.AddSimpleStateChecker(falseChecker); + state1.AddSimpleStateChecker(trueChecker); + + var state2 = new MyStateEntity(); + state2.AddSimpleStateChecker(trueChecker); + + var result = await SimpleStateCheckerManager.IsEnabledAsync(new[] { state1, state2 }); + + result[state1].ShouldBeFalse(); // before the fix this was True (bug) + result[state2].ShouldBeTrue(); + } + + [Fact] + public async Task True_Result_From_One_Batch_Checker_Should_Be_AND_Combined_With_Later_False_Checker() + { + // Reverse order: true first then false. Add a second always-true state to keep + // result.Values not all-false after both checkers ran. + var trueChecker = new AlwaysTrueBatchStateChecker(); + var falseChecker = new AlwaysFalseBatchStateChecker(); + + var state1 = new MyStateEntity(); + state1.AddSimpleStateChecker(trueChecker); + state1.AddSimpleStateChecker(falseChecker); + + var state2 = new MyStateEntity(); + state2.AddSimpleStateChecker(trueChecker); + + var result = await SimpleStateCheckerManager.IsEnabledAsync(new[] { state1, state2 }); + + result[state1].ShouldBeFalse(); + result[state2].ShouldBeTrue(); + } + + [Fact] + public async Task All_True_Batch_Checkers_Should_Produce_True() + { + var trueChecker1 = new AlwaysTrueBatchStateChecker(); + var trueChecker2 = new AlwaysTrueBatchStateChecker(); + + var state = new MyStateEntity(); + state.AddSimpleStateChecker(trueChecker1); + state.AddSimpleStateChecker(trueChecker2); + + var result = await SimpleStateCheckerManager.IsEnabledAsync(new[] { state }); + + result[state].ShouldBeTrue(); + } + + public class AlwaysFalseBatchStateChecker : SimpleBatchStateCheckerBase, ITransientDependency + { + public override Task> IsEnabledAsync(SimpleBatchStateCheckerContext context) + { + var result = new SimpleStateCheckerResult(context.States); + foreach (var x in result) + { + result[x.Key] = false; + } + return Task.FromResult(result); + } + } + + public class AlwaysTrueBatchStateChecker : SimpleBatchStateCheckerBase, ITransientDependency + { + public override Task> IsEnabledAsync(SimpleBatchStateCheckerContext context) + { + var result = new SimpleStateCheckerResult(context.States); + foreach (var x in result) + { + result[x.Key] = true; + } + return Task.FromResult(result); + } + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg new file mode 100644 index 0000000000..64c1552e37 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj new file mode 100644 index 0000000000..1836c9133a --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + net10.0 + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs new file mode 100644 index 0000000000..1121eaaef9 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Modularity; +using Volo.Abp.Testing; + +namespace Volo.Abp.Application; + +public abstract class AbpDddApplicationTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs new file mode 100644 index 0000000000..ef49b36d76 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Autofac; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Application; + +[DependsOn(typeof(AbpAutofacModule))] +[DependsOn(typeof(AbpDddApplicationModule))] +[DependsOn(typeof(AbpExceptionHandlingModule))] +public class AbpDddApplicationTestModule : AbpModule +{ +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs new file mode 100644 index 0000000000..0806a1d824 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Validation; +using Xunit; + +namespace Volo.Abp.Application.Services; + +public class AbpDynamicSortingGuard_Tests : AbpDddApplicationTestBase +{ + private readonly IQueryable _users; + + public AbpDynamicSortingGuard_Tests() + { + _users = new List + { + new() { Name = "alice", Age = 30, PasswordHash = "AQAAhash_alice", Tenant = new FakeTenant { Name = "acme" } }, + new() { Name = "bob", Age = 25, PasswordHash = "BQAAhash_bob", Tenant = new FakeTenant { Name = "beta" } }, + new() { Name = "carl", Age = 40, PasswordHash = "CQAAhash_carl", Tenant = new FakeTenant { Name = "corp" } }, + }.AsQueryable(); + } + + [Theory] + [InlineData("Name")] + [InlineData("Name desc")] + [InlineData("Name asc, PasswordHash desc")] + [InlineData("Age desc")] // value type → EF/Dynamic.Core wraps selector in Convert(MemberAccess, object) + [InlineData("Tenant.Name")] // chained MemberAccess + [InlineData("Tenant.Name desc, Age asc")] // mixed chained + value-type, multi-column + [InlineData("Name.Length desc")] // Length is a property getter, not a method call + public void Should_Accept_Plain_Property_Sorting(string sorting) + { + Should.NotThrow(() => _users.OrderBy(sorting).ToList()); + } + + [Theory] + [InlineData("PasswordHash.Substring(0,1) desc")] + [InlineData("PasswordHash.StartsWith(\"A\") desc")] + [InlineData("PasswordHash.Contains(\"hash\") desc")] + [InlineData("Name asc, PasswordHash.Substring(0,1) desc")] // multi-column with attack in 2nd + public void Should_Reject_Method_Call_On_Property(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()) + .Message.ShouldBe("Sorting expression is not supported."); + } + + [Theory] + [InlineData("(PasswordHash == \"AQAA\") desc")] + [InlineData("(PasswordHash > \"M\") desc")] + [InlineData("(PasswordHash != \"AQAA\") asc")] + public void Should_Reject_Binary_Expressions(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()) + .Message.ShouldBe("Sorting expression is not supported."); + } + + [Fact] + public void Should_Not_Affect_Where_Expressions() + { + // The guard only inspects Queryable.OrderBy/ThenBy nodes. Where with the same + // sub-expression is left alone (Where is a separate vulnerability class). + Should.NotThrow(() => _users.Where("PasswordHash.StartsWith(\"A\")").ToList()); + } + + [Fact] + public void Install_Chains_Existing_QueryOptimizer() + { + // Reset guard state so Install() actually re-installs and exercises the + // `previous != null ? previous(expression) : expression` branch. + AbpDynamicSortingGuard.Reset(); + try + { + var preExistingFired = false; + ExtensibilityPoint.QueryOptimizer = e => + { + preExistingFired = true; + return e; + }; + + AbpDynamicSortingGuard.Install(); + + _users.OrderBy("Name").ToList(); + preExistingFired.ShouldBeTrue(); + } + finally + { + // Leave the AppDomain with a single-layer guard. Reset() clears whatever + // we wrapped in this test; Install() then puts a fresh guard on top of + // an empty QueryOptimizer — never double-wraps an existing guard. + AbpDynamicSortingGuard.Reset(); + AbpDynamicSortingGuard.Install(); + } + } + + [Fact] + public void Install_Reinstalls_When_QueryOptimizer_Was_Replaced() + { + // Simulate someone (e.g. a test teardown, another module) overwriting our + // optimizer. The next Install() must detect the mismatch and wrap again. + try + { + ExtensibilityPoint.QueryOptimizer = e => e; // not our wrapper + + AbpDynamicSortingGuard.Install(); + + // Guard must be active again — attack payload still gets rejected. + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(0,1) desc").ToList()); + } + finally + { + AbpDynamicSortingGuard.Reset(); + AbpDynamicSortingGuard.Install(); + } + } + + [Fact] + public void Should_Validate_Explicit_ThenBy_Selectors() + { + // ThenBy(string) is called directly by some app services, not only via multi-column OrderBy. + // The guard must reject unsafe expressions in ThenBy selectors as well. + Should.Throw(() => + _users.OrderBy("Name").ThenBy("PasswordHash.Substring(0,1)").ToList()); + } + + [Fact] + public void Should_Validate_ThenBy_With_Descending_Modifier() + { + // Dynamic.Core encodes direction in the sorting string (no separate ThenByDescending overload); + // make sure the guard still inspects the selector when desc is on the ThenBy column. + Should.Throw(() => + _users.OrderBy("Name").ThenBy("PasswordHash.Substring(0,1) desc").ToList()); + } + + [Theory] + [InlineData("(PasswordHash.Substring(0,1) == \"K\") desc")] // method + binary in one selector + [InlineData("(Name.Length > 5) desc")] // property getter + binary + [InlineData("(Age + 10) desc")] // binary arithmetic + [InlineData("(Age * Age) desc")] // binary arithmetic + [InlineData("(-Age) desc")] // unary negation wraps something; only Negate is allowed via base.VisitUnary, but the operand is a plain member so this passes — keep as accept check + public void Should_Reject_Compound_Or_Arithmetic_Expressions(string sorting) + { + // Note: the last case `(-Age) desc` actually passes — Unary(Negate(MemberAccess)) has no rejecting node. + // Filter accordingly. + if (sorting.Contains("(-")) + { + Should.NotThrow(() => _users.OrderBy(sorting).ToList()); + } + else + { + Should.Throw(() => _users.OrderBy(sorting).ToList()) + .Message.ShouldBe("Sorting expression is not supported."); + } + } + + [Theory] + [InlineData("Name.ToUpper().Substring(0,1) desc")] // chained method calls + [InlineData("PasswordHash.Substring(0,1).Length desc")] // method then property + public void Should_Reject_Chained_Method_Calls(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()); + } + + [Fact] + public void Should_Not_Interfere_With_DynamicCore_Validation_For_Empty_Or_Null_Sorting() + { + // Dynamic.Core itself rejects null / empty / whitespace ordering with ArgumentException + // before the QueryOptimizer is invoked. The guard must not turn those into + // AbpValidationException by accident. + Should.NotThrow(() => + { + try { _users.OrderBy((string)null!).ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType(); } + try { _users.OrderBy("").ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType(); } + try { _users.OrderBy(" ").ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType(); } + }); + } + + [Fact] + public void Should_Validate_OrderBy_With_Args_Parameter() + { + // Dynamic.Core's OrderBy supports parameterized expressions through `@0`, `@1`, ... and args. + // The guard must validate the expanded selector, not the literal sorting string. + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(@0, 1) desc", 0).ToList()); + } + + [Fact] + public async Task Install_Is_Thread_Safe_Under_Concurrent_Calls() + { + // 50 concurrent Install() callers must not deadlock, throw, or leave the guard in a torn state. + AbpDynamicSortingGuard.Reset(); + try + { + await Task.WhenAll(Enumerable.Range(0, 50) + .Select(_ => Task.Run(AbpDynamicSortingGuard.Install))); + + // After concurrent installs, the guard must still reject attack payloads. + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(0,1) desc").ToList()); + } + finally + { + AbpDynamicSortingGuard.Reset(); + AbpDynamicSortingGuard.Install(); + } + } + + [Theory] + [InlineData("tr-TR")] // Turkish: uppercase 'I' is dotless, common locale pitfall for string ops + [InlineData("de-DE")] + [InlineData("ja-JP")] + [InlineData("en-US")] + public void Should_Reject_Method_Calls_Regardless_Of_CurrentCulture(string culture) + { + var saved = System.Globalization.CultureInfo.CurrentCulture; + try + { + System.Globalization.CultureInfo.CurrentCulture = new System.Globalization.CultureInfo(culture); + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(0,1) desc").ToList()); + } + finally + { + System.Globalization.CultureInfo.CurrentCulture = saved; + } + } + + [Theory] + [InlineData("Name DESC")] // upper-case direction modifier + [InlineData("name desc")] // lower-case property name (Dynamic.Core default case-insensitive lookup) + [InlineData("NAME ASC")] + public void Should_Accept_Property_Names_Regardless_Of_Casing(string sorting) + { + Should.NotThrow(() => _users.OrderBy(sorting).ToList()); + } + + [Theory] + [InlineData("passwordhash.substring(0,1) desc")] // lower-case attack + [InlineData("PASSWORDHASH.SUBSTRING(0,1) DESC")] // upper-case attack — method call detection cannot rely on case + public void Should_Reject_Method_Calls_Regardless_Of_Casing(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()); + } + + [Fact] + public void Should_Allow_Inherited_Property_Sorting() + { + var children = new List + { + new() { Name = "alpha", Age = 30, ExtraField = "x" }, + new() { Name = "beta", Age = 25, ExtraField = "y" }, + }.AsQueryable(); + + Should.NotThrow(() => children.OrderBy("Name desc, ExtraField asc").ToList()); + } + + private class FakeUser + { + public string Name { get; set; } = ""; + public int Age { get; set; } + public string PasswordHash { get; set; } = ""; + public FakeTenant Tenant { get; set; } = new(); + } + + private class FakeChildUser : FakeUser + { + public string ExtraField { get; set; } = ""; + } + + private class FakeTenant + { + public string Name { get; set; } = ""; + } +} diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/ClientProxyResponseDisposal_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/ClientProxyResponseDisposal_Tests.cs new file mode 100644 index 0000000000..810781fe15 --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/ClientProxyResponseDisposal_Tests.cs @@ -0,0 +1,157 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Shouldly; +using Volo.Abp.AspNetCore.TestBase; +using Volo.Abp.Http.Client; +using Volo.Abp.Http.Client.Proxying; +using Xunit; + +namespace Volo.Abp.Http.DynamicProxying; + +public class ClientProxyResponseDisposal_Tests : AbpHttpClientTestBase +{ + private readonly IRegularTestController _controller; + private readonly StubProxyHttpClientFactory _factory; + + public ClientProxyResponseDisposal_Tests() + { + _controller = ServiceProvider.GetRequiredService(); + _factory = (StubProxyHttpClientFactory)ServiceProvider.GetRequiredService(); + } + + protected override void ConfigureServices(IServiceCollection services) + { + services.Replace(ServiceDescriptor.Singleton()); + } + + [Fact] + public async Task Non_Abp_Error_Response_Should_Dispose_HttpContent() + { + var content = new TrackingHttpContent("Bad Gateway from upstream"); + _factory.SetStubResponse(() => new HttpResponseMessage(HttpStatusCode.BadGateway) + { + Content = content + }); + + var exception = await Assert.ThrowsAsync( + () => _controller.IncrementValueAsync(1)); + + exception.HttpStatusCode.ShouldBe((int)HttpStatusCode.BadGateway); + content.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task Successful_Response_With_Json_Return_Should_Dispose_HttpContent() + { + var content = new TrackingHttpContent("42"); + _factory.SetStubResponse(() => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = content + }); + + var result = await _controller.IncrementValueAsync(0); + + result.ShouldBe(42); + content.Disposed.ShouldBeTrue(); + } + + [Fact] + public async Task Successful_Response_With_Void_Return_Should_Dispose_HttpContent() + { + var content = new TrackingHttpContent(string.Empty); + _factory.SetStubResponse(() => new HttpResponseMessage(HttpStatusCode.NoContent) + { + Content = content + }); + + await _controller.GetException1Async(); + + content.Disposed.ShouldBeTrue(); + } + + class StubProxyHttpClientFactory : IProxyHttpClientFactory + { + private readonly ITestServerAccessor _testServerAccessor; + + private int _count; + private Func _responseFactory; + + public StubProxyHttpClientFactory(ITestServerAccessor testServerAccessor) + { + _testServerAccessor = testServerAccessor; + } + + public void SetStubResponse(Func responseFactory) + { + _responseFactory = responseFactory; + } + + public HttpClient Create(string name) => Create(); + + public HttpClient Create() + { + if (_count++ == 0) + { + return _testServerAccessor.Server.CreateClient(); + } + + return new HttpClient(new StubHttpMessageHandler(() => + _responseFactory?.Invoke() + ?? throw new InvalidOperationException("Stub response is not configured."))) + { + BaseAddress = new Uri("http://localhost/") + }; + } + } + + class StubHttpMessageHandler : HttpMessageHandler + { + private readonly Func _responseFactory; + + public StubHttpMessageHandler(Func responseFactory) + { + _responseFactory = responseFactory; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(_responseFactory()); + } + } + + class TrackingHttpContent : HttpContent + { + private readonly byte[] _data; + + public bool Disposed { get; private set; } + + public TrackingHttpContent(string text) + { + _data = Encoding.UTF8.GetBytes(text); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + return stream.WriteAsync(_data, 0, _data.Length); + } + + protected override bool TryComputeLength(out long length) + { + length = _data.Length; + return true; + } + + protected override void Dispose(bool disposing) + { + Disposed = true; + base.Dispose(disposing); + } + } +} diff --git a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif new file mode 100644 index 0000000000..27f6a1e4dc Binary files /dev/null and b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/Files/abp.gif differ diff --git a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs index de249fb2e5..2696bcbd3b 100644 --- a/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs +++ b/framework/test/Volo.Abp.Imaging.Abstractions.Tests/Volo/Abp/Imaging/ImageFileHelper.cs @@ -19,7 +19,12 @@ public static class ImageFileHelper { return GetTestFileStream("abp.webp"); } - + + public static Stream GetGifTestFileStream() + { + return GetTestFileStream("abp.gif"); + } + private static Stream GetTestFileStream(string fileName) { var assembly = typeof(ImageFileHelper).Assembly; diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs new file mode 100644 index 0000000000..4c575d9f04 --- /dev/null +++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageCompressorTests.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Imaging; + +public class SkiaSharpImageCompressorTests : AbpImagingSkiaSharpTestBase +{ + public IImageCompressor ImageCompressor { get; } + + public SkiaSharpImageCompressorTests() + { + ImageCompressor = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.Quality = 50; + }); + + base.AfterAddApplication(services); + } + + [Fact] + public async Task Should_Compress_Jpg() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var compressedImage = await ImageCompressor.CompressAsync(jpegImage); + + compressedImage.ShouldNotBeNull(); + compressedImage.State.ShouldBe(ImageProcessState.Done); + compressedImage.Result.Length.ShouldBeLessThan(jpegImage.Length); + compressedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Compress_Png() + { + await using var pngImage = ImageFileHelper.GetPngTestFileStream(); + var compressedImage = await ImageCompressor.CompressAsync(pngImage); + + compressedImage.ShouldNotBeNull(); + + if (compressedImage.State == ImageProcessState.Done) + { + compressedImage.Result.Length.ShouldBeLessThan(pngImage.Length); + } + else + { + compressedImage.State.ShouldBe(ImageProcessState.Canceled); + compressedImage.Result.Length.ShouldBe(pngImage.Length); + } + + compressedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Compress_Webp() + { + await using var webpImage = ImageFileHelper.GetWebpTestFileStream(); + var compressedImage = await ImageCompressor.CompressAsync(webpImage); + + compressedImage.ShouldNotBeNull(); + compressedImage.State.ShouldBe(ImageProcessState.Done); + compressedImage.Result.Length.ShouldBeLessThan(webpImage.Length); + compressedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Compress_Stream_And_Byte_Array_The_Same() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var byteArr = await jpegImage.GetAllBytesAsync(); + + var compressedImage1 = await ImageCompressor.CompressAsync(jpegImage); + var compressedImage2 = await ImageCompressor.CompressAsync(byteArr); + + compressedImage1.ShouldNotBeNull(); + compressedImage1.State.ShouldBe(ImageProcessState.Done); + + compressedImage2.ShouldNotBeNull(); + compressedImage2.State.ShouldBe(ImageProcessState.Done); + + compressedImage1.Result.Length.ShouldBeLessThan(jpegImage.Length); + compressedImage2.Result.LongLength.ShouldBeLessThan(jpegImage.Length); + + compressedImage1.Result.Length.ShouldBe(compressedImage2.Result.LongLength); + + compressedImage1.Result.Dispose(); + } + + [Fact] + public async Task Should_Return_Compressed_Stream_Positioned_At_Start() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var compressedImage = await ImageCompressor.CompressAsync(jpegImage); + + compressedImage.ShouldNotBeNull(); + compressedImage.State.ShouldBe(ImageProcessState.Done); + compressedImage.Result.Position.ShouldBe(0); + + using var copy = new MemoryStream(); + await compressedImage.Result.CopyToAsync(copy); + copy.Length.ShouldBe(compressedImage.Result.Length); + + compressedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Return_Unsupported_For_Gif_Stream() + { + await using var gifImage = ImageFileHelper.GetGifTestFileStream(); + var compressedImage = await ImageCompressor.CompressAsync(gifImage); + + compressedImage.ShouldNotBeNull(); + compressedImage.State.ShouldBe(ImageProcessState.Unsupported); + compressedImage.Result.ShouldBe(gifImage); + } + + [Fact] + public async Task Should_Return_Unsupported_For_Gif_Bytes() + { + await using var gifImage = ImageFileHelper.GetGifTestFileStream(); + var bytes = await gifImage.GetAllBytesAsync(); + var compressedImage = await ImageCompressor.CompressAsync(bytes); + + compressedImage.ShouldNotBeNull(); + compressedImage.State.ShouldBe(ImageProcessState.Unsupported); + compressedImage.Result.ShouldBe(bytes); + } + + [Fact] + public async Task Should_Handle_Non_Seekable_Stream_Directly_On_Contributor() + { + var contributor = GetRequiredService(); + + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var bytes = await jpegImage.GetAllBytesAsync(); + await using var nonSeekable = new NonSeekableStream(new MemoryStream(bytes)); + + var compressedImage = await contributor.TryCompressAsync(nonSeekable, "image/jpeg"); + + compressedImage.ShouldNotBeNull(); + compressedImage.State.ShouldBeOneOf(ImageProcessState.Done, ImageProcessState.Canceled); + if (compressedImage.State == ImageProcessState.Done) + { + compressedImage.Result.ShouldNotBe(nonSeekable); + compressedImage.Result.Dispose(); + } + } + + private sealed class NonSeekableStream : Stream + { + private readonly Stream _inner; + public NonSeekableStream(Stream inner) { _inner = inner; } + public override bool CanRead => _inner.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _inner.ReadAsync(buffer, offset, count, cancellationToken); + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + protected override void Dispose(bool disposing) + { + if (disposing) _inner.Dispose(); + base.Dispose(disposing); + } + } +} diff --git a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs index 460c83bf5b..593beb506d 100644 --- a/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs +++ b/framework/test/Volo.Abp.Imaging.SkiaSharp.Tests/Volo/Abp/Imaging/SkiaSharpImageResizerTests.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; using Shouldly; +using SkiaSharp; using Xunit; namespace Volo.Abp.Imaging; @@ -72,4 +73,120 @@ public class SkiaSharpImageResizerTests : AbpImagingSkiaSharpTestBase resizedImage1.Result.Dispose(); } + + [Fact] + public async Task Should_Return_Resized_Stream_Positioned_At_Start() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(100, 100)); + + resizedImage.ShouldNotBeNull(); + resizedImage.State.ShouldBe(ImageProcessState.Done); + resizedImage.Result.Position.ShouldBe(0); + + using var copy = new MemoryStream(); + await resizedImage.Result.CopyToAsync(copy); + copy.Length.ShouldBe(resizedImage.Result.Length); + + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Return_Unsupported_For_Gif_Stream() + { + await using var gifImage = ImageFileHelper.GetGifTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(gifImage, new ImageResizeArgs(100, 100)); + + resizedImage.ShouldNotBeNull(); + resizedImage.State.ShouldBe(ImageProcessState.Unsupported); + resizedImage.Result.ShouldBe(gifImage); + } + + [Fact] + public async Task Should_Return_Unsupported_For_Gif_Bytes() + { + await using var gifImage = ImageFileHelper.GetGifTestFileStream(); + var bytes = await gifImage.GetAllBytesAsync(); + var resizedImage = await ImageResizer.ResizeAsync(bytes, new ImageResizeArgs(100, 100)); + + resizedImage.ShouldNotBeNull(); + resizedImage.State.ShouldBe(ImageProcessState.Unsupported); + resizedImage.Result.ShouldBe(bytes); + } + + [Theory] + [InlineData(ImageResizeMode.None)] + [InlineData(ImageResizeMode.Stretch)] + [InlineData(ImageResizeMode.Crop)] + [InlineData(ImageResizeMode.Pad)] + [InlineData(ImageResizeMode.BoxPad)] + [InlineData(ImageResizeMode.Default)] + public async Task Should_Produce_Exact_Target_Size_For_Fixed_Modes(ImageResizeMode mode) + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(120, 80, mode)); + + resizedImage.State.ShouldBe(ImageProcessState.Done); + using var decoded = SKBitmap.Decode(resizedImage.Result); + decoded.ShouldNotBeNull(); + decoded.Width.ShouldBe(120); + decoded.Height.ShouldBe(80); + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Produce_Bounded_Size_For_Max_Mode() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(120, 80, ImageResizeMode.Max)); + + resizedImage.State.ShouldBe(ImageProcessState.Done); + using var decoded = SKBitmap.Decode(resizedImage.Result); + decoded.Width.ShouldBeLessThanOrEqualTo(120); + decoded.Height.ShouldBeLessThanOrEqualTo(80); + (decoded.Width == 120 || decoded.Height == 80).ShouldBeTrue(); + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Max_Fit_Source_Larger_Than_Target_For_BoxPad_Mode() + { + using var src = new SKBitmap(400, 100); + using (var canvas = new SKCanvas(src)) + { + canvas.Clear(SKColors.Red); + } + using var srcImage = SKImage.FromBitmap(src); + using var srcData = srcImage.Encode(SKEncodedImageFormat.Png, 100); + using var srcStream = new MemoryStream(); + srcData.SaveTo(srcStream); + srcStream.Position = 0; + + var resizedImage = await ImageResizer.ResizeAsync(srcStream, new ImageResizeArgs(100, 100, ImageResizeMode.BoxPad)); + + resizedImage.State.ShouldBe(ImageProcessState.Done); + using var decoded = SKBitmap.Decode(resizedImage.Result); + decoded.Width.ShouldBe(100); + decoded.Height.ShouldBe(100); + + decoded.GetPixel(50, 0).Alpha.ShouldBe((byte)0); + decoded.GetPixel(50, 99).Alpha.ShouldBe((byte)0); + decoded.GetPixel(50, 50).Red.ShouldBeGreaterThan((byte)200); + + resizedImage.Result.Dispose(); + } + + [Fact] + public async Task Should_Produce_Bounded_Size_For_Min_Mode() + { + await using var jpegImage = ImageFileHelper.GetJpgTestFileStream(); + var resizedImage = await ImageResizer.ResizeAsync(jpegImage, new ImageResizeArgs(120, 80, ImageResizeMode.Min)); + + resizedImage.State.ShouldBe(ImageProcessState.Done); + using var decoded = SKBitmap.Decode(resizedImage.Result); + decoded.Width.ShouldBeGreaterThanOrEqualTo(120); + decoded.Height.ShouldBeGreaterThanOrEqualTo(80); + (decoded.Width == 120 || decoded.Height == 80).ShouldBeTrue(); + resizedImage.Result.Dispose(); + } } diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs index 32835cb8f4..d4ef509fd5 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs @@ -29,6 +29,10 @@ public class AbpLocalizationTestModule : AbpModule .Add("LocalizationTestCountryNames") .AddVirtualJson("/Volo/Abp/Localization/TestResources/Base/CountryNames"); + options.Resources + .Add("LocalizationTestFilesSplit") + .AddVirtualJson("/Volo/Abp/Localization/TestResources/FilesSplit"); + options.Resources .Add("en") .AddVirtualJson("/Volo/Abp/Localization/TestResources/Source") diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs index 189c1b8614..31bc5a8b4f 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs @@ -234,6 +234,35 @@ public class AbpLocalization_Tests : AbpIntegratedTest ls.Name == "Car" && ls.Value == "Auto"); + allStrings.ShouldContain(ls => ls.Name == "FortyTwo" && ls.Value == "Zweiundvierzig"); + + // From de_Book.json + allStrings.ShouldContain(ls => ls.Name == "Biography" && ls.Value == "Biografie"); + allStrings.ShouldContain(ls => ls.Name == "Enum:BookType.Undefined" && ls.Value == "Nicht definiert"); + } + } + [Fact] public void GetAllStrings_With_Parents() { diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs index dbc007ac02..32b1803c3d 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; using Shouldly; using Volo.Abp.DynamicProxy; @@ -90,4 +91,50 @@ public class AbpStringLocalizerFactory_Tests : AbpIntegratedTest ls.Name == "Base.Id" && ls.Value == "Id"); + allStrings.ShouldContain(ls => ls.Name == "Base.Name" && ls.Value == "Name"); + allStrings.ShouldContain(ls => ls.Name == "Book.Id" && ls.Value == "ISBN"); + allStrings.ShouldContain(ls => ls.Name == "Book.Name" && ls.Value == "Title"); + allStrings.ShouldContain(ls => ls.Name == "ThisIsRequiredValue" && ls.Value == "This is required value (this will be used)"); + } + } + + [Fact] + public void Should_Override_Value_From_Later_Split_File() + { + using (CultureHelper.Use("en")) + { + var localizer = _factory.CreateByResourceNameOrNull("LocalizationTestFilesSplit"); + localizer.ShouldNotBeNull(); + + // !en_First.json defines "This is required value" + // zen_Last.json defines "This is required value (this will be used)" + // zen_Last.json sorts after !en_First.json (ordinal: '!' < 'z'), so its value wins + localizer["ThisIsRequiredValue"].Value.ShouldBe("This is required value (this will be used)"); + } + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/JsonLocalizationDictionaryBuilder_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/JsonLocalizationDictionaryBuilder_Tests.cs new file mode 100644 index 0000000000..d71111136c --- /dev/null +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/JsonLocalizationDictionaryBuilder_Tests.cs @@ -0,0 +1,77 @@ +using Shouldly; +using Volo.Abp.Localization.Json; +using Xunit; + +namespace Volo.Abp.Localization; + +/// +/// Testing edge cases for +/// +public class JsonLocalizationDictionaryBuilder_Tests +{ + [Fact] + public void Should_Use_Last_Value_When_Json_Contains_Duplicate_Keys() + { + // This test locks the behavior of System.Text.Json when deserializing duplicate JSON property names. + // If STJ changes this behavior in a future version, this test will catch the regression. + var input = """ + { + "culture": "en", + "texts": { + "ThisFieldIsRequired": "This field is required", + "MaxLengthErrorMessage": "This field can be maximum of '{0}' chars", + "ThisFieldIsRequired": "This field is required again" + } + } + """; + + var localizationDictionary = JsonLocalizationDictionaryBuilder.BuildFromJsonString(input); + localizationDictionary.ShouldNotBeNull(); + var localizationString = localizationDictionary.GetOrNull("ThisFieldIsRequired"); + localizationString.ShouldNotBeNull(); + localizationString.Value.ShouldBe("This field is required again"); + } + + [Fact] + public void Should_Use_Nested_Value_When_Flat_Key_Is_Defined_Before_Nested_Object() + { + // When a flat key (e.g. "Foo__Bar") appears before a nested object (e.g. "Foo": {"Bar": ...}), + // the nested value wins because FlattenTexts processes keys in order and last-write wins. + var input = """ + { + "culture": "en", + "texts": { + "DeepLocalizationKey__DeepKey": "FlatValue", + "DeepLocalizationKey": { "DeepKey": "NestedValue" } + } + } + """; + + var localizationDictionary = JsonLocalizationDictionaryBuilder.BuildFromJsonString(input); + localizationDictionary.ShouldNotBeNull(); + var localizationString = localizationDictionary.GetOrNull("DeepLocalizationKey__DeepKey"); + localizationString.ShouldNotBeNull(); + localizationString.Value.ShouldBe("NestedValue"); + } + + [Fact] + public void JsonLocalizationDictionaryBuilder_Should_Handle_Deep_Duplicates() + { + var input = """ + { + "culture": "en", + "texts": { + "ThisFieldIsRequired": "This field is required", + "DeepLocalizationKey": { "DeepKey": "DeepValue" }, + "DeepLocalizationKey__DeepKey": "Another translation" + } + } + """; + + var localizationDictionary = JsonLocalizationDictionaryBuilder.BuildFromJsonString(input); + localizationDictionary.ShouldNotBeNull(); + var localizationString = localizationDictionary.GetOrNull("DeepLocalizationKey__DeepKey"); + localizationString.ShouldNotBeNull(); + localizationString.Value.ShouldBe("Another translation"); + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/!en_First.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/!en_First.json new file mode 100644 index 0000000000..681c77b5d5 --- /dev/null +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/!en_First.json @@ -0,0 +1,6 @@ +{ + "culture": "en", + "texts": { + "ThisIsRequiredValue": "This is required value" + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/en_Base.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/en_Base.json new file mode 100644 index 0000000000..1bcfead01b --- /dev/null +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/en_Base.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Base.Id": "Id", + "Base.Name": "Name" + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/en_Book.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/en_Book.json new file mode 100644 index 0000000000..22d6039843 --- /dev/null +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/en_Book.json @@ -0,0 +1,7 @@ +{ + "culture": "en", + "texts": { + "Book.Id": "ISBN", + "Book.Name": "Title" + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/zen_Last.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/zen_Last.json new file mode 100644 index 0000000000..a94dc23006 --- /dev/null +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/FilesSplit/zen_Last.json @@ -0,0 +1,6 @@ +{ + "culture": "en", + "texts": { + "ThisIsRequiredValue": "This is required value (this will be used)" + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de.json index ee396f1341..4ef1655764 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de.json @@ -6,11 +6,6 @@ "CarPlural": "Autos", "MaxLenghtErrorMessage": "Die Länge dieses Feldes kann maximal '{0}'-Zeichen betragen", "Universe": "Universum", - "FortyTwo": "Zweiundvierzig", - "Enum:BookType.Undefined": "Nicht definiert", - "Enum:BookType.0": "Undefiniert mit Wert 0", - "BookType.Adventure": "Abenteuer", - "BookType.1": "Abenteuer mit Wert 1", - "Biography": "Biografie" + "FortyTwo": "Zweiundvierzig" } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de_Book.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de_Book.json new file mode 100644 index 0000000000..1847c4e4fb --- /dev/null +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/de_Book.json @@ -0,0 +1,10 @@ +{ + "culture": "de", + "texts": { + "Enum:BookType.Undefined": "Nicht definiert", + "Enum:BookType.0": "Undefiniert mit Wert 0", + "BookType.Adventure": "Abenteuer", + "BookType.1": "Abenteuer mit Wert 1", + "Biography": "Biografie" + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs index 9e5fe4f8cf..9eca171e4e 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs @@ -40,6 +40,25 @@ public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest(person); + + personDto.GetProperty("Name").ShouldBe("John"); //Defined in both classes + 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() { @@ -65,6 +84,22 @@ public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest(person); + + personDto.HasProperty("Name").ShouldBe(false); + personDto.Name.ShouldBe("John"); + + personDto.HasProperty("Age").ShouldBe(false); + personDto.Age.ShouldBe(42); + } + [Fact(Skip = "Mapperly requires IHasExtraProperties.ExtraPropertyDictionary to be marked as nullable")] public void MapExtraPropertiesTo_Should_Ignored_If_ExtraProperties_Is_Null() { diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs index 5eeebe6b82..5d166f7dc8 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs @@ -1,6 +1,9 @@ +using System; using Microsoft.Extensions.DependencyInjection; using Riok.Mapperly.Abstractions; using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Mapperly.SampleClasses; using Volo.Abp.ObjectMapping; using Volo.Abp.Testing; using Xunit; @@ -83,4 +86,17 @@ public class AbpReverseMapperly_Tests : AbpIntegratedTest myClass.Id.ShouldBe("2"); myClass.Name.ShouldBe("BeforeReverseMap Test2 AfterReverseMap"); } + + [Fact] + public void MapExtraProperties_Should_Filter_With_Single_Parameter_ReverseMap() + { + var dto = new ExtensibleReverseDto { Id = Guid.NewGuid() } + .SetProperty("Tag", "ok") + .SetProperty("Secret", "leaked"); + + var entity = _objectMapper.Map(dto); + + entity.GetProperty("Tag").ShouldBe("ok"); + entity.HasProperty("Secret").ShouldBeFalse(); + } } diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs index e5453d09a3..6334422c09 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs @@ -101,6 +101,28 @@ public class ExtraProperties_Dictionary_Reference_Tests : AbpIntegratedTest(source); + + ReferenceEquals(source.ExtraProperties, originalSourceReference).ShouldBeTrue(); + ReferenceEquals(source.ExtraProperties, destination.ExtraProperties).ShouldBeFalse(); + + destination.ExtraProperties["TestProperty"].ShouldBe("TestValue"); + destination.ExtraProperties["NumberProperty"].ShouldBe(42); + } + [Fact] public void Should_Handle_Readonly_ExtraProperties_Gracefully() { diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs index 8ff5ca2eb1..5153109472 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs @@ -1,5 +1,7 @@ -using Volo.Abp.Modularity; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.Modularity; using Volo.Abp.ObjectExtending; +using Volo.Abp.Threading; namespace Volo.Abp.Mapperly; @@ -9,5 +11,17 @@ namespace Volo.Abp.Mapperly; )] public class MapperlyTestModule : AbpModule { + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + public override void PreConfigureServices(ServiceConfigurationContext context) + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance + .AddOrUpdateProperty("Tag") + .AddOrUpdateProperty("Secret") + .AddOrUpdateProperty("Tag") + .AddOrUpdateProperty("Secret"); + }); + } } diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs index eb11d16be1..218ed36e1c 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs @@ -82,4 +82,27 @@ public partial class TestEntityWithReadonlyExtraPropertiesMapper : MapperBase +{ + public override partial ExtensibleReverseDto Map(ExtensibleReverseEntity source); + + public override partial void Map(ExtensibleReverseEntity source, ExtensibleReverseDto destination); + + public override partial ExtensibleReverseEntity ReverseMap(ExtensibleReverseDto destination); + + public override partial void ReverseMap(ExtensibleReverseDto destination, ExtensibleReverseEntity source); } \ No newline at end of file diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg new file mode 100644 index 0000000000..64c1552e37 --- /dev/null +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj new file mode 100644 index 0000000000..0c4b2a2260 --- /dev/null +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + net10.0 + + + + + + + + + + diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs new file mode 100644 index 0000000000..f631ce2d51 --- /dev/null +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs @@ -0,0 +1,359 @@ +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using NSubstitute; +using RabbitMQ.Client; +using Shouldly; +using Xunit; + +namespace Volo.Abp.RabbitMQ; + +public class ChannelPool_Tests +{ + private static readonly TimeSpan RaceTimeout = TimeSpan.FromSeconds(5); + + // ---- Regression tests for issue #25310 / PR #25311 -------------------- + + /// + /// After a RabbitMQ restart the cached channel becomes closed. Two callers + /// that both observe the stale poolItem must not deadlock while the pool + /// replaces it with a fresh channel. + /// + [Fact] + public async Task AcquireAsync_Should_Not_Hang_When_Channel_Closed_With_Concurrent_Callers() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (await channelPool.AcquireAsync("q")) + { + } + + fixture.Channel1.IsClosed.Returns(true); + fixture.Channel1 + .DisposeAsync() + .Returns(_ => new ValueTask(Task.Delay(300))); + + using var barrier = new Barrier(2); + + var firstCaller = Task.Run(async () => + { + barrier.SignalAndWait(RaceTimeout); + using var accessor = await channelPool.AcquireAsync("q"); + }); + + var secondCaller = Task.Run(async () => + { + barrier.SignalAndWait(RaceTimeout); + await Task.Delay(50); + using var accessor = await channelPool.AcquireAsync("q"); + }); + + var completed = Task.WhenAll(firstCaller, secondCaller); + var winner = await Task.WhenAny(completed, Task.Delay(RaceTimeout)); + + winner.ShouldBe( + completed, + $"AcquireAsync is hanging on a stale poolItem. firstCaller={firstCaller.Status}, secondCaller={secondCaller.Status}"); + + await completed; + } + + /// + /// When several callers race through the closed-channel recovery path, only one new + /// channel must be created and the dictionary must keep exactly the channel that callers + /// receive — otherwise the replacement leaks on the broker side. + /// + [Fact] + public async Task AcquireAsync_Should_Create_Only_One_Replacement_Channel_When_Many_Callers_Race() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (await channelPool.AcquireAsync("q")) + { + } + + fixture.Channel1.IsClosed.Returns(true); + + const int callerCount = 8; + using var barrier = new Barrier(callerCount); + var tasks = new Task[callerCount]; + for (var i = 0; i < callerCount; i++) + { + tasks[i] = Task.Run(async () => + { + barrier.SignalAndWait(RaceTimeout); + using var accessor = await channelPool.AcquireAsync("q"); + return accessor.Channel; + }); + } + + var channels = await Task.WhenAll(tasks); + + foreach (var channel in channels) + { + channel.ShouldBe(fixture.Channel2, "every caller must receive the single replacement channel"); + } + + fixture.CreateChannelCalls.ShouldBe( + 2, + $"exactly one initial + one replacement channel should be created, but got {fixture.CreateChannelCalls}"); + } + + // ---- Behavior guard-rails: make sure the refactor preserves core semantics ---- + + [Fact] + public async Task AcquireAsync_Should_Return_Channel_From_ConnectionPool() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using var accessor = await channelPool.AcquireAsync("q"); + + accessor.Channel.ShouldBe(fixture.Channel1); + accessor.Name.ShouldBe("q"); + fixture.CreateChannelCalls.ShouldBe(1); + } + + [Fact] + public async Task AcquireAsync_Should_Reuse_Cached_Channel_On_Subsequent_Calls() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (var first = await channelPool.AcquireAsync("q")) + { + first.Channel.ShouldBe(fixture.Channel1); + } + using (var second = await channelPool.AcquireAsync("q")) + { + second.Channel.ShouldBe(fixture.Channel1); + } + using (var third = await channelPool.AcquireAsync("q")) + { + third.Channel.ShouldBe(fixture.Channel1); + } + + fixture.CreateChannelCalls.ShouldBe(1, "a healthy cached channel must be reused"); + } + + [Fact] + public async Task AcquireAsync_Should_Keep_Separate_PoolItems_For_Different_ChannelNames() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using var a = await channelPool.AcquireAsync("queue-a"); + using var b = await channelPool.AcquireAsync("queue-b"); + + a.Channel.ShouldBe(fixture.Channel1); + b.Channel.ShouldBe(fixture.Channel2); + a.Name.ShouldBe("queue-a"); + b.Name.ShouldBe("queue-b"); + fixture.CreateChannelCalls.ShouldBe(2); + } + + [Fact] + public async Task AcquireAsync_Should_Serialize_Concurrent_Callers_On_Same_Channel() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + var first = await channelPool.AcquireAsync("q"); + + var secondTask = Task.Run(() => channelPool.AcquireAsync("q")); + + await Task.Delay(100); + secondTask.IsCompleted.ShouldBeFalse("second caller must block while the channel is held"); + + first.Dispose(); + + var completed = await Task.WhenAny(secondTask, Task.Delay(RaceTimeout)); + completed.ShouldBe(secondTask, "second caller must be unblocked after release"); + + var secondAccessor = await secondTask; + secondAccessor.Channel.ShouldBe(fixture.Channel1, "the cached channel is reused"); + + secondAccessor.Dispose(); + fixture.CreateChannelCalls.ShouldBe(1, "channel is reused, never recreated"); + } + + [Fact] + public async Task AcquireAsync_Should_Not_Rebuild_When_Cached_Channel_Is_Healthy() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (await channelPool.AcquireAsync("q")) + { + } + // Channel stays open, so subsequent calls must not go through the rebuild branch. + using (await channelPool.AcquireAsync("q")) + { + } + + await fixture.Channel1.DidNotReceive().DisposeAsync(); + fixture.CreateChannelCalls.ShouldBe(1); + } + + [Fact] + public async Task AcquireAsync_Should_Propagate_Exception_When_CreateChannel_Fails() + { + var channel = Substitute.For(); + channel.IsClosed.Returns(false); + + var connection = Substitute.For(); + var attempts = 0; + connection + .CreateChannelAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + var n = Interlocked.Increment(ref attempts); + if (n == 1) + { + throw new InvalidOperationException("broker down"); + } + return Task.FromResult(channel); + }); + + var connectionPool = Substitute.For(); + connectionPool.GetAsync(Arg.Any()).Returns(Task.FromResult(connection)); + + var channelPool = new TestChannelPool(connectionPool); + + await Should.ThrowAsync(() => channelPool.AcquireAsync("q")); + + // Subsequent call should be able to succeed (broker came back). + using var accessor = await channelPool.AcquireAsync("q"); + accessor.Channel.ShouldBe(channel); + attempts.ShouldBe(2); + } + + [Fact] + public async Task AcquireAsync_Should_Dispose_Stale_Channel_Even_When_Recreate_Fails() + { + var staleChannel = Substitute.For(); + staleChannel.IsClosed.Returns(false); + + var connection = Substitute.For(); + var attempts = 0; + connection + .CreateChannelAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + var n = Interlocked.Increment(ref attempts); + if (n == 1) + { + return Task.FromResult(staleChannel); + } + throw new InvalidOperationException("broker still down"); + }); + + var connectionPool = Substitute.For(); + connectionPool.GetAsync(Arg.Any()).Returns(Task.FromResult(connection)); + + var channelPool = new TestChannelPool(connectionPool); + + using (await channelPool.AcquireAsync("q")) + { + } + + staleChannel.IsClosed.Returns(true); + + await Should.ThrowAsync(() => channelPool.AcquireAsync("q")); + + await staleChannel.Received(1).DisposeAsync(); + } + + [Fact] + public async Task AcquireAsync_Should_Throw_After_Pool_Disposed() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + await channelPool.DisposeAsync(); + + await Should.ThrowAsync(() => channelPool.AcquireAsync("q")); + } + + [Fact] + public async Task AcquireAsync_Should_Only_Create_One_Channel_Even_When_First_Callers_Race() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + const int callerCount = 8; + using var barrier = new Barrier(callerCount); + var tasks = new Task[callerCount]; + for (var i = 0; i < callerCount; i++) + { + tasks[i] = Task.Run(async () => + { + barrier.SignalAndWait(RaceTimeout); + using var accessor = await channelPool.AcquireAsync("q"); + return accessor.Channel; + }); + } + + var channels = await Task.WhenAll(tasks); + + foreach (var channel in channels) + { + channel.ShouldBe(fixture.Channel1, "the first-creation semaphore must serialize initial creation"); + } + fixture.CreateChannelCalls.ShouldBe(1); + } + + // ---- Fixture --------------------------------------------------------- + + private static ConnectionPoolFixture BuildConnectionPool() + { + var channel1 = Substitute.For(); + channel1.IsClosed.Returns(false); + + var channel2 = Substitute.For(); + channel2.IsClosed.Returns(false); + + var fixture = new ConnectionPoolFixture(channel1, channel2); + + var connection = Substitute.For(); + connection + .CreateChannelAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + var n = Interlocked.Increment(ref fixture.CreateChannelCallsField); + return n == 1 ? Task.FromResult(channel1) : Task.FromResult(channel2); + }); + + var connectionPool = Substitute.For(); + connectionPool.GetAsync(Arg.Any()).Returns(Task.FromResult(connection)); + + fixture.Pool = connectionPool; + return fixture; + } + + private sealed class ConnectionPoolFixture + { + public IConnectionPool Pool { get; set; } = default!; + public IChannel Channel1 { get; } + public IChannel Channel2 { get; } + public int CreateChannelCallsField; + public int CreateChannelCalls => CreateChannelCallsField; + + public ConnectionPoolFixture(IChannel channel1, IChannel channel2) + { + Channel1 = channel1; + Channel2 = channel2; + } + } + + private sealed class TestChannelPool : ChannelPool + { + public TestChannelPool(IConnectionPool connectionPool) : base(connectionPool) + { + } + } +} diff --git a/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs new file mode 100644 index 0000000000..aba81af6f3 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs @@ -0,0 +1,29 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.TextTemplating.Razor; + +public class RazorTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase +{ + private readonly RazorTemplateRenderingEngine _engine; + + public RazorTemplateRenderingEngine_IsSandboxed_Tests() + { + _engine = GetRequiredService(); + } + + [Fact] + public void Razor_Engine_Should_Not_Be_Sandboxed() + { + // Razor templates compile into fully-trusted .NET code; editing them is + // equivalent to granting server-side code execution. + _engine.IsSandboxed.ShouldBeFalse(); + } + + [Fact] + public void Razor_Engine_Should_Expose_IsSandboxed_Through_Interface() + { + ITemplateRenderingEngine asInterface = _engine; + asInterface.IsSandboxed.ShouldBeFalse(); + } +} diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl new file mode 100644 index 0000000000..9666a9e986 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl @@ -0,0 +1 @@ +danger=[{{ model.dangerous_action }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl new file mode 100644 index 0000000000..14eb44cce2 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl @@ -0,0 +1 @@ +name=[{{ model.name }}] email=[{{ model.inner.email }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl new file mode 100644 index 0000000000..6ccfcfcef6 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl @@ -0,0 +1 @@ +getType=[{{ model.GetType }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl new file mode 100644 index 0000000000..ada402ac57 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl @@ -0,0 +1 @@ +loaded=[{{ model.GetType.Assembly.GetType "System.IO.File" }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs new file mode 100644 index 0000000000..2aa0cd018f --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs @@ -0,0 +1,123 @@ +using System.Threading.Tasks; +using global::Scriban.Syntax; +using Shouldly; +using Xunit; + +namespace Volo.Abp.TextTemplating.Scriban; + +public class ScribanTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase +{ + private readonly ScribanTemplateRenderingEngine _engine; + private readonly ITemplateRenderer _templateRenderer; + + public ScribanTemplateRenderingEngine_IsSandboxed_Tests() + { + _engine = GetRequiredService(); + _templateRenderer = GetRequiredService(); + } + + [Fact] + public void Scriban_Engine_Should_Be_Sandboxed() + { + // Scriban interprets templates as a restricted DSL without .NET interop; + // editing template content is safe for non-developer users. + _engine.IsSandboxed.ShouldBeTrue(); + } + + [Fact] + public void Scriban_Engine_Should_Expose_IsSandboxed_Through_Interface() + { + ITemplateRenderingEngine asInterface = _engine; + asInterface.IsSandboxed.ShouldBeTrue(); + } + + [Fact] + public async Task Should_Hide_GetType_Member_On_Imported_Model() + { + // The engine projects .NET model objects into a ScriptObject containing + // only the model's public readable properties. Reflection entry points + // such as object.GetType are not part of the projection, so + // "{{ model.GetType }}" silently resolves to null (empty output) instead + // of leaking the runtime type. + var result = await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.ReflectionEscapeAttempt, + model: new ReflectionEscapeModel { Name = "John" }); + + result.ShouldBe("getType=[]\n"); + result.ShouldNotContain("RuntimeType"); + result.ShouldNotContain("Volo.Abp.TextTemplating"); + } + + [Fact] + public async Task Should_Block_Reflection_Escape_Chain() + { + // Direct reflection chain that an attacker would attempt: + // {{ model.GetType.Assembly.GetType "System.IO.File" }} + // Because the projected ScriptObject does not expose GetType, the first + // hop yields null and Scriban raises a ScriptRuntimeException when the + // chain dereferences a null. Surfacing an error is desirable: silent + // failure could mask escape attempts. + var ex = await Should.ThrowAsync(async () => + await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.ReflectionEscapeChain, + model: new ReflectionEscapeModel { Name = "John" })); + + // The error must reference the broken chain (null), not a leaked Type + // or Assembly value. + ex.Message.ShouldContain("null"); + ex.Message.ShouldNotContain("System.IO.File"); + ex.Message.ShouldNotContain("System.Private.CoreLib"); + } + + [Fact] + public async Task Should_Hide_Methods_Of_Imported_Model() + { + // Methods on .NET objects are not exposed by the projection so + // attackers cannot invoke side-effecting methods (e.g. repository + // mutators) even when the host accidentally imports a service-like + // object as a model. + var result = await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.MethodInvocationAttempt, + model: new ServiceLikeModel()); + + result.ShouldBe("danger=[]\n"); + result.ShouldNotContain("UNSAFE"); + } + + [Fact] + public async Task Should_Project_Properties_Including_Nested_Objects() + { + // The projection must still expose user-defined readable properties + // (and recurse into nested objects) so legitimate template usage keeps + // working after sandboxing. + var result = await _templateRenderer.RenderAsync( + ScribanTestTemplateDefinitionProvider.NestedPropertyAccess, + model: new OuterModel { Name = "John", Inner = new InnerModel { Email = "j@a.b" } }); + + result.ShouldBe("name=[John] email=[j@a.b]\n"); + } + + private class ReflectionEscapeModel + { + public string Name { get; set; } = default!; + } + + private class ServiceLikeModel + { + public string DangerousAction() + { + return "UNSAFE"; + } + } + + private class OuterModel + { + public string Name { get; set; } = default!; + public InnerModel Inner { get; set; } = default!; + } + + private class InnerModel + { + public string Email { get; set; } = default!; + } +} diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs index 0087288c32..006f2db450 100644 --- a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs @@ -2,6 +2,11 @@ public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider { + public const string ReflectionEscapeAttempt = "ReflectionEscapeAttempt"; + public const string ReflectionEscapeChain = "ReflectionEscapeChain"; + public const string MethodInvocationAttempt = "MethodInvocationAttempt"; + public const string NestedPropertyAccess = "NestedPropertyAccess"; + public override void Define(ITemplateDefinitionContext context) { context.GetOrNull(TestTemplates.WelcomeEmail)? @@ -19,5 +24,21 @@ public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider context.GetOrNull(TestTemplates.ShowDecimalNumber)? .WithVirtualFilePath("/SampleTemplates/ShowDecimalNumber.tpl", true) .WithScribanEngine(); + + context.Add(new TemplateDefinition(ReflectionEscapeAttempt) + .WithVirtualFilePath("/SampleTemplates/ReflectionEscapeAttempt.tpl", true) + .WithScribanEngine()); + + context.Add(new TemplateDefinition(ReflectionEscapeChain) + .WithVirtualFilePath("/SampleTemplates/ReflectionEscapeChain.tpl", true) + .WithScribanEngine()); + + context.Add(new TemplateDefinition(MethodInvocationAttempt) + .WithVirtualFilePath("/SampleTemplates/MethodInvocationAttempt.tpl", true) + .WithScribanEngine()); + + context.Add(new TemplateDefinition(NestedPropertyAccess) + .WithVirtualFilePath("/SampleTemplates/NestedPropertyAccess.tpl", true) + .WithScribanEngine()); } } diff --git a/latest-versions.json b/latest-versions.json index 8873bc5e88..d9c48cf830 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,4 +1,22 @@ [ + { + "version": "10.3.0", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "5.3.0" + } + }, + { + "version": "10.2.1", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "5.2.1" + } + }, { "version": "10.2.0", "releaseDate": "", diff --git a/modules/account/Volo.Abp.Account.slnx b/modules/account/Volo.Abp.Account.slnx index 031e142df9..1beae2db60 100644 --- a/modules/account/Volo.Abp.Account.slnx +++ b/modules/account/Volo.Abp.Account.slnx @@ -2,6 +2,7 @@ + diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorMappers.cs b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorMappers.cs new file mode 100644 index 0000000000..eda8b42eb9 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorMappers.cs @@ -0,0 +1,27 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Account.Blazor.MudBlazor.Pages.Account; +using Volo.Abp.Mapperly; +using Volo.Abp.Identity; + +namespace Volo.Abp.Account.Blazor.MudBlazor; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))] + [MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))] + public override partial PersonalInfoModel Map(ProfileDto source); + + [MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))] + [MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))] + public override partial void Map(ProfileDto source, PersonalInfoModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class PersonalInfoModelToUpdateProfileDtoMapper : MapperBase +{ + public override partial UpdateProfileDto Map(PersonalInfoModel source); + public override partial void Map(PersonalInfoModel source, UpdateProfileDto destination); +} diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorMudBlazorModule.cs b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorMudBlazorModule.cs new file mode 100644 index 0000000000..40427f0f43 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorMudBlazorModule.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Account.Blazor.MudBlazor.Pages.Account; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; +using Volo.Abp.Mapperly; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.Modularity; +using Volo.Abp.Threading; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.Account.Blazor.MudBlazor; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpMapperlyModule), + typeof(AbpAccountApplicationContractsModule) + )] +public class AbpAccountBlazorMudBlazorModule : AbpModule +{ + private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + + Configure(options => + { + options.MenuContributors.Add(new AbpAccountBlazorUserMenuContributor()); + }); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpAccountBlazorMudBlazorModule).Assembly); + }); + } + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + OneTimeRunner.Run(() => + { + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToUi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.User, + editFormTypes: new[] { typeof(PersonalInfoModel) } + ); + }); + } +} diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorUserMenuContributor.cs b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorUserMenuContributor.cs new file mode 100644 index 0000000000..bbb230938d --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountBlazorUserMenuContributor.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Volo.Abp.Account.Localization; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.Account.Blazor.MudBlazor; + +public class AbpAccountBlazorUserMenuContributor : IMenuContributor +{ + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name != StandardMenus.User) + { + return Task.CompletedTask; + } + + var accountResource = context.GetLocalizer(); + + context.Menu.AddItem(new ApplicationMenuItem("Account.Manage", accountResource["MyAccount"], url: "account/manage-profile", icon: "fa fa-cog")); + + return Task.CompletedTask; + } +} diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountComponentBase.cs b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountComponentBase.cs new file mode 100644 index 0000000000..b2c1f74c67 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/AbpAccountComponentBase.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Account.Localization; +using Volo.Abp.AspNetCore.Components; + +namespace Volo.Abp.Account.Blazor.MudBlazor; + +public abstract class AbpAccountComponentBase : AbpComponentBase +{ + protected AbpAccountComponentBase() + { + LocalizationResource = typeof(AccountResource); + ObjectMapperContext = typeof(AbpAccountBlazorMudBlazorModule); + } +} diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/FodyWeavers.xml b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/FodyWeavers.xsd b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/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/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Pages/Account/AccountManage.razor b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Pages/Account/AccountManage.razor new file mode 100644 index 0000000000..c25bc622f8 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Pages/Account/AccountManage.razor @@ -0,0 +1,100 @@ +@page "/account/manage-profile" +@page "/{culture}/account/manage-profile" +@using global::MudBlazor +@using Volo.Abp.Account.Localization +@using Volo.Abp.MudBlazorUI.Components.ObjectExtending +@using Volo.Abp.ObjectExtending + +@inherits AbpAccountComponentBase + + + + + + @if (ChangePasswordModel != null) + { + + + + + + @L["Save"] + + + } + + + @if (PersonalInfoModel != null) + { + + + + + + + + + + + + + + @* Extension Properties *@ + @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + { + var isAllowed = propertyInfo.Configuration.GetOrDefault(IdentityModuleExtensionConsts.ConfigurationNames.AllowUserToEdit); + + if (isAllowed == null || !isAllowed.Equals(true)) + { + continue; + } + + if (!propertyInfo.Name.EndsWith("_Text")) + { + if (propertyInfo.IsEnum()) + { + + } + else if (!propertyInfo.Lookup.Url.IsNullOrEmpty()) + { + + } + else + { + var inputType = propertyInfo.GetInputType(); + __builder.OpenComponent(0, inputType.MakeGenericType(new[] { typeof(PersonalInfoModel), typeof(AccountResource) })); + __builder.AddAttribute(1, "PropertyInfo", propertyInfo); + __builder.AddAttribute(2, "Entity", PersonalInfoModel); + __builder.AddAttribute(3, "LH", _localizerHelper); + __builder.CloseComponent(); + } + } + } + + + @L["Save"] + + + } + + + + + diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Pages/Account/AccountManage.razor.cs b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Pages/Account/AccountManage.razor.cs new file mode 100644 index 0000000000..fa0bc1c31b --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Pages/Account/AccountManage.razor.cs @@ -0,0 +1,159 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.Account.Localization; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.Identity; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Account.Blazor.MudBlazor.Pages.Account; + +public partial class AccountManage +{ + [Parameter] + public string? Culture { get; set; } + + [Inject] protected IProfileAppService ProfileAppService { get; set; } = default!; + + [Inject] protected IUiMessageService UiMessageService { get; set; } = default!; + + [Inject] protected ISnackbar Snackbar { get; set; } = default!; + + [Inject] protected IStringLocalizer Localizer { get; set; } = default!; + + protected string SelectedTab = "Password"; + + protected ChangePasswordModel? ChangePasswordModel; + + protected PersonalInfoModel? PersonalInfoModel; + + private int _activeTabIndex = 0; + private MudForm? _changePasswordForm; + private MudForm? _personalInfoForm; + private AbpMudBlazorMessageLocalizerHelper? _localizerHelper; + + protected override async Task OnInitializedAsync() + { + _localizerHelper = new AbpMudBlazorMessageLocalizerHelper(Localizer); + await GetUserInformations(); + } + + protected async Task GetUserInformations() + { + var user = await ProfileAppService.GetAsync(); + + ChangePasswordModel = new ChangePasswordModel + { + HideOldPasswordInput = !user.HasPassword + }; + + PersonalInfoModel = ObjectMapper.Map(user); + } + + protected async Task ChangePasswordAsync() + { + if (_changePasswordForm == null) + { + return; + } + + await _changePasswordForm.ValidateAsync(); + if (!_changePasswordForm.IsValid) + { + return; + } + + if (ChangePasswordModel == null || string.IsNullOrWhiteSpace(ChangePasswordModel.CurrentPassword)) + { + return; + } + + if (ChangePasswordModel.NewPassword != ChangePasswordModel.NewPasswordConfirm) + { + await UiMessageService.Warn(L["NewPasswordConfirmFailed"]); + return; + } + + if (ChangePasswordModel.CurrentPassword == ChangePasswordModel.NewPassword) + { + await UiMessageService.Warn(L["NewPasswordSameAsOld"]); + return; + } + + await ProfileAppService.ChangePasswordAsync(new ChangePasswordInput + { + CurrentPassword = ChangePasswordModel.CurrentPassword, + NewPassword = ChangePasswordModel.NewPassword + }); + + ChangePasswordModel.Clear(); + + await UiMessageService.Success(L["PasswordChanged"]); + } + + protected virtual async Task UpdatePersonalInfoAsync() + { + if (_personalInfoForm == null) + { + return; + } + + await _personalInfoForm.ValidateAsync(); + + if (!_personalInfoForm.IsValid) + { + return; + } + + if (PersonalInfoModel == null) + { + return; + } + + await ProfileAppService.UpdateAsync( + ObjectMapper.Map(PersonalInfoModel) + ); + + await UiMessageService.Success(L["PersonalSettingsSaved"]); + } + +} + +public class ChangePasswordModel +{ + public string CurrentPassword { get; set; } = string.Empty; + + public string NewPassword { get; set; } = string.Empty; + + public string NewPasswordConfirm { get; set; } = string.Empty; + + public bool HideOldPasswordInput { get; set; } + + public void Clear() + { + CurrentPassword = string.Empty; + NewPassword = string.Empty; + NewPasswordConfirm = string.Empty; + } +} + +public class PersonalInfoModel : ExtensibleObject +{ + public string UserName { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string Surname { get; set; } = string.Empty; + + public string PhoneNumber { get; set; } = string.Empty; + + public bool PhoneNumberConfirmed { get; set; } + + public bool EmailConfirmed { get; set; } + + public string ConcurrencyStamp { get; set; } = string.Empty; +} diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Volo.Abp.Account.Blazor.MudBlazor.csproj b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Volo.Abp.Account.Blazor.MudBlazor.csproj new file mode 100644 index 0000000000..8ee2deb508 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/Volo.Abp.Account.Blazor.MudBlazor.csproj @@ -0,0 +1,20 @@ + + + + + + + net10.0 + Volo.Abp.Account.Blazor.MudBlazor + + + + + + + + + + + + diff --git a/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/_Imports.razor b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/_Imports.razor new file mode 100644 index 0000000000..a5bda013b8 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor.MudBlazor/_Imports.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Volo.Abp.MudBlazorUI +@using Volo.Abp.MudBlazorUI.Components +@using MudBlazor +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorUserMenuContributor.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorUserMenuContributor.cs index 3fae85c5b5..d40170ffec 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorUserMenuContributor.cs +++ b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorUserMenuContributor.cs @@ -15,7 +15,7 @@ public class AbpAccountBlazorUserMenuContributor : IMenuContributor var accountResource = context.GetLocalizer(); - context.Menu.AddItem(new ApplicationMenuItem("Account.Manage", accountResource["MyAccount"], url: "account/manage-profile", icon: "fa fa-cog")); + context.Menu.AddItem(new ApplicationMenuItem("Account.Manage", accountResource["MyAccount"], url: "~/account/manage-profile", icon: "fa fa-cog")); return Task.CompletedTask; } diff --git a/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor b/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor index 2341b9dc5b..c5ed90e5aa 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor +++ b/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor @@ -1,4 +1,5 @@ @page "/account/manage-profile" +@page "/{culture}/account/manage-profile" @using Microsoft.AspNetCore.Components.Forms @using Volo.Abp.Account.Localization @using Volo.Abp.AspNetCore.Components.Web diff --git a/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor.cs b/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor.cs index 1aef6551c2..7cc4e6e357 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor.cs +++ b/modules/account/src/Volo.Abp.Account.Blazor/Pages/Account/AccountManage.razor.cs @@ -8,6 +8,9 @@ namespace Volo.Abp.Account.Blazor.Pages.Account; public partial class AccountManage { + [Parameter] + public string? Culture { get; set; } + [Inject] protected IProfileAppService ProfileAppService { get; set; } [Inject] protected IUiMessageService UiMessageService { get; set; } diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs index 9e3fff950b..676f37aa47 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs @@ -159,6 +159,23 @@ public class IdentityServerSupportedLoginModel : LoginModel if (result.IsNotAllowed) { + var notAllowedUser = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + if (notAllowedUser != null) + { + using (CurrentTenant.Change(notAllowedUser.TenantId)) + { + await IdentityOptions.SetAsync(); + if ((notAllowedUser.ShouldChangePasswordOnNextLogin || + await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) && + !await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password)) + { + Alerts.Danger(L["InvalidUserNameOrPassword"]); + return Page(); + } + } + } + Alerts.Warning(L["LoginIsNotAllowed"]); return Page(); } diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs index cef45c2f4f..57fcbe9e46 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -130,6 +130,23 @@ public class LoginModel : AccountPageModel if (result.IsNotAllowed) { + var notAllowedUser = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + if (notAllowedUser != null) + { + using (CurrentTenant.Change(notAllowedUser.TenantId)) + { + await IdentityOptions.SetAsync(); + if ((notAllowedUser.ShouldChangePasswordOnNextLogin || + await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) && + !await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password)) + { + Alerts.Danger(L["InvalidUserNameOrPassword"]); + return Page(); + } + } + } + Alerts.Warning(L["LoginIsNotAllowed"]); return Page(); } diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/EntityChange.cs b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/EntityChange.cs index dd12102e4a..f887095ad2 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/EntityChange.cs +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/EntityChange.cs @@ -47,7 +47,7 @@ public class EntityChange : Entity, IMultiTenant, IHasExtraProperties ChangeTime = entityChangeInfo.ChangeTime; ChangeType = entityChangeInfo.ChangeType; EntityTenantId = entityChangeInfo.EntityTenantId; - EntityId = entityChangeInfo.EntityId.Truncate(EntityChangeConsts.MaxEntityTypeFullNameLength); + EntityId = entityChangeInfo.EntityId.Truncate(EntityChangeConsts.MaxEntityIdLength); EntityTypeFullName = entityChangeInfo.EntityTypeFullName.TruncateFromBeginning(EntityChangeConsts.MaxEntityTypeFullNameLength); PropertyChanges = entityChangeInfo diff --git a/modules/basic-theme/Volo.Abp.BasicTheme.slnx b/modules/basic-theme/Volo.Abp.BasicTheme.slnx index 44256584c5..42b7f7973c 100644 --- a/modules/basic-theme/Volo.Abp.BasicTheme.slnx +++ b/modules/basic-theme/Volo.Abp.BasicTheme.slnx @@ -1,9 +1,13 @@ + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor index 973912af30..f3b2c41779 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor @@ -1,6 +1,7 @@ @namespace Volo.Abp.AspNetCore.Components.Server.BasicTheme.Themes.Basic @using Volo.Abp.Users @using Volo.Abp.MultiTenancy +@using Volo.Abp.UI.Navigation @using Microsoft.Extensions.Localization @using global::Localization.Resources.AbpUi @inject ICurrentUser CurrentUser @@ -26,13 +27,21 @@ { @foreach (var menuItem in Menu.Items) { - @menuItem.DisplayName + var componentType = menuItem.GetComponentTypeOrDefault(); + if (componentType != null) + { + + } + else + { + @menuItem.DisplayName + } } } - @L["Login"] + @L["Login"] diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor.cs index 84696d7f29..0ce5132a25 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.BasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RequestLocalization; using Volo.Abp.UI.Navigation; namespace Volo.Abp.AspNetCore.Components.Server.BasicTheme.Themes.Basic; @@ -11,6 +13,9 @@ public partial class LoginDisplay : IDisposable [Inject] protected IMenuManager MenuManager { get; set; } + [Inject] + protected IHttpContextAccessor HttpContextAccessor { get; set; } + protected ApplicationMenu Menu { get; set; } protected override async Task OnInitializedAsync() @@ -20,6 +25,12 @@ public partial class LoginDisplay : IDisposable Navigation.LocationChanged += OnLocationChanged; } + protected string GetLoginUrl() + { + var culture = AbpRequestCultureCookieHelper.GetRouteCulture(HttpContextAccessor.HttpContext); + return string.IsNullOrEmpty(culture) ? "Account/Login" : $"{culture}/Account/Login"; + } + protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) { InvokeAsync(StateHasChanged); diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/AbpAspNetCoreComponentsServerMudBlazorBasicThemeModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/AbpAspNetCoreComponentsServerMudBlazorBasicThemeModule.cs new file mode 100644 index 0000000000..9bf19a712e --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/AbpAspNetCoreComponentsServerMudBlazorBasicThemeModule.cs @@ -0,0 +1,46 @@ +using Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Bundling; +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor; +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling; +using Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; +using BlazorStandardBundles = Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor.Bundling.BlazorServerMudBlazorStandardBundles; + +namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebMudBlazorBasicThemeModule), + typeof(AbpAspNetCoreComponentsServerThemingMudBlazorModule) + )] +public class AbpAspNetCoreComponentsServerMudBlazorBasicThemeModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Contributors.Add(new MudBlazorBasicThemeToolbarContributor()); + }); + + Configure(options => + { + options + .StyleBundles + .Add(BlazorMudBlazorBasicThemeBundles.Styles.Global, bundle => + { + bundle + .AddBaseBundles(BlazorStandardBundles.Styles.Global) + .AddContributors(typeof(BlazorServerMudBlazorBasicThemeStyleContributor)); + }); + + options + .ScriptBundles + .Add(BlazorMudBlazorBasicThemeBundles.Scripts.Global, bundle => + { + bundle + .AddBaseBundles(BlazorStandardBundles.Scripts.Global) + .AddContributors(typeof(BlazorServerMudBlazorBasicThemeScriptContributor)); + }); + }); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorMudBlazorBasicThemeBundles.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorMudBlazorBasicThemeBundles.cs new file mode 100644 index 0000000000..8b1efca7ea --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorMudBlazorBasicThemeBundles.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Bundling; + +public static class BlazorMudBlazorBasicThemeBundles +{ + public static class Styles + { + public static string Global = "Blazor.MudBlazorBasicTheme.Global"; + } + + public static class Scripts + { + public static string Global = "Blazor.MudBlazorBasicTheme.Global"; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorServerMudBlazorBasicThemeScriptContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorServerMudBlazorBasicThemeScriptContributor.cs new file mode 100644 index 0000000000..d2d6bf6bb9 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorServerMudBlazorBasicThemeScriptContributor.cs @@ -0,0 +1,8 @@ +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Bundling; + +public class BlazorServerMudBlazorBasicThemeScriptContributor : BundleContributor +{ + +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorServerMudBlazorBasicThemeStyleContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorServerMudBlazorBasicThemeStyleContributor.cs new file mode 100644 index 0000000000..8e9ce50bee --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Bundling/BlazorServerMudBlazorBasicThemeStyleContributor.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Bundling; + +public class BlazorServerMudBlazorBasicThemeStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/_content/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/themes/basic/main.css"); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/FodyWeavers.xml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/FodyWeavers.xsd b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/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/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/MudBlazorBasicThemeToolbarContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/MudBlazorBasicThemeToolbarContributor.cs new file mode 100644 index 0000000000..0ba46719c4 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/MudBlazorBasicThemeToolbarContributor.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Themes.Basic; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; + +namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme; + +public class MudBlazorBasicThemeToolbarContributor : IToolbarContributor +{ + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(LoginDisplay))); + context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); + } + + return Task.CompletedTask; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LanguageSwitch.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LanguageSwitch.razor new file mode 100644 index 0000000000..601479a35f --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LanguageSwitch.razor @@ -0,0 +1,70 @@ +@using Volo.Abp.Localization +@using System.Globalization +@using System.Collections.Immutable +@using Microsoft.AspNetCore.RequestLocalization +@inject ILanguageProvider LanguageProvider +@inject NavigationManager NavigationManager +@inject IAbpRequestLocalizationOptionsProvider RequestLocalizationOptionsProvider + +@if (_otherLanguages != null && _otherLanguages.Any()) +{ + + + + + + @_currentLanguage.DisplayName + + @foreach (var language in _otherLanguages) + { + @language.DisplayName + } + + +} + +@code { + private IReadOnlyList? _otherLanguages; + private LanguageInfo? _currentLanguage; + + protected override async Task OnInitializedAsync() + { + var languages = await LanguageProvider.GetLanguagesAsync(); + var currentLanguage = languages.FindByCulture( + CultureInfo.CurrentCulture.Name, + CultureInfo.CurrentUICulture.Name + ); + + if (currentLanguage == null) + { + var localizationOptions = await RequestLocalizationOptionsProvider.GetLocalizationOptionsAsync(); + if (localizationOptions.DefaultRequestCulture != null) + { + currentLanguage = new LanguageInfo( + localizationOptions.DefaultRequestCulture.Culture.Name, + localizationOptions.DefaultRequestCulture.UICulture.Name, + localizationOptions.DefaultRequestCulture.UICulture.DisplayName); + } + else + { + currentLanguage = new LanguageInfo( + CultureInfo.CurrentCulture.Name, + CultureInfo.CurrentUICulture.Name, + CultureInfo.CurrentUICulture.DisplayName); + } + } + + _currentLanguage = currentLanguage; + _otherLanguages = languages.Where(l => l != _currentLanguage).ToImmutableList(); + } + + private void ChangeLanguage(LanguageInfo language) + { + var relativeUrl = NavigationManager.Uri.RemovePreFix(NavigationManager.BaseUri).EnsureStartsWith('/').EnsureStartsWith('~'); + + NavigationManager.NavigateTo( + $"Abp/Languages/Switch?culture={language.CultureName}&uiCulture={language.UiCultureName}&returnUrl={relativeUrl}", + forceLoad: true + ); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor new file mode 100644 index 0000000000..ee05fab1c8 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor @@ -0,0 +1,52 @@ +@namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Themes.Basic +@using Volo.Abp.Users +@using Volo.Abp.MultiTenancy +@using Microsoft.Extensions.Localization +@using global::Localization.Resources.AbpUi +@using Microsoft.AspNetCore.Components.Authorization +@using Volo.Abp.UI.Navigation +@implements IDisposable +@inject ICurrentUser CurrentUser +@inject ICurrentTenant CurrentTenant +@inject NavigationManager Navigation +@inject IStringLocalizer L + + + + + + + + + + @if (CurrentTenant.Name != null) + { + @CurrentTenant.Name\@CurrentUser.UserName + } + else + { + @CurrentUser.UserName + } + + @if (Menu != null) + { + @foreach (var menuItem in Menu.Items) + { + var componentType = menuItem.GetComponentTypeOrDefault(); + if (componentType != null) + { + + } + else + { + @menuItem.DisplayName + } + } + } + + + + + @L["Login"] + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor.cs new file mode 100644 index 0000000000..ea1db6a03c --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RequestLocalization; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.Themes.Basic; + +public partial class LoginDisplay : IDisposable +{ + [Inject] + protected IMenuManager MenuManager { get; set; } + + [Inject] + protected IHttpContextAccessor HttpContextAccessor { get; set; } + + protected ApplicationMenu Menu { get; set; } + + protected override async Task OnInitializedAsync() + { + Menu = await MenuManager.GetAsync(StandardMenus.User); + + Navigation.LocationChanged += OnLocationChanged; + } + + protected string GetLoginUrl() + { + var culture = AbpRequestCultureCookieHelper.GetRouteCulture(HttpContextAccessor.HttpContext); + return string.IsNullOrEmpty(culture) ? "Account/Login" : $"{culture}/Account/Login"; + } + + protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) + { + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + Navigation.LocationChanged -= OnLocationChanged; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/_Imports.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/_Imports.razor new file mode 100644 index 0000000000..4e91324cd8 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Themes/Basic/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor +@using MudBlazor +@using Volo.Abp.MudBlazorUI diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.csproj new file mode 100644 index 0000000000..82741a876a --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.Server.MudBlazorBasicTheme.csproj @@ -0,0 +1,19 @@ + + + + + + + net10.0 + + + + + + + + + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/AbpAspNetCoreComponentsWebMudBlazorBasicThemeModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/AbpAspNetCoreComponentsWebMudBlazorBasicThemeModule.cs new file mode 100644 index 0000000000..d0bee588e6 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/AbpAspNetCoreComponentsWebMudBlazorBasicThemeModule.cs @@ -0,0 +1,24 @@ +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule) +)] +public class AbpAspNetCoreComponentsWebMudBlazorBasicThemeModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Themes.Add(); + + if (options.DefaultThemeName == null) + { + options.DefaultThemeName = MudBlazorBasicTheme.Name; + } + }); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/FodyWeavers.xml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/FodyWeavers.xsd b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/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/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/MudBlazorBasicTheme.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/MudBlazorBasicTheme.cs new file mode 100644 index 0000000000..a777bcfc32 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/MudBlazorBasicTheme.cs @@ -0,0 +1,26 @@ +using System; +using Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.Themes.Basic; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Theming; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme; + +[ThemeName(Name)] +public class MudBlazorBasicTheme : ITheme, ITransientDependency +{ + public const string Name = "MudBlazorBasic"; + + public virtual Type GetLayout(string name, bool fallbackToDefault = true) + { + switch (name) + { + case StandardLayouts.Application: + case StandardLayouts.Account: + case StandardLayouts.Empty: + return typeof(MainLayout); + default: + return fallbackToDefault ? typeof(MainLayout) : typeof(NullLayout); + } + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/App.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/App.razor new file mode 100644 index 0000000000..84edd134d0 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/App.razor @@ -0,0 +1,28 @@ +@using Microsoft.Extensions.Options +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing +@using global::MudBlazor +@inject IOptions RouterOptions + + + + + + @if (!context.User.Identity.IsAuthenticated) + { + + } + else + { + You are not authorized to access this resource. + } + + + + + + Sorry, there's nothing at this address. + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/AppWithoutAuth.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/AppWithoutAuth.razor new file mode 100644 index 0000000000..1c1d1b1366 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/AppWithoutAuth.razor @@ -0,0 +1,14 @@ +@using Microsoft.Extensions.Options +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing +@inject IOptions RouterOptions + + + + + + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/MainLayout.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/MainLayout.razor new file mode 100644 index 0000000000..2bcfa890bc --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/MainLayout.razor @@ -0,0 +1,53 @@ +@using Volo.Abp.Ui.LayoutHooks +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components.LayoutHooks +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using Volo.Abp.Ui.Branding +@inherits LayoutComponentBase +@inject IBrandingProvider BrandingProvider + + + + + + + + + + @BrandingProvider.AppName + + + + + + + + + + + + + @Body + + + + + + + + + +@code { + private bool _drawerOpen = true; + + private void ToggleDrawer() + { + _drawerOpen = !_drawerOpen; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenu.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenu.razor new file mode 100644 index 0000000000..1ffc0db3a3 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenu.razor @@ -0,0 +1,19 @@ + + @if(Menu != null && !Menu.Items.IsNullOrEmpty()) + { + foreach (var menuItem in Menu.Items) + { + if (menuItem.Items.IsNullOrEmpty()) + { + + + @menuItem.DisplayName + + } + else + { + + } + } + } + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenu.razor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenu.razor.cs new file mode 100644 index 0000000000..8387850970 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenu.razor.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components.Web.Security; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.Themes.Basic; + +public partial class NavMenu : IDisposable +{ + [Inject] + protected IMenuManager MenuManager { get; set; } + + [Inject] + protected ApplicationConfigurationChangedService ApplicationConfigurationChangedService { get; set; } + + protected ApplicationMenu Menu { get; set; } + + protected async override Task OnInitializedAsync() + { + Menu = await MenuManager.GetMainMenuAsync(); + ApplicationConfigurationChangedService.Changed += ApplicationConfigurationChanged; + } + + private void ApplicationConfigurationChanged() + { + _ = InvokeAsync(async () => + { + Menu = await MenuManager.GetMainMenuAsync(); + StateHasChanged(); + }); + } + + public void Dispose() + { + ApplicationConfigurationChangedService.Changed -= ApplicationConfigurationChanged; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenuItem.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenuItem.razor new file mode 100644 index 0000000000..20d854dcd4 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavMenuItem.razor @@ -0,0 +1,78 @@ +@implements IDisposable +@using Volo.Abp.UI.Navigation +@using MudBlazor +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing +@using System +@{ + var customComponentType = MenuItem.GetComponentTypeOrDefault(); +} + +@if (customComponentType != null && typeof(ComponentBase).IsAssignableFrom(customComponentType)) +{ + +} +else +{ + + + + @MenuItem.DisplayName + + + @foreach (var item in MenuItem.Items) + { + @if (item.Items.Any()) + { + + } + else + { + + + @item.DisplayName + + } + } + + +} + +@code { + [Parameter] + public ApplicationMenuItem MenuItem { get; set; } = default!; + + [Inject] + private NavigationManager NavigationManager { get; set; } = default!; + + protected override void OnInitialized() + { + NavigationManager.LocationChanged += OnLocationChanged; + } + + protected virtual void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + // Menu will close automatically on navigation + } + + protected virtual bool IsExpanded(ApplicationMenuItem item) + { + if (!item.Url.IsNullOrEmpty()) + { + var menuItemPath = item.Url.Replace("~/", string.Empty).Trim('/'); + var currentPagePath = new Uri(NavigationManager.Uri.TrimEnd('/')).AbsolutePath.Trim('/'); + + if (menuItemPath.TrimEnd('/').Equals(currentPagePath, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return item.Items.Any(IsExpanded); + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavToolbar.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavToolbar.razor new file mode 100644 index 0000000000..91ded03c1e --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NavToolbar.razor @@ -0,0 +1,60 @@ +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars +@using Volo.Abp.AspNetCore.Components.Web.Security +@using System.Collections.Generic +@using System +@using Microsoft.AspNetCore.Components +@using MudBlazor + +@implements IDisposable + +@foreach (var render in ToolbarItemRenders) +{ + @render +} + +@code { + [Inject] + private IToolbarManager ToolbarManager { get; set; } = default!; + + [Inject] + protected ApplicationConfigurationChangedService ApplicationConfigurationChangedService { get; set; } = default!; + + private List ToolbarItemRenders { get; set; } = new List(); + + protected async override Task OnInitializedAsync() + { + await GetToolbarItemRendersAsync(); + ApplicationConfigurationChangedService.Changed += ApplicationConfigurationChanged; + } + + private async Task GetToolbarItemRendersAsync() + { + var toolbar = await ToolbarManager.GetAsync(StandardToolbars.Main); + + ToolbarItemRenders.Clear(); + + var sequence = 0; + foreach (var item in toolbar.Items) + { + ToolbarItemRenders.Add(builder => + { + builder.OpenComponent(sequence++, item.ComponentType); + builder.CloseComponent(); + }); + } + } + + private void ApplicationConfigurationChanged() + { + _ = InvokeAsync(async () => + { + await GetToolbarItemRendersAsync(); + StateHasChanged(); + }); + } + + public void Dispose() + { + ApplicationConfigurationChangedService.Changed -= ApplicationConfigurationChanged; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NullLayout.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NullLayout.razor new file mode 100644 index 0000000000..ad53deb649 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/NullLayout.razor @@ -0,0 +1,22 @@ +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components +@using Volo.Abp.Ui.LayoutHooks +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components.LayoutHooks +@inherits LayoutComponentBase +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout + + + + + + + + + + @Body + + + + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/RedirectToLogin.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/RedirectToLogin.razor new file mode 100644 index 0000000000..d241d93470 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/RedirectToLogin.razor @@ -0,0 +1,9 @@ +@inject NavigationManager Navigation +@using System + +@code { + protected override void OnInitialized() + { + Navigation.NavigateTo($"account/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}", true); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/_Imports.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/_Imports.razor new file mode 100644 index 0000000000..f291088477 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Themes/Basic/_Imports.razor @@ -0,0 +1,9 @@ +@using MudBlazor +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using Volo.Abp.MudBlazorUI +@using Volo.Abp.MudBlazorUI.Components \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.csproj new file mode 100644 index 0000000000..03c8c3477a --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.csproj @@ -0,0 +1,14 @@ + + + + + + + net10.0 + + + + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/wwwroot/themes/basic/main.css b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/wwwroot/themes/basic/main.css new file mode 100644 index 0000000000..03d1488842 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/wwwroot/themes/basic/main.css @@ -0,0 +1,3 @@ +/* MudBlazor Basic Theme Styles */ + +/* Add any custom theme-specific styles here */ diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs index 39cc08ddd1..8888d36fb1 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeToolbarContributor.cs @@ -14,8 +14,6 @@ public class BasicThemeToolbarContributor : IToolbarContributor { if (context.Toolbar.Name == StandardToolbars.Main) { - context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); - //TODO: Can we find a different way to understand if authentication was configured or not? var authenticationStateProvider = context.ServiceProvider .GetService(); @@ -24,6 +22,8 @@ public class BasicThemeToolbarContributor : IToolbarContributor { context.Toolbar.Items.Add(new ToolbarItem(typeof(LoginDisplay))); } + + context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); } return Task.CompletedTask; diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor index 7814000136..86a74a5929 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor @@ -1,10 +1,15 @@ -@using Volo.Abp.Localization +@using Volo.Abp.Localization @using System.Globalization @using System.Collections.Immutable @using Volo.Abp.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.WebAssembly +@using Volo.Abp.AspNetCore.Mvc.Client @inject ILanguageProvider LanguageProvider @inject IJSRuntime JsRuntime @inject ICookieService CookieService +@inject NavigationManager NavigationManager +@inject ICachedApplicationConfigurationClient ConfigurationClient +@inject IRouteBasedCultureNavigationHelper CultureNavigationHelper @if (_otherLanguages != null && _otherLanguages.Any()) { @@ -22,9 +27,13 @@ @code { private IReadOnlyList _otherLanguages; private LanguageInfo _currentLanguage; + private bool _useRouteBasedCulture; protected override async Task OnInitializedAsync() { + var config = await ConfigurationClient.GetAsync(); + _useRouteBasedCulture = config.Localization.UseRouteBasedCulture; + var selectedLanguageName = await JsRuntime.InvokeAsync( "localStorage.getItem", "Abp.SelectedLanguage" @@ -57,6 +66,13 @@ private async Task ChangeLanguageAsync(LanguageInfo language) { + if (_useRouteBasedCulture) + { + await CultureNavigationHelper.NavigateToNewCultureAsync( + NavigationManager, language, _otherLanguages.Append(_currentLanguage)); + return; + } + await JsRuntime.InvokeVoidAsync( "localStorage.setItem", "Abp.SelectedLanguage", diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor index 6c02e4ffc0..3fcd75ab8e 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor @@ -2,6 +2,7 @@ @using global::Localization.Resources.AbpUi @using Microsoft.Extensions.Options @using Volo.Abp.AspNetCore.Components.Web +@using Volo.Abp.UI.Navigation @inherits AbpComponentBase @inject IJSRuntime JsRuntime @inject NavigationManager Navigation @@ -26,7 +27,15 @@ { @foreach (var menuItem in Menu.Items) { - @menuItem.DisplayName + var componentType = menuItem.GetComponentTypeOrDefault(); + if (componentType != null) + { + + } + else + { + @menuItem.DisplayName + } } } @@ -35,6 +44,6 @@ - @UiLocalizer["Login"] + @UiLocalizer["Login"] diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs index 08d40e4bfe..7fe86a6ecc 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.JSInterop; using Volo.Abp.AspNetCore.Components.Web.Security; +using Volo.Abp.AspNetCore.Components.WebAssembly; using Volo.Abp.UI.Navigation; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic; @@ -17,10 +18,20 @@ public partial class LoginDisplay : IDisposable [Inject] protected ApplicationConfigurationChangedService ApplicationConfigurationChangedService { get; set; } + [Inject] + protected IRouteBasedCultureUrlHelper CultureUrlHelper { get; set; } + protected ApplicationMenu Menu { get; set; } + protected string LoginUrl { get; set; } = string.Empty; + + protected string LogoutUrl { get; set; } = string.Empty; + protected async override Task OnInitializedAsync() { + LoginUrl = await CultureUrlHelper.PrependCulturePrefixAsync(AuthenticationOptions.Value.LoginUrl); + LogoutUrl = await CultureUrlHelper.PrependCulturePrefixAsync(AuthenticationOptions.Value.LogoutUrl); + Menu = await MenuManager.GetAsync(StandardMenus.User); Navigation.LocationChanged += OnLocationChanged; @@ -35,6 +46,8 @@ public partial class LoginDisplay : IDisposable private async void ApplicationConfigurationChanged() { + LoginUrl = await CultureUrlHelper.PrependCulturePrefixAsync(AuthenticationOptions.Value.LoginUrl); + LogoutUrl = await CultureUrlHelper.PrependCulturePrefixAsync(AuthenticationOptions.Value.LogoutUrl); Menu = await MenuManager.GetAsync(StandardMenus.User); await InvokeAsync(StateHasChanged); } @@ -47,9 +60,11 @@ public partial class LoginDisplay : IDisposable private async Task NavigateToAsync(string uri, string target = null) { + uri = uri?.TrimStart('~', '/') ?? uri; + if (target == "_blank") { - await JsRuntime.InvokeVoidAsync("open", uri, target); + await JsRuntime.InvokeVoidAsync("open", Navigation.ToAbsoluteUri(uri).ToString(), target); } else { @@ -57,15 +72,15 @@ public partial class LoginDisplay : IDisposable } } - private void BeginSignOut() + private async Task BeginSignOut() { if (AbpAspNetCoreComponentsWebOptions.Value.IsBlazorWebApp) { - Navigation.NavigateTo(AuthenticationOptions.Value.LogoutUrl, forceLoad: true); + Navigation.NavigateTo(LogoutUrl, forceLoad: true); } else { - Navigation.NavigateToLogout(AuthenticationOptions.Value.LogoutUrl); + Navigation.NavigateToLogout(LogoutUrl); } } } diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor index f4f3e152ce..246682edf8 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor @@ -1,25 +1,22 @@ -@inject NavigationManager Navigation @using Volo.Abp.DependencyInjection @using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using Volo.Abp.AspNetCore.Components.WebAssembly.Theming @using Microsoft.Extensions.Options +@using Volo.Abp.AspNetCore.Components.WebAssembly @using Volo.Abp.AspNetCore.Components.Web @inherits RedirectToLogin @attribute [ExposeServices(typeof(RedirectToLogin))] @attribute [Dependency(ReplaceServices = true)] -@inject IOptions AuthenticationOptions +@inject NavigationManager Navigation +@inject IOptions AuthOptions +@inject IRouteBasedCultureUrlHelper CultureUrlHelper @inject IOptions AbpAspNetCoreComponentsWebOptions @code { - protected override void OnInitialized() + protected override void OnInitialized() { } + + protected override Task OnInitializedAsync() { - if (AbpAspNetCoreComponentsWebOptions.Value.IsBlazorWebApp) - { - Navigation.NavigateTo(AuthenticationOptions.Value.LoginUrl, forceLoad: true); - } - else - { - Navigation.NavigateToLogin(AuthenticationOptions.Value.LoginUrl); - } + return CultureAwareRedirectToLoginHelper.RedirectAsync(Navigation, AuthOptions.Value.LoginUrl, CultureUrlHelper, AbpAspNetCoreComponentsWebOptions); } } diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeBundlingModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeBundlingModule.cs new file mode 100644 index 0000000000..730f3936fc --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeBundlingModule.cs @@ -0,0 +1,20 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorBundlingModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyMudBlazorStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(WebAssemblyMudBlazorBasicThemeBundleStyleContributor)); + }); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/FodyWeavers.xml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/FodyWeavers.xsd b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/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/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling.csproj new file mode 100644 index 0000000000..c71770c813 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling.csproj @@ -0,0 +1,15 @@ + + + + + + + net10.0 + Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling + + + + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/WebAssemblyMudBlazorBasicThemeBundleStyleContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/WebAssemblyMudBlazorBasicThemeBundleStyleContributor.cs new file mode 100644 index 0000000000..3fed44f2a0 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling/WebAssemblyMudBlazorBasicThemeBundleStyleContributor.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling; + +public class WebAssemblyMudBlazorBasicThemeBundleStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme/themes/basic/main.css"); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeModule.cs new file mode 100644 index 0000000000..907dba6e43 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeModule.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Components.Web; +using Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; +using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Bundling; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; +using Volo.Abp.Http.Client.IdentityModel.WebAssembly; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeBundlingModule), + typeof(AbpAspNetCoreComponentsWebMudBlazorBasicThemeModule), + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule), + typeof(AbpHttpClientIdentityModelWebAssemblyModule) + )] +public class AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpAspNetCoreComponentsWebAssemblyMudBlazorBasicThemeModule).Assembly); + }); + + Configure(options => + { + options.Contributors.Add(new MudBlazorBasicThemeToolbarContributor()); + }); + + if (context.Services.ExecutePreConfiguredActions().IsBlazorWebApp) + { + Configure(options => + { + options.LoginUrl = "Account/Login"; + options.LogoutUrl = "Account/Logout"; + }); + } + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/AuthenticationOptions.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/AuthenticationOptions.cs new file mode 100644 index 0000000000..9ca7223a83 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/AuthenticationOptions.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme; + +public class AuthenticationOptions +{ + public string LoginUrl { get; set; } = "authentication/login"; + + public string LogoutUrl { get; set; } = "authentication/logout"; +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/FodyWeavers.xml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/FodyWeavers.xsd b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/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/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/MudBlazorBasicThemeToolbarContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/MudBlazorBasicThemeToolbarContributor.cs new file mode 100644 index 0000000000..31282207a4 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/MudBlazorBasicThemeToolbarContributor.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Toolbars; +using Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Themes.Basic; +using LoginDisplay = Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Themes.Basic.LoginDisplay; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme; + +public class MudBlazorBasicThemeToolbarContributor : IToolbarContributor +{ + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(LanguageSwitch))); + + //TODO: Can we find a different way to understand if authentication was configured or not? + var authenticationStateProvider = context.ServiceProvider + .GetService(); + + if (authenticationStateProvider != null) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(LoginDisplay))); + } + } + + return Task.CompletedTask; + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Pages/Authentication.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Pages/Authentication.razor new file mode 100644 index 0000000000..dae4938580 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Pages/Authentication.razor @@ -0,0 +1,20 @@ +@page "/authentication/{action}" +@page "/{culture}/authentication/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using Volo.Abp.AspNetCore.Components.Web.Security + +@inject WebAssemblyCachedApplicationConfigurationClient WebAssemblyCachedApplicationConfigurationClient +@code{ + [Parameter] public string? Culture { get; set; } + [Parameter] public string? Action { get; set; } + + private async Task OnLogInSucceeded(RemoteAuthenticationState state) + { + await WebAssemblyCachedApplicationConfigurationClient.InitializeAsync(); + } + + private async Task OnLogOutSucceeded(RemoteAuthenticationState state) + { + await WebAssemblyCachedApplicationConfigurationClient.InitializeAsync(); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LanguageSwitch.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LanguageSwitch.razor new file mode 100644 index 0000000000..3add335cd7 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LanguageSwitch.razor @@ -0,0 +1,90 @@ +@using Volo.Abp.Localization +@using System.Globalization +@using System.Collections.Immutable +@using Microsoft.JSInterop +@using Volo.Abp.AspNetCore.Components.Web +@using MudBlazor +@inject ILanguageProvider LanguageProvider +@inject IJSRuntime JsRuntime +@inject ICookieService CookieService + +@if (_otherLanguages != null && _otherLanguages.Any()) +{ + + + + + + @_currentLanguage.DisplayName + + @foreach (var language in _otherLanguages) + { + @language.DisplayName + } + + +} + +@code { + private IReadOnlyList? _otherLanguages; + private LanguageInfo? _currentLanguage; + + protected override async Task OnInitializedAsync() + { + var selectedLanguageName = await JsRuntime.InvokeAsync( + "localStorage.getItem", + "Abp.SelectedLanguage" + ); + + var allLanguages = await LanguageProvider.GetLanguagesAsync(); + _otherLanguages = allLanguages; + + if (!_otherLanguages.Any()) + { + return; + } + + if (!selectedLanguageName.IsNullOrWhiteSpace()) + { + _currentLanguage = _otherLanguages.FirstOrDefault(l => l.UiCultureName == selectedLanguageName); + } + + if (_currentLanguage == null) + { + _currentLanguage = _otherLanguages.FirstOrDefault(l => l.UiCultureName == CultureInfo.CurrentUICulture.Name); + } + + if (_currentLanguage == null) + { + _currentLanguage = _otherLanguages.FirstOrDefault(); + } + + _otherLanguages = _otherLanguages.Where(l => l != _currentLanguage).ToImmutableList(); + } + + private async Task ChangeLanguageAsync(LanguageInfo language) + { + await JsRuntime.InvokeVoidAsync( + "localStorage.setItem", + "Abp.SelectedLanguage", + language.UiCultureName + ); + + await JsRuntime.InvokeVoidAsync( + "localStorage.setItem", + "Abp.IsRtl", + CultureInfo.GetCultureInfo(language.UiCultureName).TextInfo.IsRightToLeft + ); + + await CookieService.SetAsync( + ".AspNetCore.Culture", + $"c={language.CultureName}|uic={language.UiCultureName}", + new CookieOptions + { + Path = "/" + } + ); + + await JsRuntime.InvokeVoidAsync("location.reload"); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor new file mode 100644 index 0000000000..b0cfdd4963 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor @@ -0,0 +1,57 @@ +@using Microsoft.Extensions.Localization +@using global::Localization.Resources.AbpUi +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.Extensions.Options +@using Microsoft.JSInterop +@using Volo.Abp.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Security +@using Volo.Abp.UI.Navigation +@implements IDisposable +@inherits AbpComponentBase +@inject IJSRuntime JsRuntime +@inject NavigationManager Navigation +@inject IStringLocalizer UiLocalizer +@inject IOptions AuthenticationOptions +@inject IOptions AbpAspNetCoreComponentsWebOptions + + + + + + + + + + @if (CurrentTenant.Name != null) + { + @CurrentTenant.Name\@CurrentUser.UserName + } + else + { + @CurrentUser.UserName + } + + @if (Menu != null) + { + @foreach (var menuItem in Menu.Items) + { + var componentType = menuItem.GetComponentTypeOrDefault(); + if (componentType != null) + { + + } + else + { + @menuItem.DisplayName + } + } + } + + @UiLocalizer["Logout"] + + + + + @UiLocalizer["Login"] + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor.cs new file mode 100644 index 0000000000..32c60bd407 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.JSInterop; +using Volo.Abp.AspNetCore.Components.Web.Security; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.Themes.Basic; + +public partial class LoginDisplay : IDisposable +{ + [Inject] + protected IMenuManager MenuManager { get; set; } + + [Inject] + protected ApplicationConfigurationChangedService ApplicationConfigurationChangedService { get; set; } + + protected ApplicationMenu Menu { get; set; } + + protected async override Task OnInitializedAsync() + { + Menu = await MenuManager.GetAsync(StandardMenus.User); + + Navigation.LocationChanged += OnLocationChanged; + + ApplicationConfigurationChangedService.Changed += ApplicationConfigurationChanged; + } + + protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) + { + InvokeAsync(StateHasChanged); + } + + private async void ApplicationConfigurationChanged() + { + Menu = await MenuManager.GetAsync(StandardMenus.User); + await InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + Navigation.LocationChanged -= OnLocationChanged; + ApplicationConfigurationChangedService.Changed -= ApplicationConfigurationChanged; + } + + private async Task NavigateToAsync(string uri, string target = null) + { + if (target == "_blank") + { + await JsRuntime.InvokeVoidAsync("open", uri, target); + } + else + { + Navigation.NavigateTo(uri); + } + } + + private void BeginSignOut() + { + if (AbpAspNetCoreComponentsWebOptions.Value.IsBlazorWebApp) + { + Navigation.NavigateTo(AuthenticationOptions.Value.LogoutUrl, forceLoad: true); + } + else + { + Navigation.NavigateToLogout(AuthenticationOptions.Value.LogoutUrl); + } + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor new file mode 100644 index 0000000000..e89235d49c --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor @@ -0,0 +1,22 @@ +@using Volo.Abp.DependencyInjection +@using Volo.Abp.AspNetCore.Components.Web.MudBlazorBasicTheme.Themes.Basic +@using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor +@using Microsoft.Extensions.Options +@using Volo.Abp.AspNetCore.Components.WebAssembly +@using Volo.Abp.AspNetCore.Components.Web +@inherits RedirectToLogin +@attribute [ExposeServices(typeof(RedirectToLogin))] +@attribute [Dependency(ReplaceServices = true)] +@inject NavigationManager Navigation +@inject IOptions AuthOptions +@inject IRouteBasedCultureUrlHelper CultureUrlHelper +@inject IOptions AbpAspNetCoreComponentsWebOptions + +@code { + protected override void OnInitialized() { } + + protected override Task OnInitializedAsync() + { + return CultureAwareRedirectToLoginHelper.RedirectAsync(Navigation, AuthOptions.Value.LoginUrl, CultureUrlHelper, AbpAspNetCoreComponentsWebOptions); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/_Imports.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/_Imports.razor new file mode 100644 index 0000000000..4e91324cd8 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Themes/Basic/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor +@using MudBlazor +@using Volo.Abp.MudBlazorUI diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.csproj new file mode 100644 index 0000000000..ef36174d1b --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme.csproj @@ -0,0 +1,18 @@ + + + + + + + net10.0 + Volo.Abp.AspNetCore.Components.WebAssembly.MudBlazorBasicTheme + + + + + + + + + + diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json index b7ec085115..cfc328b308 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json @@ -3,8 +3,8 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.shared": "~10.3.0", - "@abp/prismjs": "~10.3.0", - "@abp/highlight.js": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.shared": "~10.4.1", + "@abp/prismjs": "~10.4.1", + "@abp/highlight.js": "~10.4.1" } } diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock index 010fc908dc..a0ca5d6eb2 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock @@ -2,204 +2,204 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.3.0.tgz#7bfbfcae6a135065ee13afd1b7d3abc92176a203" - integrity sha512-Cah+jOzcG1KZh6PXWxo+BedU7gsqTuNlTDsCzBCk2NuV2/Blb5qumAwEP4GBKAEIY/XwcFQP/ubxuXMTNjr0jw== +"@abp/clipboard@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.4.1.tgz#eec933cef5a3d740e70e8736be6a68092dece645" + integrity sha512-xvAuXcY099h3IlBYKhLG2mwfZnTRmcu5RqBp+PJz33uOMM4ZcdzxQ5lAD73ukYX32SuSE0EJaSw5SV2jqT3RGw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" clipboard "^2.0.11" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.3.0.tgz#1b04355e84eba15c395cdfc7055c8d610296206b" - integrity sha512-cqApQta9gEs03jk/s+yQsm1mnHGNDZPHnkGYgxYKI41Nl9meV6+5Dkz0b4gnQoUKXvFlmuOuf2DOjot7a2yB7g== +"@abp/highlight.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.4.1.tgz#9e956ae62c9640253db321aa7eaa31f99220fa41" + integrity sha512-AU//tBPE7jsONdZHXAS6eFsiqRCRT4p0MeasHqVfFbdZztzlGbnHr4En++9b6Jpqf0WHQts0sBr06wakmI/oVQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.3.0.tgz#4ea33ece50cefcfc8a89c17b0d265b484b7205ee" - integrity sha512-WvkLyg+izkcX6sEOYy41B9lebiniYxMQZ++c+ysyOQKZzrYEEFOOLlnFS9jPqSKLv9cNRizhG1CCfiwoYCxufQ== +"@abp/prismjs@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.4.1.tgz#cb1144481ba34b1150ba2587ed3940d217d16abd" + integrity sha512-kC9VnxwTLLToiRXP6vHQD0nBoHQvQKHNssTHM4OsNjazToAVgqfHJyEqGsJwb1uulwR05vMspST2MbMFS8fx7g== dependencies: - "@abp/clipboard" "~10.3.0" - "@abp/core" "~10.3.0" + "@abp/clipboard" "~10.4.1" + "@abp/core" "~10.4.1" prismjs "^1.30.0" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -307,10 +307,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json index d6105f02a8..4171ca7f7e 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json @@ -3,8 +3,8 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0", - "@abp/prismjs": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1", + "@abp/prismjs": "~10.4.1" }, "devDependencies": {} } diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock index 59dc5b16a6..73021ba83c 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock @@ -2,203 +2,203 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.3.0.tgz#7bfbfcae6a135065ee13afd1b7d3abc92176a203" - integrity sha512-Cah+jOzcG1KZh6PXWxo+BedU7gsqTuNlTDsCzBCk2NuV2/Blb5qumAwEP4GBKAEIY/XwcFQP/ubxuXMTNjr0jw== +"@abp/clipboard@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.4.1.tgz#eec933cef5a3d740e70e8736be6a68092dece645" + integrity sha512-xvAuXcY099h3IlBYKhLG2mwfZnTRmcu5RqBp+PJz33uOMM4ZcdzxQ5lAD73ukYX32SuSE0EJaSw5SV2jqT3RGw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" clipboard "^2.0.11" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.3.0.tgz#4ea33ece50cefcfc8a89c17b0d265b484b7205ee" - integrity sha512-WvkLyg+izkcX6sEOYy41B9lebiniYxMQZ++c+ysyOQKZzrYEEFOOLlnFS9jPqSKLv9cNRizhG1CCfiwoYCxufQ== +"@abp/prismjs@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.4.1.tgz#cb1144481ba34b1150ba2587ed3940d217d16abd" + integrity sha512-kC9VnxwTLLToiRXP6vHQD0nBoHQvQKHNssTHM4OsNjazToAVgqfHJyEqGsJwb1uulwR05vMspST2MbMFS8fx7g== dependencies: - "@abp/clipboard" "~10.3.0" - "@abp/core" "~10.3.0" + "@abp/clipboard" "~10.4.1" + "@abp/core" "~10.4.1" prismjs "^1.30.0" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -306,10 +306,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/blogging/app/Volo.BloggingTestApp/package.json b/modules/blogging/app/Volo.BloggingTestApp/package.json index da9f44eeae..fd5daaa1b3 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/package.json +++ b/modules/blogging/app/Volo.BloggingTestApp/package.json @@ -3,7 +3,7 @@ "name": "volo.blogtestapp", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0", - "@abp/blogging": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1", + "@abp/blogging": "~10.4.1" } } diff --git a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock index cd247c048d..011d7af860 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock +++ b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock @@ -2,229 +2,229 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/blogging@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.3.0.tgz#cf2c43cf10a3c7f6678682216baa98d297bde3a6" - integrity sha512-A3em39jepJp9TCZVcXeQNP+UnCmgggTmbVDbs5agAA2DiaWA+92oTEcAGlooHw48fiGc+1oANC7WaWS5FdNMGA== +"@abp/blogging@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.4.1.tgz#04a2409ac5fb52a7ebee66b1c1b6e9471076fd06" + integrity sha512-G6wwFKfDnxqlUMQVEROTOg6gqyP5epJxh0wWF2jzhxIiwsrzIWK8CQdNIJJt7+IF7aWJBtuo6gWkftuv1wpGhw== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" - "@abp/owl.carousel" "~10.3.0" - "@abp/prismjs" "~10.3.0" - "@abp/tui-editor" "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" + "@abp/owl.carousel" "~10.4.1" + "@abp/prismjs" "~10.4.1" + "@abp/tui-editor" "~10.4.1" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.3.0.tgz#7bfbfcae6a135065ee13afd1b7d3abc92176a203" - integrity sha512-Cah+jOzcG1KZh6PXWxo+BedU7gsqTuNlTDsCzBCk2NuV2/Blb5qumAwEP4GBKAEIY/XwcFQP/ubxuXMTNjr0jw== +"@abp/clipboard@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.4.1.tgz#eec933cef5a3d740e70e8736be6a68092dece645" + integrity sha512-xvAuXcY099h3IlBYKhLG2mwfZnTRmcu5RqBp+PJz33uOMM4ZcdzxQ5lAD73ukYX32SuSE0EJaSw5SV2jqT3RGw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" clipboard "^2.0.11" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/owl.carousel@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.3.0.tgz#840718a4b8ad7636bcb42d40169f42b863ac2537" - integrity sha512-r0Yfbf6MiKRa3fGrz7b/UBeRoPOBUPA39KV6JXIR5UDkn+Vu3BInacpB0HyksADzVcYyz06QvHGzlCOmikXzrg== +"@abp/owl.carousel@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.4.1.tgz#4f05616f56c37efa32c90a0ee92d528f12816e3b" + integrity sha512-FqyqIG6ezNe2aFs/Xg3UIlVER03mF+alP0jE/wMewQAzB9btbhcBC6icG0RRqs6z5PPt3I85j/EhyjMQQAQMeg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" owl.carousel "^2.3.4" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.3.0.tgz#4ea33ece50cefcfc8a89c17b0d265b484b7205ee" - integrity sha512-WvkLyg+izkcX6sEOYy41B9lebiniYxMQZ++c+ysyOQKZzrYEEFOOLlnFS9jPqSKLv9cNRizhG1CCfiwoYCxufQ== +"@abp/prismjs@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.4.1.tgz#cb1144481ba34b1150ba2587ed3940d217d16abd" + integrity sha512-kC9VnxwTLLToiRXP6vHQD0nBoHQvQKHNssTHM4OsNjazToAVgqfHJyEqGsJwb1uulwR05vMspST2MbMFS8fx7g== dependencies: - "@abp/clipboard" "~10.3.0" - "@abp/core" "~10.3.0" + "@abp/clipboard" "~10.4.1" + "@abp/core" "~10.4.1" prismjs "^1.30.0" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/tui-editor@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.3.0.tgz#4fe3879111fe5ee312048841fca1cac43bb9bb62" - integrity sha512-6zGfaYSggalgpS9tKjESXT6w00KK7fKN71rG/hxRLNRpLGn5aapHdlAZRNEUgEFYo9czFdMW8POHpVA+sjcDMw== +"@abp/tui-editor@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.4.1.tgz#e7012ed869a5de852f454e14fbe1342c55d1c07b" + integrity sha512-lFcSkESJnAjIqiU12F19U8BYko/jLTXWN6s0uqvgQ14qhPPWMr13PidMjijj/x/m0wtFg2lHwbAFt3nl+6O+sA== dependencies: - "@abp/jquery" "~10.3.0" - "@abp/prismjs" "~10.3.0" + "@abp/jquery" "~10.4.1" + "@abp/prismjs" "~10.4.1" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -332,10 +332,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json index 443413a399..0f2ec115ca 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json @@ -3,6 +3,6 @@ "name": "client-simulation-web", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1" } } diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock index 2e8f03c042..1504b8a6e1 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -268,10 +268,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/cms-kit/.abpstudio/state.json b/modules/cms-kit/.abpstudio/state.json deleted file mode 100644 index b0ef88d2f2..0000000000 --- a/modules/cms-kit/.abpstudio/state.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "selectedKubernetesProfile": null, - "solutionRunner": { - "selectedProfile": null, - "targetFrameworks": [], - "applicationsStartingWithoutBuild": [], - "applicationsWithoutAutoRefreshBrowserOnRestart": [], - "applicationBatchStartStates": [], - "folderBatchStartStates": [] - } -} \ No newline at end of file diff --git a/modules/cms-kit/angular/package.json b/modules/cms-kit/angular/package.json index 76ed594cef..6f1ede2adc 100644 --- a/modules/cms-kit/angular/package.json +++ b/modules/cms-kit/angular/package.json @@ -15,11 +15,11 @@ }, "private": true, "dependencies": { - "@abp/ng.account": "~10.3.0", - "@abp/ng.identity": "~10.3.0", - "@abp/ng.setting-management": "~10.3.0", - "@abp/ng.tenant-management": "~10.3.0", - "@abp/ng.theme.basic": "~10.3.0", + "@abp/ng.account": "~10.4.1", + "@abp/ng.identity": "~10.4.1", + "@abp/ng.setting-management": "~10.4.1", + "@abp/ng.tenant-management": "~10.4.1", + "@abp/ng.theme.basic": "~10.4.1", "@angular/animations": "~10.0.0", "@angular/common": "~10.0.0", "@angular/compiler": "~10.0.0", diff --git a/modules/cms-kit/angular/projects/cms-kit/package.json b/modules/cms-kit/angular/projects/cms-kit/package.json index 752961a86e..27fac1bc53 100644 --- a/modules/cms-kit/angular/projects/cms-kit/package.json +++ b/modules/cms-kit/angular/projects/cms-kit/package.json @@ -4,8 +4,8 @@ "peerDependencies": { "@angular/common": "^9.1.11", "@angular/core": "^9.1.11", - "@abp/ng.core": ">=10.3.0", - "@abp/ng.theme.shared": ">=10.3.0" + "@abp/ng.core": ">=10.4.1", + "@abp/ng.theme.shared": ">=10.4.1" }, "dependencies": { "tslib": "^2.0.0" diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json index 821ff58fe3..ee17e2827b 100644 --- a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json +++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json @@ -3,6 +3,6 @@ "name": "my-app-identityserver", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock index 2e8f03c042..1504b8a6e1 100644 --- a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -268,10 +268,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json b/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json index abb49011d8..0bb873130f 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock index 2e8f03c042..1504b8a6e1 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -268,10 +268,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json index 8b258a31f0..51c80608b4 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0", - "@abp/cms-kit": "10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1", + "@abp/cms-kit": "10.4.1" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock index fc0d3f8801..e0ea060438 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock @@ -2,294 +2,294 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.3.0.tgz#7bfbfcae6a135065ee13afd1b7d3abc92176a203" - integrity sha512-Cah+jOzcG1KZh6PXWxo+BedU7gsqTuNlTDsCzBCk2NuV2/Blb5qumAwEP4GBKAEIY/XwcFQP/ubxuXMTNjr0jw== +"@abp/clipboard@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.4.1.tgz#eec933cef5a3d740e70e8736be6a68092dece645" + integrity sha512-xvAuXcY099h3IlBYKhLG2mwfZnTRmcu5RqBp+PJz33uOMM4ZcdzxQ5lAD73ukYX32SuSE0EJaSw5SV2jqT3RGw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" clipboard "^2.0.11" -"@abp/cms-kit.admin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.3.0.tgz#3ac7c8e37f7de1345fbb7634ae0d797d28b6751b" - integrity sha512-zvFpdfvBJHg+XC7xM/h9YBe8gPgRrb6AJx+S9eT1+MNgyE+Lm3ghxAa6SBIg5MgPxrVpLO7N8HHrzu5Kx2ksUw== +"@abp/cms-kit.admin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.4.1.tgz#11b3359298f24522fd37c2184971a5d7757693d6" + integrity sha512-AENxQkn9s21xitgbIppA0FiqJjKTICvctx/gyITFn5NzyldENTZYvqSfBSttd9fweJoxgs6xWzaKd2aM1GvsGA== dependencies: - "@abp/codemirror" "~10.3.0" - "@abp/jstree" "~10.3.0" - "@abp/markdown-it" "~10.3.0" - "@abp/slugify" "~10.3.0" - "@abp/tui-editor" "~10.3.0" - "@abp/uppy" "~10.3.0" + "@abp/codemirror" "~10.4.1" + "@abp/jstree" "~10.4.1" + "@abp/markdown-it" "~10.4.1" + "@abp/slugify" "~10.4.1" + "@abp/tui-editor" "~10.4.1" + "@abp/uppy" "~10.4.1" -"@abp/cms-kit.public@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.3.0.tgz#2331751380b3f224d28481c4472f31fbfc1e8e1a" - integrity sha512-t7xHhYZqEneV4BwMk7StfQXB1xy0ohoCBDduYwRPGkAvgvWifFNwZ9ZuFwBwItPPeg7llzYPLxLyZg265tNDeQ== +"@abp/cms-kit.public@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.4.1.tgz#d464e1e04acd6411c20403c708494b041a024d4b" + integrity sha512-e54WAu7/1yyJP6y6Ntgp75LxXR7LIvDEYlz01xdjVTVyiyU5M7QwwnNib8tNOEs4I5UDpj2pa+O7q641pOAyhA== dependencies: - "@abp/highlight.js" "~10.3.0" - "@abp/star-rating-svg" "~10.3.0" + "@abp/highlight.js" "~10.4.1" + "@abp/star-rating-svg" "~10.4.1" -"@abp/cms-kit@10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.3.0.tgz#a67d33eda79a55d6f327d41e320a3bf88360a3ab" - integrity sha512-/nMSj95DkxnI6SG11I0ltdDMaMKNKQ6Fp5/aDNNbFyBXbbOpPzKulhHJIighynXk/B9N01mztL1B5MSL5ViSQg== +"@abp/cms-kit@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.4.1.tgz#f275d861ea5012987ebe50ee3e0cff9dfc437c43" + integrity sha512-I2s1ma/ovMfGz2rmhkazVy1pPmPVDdV6wZoCo9G2mJA0OiqOs5sCbztIsrcOcQMKie3yczsGYLlGUr2RMrw8Xg== dependencies: - "@abp/cms-kit.admin" "~10.3.0" - "@abp/cms-kit.public" "~10.3.0" + "@abp/cms-kit.admin" "~10.4.1" + "@abp/cms-kit.public" "~10.4.1" -"@abp/codemirror@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.3.0.tgz#57345c0d68850a4b1d0c62faf2c8cde221bada17" - integrity sha512-vo53MxaQE3l/la5kTXQLh0K4wgCPmfvAO0g552xEKzGEpAtD9Cl3EvmYmdJGxXlfFnAz4ic6/YZ6SJ+LCprFvw== +"@abp/codemirror@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.4.1.tgz#450cb7bd5091fa6fdf64a4115cf91c1864c51a32" + integrity sha512-yL0joL60tr2fpatrmu2tsSPlJNtwWU6neZURfXnWYdyP2D/LRDZK3OKw7YUA/XS7Mg5GdVfmht2DowYmWpRnQg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" codemirror "^5.65.1" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.3.0.tgz#1b04355e84eba15c395cdfc7055c8d610296206b" - integrity sha512-cqApQta9gEs03jk/s+yQsm1mnHGNDZPHnkGYgxYKI41Nl9meV6+5Dkz0b4gnQoUKXvFlmuOuf2DOjot7a2yB7g== +"@abp/highlight.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.4.1.tgz#9e956ae62c9640253db321aa7eaa31f99220fa41" + integrity sha512-AU//tBPE7jsONdZHXAS6eFsiqRCRT4p0MeasHqVfFbdZztzlGbnHr4En++9b6Jpqf0WHQts0sBr06wakmI/oVQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/jstree@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.3.0.tgz#ad6d3a7fe6ec2d0a55ad1fa028a61ec0e7e1157d" - integrity sha512-MR7MIbjDSHq/MUk8FDjIKrram2aWYuuC9OKZLCZ8KH2rsfx9Y0wjYTKcoouwMldLQ/71O7/pY/RRVFCxp5ROvQ== +"@abp/jstree@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.4.1.tgz#857b633756adb04199739de38a140ea53f52371e" + integrity sha512-QszVxmUSeW1XeMELRfL6z4C6KGH51PaQf0TsE2ygyiVJiV3Xvw8dhb6DkVMlNo+7icQbDojAEJZXDBG34T7o7Q== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jstree "^3.3.17" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/markdown-it@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.3.0.tgz#fbc5fff1e9dd1c31c8b96ebd6b481326809ddff0" - integrity sha512-IN01vs2axD+zrygwF5IyYmmFV+GqV7O4NK02ThqX97q0AZ5ZwqOccwcY/kkBkp+qAWoTMcPb6eOQZrTG631qUg== +"@abp/markdown-it@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.4.1.tgz#2f3b37b7107d7dac31b45a00a4527163e8860d41" + integrity sha512-bPfvJACsIdZFHs9ewTBzDOT6BEceea3s4fAiOcf2/xaRHXIT1H1xsg7lulEdsMSA9+sthKLHHgcGW9ao5lfXEw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" markdown-it "^14.1.0" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.3.0.tgz#4ea33ece50cefcfc8a89c17b0d265b484b7205ee" - integrity sha512-WvkLyg+izkcX6sEOYy41B9lebiniYxMQZ++c+ysyOQKZzrYEEFOOLlnFS9jPqSKLv9cNRizhG1CCfiwoYCxufQ== +"@abp/prismjs@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.4.1.tgz#cb1144481ba34b1150ba2587ed3940d217d16abd" + integrity sha512-kC9VnxwTLLToiRXP6vHQD0nBoHQvQKHNssTHM4OsNjazToAVgqfHJyEqGsJwb1uulwR05vMspST2MbMFS8fx7g== dependencies: - "@abp/clipboard" "~10.3.0" - "@abp/core" "~10.3.0" + "@abp/clipboard" "~10.4.1" + "@abp/core" "~10.4.1" prismjs "^1.30.0" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/slugify@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.3.0.tgz#629f96a44b07d77439d9056531b43677d496caf3" - integrity sha512-+0WQAjKXLflXOobVkA41xAsvXVuMKZeoNSxGn97VDRLwmAPwb+moHV96fWF0iwju0kUZXNvbWFqbZfV07LitfQ== +"@abp/slugify@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.4.1.tgz#838904a9f8206eb73daf0976cc24be7e79594a98" + integrity sha512-lAcyFBiQdBVHDhoOa9QCqe3U/gd6R7bpxfSiZYI6aiGXh/2E32j9wZw70Vdl8zbBjdnzYgOd7Cniw9uMkead7w== dependencies: slugify "^1.6.6" -"@abp/star-rating-svg@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.3.0.tgz#195d8175df2c82384752439d0677e27480c3f49d" - integrity sha512-7xXLgz2jxdkBceelG+uvnD9aKwwTV2FIDVCmpRF3AF+0c7jaJvtyM9vlx6xdTFQSSzDnSGG10k/jeYP7O2AjUg== +"@abp/star-rating-svg@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.4.1.tgz#9d51107d8e349fbd5b347c05178b8a51faafc193" + integrity sha512-zerJfy1wjFnl3UN8rPrqxSWXv9q7Os1Y/G5hJWCX5JPfgka9lMATruvBpGBg6pEOUJGkYDS4NJamL4zDpOsu5g== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" star-rating-svg "^3.5.0" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/tui-editor@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.3.0.tgz#4fe3879111fe5ee312048841fca1cac43bb9bb62" - integrity sha512-6zGfaYSggalgpS9tKjESXT6w00KK7fKN71rG/hxRLNRpLGn5aapHdlAZRNEUgEFYo9czFdMW8POHpVA+sjcDMw== +"@abp/tui-editor@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.4.1.tgz#e7012ed869a5de852f454e14fbe1342c55d1c07b" + integrity sha512-lFcSkESJnAjIqiU12F19U8BYko/jLTXWN6s0uqvgQ14qhPPWMr13PidMjijj/x/m0wtFg2lHwbAFt3nl+6O+sA== dependencies: - "@abp/jquery" "~10.3.0" - "@abp/prismjs" "~10.3.0" + "@abp/jquery" "~10.4.1" + "@abp/prismjs" "~10.4.1" -"@abp/uppy@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.3.0.tgz#3b333f4f9618cc24dba989c6e2a9cb8c9bad0b2b" - integrity sha512-5RGZv2pk6xIFOGbIQdMvLBtpencqYICCtL0wuSP58zU97FQuQdbV3Du/2ZZvqCyxinJG+SwUBog4t9PAI1qM8w== +"@abp/uppy@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.4.1.tgz#81aaf6f0779fb5ba9d1ca5297e2d4ea57e17f4d5" + integrity sha512-olNAAg+jjBFRQYIUhjx4PR5TcGhA3aAOPMySCVSxDhA3nWavc1Tb60SkqlOHScqmxM6RonooUTRVtD9G5RKixA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" uppy "^5.1.2" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -2275,6 +2275,11 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + lowercase-keys@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json index dc870f6878..8b8d9a19a5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json @@ -406,7 +406,7 @@ "uniqueName": "MoveAllBlogPostsAsyncByBlogIdAndAssignToBlogId", "name": "MoveAllBlogPostsAsync", "httpMethod": "PUT", - "url": "api/cms-kit-admin/blogs/{id}/move-all-blog-posts", + "url": "api/cms-kit-admin/blogs/{blogId}/move-all-blog-posts", "supportedVersions": [], "parametersOnMethod": [ { @@ -436,7 +436,7 @@ "isOptional": false, "defaultValue": null, "constraintTypes": null, - "bindingSourceId": "ModelBinding", + "bindingSourceId": "Path", "descriptorName": "" }, { @@ -450,18 +450,6 @@ "constraintTypes": null, "bindingSourceId": "Query", "descriptorName": "" - }, - { - "nameOnMethod": "id", - "name": "id", - "jsonName": null, - "type": null, - "typeSimple": null, - "isOptional": false, - "defaultValue": null, - "constraintTypes": [], - "bindingSourceId": "Path", - "descriptorName": "" } ], "returnValue": { diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs index f90215ed38..9d56b60231 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs @@ -71,7 +71,7 @@ public class BlogAdminController : CmsKitAdminController, IBlogAdminAppService } [HttpPut] - [Route("{id}/move-all-blog-posts")] + [Route("{blogId}/move-all-blog-posts")] [Authorize(CmsKitAdminPermissions.Blogs.Delete)] public Task MoveAllBlogPostsAsync(Guid blogId, [FromQuery]Guid? assignToBlogId) { diff --git a/modules/docs/.abpstudio/state.json b/modules/docs/.abpstudio/state.json deleted file mode 100644 index fefde3c294..0000000000 --- a/modules/docs/.abpstudio/state.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "selectedKubernetesProfile": null, - "solutionRunner": { - "selectedProfile": "Default", - "targetFrameworks": [], - "applicationsStartingWithoutBuild": [], - "applicationBatchStartStates": [ - { - "profile": "Default", - "applicationOrFolder": "VoloDocs.Web", - "value": 0 - } - ], - "folderBatchStartStates": [] - } -} \ No newline at end of file diff --git a/modules/docs/app/VoloDocs.Web/package.json b/modules/docs/app/VoloDocs.Web/package.json index 0cbb4fa917..277b3c28ad 100644 --- a/modules/docs/app/VoloDocs.Web/package.json +++ b/modules/docs/app/VoloDocs.Web/package.json @@ -3,7 +3,7 @@ "name": "volo.docstestapp", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0", - "@abp/docs": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1", + "@abp/docs": "~10.4.1" } } diff --git a/modules/docs/app/VoloDocs.Web/yarn.lock b/modules/docs/app/VoloDocs.Web/yarn.lock index 55561a2a30..bd79633355 100644 --- a/modules/docs/app/VoloDocs.Web/yarn.lock +++ b/modules/docs/app/VoloDocs.Web/yarn.lock @@ -2,222 +2,222 @@ # yarn lockfile v1 -"@abp/anchor-js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.3.0.tgz#3c1c68fc41d770ac90ebf45d9aaa77b964dfb722" - integrity sha512-O4l8Kse3JOoSXKMyfzbQvErCrs3McCFevzYGakUb1cI/wgDfcMITO88oytSZl9R352haaKCpa7bC7EXHvMNgfw== +"@abp/anchor-js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.4.1.tgz#93e482ab4a0a34c5764284246d3515fd1ba78a88" + integrity sha512-6Vmwie96ThlGYGouP7dteTuviP0nIzg5gpMOSwL037Buu8M6a+ScXhQ9VsPdlTCf/kjwoCG1jfyIpCyp6YGmXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" anchor-js "^5.0.0" -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.3.0.tgz#7bfbfcae6a135065ee13afd1b7d3abc92176a203" - integrity sha512-Cah+jOzcG1KZh6PXWxo+BedU7gsqTuNlTDsCzBCk2NuV2/Blb5qumAwEP4GBKAEIY/XwcFQP/ubxuXMTNjr0jw== +"@abp/clipboard@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.4.1.tgz#eec933cef5a3d740e70e8736be6a68092dece645" + integrity sha512-xvAuXcY099h3IlBYKhLG2mwfZnTRmcu5RqBp+PJz33uOMM4ZcdzxQ5lAD73ukYX32SuSE0EJaSw5SV2jqT3RGw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" clipboard "^2.0.11" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/docs@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.3.0.tgz#9eb45987868543440a3cbc3ff518a581f304860c" - integrity sha512-tjCXLBJKi011FjvnpvilYBuZtmQU1n9h52Vtr4QyWjy26oUIoluc2XJwnth+C6L5l3a3zfmOnSnZmtaphAsp1g== +"@abp/docs@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.4.1.tgz#eac6736b3f5f0073b278df88327506b8a01064ce" + integrity sha512-U3R6IgLwj2Gkup1EgFWFAEoWMkC3P7EQztQV2EsMZmqirr4CgM0DxoBd29h/gN6cAYX3q7Vwwo+j59dR+JNssQ== dependencies: - "@abp/anchor-js" "~10.3.0" - "@abp/clipboard" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/popper.js" "~10.3.0" - "@abp/prismjs" "~10.3.0" + "@abp/anchor-js" "~10.4.1" + "@abp/clipboard" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/popper.js" "~10.4.1" + "@abp/prismjs" "~10.4.1" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.3.0.tgz#4ea33ece50cefcfc8a89c17b0d265b484b7205ee" - integrity sha512-WvkLyg+izkcX6sEOYy41B9lebiniYxMQZ++c+ysyOQKZzrYEEFOOLlnFS9jPqSKLv9cNRizhG1CCfiwoYCxufQ== +"@abp/prismjs@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.4.1.tgz#cb1144481ba34b1150ba2587ed3940d217d16abd" + integrity sha512-kC9VnxwTLLToiRXP6vHQD0nBoHQvQKHNssTHM4OsNjazToAVgqfHJyEqGsJwb1uulwR05vMspST2MbMFS8fx7g== dependencies: - "@abp/clipboard" "~10.3.0" - "@abp/core" "~10.3.0" + "@abp/clipboard" "~10.4.1" + "@abp/core" "~10.4.1" prismjs "^1.30.0" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -330,10 +330,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs b/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs index ea387b2794..9bd3c0aefd 100644 --- a/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs +++ b/modules/docs/src/Volo.Docs.Application.Contracts/Volo/Docs/Documents/DocumentParameterDto.cs @@ -9,5 +9,7 @@ namespace Volo.Docs.Documents public string DisplayName { get; set; } public Dictionary Values { get; set; } + + public Dictionary> DependsOn { get; set; } } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index 521ba70ffd..a68298a431 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -474,7 +474,12 @@
- @if (Model.DocumentPreferences != null && Model.DocumentPreferences.Parameters != null && Model.DocumentPreferences.Parameters.Any()) + @{ + var visibleParameters = Model.DocumentPreferences?.Parameters? + .Where(p => Model.IsParameterVisible(p)) + .ToList() ?? new List(); + } + @if (visibleParameters.Count > 0) {
@@ -488,10 +493,11 @@ @{ const int maxCellCount = 3; - var count = Model.DocumentPreferences.Parameters.Count; + var count = visibleParameters.Count; var rowCount = count / maxCellCount + (count % maxCellCount > 0 ? 1 : 0); var cellSize = 12 / (count > maxCellCount ? maxCellCount : count); - var latestCellSize = 12 / count - (rowCount - 1) * maxCellCount; + var lastRowCellCount = count - (rowCount - 1) * maxCellCount; + var latestCellSize = 12 / lastRowCellCount; string BuildParameterDivClass(int index) { @@ -503,7 +509,7 @@ for (var i = 0; i < count; ++i) { - var parameter = Model.DocumentPreferences.Parameters[i]; + var parameter = visibleParameters[i];
diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index 35e3c05f15..6eb3c02a8c 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -90,6 +90,29 @@ namespace Volo.Docs.Pages.Documents.Project public DocumentRenderParameters UserPreferences { get; set; } = new DocumentRenderParameters(); + private HashSet? _renderedParameterNamesCache; + private HashSet RenderedParameterNames => + _renderedParameterNamesCache ??= (DocumentPreferences?.Parameters?.Select(p => p.Name).ToHashSet() ?? new HashSet()); + + public virtual bool IsParameterVisible(DocumentParameterDto parameter) + { + return IsParameterVisibleGiven(parameter, UserPreferences); + } + + protected virtual bool IsParameterVisibleGiven(DocumentParameterDto parameter, IReadOnlyDictionary selectedValues) + { + if (parameter.DependsOn == null || parameter.DependsOn.Count == 0) + { + return true; + } + + return parameter.DependsOn.All(rule => + rule.Value == null || !RenderedParameterNames.Contains(rule.Key) + || (rule.Value.Count > 0 + && selectedValues.TryGetValue(rule.Key, out var current) + && rule.Value.Contains(current))); + } + public List AlternativeOptionLinkQueries { get; set; } = new List(); public bool FullSearchEnabled { get; set; } @@ -827,7 +850,8 @@ namespace Volo.Docs.Pages.Documents.Project { Name = parameter.Name, DisplayName = parameter.DisplayName, - Values = new Dictionary() + Values = new Dictionary(), + DependsOn = parameter.DependsOn }; foreach (var value in parameter.Values) @@ -847,13 +871,13 @@ namespace Volo.Docs.Pages.Documents.Project { if (!DocumentPreferences?.Parameters?.Any() ?? true) { - return; + return; } - AlternativeOptionLinkQueries = CollectAlternativeOptionLinksRecursively(); + AlternativeOptionLinkQueries = CollectAlternativeOptionLinksRecursively(0, new Dictionary()); } - private List CollectAlternativeOptionLinksRecursively(int index = 0) + private List CollectAlternativeOptionLinksRecursively(int index, Dictionary selected) { if (index >= DocumentPreferences.Parameters.Count) { @@ -861,13 +885,20 @@ namespace Volo.Docs.Pages.Documents.Project } var option = DocumentPreferences.Parameters[index]; + + if (!IsParameterVisibleGiven(option, selected)) + { + return CollectAlternativeOptionLinksRecursively(index + 1, selected); + } + var queries = new List(); foreach (var key in option.Values.Keys) { - var linkQuery = new StringBuilder($"{option.Name}={key}"); + var linkQuery = $"{option.Name}={key}"; - var restOfQueries = CollectAlternativeOptionLinksRecursively(index + 1); + var nextSelected = new Dictionary(selected) { [option.Name] = key }; + var restOfQueries = CollectAlternativeOptionLinksRecursively(index + 1, nextSelected); if (restOfQueries.Any()) { @@ -878,7 +909,7 @@ namespace Volo.Docs.Pages.Documents.Project } else { - queries.Add($"{linkQuery}"); + queries.Add(linkQuery); } } diff --git a/modules/feature-management/Volo.Abp.FeatureManagement.slnx b/modules/feature-management/Volo.Abp.FeatureManagement.slnx index f246fb78b0..8b504d625b 100644 --- a/modules/feature-management/Volo.Abp.FeatureManagement.slnx +++ b/modules/feature-management/Volo.Abp.FeatureManagement.slnx @@ -2,6 +2,9 @@ + + + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs index af0bce93db..dd139e9b61 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs @@ -38,6 +38,7 @@ public class FeatureAppService : FeatureManagementAppServiceBase, IFeatureAppSer { var groupDto = CreateFeatureGroupDto(group); + var includedFeatures = new HashSet(); foreach (var featureDefinition in group.GetFeaturesWithChildren()) { if (providerName == TenantFeatureValueProvider.ProviderName && @@ -48,6 +49,18 @@ public class FeatureAppService : FeatureManagementAppServiceBase, IFeatureAppSer continue; } + if (featureDefinition.AllowedProviders.Any() && + !featureDefinition.AllowedProviders.Contains(providerName)) + { + continue; + } + + if (featureDefinition.Parent != null && !includedFeatures.Contains(featureDefinition.Parent)) + { + continue; + } + + includedFeatures.Add(featureDefinition); var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey); groupDto.Features.Add(CreateFeatureDto(feature, featureDefinition)); } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/AbpFeatureManagementBlazorMudBlazorServerModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/AbpFeatureManagementBlazorMudBlazorServerModule.cs new file mode 100644 index 0000000000..68bd8d2a84 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/AbpFeatureManagementBlazorMudBlazorServerModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server; + +[DependsOn( + typeof(AbpFeatureManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsServerThemingMudBlazorModule) +)] +public class AbpFeatureManagementBlazorMudBlazorServerModule : AbpModule +{ +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/FodyWeavers.xml b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/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/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server.csproj b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server.csproj new file mode 100644 index 0000000000..43bddde3b2 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server/Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server.csproj @@ -0,0 +1,15 @@ + + + + + + + net10.0 + + + + + + + + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/AbpFeatureManagementBlazorMudBlazorWebAssemblyModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/AbpFeatureManagementBlazorMudBlazorWebAssemblyModule.cs new file mode 100644 index 0000000000..81ff2be458 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/AbpFeatureManagementBlazorMudBlazorWebAssemblyModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly; + +[DependsOn( + typeof(AbpFeatureManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule), + typeof(AbpFeatureManagementHttpApiClientModule) +)] +public class AbpFeatureManagementBlazorMudBlazorWebAssemblyModule : AbpModule +{ +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/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/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly.csproj b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly.csproj new file mode 100644 index 0000000000..4e5f72aa1a --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + + + + + + + + + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/AbpFeatureManagementBlazorMudBlazorModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/AbpFeatureManagementBlazorMudBlazorModule.cs new file mode 100644 index 0000000000..08d86950d1 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/AbpFeatureManagementBlazorMudBlazorModule.cs @@ -0,0 +1,34 @@ +using Localization.Resources.AbpUi; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor.Settings; +using Volo.Abp.FeatureManagement.Localization; +using Volo.Abp.Features; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.SettingManagement.Blazor.MudBlazor; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpFeatureManagementApplicationContractsModule), + typeof(AbpFeaturesModule), + typeof(AbpSettingManagementBlazorMudBlazorModule) +)] +public class AbpFeatureManagementBlazorMudBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Contributors.Add(new FeatureSettingManagementComponentContributor()); + }); + + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes(typeof(AbpUiResource)); + }); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/AbpFeatureManagementComponentBase.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/AbpFeatureManagementComponentBase.cs new file mode 100644 index 0000000000..68ed6fc8fc --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/AbpFeatureManagementComponentBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.Components; +using Volo.Abp.FeatureManagement.Localization; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor; + +public abstract class AbpFeatureManagementComponentBase : AbpComponentBase +{ + protected AbpFeatureManagementComponentBase() + { + LocalizationResource = typeof(AbpFeatureManagementResource); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor new file mode 100644 index 0000000000..fd4711c104 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor @@ -0,0 +1,83 @@ +@using global::MudBlazor +@using JetBrains.Annotations +@using Volo.Abp.Validation.StringValues +@using Microsoft.Extensions.Localization +@using Volo.Abp.FeatureManagement.Localization +@inherits AbpFeatureManagementComponentBase + + + + @L["Features"]@ProviderKeyDisplayName + + + @if (Groups == null || !Groups.Any()) + { + @L["NoFeatureFoundMessage"] + } + else + { + + @foreach (var group in Groups) + { + + + @group.DisplayName + @foreach (var feature in group.Features) + { + var disabled = IsDisabled(feature); +
+ @if (feature.ValueType is FreeTextStringValueType) + { + + @if (feature.Description != null) + { + @feature.Description + } + } + + @if (feature.ValueType is SelectionStringValueType) + { + var items = ((SelectionStringValueType)feature.ValueType).ItemSource.Items; + var selectedValue = SelectionStringValues[feature.Name]; + + @foreach (var item in items) + { + + @CreateStringLocalizer(item.DisplayText.ResourceName).GetString(item.DisplayText.Name) + + } + + @if (feature.Description != null) + { + @feature.Description + } + } + + @if (feature.ValueType is ToggleStringValueType) + { + + @if (feature.Description != null) + { + @feature.Description + } + } +
+ } +
+
+ } +
+ } +
+ + @L["Cancel"] + @L["ResetToDefault"] + @L["Save"] + +
diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs new file mode 100644 index 0000000000..c913b05f3c --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureManagementModal.razor.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.AspNetCore.Components.Web.Configuration; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor; +using Volo.Abp.Features; +using Volo.Abp.Localization; +using Volo.Abp.Validation.StringValues; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components; + +public partial class FeatureManagementModal +{ + [Inject] protected IFeatureAppService FeatureAppService { get; set; } = default!; + + [Inject] protected IUiMessageService UiMessageService { get; set; } = default!; + + [Inject] protected IStringLocalizerFactory HtmlLocalizerFactory { get; set; } = default!; + + [Inject] protected IOptions LocalizationOptions { get; set; } = default!; + + [Inject] protected ICurrentApplicationConfigurationCacheResetService CurrentApplicationConfigurationCacheResetService { get; set; } = default!; + + [Inject] protected ISnackbar Snackbar { get; set; } = default!; + + [Inject] protected IDialogService DialogService { get; set; } = default!; + + protected bool _isVisible; + protected int _activeTabIndex = 0; + + protected string? ProviderName; + protected string? ProviderKey; + protected string? ProviderKeyDisplayName; + + protected List? Groups { get; set; } + + protected Dictionary ToggleValues = new(); + + protected Dictionary SelectionStringValues = new(); + + public virtual async Task OpenAsync(string providerName, string? providerKey = null, string? providerKeyDisplayName = null) + { + try + { + ProviderName = providerName; + ProviderKey = providerKey; + + ToggleValues = new Dictionary(); + SelectionStringValues = new Dictionary(); + + if (!providerKeyDisplayName.IsNullOrWhiteSpace()) + { + ProviderKeyDisplayName = $" - {providerKeyDisplayName}"; + } + else + { + ProviderKeyDisplayName = string.Empty; + } + + var result = await FeatureAppService.GetAsync(ProviderName, ProviderKey); + + Groups = result?.Groups ?? new List(); + + if (Groups.Any()) + { + _activeTabIndex = 0; + } + + foreach (var featureGroupDto in Groups) + { + foreach (var featureDto in featureGroupDto.Features) + { + if (featureDto.ValueType is ToggleStringValueType) + { + ToggleValues.Add(featureDto.Name, bool.Parse(featureDto.Value)); + } + + if (featureDto.ValueType is SelectionStringValueType) + { + SelectionStringValues.Add(featureDto.Name, featureDto.Value); + } + } + } + + _isVisible = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + public virtual Task CloseModal() + { + _isVisible = false; + return InvokeAsync(StateHasChanged); + } + + protected virtual async Task SaveAsync() + { + try + { + if (Groups == null) + { + return; + } + + var features = new UpdateFeaturesDto + { + Features = Groups.SelectMany(g => g.Features).Select(f => new UpdateFeatureDto + { + Name = f.Name, + Value = f.ValueType is ToggleStringValueType ? ToggleValues[f.Name].ToString() : + f.ValueType is SelectionStringValueType ? SelectionStringValues[f.Name] : f.Value + }).ToList() + }; + + await FeatureAppService.UpdateAsync(ProviderName!, ProviderKey, features); + + await CurrentApplicationConfigurationCacheResetService.ResetAsync(); + + _isVisible = false; + await InvokeAsync(StateHasChanged); + Snackbar.Add(L["SavedSuccessfully"], Severity.Success); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + public virtual async Task DeleteAsync(string providerName, string? providerKey = null) + { + try + { + var confirmed = await DialogService.ShowMessageBoxAsync( + L["AreYouSure"], + L["AreYouSureToResetToDefault"], + yesText: L["Yes"], + cancelText: L["Cancel"]); + + if (confirmed != true) + { + return; + } + + await FeatureAppService.DeleteAsync(ProviderName!, ProviderKey); + Snackbar.Add(L["ResetedToDefault"], Severity.Success); + + await CloseModal(); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual string GetNormalizedGroupName(string name) + { + return "FeatureGroup_" + name.Replace(".", "_"); + } + + protected virtual string GetFeatureStyles(FeatureDto feature) + { + return $"margin-inline-start: {feature.Depth * 20}px"; + } + + protected virtual bool IsDisabled(FeatureDto feature) + { + return feature.Value != null && + feature.Provider.Name != null && + feature.Provider.Name != ProviderName && + feature.Provider.Name != DefaultValueFeatureValueProvider.ProviderName; + } + + public virtual string GetShownName(FeatureDto featureDto) + { + return !IsDisabled(featureDto) + ? featureDto.DisplayName + : $"{featureDto.DisplayName} ({featureDto.Provider.Name})"; + } + + protected virtual async Task OnFeatureValueChangedAsync(string? value, FeatureDto feature) + { + if (feature?.ValueType?.Validator.IsValid(value) == true) + { + feature.Value = value; + } + else + { + await UiMessageService.Warn(L["Volo.Abp.FeatureManagement:InvalidFeatureValue", feature.DisplayName]); + } + } + + protected virtual Task OnSelectedValueChangedAsync(bool value, FeatureDto feature) + { + ToggleValues[feature.Name] = value; + + if (value) + { + CheckParents(feature.ParentName); + } + else + { + UncheckChildren(feature.Name); + } + + return Task.CompletedTask; + } + + protected virtual void CheckParents(string? parentName) + { + if (parentName.IsNullOrWhiteSpace() || Groups == null) + { + return; + } + + foreach (var featureGroupDto in Groups) + { + foreach (var featureDto in featureGroupDto.Features) + { + if (featureDto.Name == parentName && ToggleValues.ContainsKey(featureDto.Name)) + { + ToggleValues[featureDto.Name] = true; + CheckParents(featureDto.ParentName); + } + } + } + } + + protected virtual void UncheckChildren(string featureName) + { + if (Groups == null) + { + return; + } + + foreach (var featureGroupDto in Groups) + { + foreach (var featureDto in featureGroupDto.Features) + { + if (featureDto.ParentName == featureName && ToggleValues.ContainsKey(featureDto.Name)) + { + ToggleValues[featureDto.Name] = false; + UncheckChildren(featureDto.Name); + } + } + } + } + + protected virtual IStringLocalizer CreateStringLocalizer(string resourceName) + { + return HtmlLocalizerFactory.CreateByResourceNameOrNull(resourceName) ?? + HtmlLocalizerFactory.CreateDefaultOrNull(); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor new file mode 100644 index 0000000000..b1ebc29502 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor @@ -0,0 +1,15 @@ +@using global::MudBlazor +@inherits Volo.Abp.AspNetCore.Components.AbpComponentBase + +@if (Settings != null && Settings.HasManageHostFeaturesPermission) +{ + @L["ManageHostFeaturesText"] + + @L["ManageHostFeatures"] + + + +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs new file mode 100644 index 0000000000..8c04c6868d --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Volo.Abp.AspNetCore.Components; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components; +using Volo.Abp.FeatureManagement.Localization; +using Volo.Abp.Features; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components.FeatureSettingGroup; + +public partial class FeatureSettingManagementComponent : AbpComponentBase +{ + [Inject] + protected PermissionChecker PermissionChecker { get; set; } = default!; + + protected FeatureManagementModal? FeatureManagementModal; + + protected FeatureSettingViewModel? Settings; + + public FeatureSettingManagementComponent() + { + LocalizationResource = typeof(AbpFeatureManagementResource); + } + + protected async override Task OnInitializedAsync() + { + Settings = new FeatureSettingViewModel + { + HasManageHostFeaturesPermission = await AuthorizationService.IsGrantedAsync(FeatureManagementPermissions.ManageHostFeatures) + }; + } + + protected virtual async Task OnManageHostFeaturesClicked() + { + if (FeatureManagementModal != null) + { + await FeatureManagementModal.OpenAsync(TenantFeatureValueProvider.ProviderName); + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingViewModel.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingViewModel.cs new file mode 100644 index 0000000000..b933647b73 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Components/FeatureSettingGroup/FeatureSettingViewModel.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components.FeatureSettingGroup; + +public class FeatureSettingViewModel +{ + public bool HasManageHostFeaturesPermission { get; set; } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/FodyWeavers.xml b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/FodyWeavers.xsd b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/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/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Settings/FeatureSettingManagementComponentContributor.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Settings/FeatureSettingManagementComponentContributor.cs new file mode 100644 index 0000000000..50bec2f38c --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Settings/FeatureSettingManagementComponentContributor.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components.FeatureSettingGroup; +using Volo.Abp.FeatureManagement.Localization; +using Volo.Abp.SettingManagement.Blazor.MudBlazor; + +namespace Volo.Abp.FeatureManagement.Blazor.MudBlazor.Settings; + +public class FeatureSettingManagementComponentContributor: ISettingComponentContributor +{ + public virtual async Task ConfigureAsync(SettingComponentCreationContext context) + { + if (!await CheckPermissionsInternalAsync(context)) + { + return; + } + + var l = context.ServiceProvider.GetRequiredService>(); + context.Groups.Add( + new SettingComponentGroup( + "Volo.Abp.Feature", + l["Permission:FeatureManagement"], + typeof(FeatureSettingManagementComponent) + ) + ); + } + + public virtual async Task CheckPermissionsAsync(SettingComponentCreationContext context) + { + return await CheckPermissionsInternalAsync(context); + } + + protected virtual async Task CheckPermissionsInternalAsync(SettingComponentCreationContext context) + { + var authorizationService = context.ServiceProvider.GetRequiredService(); + + return await authorizationService.IsGrantedAsync(FeatureManagementPermissions.ManageHostFeatures); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Volo.Abp.FeatureManagement.Blazor.MudBlazor.csproj b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Volo.Abp.FeatureManagement.Blazor.MudBlazor.csproj new file mode 100644 index 0000000000..3a568524e3 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/Volo.Abp.FeatureManagement.Blazor.MudBlazor.csproj @@ -0,0 +1,20 @@ + + + + + + + net10.0 + + + + + + + + + + + + + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/_Imports.razor b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/_Imports.razor new file mode 100644 index 0000000000..b1be76fe73 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor.MudBlazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using MudBlazor +@using Volo.Abp.MudBlazorUI diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs index c105779546..9ac5a894d4 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs @@ -93,8 +93,17 @@ public class FeatureManager : IFeatureManager, ISingletonDependency foreach (var feature in featureDefinitions) { + var featureProviderList = feature.AllowedProviders.Any() + ? providerList.Where(p => feature.AllowedProviders.Contains(p.Name)).ToList() + : providerList; + + if (!featureProviderList.Any()) + { + continue; + } + var featureNameValueWithGrantedProvider = new FeatureNameValueWithGrantedProvider(feature.Name, null); - foreach (var provider in providerList) + foreach (var provider in featureProviderList) { string pk = null; if (provider.Compatible(providerName)) @@ -137,6 +146,11 @@ public class FeatureManager : IFeatureManager, ISingletonDependency throw new FeatureValueInvalidException(feature.DisplayName.Localize(StringLocalizerFactory)); } + if (feature.AllowedProviders.Any() && !feature.AllowedProviders.Contains(providerName)) + { + throw new AbpException($"The feature named '{name}' is not compatible with the provider named '{providerName}'"); + } + var providers = Enumerable .Reverse(Providers) .SkipWhile(p => p.Name != providerName) @@ -195,6 +209,11 @@ public class FeatureManager : IFeatureManager, ISingletonDependency providers = providers.SkipWhile(c => c.Name != providerName); } + if (feature.AllowedProviders.Any()) + { + providers = providers.Where(p => feature.AllowedProviders.Contains(p.Name)); + } + var featureNameValueWithGrantedProvider = new FeatureNameValueWithGrantedProvider(name, null); foreach (var provider in providers) { diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs index f3475d6ded..6803ef748e 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs @@ -112,6 +112,72 @@ public class FeatureAppService_Tests : FeatureManagementApplicationTestBase Assert.Null(exception); } + [Fact] + public async Task GetAsync_Should_Not_Return_Features_With_Disallowed_Provider() + { + Login(_testData.User1Id); + + var editionFeatures = await _featureAppService.GetAsync( + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString()); + + editionFeatures.Groups.SelectMany(g => g.Features) + .ShouldNotContain(feature => feature.Name == TestFeatureDefinitionProvider.TenantOnlyFeature); + + var tenantFeatures = await _featureAppService.GetAsync( + TenantFeatureValueProvider.ProviderName, + Guid.NewGuid().ToString()); + + tenantFeatures.Groups.SelectMany(g => g.Features) + .ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.TenantOnlyFeature); + } + + [Fact] + public async Task GetAsync_Should_Not_Return_Orphan_Child_When_Parent_Is_Disallowed() + { + Login(_testData.User1Id); + + var editionFeatures = await _featureAppService.GetAsync( + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString()); + + var featureNames = editionFeatures.Groups.SelectMany(g => g.Features).Select(f => f.Name).ToList(); + featureNames.ShouldNotContain(TestFeatureDefinitionProvider.TenantOnlyParentFeature); + featureNames.ShouldNotContain(TestFeatureDefinitionProvider.OrphanChildOfTenantOnly); + + var tenantFeatures = await _featureAppService.GetAsync( + TenantFeatureValueProvider.ProviderName, + Guid.NewGuid().ToString()); + + var tenantNames = tenantFeatures.Groups.SelectMany(g => g.Features).Select(f => f.Name).ToList(); + tenantNames.ShouldContain(TestFeatureDefinitionProvider.TenantOnlyParentFeature); + tenantNames.ShouldContain(TestFeatureDefinitionProvider.OrphanChildOfTenantOnly); + } + + [Fact] + public async Task UpdateAsync_Should_Throw_Exception_For_Disallowed_Provider() + { + Login(_testData.User1Id); + + await Assert.ThrowsAsync(async () => + { + await _featureAppService.UpdateAsync( + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString(), + new UpdateFeaturesDto + { + Features = new List + { + new UpdateFeatureDto + { + Name = TestFeatureDefinitionProvider.TenantOnlyFeature, + Value = true.ToString().ToLowerInvariant() + } + } + }); + }); + } + private void Login(Guid userId) { _currentUser.Id.Returns(userId); diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.Domain.Tests/Volo/Abp/FeatureManagement/FeatureManager_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.Domain.Tests/Volo/Abp/FeatureManagement/FeatureManager_Tests.cs index fd4ea324ea..5bebe03ded 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.Domain.Tests/Volo/Abp/FeatureManagement/FeatureManager_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.Domain.Tests/Volo/Abp/FeatureManagement/FeatureManager_Tests.cs @@ -217,7 +217,118 @@ public class FeatureManager_Tests : FeatureManagementDomainTestBase { await _featureManager.SetAsync(TestFeatureDefinitionProvider.EmailSupport, "true", "UndefinedProvider", "Test"); }); - + exception.Message.ShouldBe("Unknown feature value provider: UndefinedProvider"); } + + [Fact] + public async Task Set_Should_Throw_Exception_If_Provider_Not_In_AllowedProviders() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _featureManager.SetAsync( + TestFeatureDefinitionProvider.TenantOnlyFeature, + "true", + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Enterprise.ToString()); + }); + + exception.Message.ShouldContain(TestFeatureDefinitionProvider.TenantOnlyFeature); + exception.Message.ShouldContain(EditionFeatureValueProvider.ProviderName); + } + + [Fact] + public async Task Set_Should_Allow_Setting_For_Provider_In_AllowedProviders() + { + var tenantId = Guid.NewGuid(); + + await _featureManager.SetAsync( + TestFeatureDefinitionProvider.TenantOnlyFeature, + "true", + TenantFeatureValueProvider.ProviderName, + tenantId.ToString()); + + (await _featureManager.GetOrNullForTenantAsync( + TestFeatureDefinitionProvider.TenantOnlyFeature, + tenantId)).ShouldBe("true"); + } + + [Fact] + public async Task GetAllWithProvider_Should_Not_Return_Features_With_Disallowed_Provider() + { + var editionFeatures = await _featureManager.GetAllWithProviderAsync( + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Enterprise.ToString()); + + editionFeatures.ShouldNotContain(x => x.Name == TestFeatureDefinitionProvider.TenantOnlyFeature); + + var tenantId = Guid.NewGuid(); + await _featureManager.SetForTenantAsync( + tenantId, + TestFeatureDefinitionProvider.TenantOnlyFeature, + "true"); + + var tenantFeatures = await _featureManager.GetAllWithProviderAsync( + TenantFeatureValueProvider.ProviderName, + tenantId.ToString()); + + tenantFeatures.ShouldContain(x => + x.Name == TestFeatureDefinitionProvider.TenantOnlyFeature && x.Value == "true"); + } + + [Fact] + public async Task GetOrNullWithProvider_Should_Not_Read_From_Disallowed_Provider() + { + var editionId = TestEditionIds.Enterprise.ToString(); + await _featureValueRepository.InsertAsync(new FeatureValue( + Guid.NewGuid(), + TestFeatureDefinitionProvider.TenantOnlyFeature, + "true", + EditionFeatureValueProvider.ProviderName, + editionId)); + + var value = await _featureManager.GetOrNullWithProviderAsync( + TestFeatureDefinitionProvider.TenantOnlyFeature, + EditionFeatureValueProvider.ProviderName, + editionId); + + value.Value.ShouldBeNull(); + } + + [Fact] + public async Task GetOrNullForEdition_Should_Read_Value_For_Edition_Only_Feature() + { + var editionId = Guid.NewGuid(); + + await _featureManager.SetForEditionAsync( + editionId, + TestFeatureDefinitionProvider.EditionOnlyFeature, + "true"); + + (await _featureManager.GetOrNullForEditionAsync( + TestFeatureDefinitionProvider.EditionOnlyFeature, + editionId)).ShouldBe("true"); + } + + [Fact] + public async Task Set_Should_Not_Clear_Tenant_Value_Due_To_Stale_Disallowed_Provider_Fallback() + { + var tenantId = Guid.NewGuid(); + + await _featureValueRepository.InsertAsync(new FeatureValue( + Guid.NewGuid(), + TestFeatureDefinitionProvider.TenantOnlyFeature, + "true", + EditionFeatureValueProvider.ProviderName, + null)); + + await _featureManager.SetForTenantAsync( + tenantId, + TestFeatureDefinitionProvider.TenantOnlyFeature, + "true"); + + (await _featureManager.GetOrNullForTenantAsync( + TestFeatureDefinitionProvider.TenantOnlyFeature, + tenantId)).ShouldBe("true"); + } } diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/TestFeatureDefinitionProvider.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/TestFeatureDefinitionProvider.cs index 0a19e56956..43d8416ea1 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/TestFeatureDefinitionProvider.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/TestFeatureDefinitionProvider.cs @@ -12,6 +12,10 @@ public class TestFeatureDefinitionProvider : FeatureDefinitionProvider public const string UserCount = "UserCount"; public const string ProjectCount = "ProjectCount"; public const string BackupCount = "BackupCount"; + public const string TenantOnlyFeature = "TenantOnlyFeature"; + public const string TenantOnlyParentFeature = "TenantOnlyParentFeature"; + public const string OrphanChildOfTenantOnly = "OrphanChildOfTenantOnly"; + public const string EditionOnlyFeature = "EditionOnlyFeature"; public override void Define(IFeatureDefinitionContext context) { @@ -22,6 +26,29 @@ public class TestFeatureDefinitionProvider : FeatureDefinitionProvider valueType: new ToggleStringValueType() ); + group.AddFeature( + TenantOnlyFeature, + defaultValue: false.ToString().ToLowerInvariant(), + valueType: new ToggleStringValueType() + ).WithProviders(TenantFeatureValueProvider.ProviderName); + + var tenantOnlyParent = group.AddFeature( + TenantOnlyParentFeature, + defaultValue: false.ToString().ToLowerInvariant(), + valueType: new ToggleStringValueType() + ).WithProviders(TenantFeatureValueProvider.ProviderName); + + tenantOnlyParent.CreateChild( + OrphanChildOfTenantOnly, + defaultValue: false.ToString().ToLowerInvariant(), + valueType: new ToggleStringValueType()); + + group.AddFeature( + EditionOnlyFeature, + defaultValue: false.ToString().ToLowerInvariant(), + valueType: new ToggleStringValueType() + ).WithProviders(EditionFeatureValueProvider.ProviderName); + var emailSupport = group.AddFeature( EmailSupport, "true", diff --git a/modules/identity/.abpstudio/state.json b/modules/identity/.abpstudio/state.json deleted file mode 100644 index 11b53dd162..0000000000 --- a/modules/identity/.abpstudio/state.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "selectedRunnerProfile": null, - "selectedKubernetesProfile": null -} \ No newline at end of file diff --git a/modules/identity/Volo.Abp.Identity.slnx b/modules/identity/Volo.Abp.Identity.slnx index 3205de45ba..968864e7f7 100644 --- a/modules/identity/Volo.Abp.Identity.slnx +++ b/modules/identity/Volo.Abp.Identity.slnx @@ -3,6 +3,9 @@ + + + diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs index 55946e21ab..6a175303e5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs @@ -22,4 +22,6 @@ public interface IIdentityUserAppService Task FindByUsernameAsync(string userName); Task FindByEmailAsync(string email); + + Task FindByIdAsync(Guid id); } diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs index f9b5a53a46..23499937f5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs @@ -180,6 +180,14 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe ); } + [Authorize(IdentityPermissions.Users.Default)] + public virtual async Task FindByIdAsync(Guid id) + { + return ObjectMapper.Map( + await UserManager.FindByIdAsync(id.ToString()) + ); + } + protected virtual async Task UpdateUserByInput(IdentityUser user, IdentityUserCreateOrUpdateDtoBase input) { if (!string.Equals(user.Email, input.Email, StringComparison.InvariantCultureIgnoreCase)) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProvider.cs new file mode 100644 index 0000000000..45e3e8eccb --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProvider.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Volo.Abp.Identity; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace Volo.Abp.Identity.AspNetCore; + +/// +/// Single-use email 2FA code provider; replaces the ASP.NET Core Identity TOTP-based +/// EmailTokenProvider<TUser> under . +/// +public class AbpEmailTwoFactorTokenProvider : AbpTwoFactorTokenProvider +{ + public const string ProviderName = "AbpEmailTwoFactor"; + + public override string Name => ProviderName; + + public AbpEmailTwoFactorTokenProvider( + IOptions options, + IIdentityUserRepository userRepository, + ICancellationTokenProvider cancellationTokenProvider, + IClock clock, + IDataProtectionProvider dataProtectionProvider) + : base(options.Value, userRepository, cancellationTokenProvider, clock, dataProtectionProvider) + { + } + + public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, IdentityUser user) + { + var email = await manager.GetEmailAsync(user); + return !string.IsNullOrWhiteSpace(email) && await manager.IsEmailConfirmedAsync(user); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProviderOptions.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProviderOptions.cs new file mode 100644 index 0000000000..6ec9e96e1a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProviderOptions.cs @@ -0,0 +1,5 @@ +namespace Volo.Abp.Identity.AspNetCore; + +public class AbpEmailTwoFactorTokenProviderOptions : AbpTwoFactorTokenProviderOptions +{ +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs index 3284a31601..eb8a1a7c54 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -23,6 +23,8 @@ public class AbpIdentityAspNetCoreModule : AbpModule .AddTokenProvider(AbpPasswordResetTokenProvider.ProviderName) .AddTokenProvider(AbpEmailConfirmationTokenProvider.ProviderName) .AddTokenProvider(AbpChangeEmailTokenProvider.ProviderName) + .AddTokenProvider(TokenOptions.DefaultEmailProvider) + .AddTokenProvider(TokenOptions.DefaultPhoneProvider) .AddSignInManager() .AddUserValidator(); }); diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProvider.cs new file mode 100644 index 0000000000..068e94cb20 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProvider.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Volo.Abp.Identity; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace Volo.Abp.Identity.AspNetCore; + +/// +/// Single-use phone 2FA code provider; replaces the ASP.NET Core Identity TOTP-based +/// PhoneNumberTokenProvider<TUser> under . +/// +public class AbpPhoneNumberTwoFactorTokenProvider : AbpTwoFactorTokenProvider +{ + public const string ProviderName = "AbpPhoneNumberTwoFactor"; + + public override string Name => ProviderName; + + public AbpPhoneNumberTwoFactorTokenProvider( + IOptions options, + IIdentityUserRepository userRepository, + ICancellationTokenProvider cancellationTokenProvider, + IClock clock, + IDataProtectionProvider dataProtectionProvider) + : base(options.Value, userRepository, cancellationTokenProvider, clock, dataProtectionProvider) + { + } + + public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, IdentityUser user) + { + var phoneNumber = await manager.GetPhoneNumberAsync(user); + return !string.IsNullOrWhiteSpace(phoneNumber) && await manager.IsPhoneNumberConfirmedAsync(user); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProviderOptions.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProviderOptions.cs new file mode 100644 index 0000000000..a477ecc594 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProviderOptions.cs @@ -0,0 +1,5 @@ +namespace Volo.Abp.Identity.AspNetCore; + +public class AbpPhoneNumberTwoFactorTokenProviderOptions : AbpTwoFactorTokenProviderOptions +{ +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSingleActiveTokenProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSingleActiveTokenProvider.cs index aa6454085c..835b52b3b0 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSingleActiveTokenProvider.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSingleActiveTokenProvider.cs @@ -51,7 +51,7 @@ public abstract class AbpSingleActiveTokenProvider : DataProtectorTokenProvider< var tokenHash = ComputeSha256Hash(token); user.SetToken(InternalLoginProvider, Options.Name + ":" + purpose, tokenHash); - await manager.UpdateAsync(user); + (await manager.UpdateAsync(user)).CheckErrors(); return token; } diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProvider.cs new file mode 100644 index 0000000000..11fabf3c6b --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProvider.cs @@ -0,0 +1,224 @@ +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Identity; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace Volo.Abp.Identity.AspNetCore; + +/// +/// Base class for ABP two-factor verification code providers (e.g. Email, Phone). +/// Generates a numeric OTP, stores it encrypted under +/// with an absolute UTC expiration, and removes the stored entry on successful +/// validation (single-use). Expected to be combined with Identity lockout for +/// rate-limiting. +/// +public abstract class AbpTwoFactorTokenProvider : IUserTwoFactorTokenProvider +{ + public const string InternalLoginProvider = "[AbpTwoFactorToken]"; + + protected const char StoredValueSeparator = '|'; + + protected const string DataProtectionPurposeRoot = "Volo.Abp.Identity.AspNetCore.AbpTwoFactorTokenProvider"; + + protected AbpTwoFactorTokenProviderOptions Options { get; } + + protected IIdentityUserRepository UserRepository { get; } + + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + protected IClock Clock { get; } + + protected IDataProtectionProvider DataProtectionProvider { get; } + + /// Unique provider name; used as the stored-token key prefix and DataProtection purpose segment. + public abstract string Name { get; } + + protected AbpTwoFactorTokenProvider( + AbpTwoFactorTokenProviderOptions options, + IIdentityUserRepository userRepository, + ICancellationTokenProvider cancellationTokenProvider, + IClock clock, + IDataProtectionProvider dataProtectionProvider) + { + Options = options; + UserRepository = userRepository; + CancellationTokenProvider = cancellationTokenProvider; + Clock = clock; + DataProtectionProvider = dataProtectionProvider; + } + + public abstract Task CanGenerateTwoFactorTokenAsync(UserManager manager, IdentityUser user); + + public virtual async Task GenerateAsync(string purpose, UserManager manager, IdentityUser user) + { + var code = GenerateNumericCode(Options.CodeLength); + var protectedCode = CreateProtector(purpose).Protect(code); + var expiresAtUnixSeconds = ToUnixSeconds(Clock.Now.Add(Options.TokenLifespan)); + var storedValue = protectedCode + StoredValueSeparator + expiresAtUnixSeconds.ToString(CultureInfo.InvariantCulture); + + await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Tokens, CancellationTokenProvider.Token); + user.SetToken(InternalLoginProvider, GetTokenName(purpose), storedValue); + + (await manager.UpdateAsync(user)).CheckErrors(); + + return code; + } + + public virtual async Task ValidateAsync(string purpose, string token, UserManager manager, IdentityUser user) + { + if (string.IsNullOrEmpty(token)) + { + return false; + } + + await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Tokens, CancellationTokenProvider.Token); + + var tokenName = GetTokenName(purpose); + var stored = user.FindToken(InternalLoginProvider, tokenName)?.Value; + if (stored == null) + { + return false; + } + + if (!TryParseStoredValue(stored, out var protectedCode, out var expiresAtUnixSeconds)) + { + await TryRemoveStoredTokenAsync(manager, user, tokenName); + return false; + } + + if (ToUnixSeconds(Clock.Now) >= expiresAtUnixSeconds) + { + await TryRemoveStoredTokenAsync(manager, user, tokenName); + return false; + } + + string storedCode; + try + { + storedCode = CreateProtector(purpose).Unprotect(protectedCode); + } + catch (CryptographicException) + { + await TryRemoveStoredTokenAsync(manager, user, tokenName); + return false; + } + catch (FormatException) + { + await TryRemoveStoredTokenAsync(manager, user, tokenName); + return false; + } + + var storedBytes = Encoding.UTF8.GetBytes(storedCode); + var inputBytes = Encoding.UTF8.GetBytes(token); + if (storedBytes.Length != inputBytes.Length || + !CryptographicOperations.FixedTimeEquals(storedBytes, inputBytes)) + { + // Keep stored entry so the user can retry until expiration. Callers must rate-limit. + return false; + } + + // Translate ConcurrencyStamp failure (another request won the consume race) to false, + // so legitimate concurrent verification doesn't surface as a 500. + try + { + await RemoveStoredTokenAsync(manager, user, tokenName); + } + catch (AbpIdentityResultException) + { + return false; + } + + return true; + } + + /// + /// Removes the stored entry. Throws on persistence + /// failure so the successful-verification path knows the consume actually committed. + /// Cleanup paths use instead. + /// + protected virtual async Task RemoveStoredTokenAsync(UserManager manager, IdentityUser user, string tokenName) + { + user.RemoveToken(InternalLoginProvider, tokenName); + (await manager.UpdateAsync(user)).CheckErrors(); + } + + /// + /// Cleanup variant that swallows concurrent-update failures. The next + /// will overwrite whatever remains. + /// + protected virtual async Task TryRemoveStoredTokenAsync(UserManager manager, IdentityUser user, string tokenName) + { + try + { + await RemoveStoredTokenAsync(manager, user, tokenName); + } + catch (AbpIdentityResultException) + { + } + } + + protected virtual string GetTokenName(string purpose) + { + return Name + ":" + purpose; + } + + protected virtual IDataProtector CreateProtector(string purpose) + { + return DataProtectionProvider.CreateProtector(DataProtectionPurposeRoot, Name, purpose); + } + + protected virtual string GenerateNumericCode(int length) + { + if (length is <= 0 or > 9) + { + // Cap at 9 to stay comfortably within Int32 range. + throw new ArgumentOutOfRangeException(nameof(length), length, "Code length must be between 1 and 9."); + } + + var upperBound = (int)Math.Pow(10, length); + var value = RandomNumberGenerator.GetInt32(0, upperBound); + return value.ToString(new string('0', length), CultureInfo.InvariantCulture); + } + + protected virtual long ToUnixSeconds(DateTime moment) + { + // Treat Unspecified as local time to match IClock.Now's default behaviour. + if (moment.Kind == DateTimeKind.Unspecified) + { + moment = DateTime.SpecifyKind(moment, DateTimeKind.Local); + } + + return new DateTimeOffset(moment).ToUnixTimeSeconds(); + } + + private static bool TryParseStoredValue(string stored, out string protectedCode, out long expiresAtUnixSeconds) + { + protectedCode = string.Empty; + expiresAtUnixSeconds = 0; + + var separatorIndex = stored.LastIndexOf(StoredValueSeparator); + if (separatorIndex <= 0 || separatorIndex == stored.Length - 1) + { + return false; + } + + var protectedPart = stored.Substring(0, separatorIndex); + var secondsPart = stored.Substring(separatorIndex + 1); + + if (!long.TryParse(secondsPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)) + { + return false; + } + + protectedCode = protectedPart; + expiresAtUnixSeconds = seconds; + return true; + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProviderOptions.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProviderOptions.cs new file mode 100644 index 0000000000..973d677091 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProviderOptions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Volo.Abp.Identity.AspNetCore; + +public abstract class AbpTwoFactorTokenProviderOptions +{ + /// Default: 3 minutes. + public TimeSpan TokenLifespan { get; set; } = TimeSpan.FromMinutes(3); + + /// Default: 6. Valid range: 1 to 9 (inclusive). + public int CodeLength { get; set; } = 6; +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/AbpIdentityBlazorMudBlazorServerModule.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/AbpIdentityBlazorMudBlazorServerModule.cs new file mode 100644 index 0000000000..9abc9222f6 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/AbpIdentityBlazorMudBlazorServerModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server; + +namespace Volo.Abp.Identity.Blazor.MudBlazor.Server; + +[DependsOn( + typeof(AbpIdentityBlazorMudBlazorModule), + typeof(AbpPermissionManagementBlazorMudBlazorServerModule) +)] +public class AbpIdentityBlazorMudBlazorServerModule : AbpModule +{ +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/FodyWeavers.xml b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/FodyWeavers.xsd b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/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/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/Volo.Abp.Identity.Blazor.MudBlazor.Server.csproj b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/Volo.Abp.Identity.Blazor.MudBlazor.Server.csproj new file mode 100644 index 0000000000..544df7438d --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.Server/Volo.Abp.Identity.Blazor.MudBlazor.Server.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + + + + + + + + + diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/AbpIdentityBlazorMudBlazorWebAssemblyModule.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/AbpIdentityBlazorMudBlazorWebAssemblyModule.cs new file mode 100644 index 0000000000..7293d59e71 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/AbpIdentityBlazorMudBlazorWebAssemblyModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly; + +namespace Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly; + +[DependsOn( + typeof(AbpIdentityBlazorMudBlazorModule), + typeof(AbpPermissionManagementBlazorMudBlazorWebAssemblyModule), + typeof(AbpIdentityHttpApiClientModule) +)] +public class AbpIdentityBlazorMudBlazorWebAssemblyModule : AbpModule +{ +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/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/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly.csproj b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly.csproj new file mode 100644 index 0000000000..787796d87e --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly/Volo.Abp.Identity.Blazor.MudBlazor.WebAssembly.csproj @@ -0,0 +1,17 @@ + + + + + + + net10.0 + + + + + + + + + + diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityBlazorMudBlazorMappers.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityBlazorMudBlazorMappers.cs new file mode 100644 index 0000000000..3edbc1cfa1 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityBlazorMudBlazorMappers.cs @@ -0,0 +1,25 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Identity.Blazor.MudBlazor; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityUserDtoToIdentityUserUpdateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))] + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial IdentityUserUpdateDto Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))] + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial void Map(IdentityUserDto source, IdentityUserUpdateDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityRoleDtoToIdentityRoleUpdateDtoMapper : MapperBase +{ + public override partial IdentityRoleUpdateDto Map(IdentityRoleDto source); + public override partial void Map(IdentityRoleDto source, IdentityRoleUpdateDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityBlazorMudBlazorModule.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityBlazorMudBlazorModule.cs new file mode 100644 index 0000000000..37227e0573 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityBlazorMudBlazorModule.cs @@ -0,0 +1,72 @@ +using Localization.Resources.AbpUi; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; +using Volo.Abp.Mapperly; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.Modularity; +using Volo.Abp.PermissionManagement.Blazor.MudBlazor; +using Volo.Abp.Threading; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.Identity.Blazor.MudBlazor; + +[DependsOn( + typeof(AbpIdentityApplicationContractsModule), + typeof(AbpMapperlyModule), + typeof(AbpPermissionManagementBlazorMudBlazorModule), + typeof(AbpMudBlazorUIModule) + )] +public class AbpIdentityBlazorMudBlazorModule : AbpModule +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + + Configure(options => + { + options.MenuContributors.Add(new AbpIdentityWebMainMenuContributor()); + }); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpIdentityBlazorMudBlazorModule).Assembly); + }); + + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes( + typeof(AbpUiResource) + ); + }); + } + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + OneTimeRunner.Run(() => + { + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToUi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.Role, + createFormTypes: new[] { typeof(IdentityRoleCreateDto) }, + editFormTypes: new[] { typeof(IdentityRoleUpdateDto) } + ); + + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToUi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.User, + createFormTypes: new[] { typeof(IdentityUserCreateDto) }, + editFormTypes: new[] { typeof(IdentityUserUpdateDto) } + ); + }); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityWebMainMenuContributor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityWebMainMenuContributor.cs new file mode 100644 index 0000000000..4b2a2313e0 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/AbpIdentityWebMainMenuContributor.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Volo.Abp.Identity.Localization; +using Volo.Abp.UI.Navigation; +using Volo.Abp.Authorization.Permissions; + +namespace Volo.Abp.Identity.Blazor.MudBlazor; + +public class AbpIdentityWebMainMenuContributor : IMenuContributor +{ + public virtual Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name != StandardMenus.Main) + { + return Task.CompletedTask; + } + + var administrationMenu = context.Menu.GetAdministration(); + + var l = context.GetLocalizer(); + + var identityMenuItem = new ApplicationMenuItem(IdentityMenuNames.GroupName, l["Menu:IdentityManagement"], + icon: "far fa-id-card"); + administrationMenu.AddItem(identityMenuItem); + + identityMenuItem.AddItem(new ApplicationMenuItem( + IdentityMenuNames.Roles, + l["Roles"], + url: "~/identity/roles").RequirePermissions(IdentityPermissions.Roles.Default)); + + identityMenuItem.AddItem(new ApplicationMenuItem( + IdentityMenuNames.Users, + l["Users"], + url: "~/identity/users").RequirePermissions(IdentityPermissions.Users.Default)); + + return Task.CompletedTask; + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/FodyWeavers.xml b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/FodyWeavers.xsd b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/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/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/IdentityMenuNames.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/IdentityMenuNames.cs new file mode 100644 index 0000000000..eab8728919 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/IdentityMenuNames.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Identity.Blazor.MudBlazor; + +public class IdentityMenuNames +{ + public const string GroupName = "AbpIdentity"; + + public const string Roles = GroupName + ".Roles"; + public const string Users = GroupName + ".Users"; +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleManagement.razor new file mode 100644 index 0000000000..d9f8700ee2 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleManagement.razor @@ -0,0 +1,87 @@ +@page "/identity/roles" +@page "/{culture}/identity/roles" +@attribute [Authorize(IdentityPermissions.Roles.Default)] +@using global::MudBlazor +@using Volo.Abp.Identity +@using Microsoft.AspNetCore.Authorization +@using Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components +@using Volo.Abp.Identity.Localization +@using Volo.Abp.MudBlazorUI.Components.ObjectExtending +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation + +@inherits AbpMudCrudPageBase + + + + + + + + + +@* ************************* CREATE DIALOG ************************* *@ +@if (HasCreatePermission) +{ + + + @L["NewRole"] + + + + + + + + + + + @L["Cancel"] + @L["Save"] + + +} + +@* ************************* EDIT DIALOG ************************* *@ +@if (HasUpdatePermission) +{ + + + @L["Edit"] + + + + + + + + + + + + @L["Cancel"] + @L["Save"] + + +} + +@if (HasManagePermissionsPermission) +{ + +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleManagement.razor.cs new file mode 100644 index 0000000000..7037b7b570 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleManagement.razor.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; +using Volo.Abp.Identity.Localization; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.ObjectExtending; +using Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components; + +namespace Volo.Abp.Identity.Blazor.MudBlazor.Pages.Identity; + +public partial class RoleManagement +{ + [Parameter] + public string? Culture { get; set; } + + protected const string PermissionProviderName = "R"; + + protected PermissionManagementModal? PermissionManagementModal; + + protected string? ManagePermissionsPolicyName; + + protected bool HasManagePermissionsPermission { get; set; } + + protected PageToolbar Toolbar { get; } = new(); + + protected List RoleManagementTableColumns => TableColumns.Get(); + + private AbpMudBlazorMessageLocalizerHelper? _localizerHelper; + + [Inject] + protected IStringLocalizer Localizer { get; set; } = default!; + + public RoleManagement() + { + ObjectMapperContext = typeof(AbpIdentityBlazorMudBlazorModule); + LocalizationResource = typeof(IdentityResource); + + CreatePolicyName = IdentityPermissions.Roles.Create; + UpdatePolicyName = IdentityPermissions.Roles.Update; + DeletePolicyName = IdentityPermissions.Roles.Delete; + ManagePermissionsPolicyName = IdentityPermissions.Roles.ManagePermissions; + } + + protected override async Task OnInitializedAsync() + { + _localizerHelper = new AbpMudBlazorMessageLocalizerHelper(Localizer); + await base.OnInitializedAsync(); + } + + protected override ValueTask SetBreadcrumbItemsAsync() + { + BreadcrumbItems.Add(new BreadcrumbItem(LUiNavigation["Menu:Administration"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Menu:IdentityManagement"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Roles"].Value, null, disabled: true)); + return base.SetBreadcrumbItemsAsync(); + } + + protected override ValueTask SetEntityActionsAsync() + { + EntityActions + .Get() + .AddRange(new EntityAction[] + { + new EntityAction + { + Text = L["Edit"], + Visible = (data) => HasUpdatePermission, + Clicked = async (data) => { await OpenEditDialogAsync(data.As()); } + }, + new EntityAction + { + Text = L["Permissions"], + Visible = (data) => HasManagePermissionsPermission, + Clicked = async (data) => + { + if (PermissionManagementModal != null) + { + await PermissionManagementModal.OpenAsync(PermissionProviderName, + data.As().Name); + } + } + }, + new EntityAction + { + Text = L["Delete"], + Visible = (data) => HasDeletePermission && !data.As().IsStatic, + Clicked = async (data) => await DeleteEntityAsync(data.As()), + ConfirmationMessage = (data) => GetDeleteConfirmationMessage(data.As()) + } + }); + + return base.SetEntityActionsAsync(); + } + + protected override async ValueTask SetTableColumnsAsync() + { + RoleManagementTableColumns + .AddRange(new TableColumn[] + { + new TableColumn + { + Title = L["Actions"], + Actions = EntityActions.Get(), + }, + new TableColumn + { + Title = L["RoleName"], + Sortable = true, + Data = nameof(IdentityRoleDto.Name), + Component = typeof(RoleNameComponent) + }, + }); + + RoleManagementTableColumns.AddRange(await GetExtensionTableColumnsAsync(IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.Role)); + + await base.SetTableColumnsAsync(); + } + + protected override async Task SetPermissionsAsync() + { + await base.SetPermissionsAsync(); + + HasManagePermissionsPermission = + await AuthorizationService.IsGrantedAsync(IdentityPermissions.Roles.ManagePermissions); + } + + protected override string GetDeleteConfirmationMessage(IdentityRoleDto entity) + { + return string.Format(L["RoleDeletionConfirmationMessage"], entity.Name); + } + + protected override ValueTask SetToolbarItemsAsync() + { + Toolbar.AddMudButton(L["NewRole"], + OpenCreateDialogAsync, + icon: Icons.Material.Filled.Add.ToString(), + requiredPolicyName: CreatePolicyName); + + return base.SetToolbarItemsAsync(); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleNameComponent.razor b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleNameComponent.razor new file mode 100644 index 0000000000..890671d13c --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleNameComponent.razor @@ -0,0 +1,16 @@ +@using System; +@using Volo.Abp.Identity +@using Microsoft.Extensions.Localization +@using Volo.Abp.Identity.Localization + +@inject IStringLocalizer L + +@(Data.As().Name) +@if (Data.As().IsDefault) +{ + @L["DisplayName:IsDefault"] +} +@if (Data.As().IsPublic) +{ + @L["DisplayName:IsPublic"] +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleNameComponent.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleNameComponent.razor.cs new file mode 100644 index 0000000000..1b209effe0 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/RoleNameComponent.razor.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Components; + +namespace Volo.Abp.Identity.Blazor.MudBlazor.Pages.Identity; + +public partial class RoleNameComponent : ComponentBase +{ + [Parameter] public object Data { get; set; } = default!; +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/UserManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/UserManagement.razor new file mode 100644 index 0000000000..061a1e33b6 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/UserManagement.razor @@ -0,0 +1,179 @@ +@page "/identity/users" +@page "/{culture}/identity/users" +@attribute [Authorize(IdentityPermissions.Users.Default)] +@using Microsoft.AspNetCore.Authorization +@using Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components +@using Volo.Abp.MudBlazorUI.Components.ObjectExtending +@using Volo.Abp.Identity.Localization +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation + +@inherits AbpMudCrudPageBase + + + + + + + + + + + + + + + + +@* ************************* CREATE DIALOG ************************* *@ +@if (HasCreatePermission) +{ + + + @L["NewUser"] + + + + + + + + + + + + + + + + + + + + + + + @if (NewUserRoles != null) + { + @foreach (var role in NewUserRoles) + { + + + } + } + + + + + + @L["Cancel"] + @L["Save"] + + +} + + +@* ************************* EDIT DIALOG ************************* *@ +@if (HasUpdatePermission) +{ + + + @L["Edit"] + + + + + + + + + + + + + @if (!IsEditCurrentUser) + { + + } + + + + + + + + + + @if (EditUserRoles != null && EditUserRoles.Any()) + { + + @foreach (var role in EditUserRoles) + { + + + } + + } + + + + + @L["Cancel"] + @L["Save"] + + +} + +@if (HasManagePermissionsPermission) +{ + +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/UserManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/UserManagement.razor.cs new file mode 100644 index 0000000000..f657523cc3 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Pages/Identity/UserManagement.razor.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Identity.Localization; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.ObjectExtending; +using Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components; +using Volo.Abp.Users; + +namespace Volo.Abp.Identity.Blazor.MudBlazor.Pages.Identity; + +public partial class UserManagement +{ + [Parameter] + public string? Culture { get; set; } + + protected const string PermissionProviderName = "U"; + + protected const string DefaultSelectedTab = "UserInformations"; + + protected PermissionManagementModal? PermissionManagementModal; + + protected IReadOnlyList? Roles; + + protected AssignedRoleViewModel[]? NewUserRoles; + + protected AssignedRoleViewModel[]? EditUserRoles; + + protected string? ManagePermissionsPolicyName; + + protected bool HasManagePermissionsPermission { get; set; } + + protected int _createTabIndex = 0; + protected int _editTabIndex = 0; + protected bool _showPassword; + protected bool IsEditCurrentUser; + + protected PageToolbar Toolbar { get; } = new(); + + private List UserManagementTableColumns => TableColumns.Get(); + private AbpMudBlazorMessageLocalizerHelper? _localizerHelper; + + [Inject] + protected IPermissionChecker PermissionChecker { get; set; } = default!; + + [Inject] + protected IStringLocalizer Localizer { get; set; } = default!; + + public UserManagement() + { + ObjectMapperContext = typeof(AbpIdentityBlazorMudBlazorModule); + LocalizationResource = typeof(IdentityResource); + + CreatePolicyName = IdentityPermissions.Users.Create; + UpdatePolicyName = IdentityPermissions.Users.Update; + DeletePolicyName = IdentityPermissions.Users.Delete; + ManagePermissionsPolicyName = IdentityPermissions.Users.ManagePermissions; + } + + protected override async Task OnInitializedAsync() + { + _localizerHelper = new AbpMudBlazorMessageLocalizerHelper(Localizer); + await base.OnInitializedAsync(); + + try + { + Roles = (await AppService.GetAssignableRolesAsync()).Items; + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected override ValueTask SetBreadcrumbItemsAsync() + { + BreadcrumbItems.Add(new BreadcrumbItem(LUiNavigation["Menu:Administration"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Menu:IdentityManagement"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Users"].Value, null, disabled: true)); + return base.SetBreadcrumbItemsAsync(); + } + + protected virtual async Task OnSearchKeyUp(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + CurrentPage = 1; + await _dataGrid.ReloadServerDataAsync(); + } + } + + protected override async Task SetPermissionsAsync() + { + await base.SetPermissionsAsync(); + + HasManagePermissionsPermission = + await AuthorizationService.IsGrantedAsync(IdentityPermissions.Users.ManagePermissions); + } + + protected override async Task OpenCreateDialogAsync() + { + _createTabIndex = 0; + + if (Roles != null) + { + NewUserRoles = Roles.Select(x => new AssignedRoleViewModel + { + Name = x.Name, + IsAssigned = x.IsDefault, + IsAssignable = true + }).ToArray(); + } + + _showPassword = false; + await base.OpenCreateDialogAsync(); + + NewEntity.IsActive = true; + NewEntity.LockoutEnabled = true; + } + + protected override Task OnCreatingEntityAsync() + { + // apply roles before saving + if (NewUserRoles != null) + { + NewEntity.RoleNames = NewUserRoles.Where(x => x.IsAssigned).Select(x => x.Name).ToArray(); + } + + return base.OnCreatingEntityAsync(); + } + + protected override async Task OpenEditDialogAsync(IdentityUserDto entity) + { + try + { + _editTabIndex = 0; + IsEditCurrentUser = entity.Id == CurrentUser.Id; + + if (await PermissionChecker.IsGrantedAsync(IdentityPermissions.Users.ManageRoles)) + { + var assignableRoles = Roles ?? (await AppService.GetAssignableRolesAsync()).Items; + var currentRoles = (await AppService.GetRolesAsync(entity.Id)).Items; + + var combinedRoles = assignableRoles + .Concat(currentRoles) + .GroupBy(role => role.Id) + .Select(group => group.First()) + .ToList(); + + var currentRoleIds = currentRoles.Select(r => r.Id).ToHashSet(); + var assignableRoleIds = assignableRoles.Select(r => r.Id).ToHashSet(); + + EditUserRoles = combinedRoles.Select(x => new AssignedRoleViewModel + { + Name = x.Name, + IsAssigned = currentRoleIds.Contains(x.Id), + IsAssignable = assignableRoleIds.Contains(x.Id) + }).ToArray(); + + _showPassword = false; + } + await base.OpenEditDialogAsync(entity); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected override Task OnUpdatingEntityAsync() + { + // apply roles before saving + if (EditUserRoles != null) + { + EditingEntity.RoleNames = EditUserRoles.Where(x => x.IsAssigned).Select(x => x.Name).ToArray(); + } + return base.OnUpdatingEntityAsync(); + } + + protected override string GetDeleteConfirmationMessage(IdentityUserDto entity) + { + return string.Format(L["UserDeletionConfirmationMessage"], entity.UserName); + } + + protected override ValueTask SetEntityActionsAsync() + { + EntityActions + .Get() + .AddRange(new EntityAction[] + { + new EntityAction + { + Text = L["Edit"], + Visible = (data) => HasUpdatePermission, + Clicked = async (data) => await OpenEditDialogAsync(data.As()) + }, + new EntityAction + { + Text = L["Permissions"], + Visible = (data) => HasManagePermissionsPermission, + Clicked = async (data) => + { + if (PermissionManagementModal != null) + { + await PermissionManagementModal.OpenAsync(PermissionProviderName, + data.As().Id.ToString(), + data.As().UserName); + } + } + }, + new EntityAction + { + Text = L["Delete"], + Visible = (data) => HasDeletePermission && CurrentUser.GetId() != data.As().Id, + Clicked = async (data) => await DeleteEntityAsync(data.As()), + ConfirmationMessage = (data) => GetDeleteConfirmationMessage(data.As()) + } + }); + + return base.SetEntityActionsAsync(); + } + + protected override async ValueTask SetTableColumnsAsync() + { + UserManagementTableColumns + .AddRange(new TableColumn[] + { + new TableColumn + { + Title = L["Actions"], + Actions = EntityActions.Get(), + }, + new TableColumn + { + Title = L["UserName"], + Data = nameof(IdentityUserDto.UserName), + Sortable = true, + }, + new TableColumn + { + Title = L["EmailAddress"], + Data = nameof(IdentityUserDto.Email), + Sortable = true, + }, + new TableColumn + { + Title = L["PhoneNumber"], + Data = nameof(IdentityUserDto.PhoneNumber), + Sortable = true, + } + }); + + UserManagementTableColumns.AddRange(await GetExtensionTableColumnsAsync(IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.User)); + await base.SetTableColumnsAsync(); + } + + protected override ValueTask SetToolbarItemsAsync() + { + Toolbar.AddMudButton(L["NewUser"], OpenCreateDialogAsync, + icon: Icons.Material.Filled.Add.ToString(), + requiredPolicyName: CreatePolicyName); + + return base.SetToolbarItemsAsync(); + } +} + +public class AssignedRoleViewModel +{ + public string Name { get; set; } = string.Empty; + + public bool IsAssigned { get; set; } + + public bool IsAssignable { get; set; } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Volo.Abp.Identity.Blazor.MudBlazor.csproj b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Volo.Abp.Identity.Blazor.MudBlazor.csproj new file mode 100644 index 0000000000..65d42e0683 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/Volo.Abp.Identity.Blazor.MudBlazor.csproj @@ -0,0 +1,21 @@ + + + + + + + net10.0 + enable + Nullable + + + + + + + + + + + + diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/_Imports.razor b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/_Imports.razor new file mode 100644 index 0000000000..f1f028011b --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor.MudBlazor/_Imports.razor @@ -0,0 +1,7 @@ +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Forms +@using Volo.Abp.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using Volo.Abp.MudBlazorUI +@using Volo.Abp.MudBlazorUI.Components +@using global::MudBlazor diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor index 2dcaea5eac..2db9953cef 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor @@ -1,4 +1,5 @@ @page "/identity/roles" +@page "/{culture}/identity/roles" @attribute [Authorize(IdentityPermissions.Roles.Default)] @using Volo.Abp.Identity @using Microsoft.AspNetCore.Authorization diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs index 4e9a6576d1..485f2442d0 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Blazorise; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -16,6 +17,9 @@ namespace Volo.Abp.Identity.Blazor.Pages.Identity; public partial class RoleManagement { + [Parameter] + public string? Culture { get; set; } + protected const string PermissionProviderName = "R"; protected PermissionManagementModal PermissionManagementModal; diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor index 28cb7f0bed..c54970bd33 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor @@ -1,4 +1,5 @@ @page "/identity/users" +@page "/{culture}/identity/users" @attribute [Authorize(IdentityPermissions.Users.Default)] @using Microsoft.AspNetCore.Authorization @using Volo.Abp.PermissionManagement.Blazor.Components diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs index fc35245801..ccce1cc363 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs @@ -18,6 +18,9 @@ namespace Volo.Abp.Identity.Blazor.Pages.Identity; public partial class UserManagement { + [Parameter] + public string? Culture { get; set; } + protected const string PermissionProviderName = "U"; protected const string DefaultSelectedTab = "UserInformations"; diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.abppkg.analyze.json b/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.abppkg.analyze.json index 05f8f7d4c0..5a3345f207 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.abppkg.analyze.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.abppkg.analyze.json @@ -3677,6 +3677,23 @@ "isOptional": false } ] + }, + { + "returnType": "IdentityUser", + "namespace": "Volo.Abp.Identity", + "name": "FindSharedUserByIdAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "userId", + "isOptional": false + } + ] } ], "implementingInterfaces": [ diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index 46f7a635f0..73f997d982 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -555,22 +555,18 @@ public class IdentityUserManager : UserManager, IDomainService } else if (Options.User.AllowedUserNameCharacters.Where(char.IsDigit).Distinct().Count() >= 4) { - // The AllowedUserNameCharacters includes 4 numbers. So, we are generating 4 random numbers and appending to the username. - var numbers = Options.User.AllowedUserNameCharacters.Where(char.IsDigit).OrderBy(x => Guid.NewGuid()).Take(4).ToArray(); - var minArray = numbers.OrderBy(x => x).ToArray(); - if (minArray[0] == '0') - { - var secondItem = minArray[1]; - minArray[0] = secondItem; - minArray[1] = '0'; - } - var min = int.Parse(new string(minArray)); - var max = int.Parse(new string(numbers.OrderByDescending(x => x).ToArray())); + // The AllowedUserNameCharacters includes at least 4 distinct digits. So, we are picking 4 random digits from them and appending to the username. + var allowedDigits = Options.User.AllowedUserNameCharacters.Where(char.IsDigit).Distinct().ToArray(); tryCount = 0; do { - var randomUserName = userName + RandomHelper.GetRandom(min, max); - if ( await ValidateUserNameAsync(randomUserName)) + var randomDigits = new char[4]; + for (var i = 0; i < randomDigits.Length; i++) + { + randomDigits[i] = allowedDigits[RandomHelper.GetRandom(0, allowedDigits.Length)]; + } + var randomUserName = userName + new string(randomDigits); + if (await ValidateUserNameAsync(randomUserName)) { return randomUserName; } diff --git a/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/Volo/Abp/Identity/IdentityUserClientProxy.Generated.cs b/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/Volo/Abp/Identity/IdentityUserClientProxy.Generated.cs index b6e18e1e0f..11c0bb54c3 100644 --- a/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/Volo/Abp/Identity/IdentityUserClientProxy.Generated.cs +++ b/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/Volo/Abp/Identity/IdentityUserClientProxy.Generated.cs @@ -58,6 +58,14 @@ public partial class IdentityUserClientProxy : ClientProxyBase FindByIdAsync(Guid id) + { + return await RequestAsync(nameof(FindByIdAsync), new ClientProxyRequestTypeValue + { + { typeof(Guid), id } + }); + } + public virtual async Task> GetRolesAsync(Guid id) { return await RequestAsync>(nameof(GetRolesAsync), new ClientProxyRequestTypeValue diff --git a/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/identity-generate-proxy.json b/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/identity-generate-proxy.json index 3937d99aa9..d66a185094 100644 --- a/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/identity-generate-proxy.json +++ b/modules/identity/src/Volo.Abp.Identity.HttpApi.Client/ClientProxies/identity-generate-proxy.json @@ -487,6 +487,23 @@ "typeSimple": "Volo.Abp.Identity.IdentityUserDto" } }, + { + "name": "FindByIdAsync", + "parametersOnMethod": [ + { + "name": "id", + "typeAsString": "System.Guid, System.Private.CoreLib", + "type": "System.Guid", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null + } + ], + "returnValue": { + "type": "Volo.Abp.Identity.IdentityUserDto", + "typeSimple": "Volo.Abp.Identity.IdentityUserDto" + } + }, { "name": "GetAsync", "parametersOnMethod": [ @@ -1019,6 +1036,43 @@ }, "allowAnonymous": null, "implementFrom": "Volo.Abp.Identity.IIdentityUserAppService" + }, + "FindByIdAsyncById": { + "uniqueName": "FindByIdAsyncById", + "name": "FindByIdAsync", + "httpMethod": "GET", + "url": "api/identity/users/by-id/{id}", + "supportedVersions": [], + "parametersOnMethod": [ + { + "name": "id", + "typeAsString": "System.Guid, System.Private.CoreLib", + "type": "System.Guid", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null + } + ], + "parameters": [ + { + "nameOnMethod": "id", + "name": "id", + "jsonName": null, + "type": "System.Guid", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null, + "constraintTypes": [], + "bindingSourceId": "Path", + "descriptorName": "" + } + ], + "returnValue": { + "type": "Volo.Abp.Identity.IdentityUserDto", + "typeSimple": "Volo.Abp.Identity.IdentityUserDto" + }, + "allowAnonymous": null, + "implementFrom": "Volo.Abp.Identity.IIdentityUserAppService" } } }, diff --git a/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserController.cs b/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserController.cs index 67039a1800..54f0fde491 100644 --- a/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserController.cs +++ b/modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityUserController.cs @@ -53,6 +53,13 @@ public class IdentityUserController : AbpControllerBase, IIdentityUserAppService return UserAppService.DeleteAsync(id); } + [HttpGet] + [Route("by-id/{id}")] + public virtual Task FindByIdAsync(Guid id) + { + return UserAppService.FindByIdAsync(id); + } + [HttpGet] [Route("{id}/roles")] public virtual Task> GetRolesAsync(Guid id) diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs index 08ea27081b..b9f89b2446 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs @@ -83,8 +83,8 @@ public class EditModalModel : IdentityPageModel return null; } - var user = await IdentityUserAppService.GetAsync(userId.Value); - return user.UserName; + var user = await IdentityUserAppService.FindByIdAsync(userId.Value); + return user?.UserName; } public virtual async Task OnPostAsync() diff --git a/modules/identity/src/Volo.Abp.Identity.Web/wwwroot/client-proxies/identity-proxy.js b/modules/identity/src/Volo.Abp.Identity.Web/wwwroot/client-proxies/identity-proxy.js index 6de3aeb8d5..b30bd0b20d 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/wwwroot/client-proxies/identity-proxy.js +++ b/modules/identity/src/Volo.Abp.Identity.Web/wwwroot/client-proxies/identity-proxy.js @@ -102,6 +102,13 @@ }, ajaxParams)); }; + volo.abp.identity.identityUser.findById = function(id, ajaxParams) { + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/identity/users/by-id/' + id + '', + type: 'GET' + }, ajaxParams)); + }; + volo.abp.identity.identityUser.getRoles = function(id, ajaxParams) { return abp.ajax($.extend(true, { url: abp.appPath + 'api/identity/users/' + id + '/roles', diff --git a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs index 0c1a8d6ffc..ff3c503657 100644 --- a/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs @@ -51,6 +51,37 @@ public class IdentityUserAppService_Tests : AbpIdentityApplicationTestBase result.PhoneNumber.ShouldBe(johnNash.PhoneNumber); } + [Fact] + public async Task FindByIdAsync_Should_Return_User_When_Exists() + { + var johnNash = GetUser("john.nash"); + + var result = await _userAppService.FindByIdAsync(johnNash.Id); + + result.ShouldNotBeNull(); + result.Id.ShouldBe(johnNash.Id); + result.UserName.ShouldBe(johnNash.UserName); + } + + [Fact] + public async Task FindByIdAsync_Should_Return_Null_When_User_Does_Not_Exist() + { + var result = await _userAppService.FindByIdAsync(Guid.NewGuid()); + + result.ShouldBeNull(); + } + + [Fact] + public async Task FindByIdAsync_Should_Return_Null_When_User_Is_Soft_Deleted() + { + var johnNash = GetUser("john.nash"); + await _userAppService.DeleteAsync(johnNash.Id); + + var result = await _userAppService.FindByIdAsync(johnNash.Id); + + result.ShouldBeNull(); + } + [Fact] public async Task GetListAsync() { diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProvider_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProvider_Tests.cs new file mode 100644 index 0000000000..a967bdcd5b --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpEmailTwoFactorTokenProvider_Tests.cs @@ -0,0 +1,236 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class AbpEmailTwoFactorTokenProvider_Tests : AbpTwoFactorTokenProviderTestBase +{ + protected override string GetTokenProviderName() => TokenOptions.DefaultEmailProvider; + + protected override string GetInternalProviderName() => AbpEmailTwoFactorTokenProvider.ProviderName; + + [Fact] + public void AbpEmailTwoFactorTokenProvider_Should_Replace_Default_Email_Provider() + { + var identityOptions = GetRequiredService>().Value; + + identityOptions.Tokens.ProviderMap.ShouldContainKey(TokenOptions.DefaultEmailProvider); + identityOptions.Tokens.ProviderMap[TokenOptions.DefaultEmailProvider].ProviderType + .ShouldBe(typeof(AbpEmailTwoFactorTokenProvider)); + } + + [Fact] + public void Default_Options_Should_Match_Documented_Defaults() + { + var options = GetRequiredService>().Value; + + options.CodeLength.ShouldBe(6); + options.TokenLifespan.ShouldBe(TimeSpan.FromMinutes(3)); + } + + [Fact] + public async Task CanGenerateTwoFactorTokenAsync_Should_Require_Confirmed_Email() + { + using var uow = UnitOfWorkManager.Begin(); + + var provider = GetRequiredService(); + var john = await UserRepository.GetAsync(TestData.UserJohnId); + + // john.EmailConfirmed defaults to false in the test seed. + (await provider.CanGenerateTwoFactorTokenAsync(UserManager, john)).ShouldBeFalse(); + + john.SetEmailConfirmed(true); + (await UserManager.UpdateAsync(john)).Succeeded.ShouldBeTrue(); + + john = await UserRepository.GetAsync(TestData.UserJohnId); + (await provider.CanGenerateTwoFactorTokenAsync(UserManager, john)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Email_And_Phone_Tokens_For_Same_User_Should_Be_Independent() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var emailCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider, TwoFactorPurpose); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var phoneCode = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, TwoFactorPurpose); + + // Consuming the email code must not invalidate the phone code. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider, TwoFactorPurpose, emailCode)) + .ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, TwoFactorPurpose, phoneCode)) + .ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Email_Code_Should_Survive_Security_Stamp_Change() + { + // Unlike the default DataProtector-backed token providers, this 2FA provider does + // not bind the code to the user's SecurityStamp, so rotating the stamp (e.g. a + // concurrent password change) must NOT invalidate an outstanding 2FA code. + // This keeps the login flow resilient to legitimate state changes happening + // between the credential step and the verification step. + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.UpdateSecurityStampAsync(user)).Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Custom_CodeLength_Should_Produce_Code_Of_That_Length() + { + using var uow = UnitOfWorkManager.Begin(); + + var customProvider = new AbpEmailTwoFactorTokenProvider( + Microsoft.Extensions.Options.Options.Create(new AbpEmailTwoFactorTokenProviderOptions { CodeLength = 8 }), + UserRepository, + GetRequiredService(), + Clock, + GetRequiredService()); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await customProvider.GenerateAsync(TwoFactorPurpose, UserManager, user); + + code.Length.ShouldBe(8); + code.ShouldMatch(@"^\d{8}$"); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await customProvider.ValidateAsync(TwoFactorPurpose, code, UserManager, user)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Custom_TokenLifespan_Should_Be_Honored() + { + using var uow = UnitOfWorkManager.Begin(); + + var customLifespan = TimeSpan.FromMinutes(30); + + var customProvider = new AbpEmailTwoFactorTokenProvider( + Microsoft.Extensions.Options.Options.Create(new AbpEmailTwoFactorTokenProviderOptions { TokenLifespan = customLifespan }), + UserRepository, + GetRequiredService(), + Clock, + GetRequiredService()); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var beforeGenerate = new DateTimeOffset(Clock.Now.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(Clock.Now, DateTimeKind.Local) + : Clock.Now).ToUnixTimeSeconds(); + await customProvider.GenerateAsync(TwoFactorPurpose, UserManager, user); + var afterGenerate = new DateTimeOffset(Clock.Now.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(Clock.Now, DateTimeKind.Local) + : Clock.Now).ToUnixTimeSeconds(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var stored = await UserManager.GetAuthenticationTokenAsync( + user, + AbpTwoFactorTokenProvider.InternalLoginProvider, + AbpEmailTwoFactorTokenProvider.ProviderName + ":" + TwoFactorPurpose); + stored.ShouldNotBeNull(); + + var separator = stored.LastIndexOf('|'); + separator.ShouldBeGreaterThan(0); + var secondsPart = stored.Substring(separator + 1); + long.TryParse(secondsPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var storedExpiresUnix) + .ShouldBeTrue(); + + storedExpiresUnix.ShouldBeGreaterThanOrEqualTo(beforeGenerate + (long)customLifespan.TotalSeconds); + storedExpiresUnix.ShouldBeLessThanOrEqualTo(afterGenerate + (long)customLifespan.TotalSeconds); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Direct_Provider_Generate_And_Validate_Should_Round_Trip() + { + using var uow = UnitOfWorkManager.Begin(); + + var provider = GetRequiredService(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await provider.GenerateAsync(TwoFactorPurpose, UserManager, user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await provider.ValidateAsync(TwoFactorPurpose, code, UserManager, user)).ShouldBeTrue(); + + // Consumed by the previous successful validation. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await provider.ValidateAsync(TwoFactorPurpose, code, UserManager, user)).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public void Provider_Name_Should_Be_The_Published_Constant() + { + var provider = GetRequiredService(); + + provider.Name.ShouldBe(AbpEmailTwoFactorTokenProvider.ProviderName); + AbpEmailTwoFactorTokenProvider.ProviderName.ShouldBe("AbpEmailTwoFactor"); + } + + [Fact] + public async Task Unprotectable_Stored_Payload_From_Wrong_Protector_Should_Fail_And_Cleanup() + { + // Protect a code under a DIFFERENT DataProtection purpose chain to simulate data + // carried over from another module/provider. ValidateAsync must refuse it (Unprotect + // throws) and clean up the stored entry. + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + var tokenName = GetTokenName(TwoFactorPurpose); + + var dpp = GetRequiredService(); + var wrongProtector = dpp.CreateProtector("some-unrelated-purpose"); + var wrongPayload = wrongProtector.Protect("123456"); + + var futureSeconds = new DateTimeOffset(Clock.Now.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(Clock.Now.AddMinutes(1), DateTimeKind.Local) + : Clock.Now.AddMinutes(1)) + .ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, wrongPayload + "|" + futureSeconds)) + .Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName)) + .ShouldBeNull(); + + await uow.CompleteAsync(); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProvider_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProvider_Tests.cs new file mode 100644 index 0000000000..c56c31ffd6 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpPhoneNumberTwoFactorTokenProvider_Tests.cs @@ -0,0 +1,223 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class AbpPhoneNumberTwoFactorTokenProvider_Tests : AbpTwoFactorTokenProviderTestBase +{ + protected override string GetTokenProviderName() => TokenOptions.DefaultPhoneProvider; + + protected override string GetInternalProviderName() => AbpPhoneNumberTwoFactorTokenProvider.ProviderName; + + [Fact] + public void AbpPhoneNumberTwoFactorTokenProvider_Should_Replace_Default_Phone_Provider() + { + var identityOptions = GetRequiredService>().Value; + + identityOptions.Tokens.ProviderMap.ShouldContainKey(TokenOptions.DefaultPhoneProvider); + identityOptions.Tokens.ProviderMap[TokenOptions.DefaultPhoneProvider].ProviderType + .ShouldBe(typeof(AbpPhoneNumberTwoFactorTokenProvider)); + } + + [Fact] + public void Default_Options_Should_Match_Documented_Defaults() + { + var options = GetRequiredService>().Value; + + options.CodeLength.ShouldBe(6); + options.TokenLifespan.ShouldBe(TimeSpan.FromMinutes(3)); + } + + [Fact] + public async Task CanGenerateTwoFactorTokenAsync_Should_Require_Confirmed_Phone_Number() + { + using var uow = UnitOfWorkManager.Begin(); + + var provider = GetRequiredService(); + var john = await UserRepository.GetAsync(TestData.UserJohnId); + + // No phone number set by the seed. + (await provider.CanGenerateTwoFactorTokenAsync(UserManager, john)).ShouldBeFalse(); + + (await UserManager.SetPhoneNumberAsync(john, "+1-555-0100")).Succeeded.ShouldBeTrue(); + + john = await UserRepository.GetAsync(TestData.UserJohnId); + // Phone present but not confirmed yet. + (await provider.CanGenerateTwoFactorTokenAsync(UserManager, john)).ShouldBeFalse(); + + john.SetPhoneNumberConfirmed(true); + (await UserManager.UpdateAsync(john)).Succeeded.ShouldBeTrue(); + + john = await UserRepository.GetAsync(TestData.UserJohnId); + (await provider.CanGenerateTwoFactorTokenAsync(UserManager, john)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task ChangePhoneNumber_Token_Should_Be_Single_Use() + { + // IdentityOptions.Tokens.ChangePhoneNumberTokenProvider defaults to the same + // "Phone" provider name, so GenerateChangePhoneNumberTokenAsync now routes + // through AbpPhoneNumberTwoFactorTokenProvider and inherits single-use semantics. + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + const string newPhone = "+1-555-0199"; + var token = await UserManager.GenerateChangePhoneNumberTokenAsync(user, newPhone); + + token.ShouldNotBeNullOrEmpty(); + token.Length.ShouldBe(6); + token.ShouldMatch(@"^\d{6}$"); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.VerifyChangePhoneNumberTokenAsync(user, token, newPhone)).ShouldBeTrue(); + + // Single-use: second verification must fail. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.VerifyChangePhoneNumberTokenAsync(user, token, newPhone)).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task ChangePhoneNumber_Token_Should_Be_Bound_To_The_Target_Phone_Number() + { + // Purpose is "ChangePhoneNumber:{phoneNumber}" so a token issued for one + // target phone number must not validate against a different one. + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var token = await UserManager.GenerateChangePhoneNumberTokenAsync(user, "+1-555-0111"); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.VerifyChangePhoneNumberTokenAsync(user, token, "+1-555-0222")).ShouldBeFalse(); + + // Original target still works. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.VerifyChangePhoneNumberTokenAsync(user, token, "+1-555-0111")).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Phone_Code_Should_Survive_Security_Stamp_Change() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.UpdateSecurityStampAsync(user)).Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Custom_CodeLength_Should_Produce_Code_Of_That_Length() + { + using var uow = UnitOfWorkManager.Begin(); + + var customProvider = new AbpPhoneNumberTwoFactorTokenProvider( + Microsoft.Extensions.Options.Options.Create(new AbpPhoneNumberTwoFactorTokenProviderOptions { CodeLength = 4 }), + UserRepository, + GetRequiredService(), + Clock, + GetRequiredService()); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await customProvider.GenerateAsync(TwoFactorPurpose, UserManager, user); + + code.Length.ShouldBe(4); + code.ShouldMatch(@"^\d{4}$"); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await customProvider.ValidateAsync(TwoFactorPurpose, code, UserManager, user)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Custom_TokenLifespan_Should_Be_Honored() + { + using var uow = UnitOfWorkManager.Begin(); + + var customLifespan = TimeSpan.FromMinutes(15); + + var customProvider = new AbpPhoneNumberTwoFactorTokenProvider( + Microsoft.Extensions.Options.Options.Create(new AbpPhoneNumberTwoFactorTokenProviderOptions { TokenLifespan = customLifespan }), + UserRepository, + GetRequiredService(), + Clock, + GetRequiredService()); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var beforeGenerate = new DateTimeOffset(Clock.Now.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(Clock.Now, DateTimeKind.Local) + : Clock.Now).ToUnixTimeSeconds(); + await customProvider.GenerateAsync(TwoFactorPurpose, UserManager, user); + var afterGenerate = new DateTimeOffset(Clock.Now.Kind == DateTimeKind.Unspecified + ? DateTime.SpecifyKind(Clock.Now, DateTimeKind.Local) + : Clock.Now).ToUnixTimeSeconds(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var stored = await UserManager.GetAuthenticationTokenAsync( + user, + AbpTwoFactorTokenProvider.InternalLoginProvider, + AbpPhoneNumberTwoFactorTokenProvider.ProviderName + ":" + TwoFactorPurpose); + stored.ShouldNotBeNull(); + + var separator = stored.LastIndexOf('|'); + separator.ShouldBeGreaterThan(0); + var secondsPart = stored.Substring(separator + 1); + long.TryParse(secondsPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var storedExpiresUnix) + .ShouldBeTrue(); + + storedExpiresUnix.ShouldBeGreaterThanOrEqualTo(beforeGenerate + (long)customLifespan.TotalSeconds); + storedExpiresUnix.ShouldBeLessThanOrEqualTo(afterGenerate + (long)customLifespan.TotalSeconds); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Direct_Provider_Generate_And_Validate_Should_Round_Trip() + { + using var uow = UnitOfWorkManager.Begin(); + + var provider = GetRequiredService(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await provider.GenerateAsync(TwoFactorPurpose, UserManager, user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await provider.ValidateAsync(TwoFactorPurpose, code, UserManager, user)).ShouldBeTrue(); + + // Consumed by the previous successful validation. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await provider.ValidateAsync(TwoFactorPurpose, code, UserManager, user)).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public void Provider_Name_Should_Be_The_Published_Constant() + { + var provider = GetRequiredService(); + + provider.Name.ShouldBe(AbpPhoneNumberTwoFactorTokenProvider.ProviderName); + AbpPhoneNumberTwoFactorTokenProvider.ProviderName.ShouldBe("AbpPhoneNumberTwoFactor"); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs index 7aa540f420..ab2adb6c2f 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs @@ -1,8 +1,10 @@ -using System.Net; +using System; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Shouldly; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.Identity.AspNetCore; @@ -45,4 +47,36 @@ public class AbpSignInManager_Tests : AbpIdentityAspNetCoreTestBase result.ShouldBe("NotAllowed"); } + + [Fact] + public async Task Should_Return_NotAllowed_For_User_That_Should_Change_Password_Regardless_Of_Password() + { + // PreSignInCheck rejects users with ShouldChangePasswordOnNextLogin=true before the + // password is verified, so PasswordSignInAsync returns NotAllowed for both right and + // wrong passwords. Callers that surface a login error to the user (Login page) need + // to recheck the password themselves to distinguish the two cases. + var userManager = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + const string userName = "must-change-password"; + const string password = "1q2w3E*"; + + using (var uow = unitOfWorkManager.Begin()) + { + var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io"); + user.SetShouldChangePasswordOnNextLogin(true); + (await userManager.CreateAsync(user, password)).Succeeded.ShouldBeTrue(); + await uow.CompleteAsync(); + } + + var withWrongPassword = await GetResponseAsStringAsync( + $"api/signin-test/password?userName={userName}&password=WRONG_PASSWORD" + ); + withWrongPassword.ShouldBe("NotAllowed"); + + var withCorrectPassword = await GetResponseAsStringAsync( + $"api/signin-test/password?userName={userName}&password={password}" + ); + withCorrectPassword.ShouldBe("NotAllowed"); + } } diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProviderTestBase.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProviderTestBase.cs new file mode 100644 index 0000000000..6f72dee980 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpTwoFactorTokenProviderTestBase.cs @@ -0,0 +1,612 @@ +using System; +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Shouldly; +using Volo.Abp.Timing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +/// +/// Abstract base class that exercises the common behaviour of every +/// subclass. Concrete subclasses wire up +/// the Identity API calls that route to the provider under test. +/// +public abstract class AbpTwoFactorTokenProviderTestBase : AbpIdentityAspNetCoreTestBase +{ + protected const string TwoFactorPurpose = "TwoFactor"; + protected const string OtherPurpose = "SomeOtherPurpose"; + + protected IIdentityUserRepository UserRepository { get; } + protected IdentityUserManager UserManager { get; } + protected IdentityTestData TestData { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected IClock Clock { get; } + + protected AbpTwoFactorTokenProviderTestBase() + { + UserRepository = GetRequiredService(); + UserManager = GetRequiredService(); + TestData = GetRequiredService(); + UnitOfWorkManager = GetRequiredService(); + Clock = GetRequiredService(); + } + + /// Identity provider name (e.g. "Email", "Phone") routed to the provider under test. + protected abstract string GetTokenProviderName(); + + /// Internal used as the stored-token key prefix. + protected abstract string GetInternalProviderName(); + + protected virtual Task GenerateTokenAsync(IdentityUser user, string purpose = TwoFactorPurpose) + => UserManager.GenerateUserTokenAsync(user, GetTokenProviderName(), purpose); + + protected virtual Task VerifyTokenAsync(IdentityUser user, string token, string purpose = TwoFactorPurpose) + => UserManager.VerifyUserTokenAsync(user, GetTokenProviderName(), purpose, token); + + protected string GetTokenName(string purpose) => GetInternalProviderName() + ":" + purpose; + + /// Deterministically produce a code that is guaranteed not to equal . + private static string DifferentCode(string code) + { + if (string.IsNullOrEmpty(code)) + { + return "000000"; + } + + var firstChar = code[0]; + var replacement = firstChar == '9' ? '0' : (char)(firstChar + 1); + return replacement + code.Substring(1); + } + + private long ToUnixSeconds(DateTime moment) + { + if (moment.Kind == DateTimeKind.Unspecified) + { + moment = DateTime.SpecifyKind(moment, DateTimeKind.Local); + } + + return new DateTimeOffset(moment).ToUnixTimeSeconds(); + } + + [Fact] + public async Task Generate_Should_Produce_Six_Digit_Numeric_Code() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + code.ShouldNotBeNullOrEmpty(); + code.Length.ShouldBe(6); + code.ShouldMatch(@"^\d{6}$"); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Generate_And_Verify_Token_Should_Succeed() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Invalid_Token_Should_Fail_Verification() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, DifferentCode(code))).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Wrong_Code_Should_Not_Consume_Stored_Entry() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + // A wrong attempt must leave the stored entry intact so the user can retry. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, DifferentCode(code))).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Successful_Verification_Should_Consume_The_Code() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + + // Single-use: the same code must not validate twice. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Second_Token_Generation_Should_Invalidate_First_Token() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var firstCode = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var secondCode = await GenerateTokenAsync(user); + + // Even if first/second codes happen to collide numerically, the first was + // generated against a now-overwritten stored entry, so verifying the FIRST + // code against the NEW stored entry is what counts. We only assert the + // old token no longer validates and the new one does. + user = await UserRepository.GetAsync(TestData.UserJohnId); + if (firstCode != secondCode) + { + (await VerifyTokenAsync(user, firstCode)).ShouldBeFalse(); + user = await UserRepository.GetAsync(TestData.UserJohnId); + } + + (await VerifyTokenAsync(user, secondCode)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Expired_Token_Should_Fail_And_Be_Cleaned_Up() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + // Keep the protected payload but rewrite the expiration into the past. + user = await UserRepository.GetAsync(TestData.UserJohnId); + var tokenName = GetTokenName(TwoFactorPurpose); + var stored = await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName); + stored.ShouldNotBeNull(); + + var separator = stored.LastIndexOf('|'); + separator.ShouldBeGreaterThan(0); + var protectedPart = stored.Substring(0, separator); + var expiredValue = protectedPart + "|" + + ToUnixSeconds(Clock.Now.AddMinutes(-1)).ToString(CultureInfo.InvariantCulture); + + (await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, expiredValue)) + .Succeeded.ShouldBeTrue(); + + // Verification must fail and also wipe the expired entry. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var remaining = await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName); + remaining.ShouldBeNull(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Corrupted_Stored_Value_Should_Return_False_Instead_Of_Throwing() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + var tokenName = GetTokenName(TwoFactorPurpose); + + // Overwrite with a malformed value (no separator, garbage chars). + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, "not-a-valid-stored-value")) + .Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + // Corrupt entry is cleaned up. + user = await UserRepository.GetAsync(TestData.UserJohnId); + var remaining = await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName); + remaining.ShouldBeNull(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Different_Purposes_Should_Be_Isolated() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var twoFactorCode = await GenerateTokenAsync(user, TwoFactorPurpose); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var otherCode = await GenerateTokenAsync(user, OtherPurpose); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + // Wrong-purpose validation must fail. + (await VerifyTokenAsync(user, otherCode, TwoFactorPurpose)).ShouldBeFalse(); + (await VerifyTokenAsync(user, twoFactorCode, OtherPurpose)).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + // Correct purposes still work independently. + (await VerifyTokenAsync(user, twoFactorCode, TwoFactorPurpose)).ShouldBeTrue(); + (await VerifyTokenAsync(user, otherCode, OtherPurpose)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Stored_Token_Data_Should_Persist_Across_UnitOfWork_Boundaries() + { + string code; + + using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) + { + var user = await UserRepository.GetAsync(TestData.UserJohnId); + code = await GenerateTokenAsync(user); + await uow.CompleteAsync(); + } + + using (var uow = UnitOfWorkManager.Begin(requiresNew: true)) + { + var user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Verify_Without_Generate_Should_Fail() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, "123456")).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Empty_Token_Should_Fail_Verification() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, string.Empty)).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Different_Users_Should_Be_Isolated() + { + using var uow = UnitOfWorkManager.Begin(); + + var john = await UserRepository.GetAsync(TestData.UserJohnId); + var johnCode = await GenerateTokenAsync(john); + + var david = await UserRepository.GetAsync(TestData.UserDavidId); + var davidCode = await GenerateTokenAsync(david); + + // Neither user's code should validate for the other (assuming distinct codes; + // even if they collide by chance the FixedTimeEquals path still runs, but the + // DataProtection payload is bound to a different user-scoped record). + if (johnCode != davidCode) + { + john = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(john, davidCode)).ShouldBeFalse(); + + david = await UserRepository.GetAsync(TestData.UserDavidId); + (await VerifyTokenAsync(david, johnCode)).ShouldBeFalse(); + } + + // Both still validate against their own user, proving that generating David's + // did not consume John's stored entry (or vice versa). + john = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(john, johnCode)).ShouldBeTrue(); + + david = await UserRepository.GetAsync(TestData.UserDavidId); + (await VerifyTokenAsync(david, davidCode)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Multiple_Purposes_Can_Coexist_And_Consume_Independently() + { + using var uow = UnitOfWorkManager.Begin(); + + const string purposeA = "PurposeA"; + const string purposeB = "PurposeB"; + const string purposeC = "PurposeC"; + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var codeA = await GenerateTokenAsync(user, purposeA); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var codeB = await GenerateTokenAsync(user, purposeB); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var codeC = await GenerateTokenAsync(user, purposeC); + + // Consume B; A and C must still be valid. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, codeB, purposeB)).ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, codeA, purposeA)).ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, codeC, purposeC)).ShouldBeTrue(); + + // Each is single-use and now consumed. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, codeA, purposeA)).ShouldBeFalse(); + (await VerifyTokenAsync(user, codeB, purposeB)).ShouldBeFalse(); + (await VerifyTokenAsync(user, codeC, purposeC)).ShouldBeFalse(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Stored_Value_Missing_Separator_Should_Return_False_And_Cleanup() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + var tokenName = GetTokenName(TwoFactorPurpose); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, "no-separator-here")) + .Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName)) + .ShouldBeNull(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Stored_Value_With_Non_Numeric_Expiration_Should_Return_False_And_Cleanup() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + var tokenName = GetTokenName(TwoFactorPurpose); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, "validlookingpayload|not-a-number")) + .Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName)) + .ShouldBeNull(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Stored_Value_With_Unprotectable_Payload_Should_Return_False_And_Cleanup() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + var tokenName = GetTokenName(TwoFactorPurpose); + + var futureSeconds = ToUnixSeconds(Clock.Now.AddMinutes(1)).ToString(CultureInfo.InvariantCulture); + + // Valid expiration, but the protected payload is not a real DataProtection output. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, $"not-a-real-protected-payload|{futureSeconds}")) + .Succeeded.ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeFalse(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName)) + .ShouldBeNull(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Generation_Should_Not_Touch_Pre_Existing_Unrelated_Tokens() + { + // John already has a pre-seeded token ("test-provider"/"test-name" = "test-value"). + // Generating a 2FA code must not interfere with it. + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var preExisting = await UserManager.GetAuthenticationTokenAsync(user, "test-provider", "test-name"); + preExisting.ShouldBe("test-value"); + + await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync(user, "test-provider", "test-name")) + .ShouldBe("test-value"); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Successful_Verification_Should_Not_Touch_Pre_Existing_Unrelated_Tokens() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, code)).ShouldBeTrue(); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync(user, "test-provider", "test-name")) + .ShouldBe("test-value"); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Token_Should_Be_Stored_Under_Internal_Login_Provider_Name() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var stored = await UserManager.GetAuthenticationTokenAsync( + user, + AbpTwoFactorTokenProvider.InternalLoginProvider, + GetTokenName(TwoFactorPurpose)); + + stored.ShouldNotBeNullOrEmpty(); + stored.ShouldContain("|"); + + // Must not leak under the Identity provider name itself. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await UserManager.GetAuthenticationTokenAsync(user, GetTokenProviderName(), TwoFactorPurpose)) + .ShouldBeNull(); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Stored_Value_Should_Be_Encrypted_Not_The_Raw_Code() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var code = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var stored = await UserManager.GetAuthenticationTokenAsync( + user, + AbpTwoFactorTokenProvider.InternalLoginProvider, + GetTokenName(TwoFactorPurpose)); + + stored.ShouldNotBeNull(); + stored.ShouldNotContain(code); + + // DataProtection output is substantially longer than a 6-digit code, which is a + // cheap sanity check that we're not accidentally storing the plaintext. + var protectedPart = stored.Substring(0, stored.LastIndexOf('|')); + protectedPart.Length.ShouldBeGreaterThan(code.Length * 4); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Consecutive_Generations_Should_Produce_Different_Protected_Payloads() + { + // DataProtection embeds per-call randomness, so even if the two underlying codes + // collide, their stored payloads will differ. This replaces the old + // "two generations produce different codes" assertion, which had a ~1e-6 flake rate. + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var firstStored = await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, GetTokenName(TwoFactorPurpose)); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + var secondStored = await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, GetTokenName(TwoFactorPurpose)); + + firstStored.ShouldNotBeNull(); + secondStored.ShouldNotBeNull(); + secondStored.ShouldNotBe(firstStored); + + await uow.CompleteAsync(); + } + + [Fact] + public async Task Expired_Entry_Should_Not_Block_Future_Generation() + { + using var uow = UnitOfWorkManager.Begin(); + + var user = await UserRepository.GetAsync(TestData.UserJohnId); + var firstCode = await GenerateTokenAsync(user); + + // Force first entry into the past. + user = await UserRepository.GetAsync(TestData.UserJohnId); + var tokenName = GetTokenName(TwoFactorPurpose); + var stored = await UserManager.GetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName); + var protectedPart = stored!.Substring(0, stored.LastIndexOf('|')); + var expiredValue = protectedPart + "|" + + ToUnixSeconds(Clock.Now.AddMinutes(-1)).ToString(CultureInfo.InvariantCulture); + await UserManager.SetAuthenticationTokenAsync( + user, AbpTwoFactorTokenProvider.InternalLoginProvider, tokenName, expiredValue); + + // Verifying the expired code fails and cleans up. + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, firstCode)).ShouldBeFalse(); + + // New generation must succeed and verify. + user = await UserRepository.GetAsync(TestData.UserJohnId); + var secondCode = await GenerateTokenAsync(user); + + user = await UserRepository.GetAsync(TestData.UserJohnId); + (await VerifyTokenAsync(user, secondCode)).ShouldBeTrue(); + + await uow.CompleteAsync(); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index 82c133e698..f027444e55 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -441,7 +441,7 @@ public class IdentityUserManager_Tests : AbpIdentityDomainTestBase username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); username.Length.ShouldBe(9); //admin and random 4 numbers username.ShouldContain("admin"); - Regex.IsMatch(username, @"[0-4]{3}$").ShouldBeTrue(); + Regex.IsMatch(username, @"[0-4]{4}$").ShouldBeTrue(); _identityUserManager.Options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz"; username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); @@ -462,6 +462,51 @@ public class IdentityUserManager_Tests : AbpIdentityDomainTestBase Regex.IsMatch(username, @"[0-9]{4}$").ShouldBeTrue(); } + [Fact] + public async Task GetUserNameFromEmailAsync_Should_Only_Use_Allowed_Digits_When_AllowedUserNameCharacters_Has_Partial_Digits() + { + _identityUserManager.Options.User.AllowedUserNameCharacters = "admin01234"; + for (var i = 0; i < 200; i++) + { + var username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); + username.ShouldStartWith("admin"); + username.All(c => "admin01234".Contains(c)).ShouldBeTrue($"username '{username}' contains chars outside AllowedUserNameCharacters"); + } + } + + [Fact] + public async Task GetUserNameFromEmailAsync_Should_Only_Use_Allowed_Digits_With_Exact_Four_Digits() + { + _identityUserManager.Options.User.AllowedUserNameCharacters = "admin1234"; + for (var i = 0; i < 200; i++) + { + var username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); + username.ShouldStartWith("admin"); + username.Substring(5).All(c => "1234".Contains(c)).ShouldBeTrue($"suffix of '{username}' contains chars outside the allowed digits"); + } + } + + [Fact] + public async Task GetUserNameFromEmailAsync_Should_Allow_Leading_Zero_In_Random_Digits() + { + _identityUserManager.Options.User.AllowedUserNameCharacters = "admin0123"; + var sawLeadingZero = false; + for (var i = 0; i < 100; i++) + { + var username = await _identityUserManager.GetUserNameFromEmailAsync("admin@abp.io"); + username.Length.ShouldBe(9); + username.ShouldStartWith("admin"); + username.Substring(5).All(c => "0123".Contains(c)).ShouldBeTrue($"suffix of '{username}' contains chars outside the allowed digits"); + if (username[5] == '0') + { + sawLeadingZero = true; + } + } + sawLeadingZero.ShouldBeTrue("expected at least one username with leading-zero random suffix across 100 runs"); + } + private async Task CreateRandomDefaultRoleAsync() { await _identityRoleRepository.InsertAsync( diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/package.json b/modules/openiddict/app/OpenIddict.Demo.Server/package.json index abb49011d8..0bb873130f 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/package.json +++ b/modules/openiddict/app/OpenIddict.Demo.Server/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1" } } diff --git a/modules/openiddict/app/angular/package.json b/modules/openiddict/app/angular/package.json index 8e57ee5313..660bf5fd33 100644 --- a/modules/openiddict/app/angular/package.json +++ b/modules/openiddict/app/angular/package.json @@ -12,15 +12,15 @@ }, "private": true, "dependencies": { - "@abp/ng.account": "~10.3.0", - "@abp/ng.components": "~10.3.0", - "@abp/ng.core": "~10.3.0", - "@abp/ng.oauth": "~10.3.0", - "@abp/ng.identity": "~10.3.0", - "@abp/ng.setting-management": "~10.3.0", - "@abp/ng.tenant-management": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", - "@abp/ng.theme.lepton-x": "~5.3.0", + "@abp/ng.account": "~10.4.1", + "@abp/ng.components": "~10.4.1", + "@abp/ng.core": "~10.4.1", + "@abp/ng.oauth": "~10.4.1", + "@abp/ng.identity": "~10.4.1", + "@abp/ng.setting-management": "~10.4.1", + "@abp/ng.tenant-management": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", + "@abp/ng.theme.lepton-x": "~5.4.1", "@angular/animations": "^15.0.1", "@angular/common": "^15.0.1", "@angular/compiler": "^15.0.1", @@ -36,7 +36,7 @@ "zone.js": "~0.11.4" }, "devDependencies": { - "@abp/ng.schematics": "~10.3.0", + "@abp/ng.schematics": "~10.4.1", "@angular-devkit/build-angular": "^15.0.1", "@angular-eslint/builder": "~15.1.0", "@angular-eslint/eslint-plugin": "~15.1.0", diff --git a/modules/permission-management/Volo.Abp.PermissionManagement.slnx b/modules/permission-management/Volo.Abp.PermissionManagement.slnx index fbfc4f71b8..b1220e4a26 100644 --- a/modules/permission-management/Volo.Abp.PermissionManagement.slnx +++ b/modules/permission-management/Volo.Abp.PermissionManagement.slnx @@ -2,6 +2,9 @@ + + + diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/AbpPermissionManagementBlazorMudBlazorServerModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/AbpPermissionManagementBlazorMudBlazorServerModule.cs new file mode 100644 index 0000000000..5b38c31260 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/AbpPermissionManagementBlazorMudBlazorServerModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server; + +[DependsOn( + typeof(AbpPermissionManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsServerThemingMudBlazorModule) +)] +public class AbpPermissionManagementBlazorMudBlazorServerModule : AbpModule +{ +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/FodyWeavers.xml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/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/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server.csproj b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server.csproj new file mode 100644 index 0000000000..df904cf169 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server/Volo.Abp.PermissionManagement.Blazor.MudBlazor.Server.csproj @@ -0,0 +1,15 @@ + + + + + + + net10.0 + + + + + + + + diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/AbpPermissionManagementBlazorMudBlazorWebAssemblyModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/AbpPermissionManagementBlazorMudBlazorWebAssemblyModule.cs new file mode 100644 index 0000000000..074a9e7be4 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/AbpPermissionManagementBlazorMudBlazorWebAssemblyModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly; + +[DependsOn( + typeof(AbpPermissionManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule), + typeof(AbpPermissionManagementHttpApiClientModule) +)] +public class AbpPermissionManagementBlazorMudBlazorWebAssemblyModule : AbpModule +{ +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/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/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly.csproj b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly.csproj new file mode 100644 index 0000000000..bf8592660a --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.PermissionManagement.Blazor.MudBlazor.WebAssembly.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + + + + + + + + + diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/AbpPermissionManagementBlazorMudBlazorModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/AbpPermissionManagementBlazorMudBlazorModule.cs new file mode 100644 index 0000000000..161b8c10c2 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/AbpPermissionManagementBlazorMudBlazorModule.cs @@ -0,0 +1,26 @@ +using Localization.Resources.AbpUi; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.Localization; + +namespace Volo.Abp.PermissionManagement.Blazor.MudBlazor; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpPermissionManagementApplicationContractsModule) + )] +public class AbpPermissionManagementBlazorMudBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes( + typeof(AbpUiResource) + ); + }); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor new file mode 100644 index 0000000000..e26ee3beb1 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor @@ -0,0 +1,78 @@ +@inherits Volo.Abp.AspNetCore.Components.AbpComponentBase +@using global::MudBlazor +@using JetBrains.Annotations +@using Volo.Abp.PermissionManagement.Localization + + + + @L["Permissions"] - @_entityDisplayName + + + + + + + + + + + + + @L["PermissionGroup"] + + @if (_groups != null && _groups.Any()) + { + + @foreach (var group in _groups) + { + x.IsGranted)})")"> +
+ + + + @foreach (var permission in group.Permissions) + { + var depth = GetPermissionDepthOrDefault(permission.Name); +
+ +
+ } +
+
+ } +
+ } +
+
+ + @L["Cancel"] + @L["Save"] + +
diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor.cs new file mode 100644 index 0000000000..9ed212236c --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/PermissionManagementModal.razor.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.AspNetCore.Components.Web.Configuration; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; +using Volo.Abp.PermissionManagement.Localization; + +namespace Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components; + +public partial class PermissionManagementModal +{ + [Inject] protected IPermissionAppService PermissionAppService { get; set; } = default!; + [Inject] protected ICurrentApplicationConfigurationCacheResetService CurrentApplicationConfigurationCacheResetService { get; set; } = default!; + [Inject] protected IOptions LocalizationOptions { get; set; } = default!; + [Inject] protected IDialogService DialogService { get; set; } = default!; + [Inject] protected ISnackbar Snackbar { get; set; } = default!; + + protected bool _isVisible; + + protected string? _providerName; + protected string? _providerKey; + + protected string? _entityDisplayName; + protected List? _allGroups; + protected List? _groups; + + protected int _activeTabIndex = 0; + + protected bool _selectAllDisabled; + + protected string? _permissionGroupSearchText; + + protected bool GrantAll { get; set; } + protected bool GrantAny { get; set; } + + protected Dictionary _permissionDepths = new Dictionary(); + + public PermissionManagementModal() + { + LocalizationResource = typeof(AbpPermissionManagementResource); + } + + public virtual async Task OpenAsync(string providerName, string providerKey, string? entityDisplayName = null) + { + try + { + _providerName = providerName; + _providerKey = providerKey; + _permissionGroupSearchText = null; + + var result = await PermissionAppService.GetAsync(_providerName, _providerKey); + + _entityDisplayName = entityDisplayName ?? result.EntityDisplayName; + _allGroups = result.Groups.OrderBy(x => x.DisplayName).ToList(); + _groups = _allGroups.ToList(); + + NormalizePermissionGroup(); + + GrantAll = _allGroups.SelectMany(x => x.Permissions).All(p => p.IsGranted); + GrantAny = !GrantAll && _allGroups.SelectMany(x => x.Permissions).Any(p => p.IsGranted); + + _isVisible = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual async Task GrantAllAsync(bool grantAll) + { + GrantAll = grantAll; + GrantAny = false; + + if (_allGroups == null) + { + return; + } + + await ResetSearchTextAsync(); + + foreach (var permission in _allGroups.SelectMany(x => x.Permissions)) + { + if (IsDisabledPermission(permission)) + { + continue; + } + + permission.IsGranted = grantAll; + } + + await InvokeAsync(StateHasChanged); + } + + protected virtual void NormalizePermissionGroup(bool checkDisabledPermissions = true) + { + if (_groups == null) + { + return; + } + + _selectAllDisabled = _groups.All(IsPermissionGroupDisabled); + + foreach (var group in _groups) + { + SetPermissionDepths(group.Permissions, null, 0); + } + + if (_groups.Count != 0) + { + _activeTabIndex = 0; + } + } + + protected Task CloseModal() + { + _isVisible = false; + return InvokeAsync(StateHasChanged); + } + + protected virtual async Task SaveAsync() + { + try + { + if (_allGroups == null) + { + return; + } + + var updateDto = new UpdatePermissionsDto + { + Permissions = _allGroups + .SelectMany(g => g.Permissions) + .Select(p => new UpdatePermissionDto { IsGranted = p.IsGranted, Name = p.Name }) + .ToArray() + }; + + if (!updateDto.Permissions.Any(x => x.IsGranted)) + { + var confirmed = await DialogService.ShowMessageBoxAsync( + L["Warning"], + L["SaveWithoutAnyPermissionsWarningMessage"], + yesText: L["Yes"], + cancelText: L["Cancel"]); + + if (confirmed != true) + { + return; + } + } + + await PermissionAppService.UpdateAsync(_providerName!, _providerKey!, updateDto); + + Guid? userId = null; + if (_providerName == UserPermissionValueProvider.ProviderName && Guid.TryParse(_providerKey, out var parsedUserId)) + { + userId = parsedUserId; + } + + await CurrentApplicationConfigurationCacheResetService.ResetAsync(userId); + + _isVisible = false; + await InvokeAsync(StateHasChanged); + Snackbar.Add(L["SavedSuccessfully"], Severity.Success); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual string GetNormalizedGroupName(string name) + { + return "PermissionGroup_" + name.Replace(".", "_"); + } + + protected virtual void SetPermissionDepths(List permissions, string? currentParent, int currentDepth) + { + foreach (var item in permissions) + { + if (item.ParentName == currentParent) + { + _permissionDepths[item.Name] = currentDepth; + SetPermissionDepths(permissions, item.Name, currentDepth + 1); + } + } + } + + protected virtual int GetPermissionDepthOrDefault(string name) + { + return _permissionDepths.GetValueOrDefault(name, 0); + } + + protected virtual async Task GroupGrantAllChanged(bool value, PermissionGroupDto permissionGroup) + { + foreach (var permission in permissionGroup.Permissions) + { + if (!IsDisabledPermission(permission)) + { + SetPermissionGrant(permission, value); + } + } + + if (_allGroups != null) + { + GrantAll = _allGroups.SelectMany(x => x.Permissions).All(p => p.IsGranted); + GrantAny = !GrantAll && _allGroups.SelectMany(x => x.Permissions).Any(p => p.IsGranted); + } + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task PermissionChanged(bool value, PermissionGroupDto permissionGroup, PermissionGrantInfoDto permission) + { + SetPermissionGrant(permission, value); + + if (value) + { + SetParentPermissionGrant(permissionGroup, permission); + } + else + { + var childPermissions = GetChildPermissions(permissionGroup, permission); + + foreach (var childPermission in childPermissions) + { + SetPermissionGrant(childPermission, false); + } + } + + if (_allGroups != null) + { + GrantAll = _allGroups.SelectMany(x => x.Permissions).All(p => p.IsGranted); + GrantAny = !GrantAll && _allGroups.SelectMany(x => x.Permissions).Any(p => p.IsGranted); + } + await InvokeAsync(StateHasChanged); + } + + private void SetParentPermissionGrant(PermissionGroupDto permissionGroup, PermissionGrantInfoDto permission) + { + if (permission.ParentName == null) + { + return; + } + + var parentPermission = GetParentPermission(permissionGroup, permission); + SetPermissionGrant(parentPermission, true); + + SetParentPermissionGrant(permissionGroup, parentPermission); + } + + private void SetPermissionGrant(PermissionGrantInfoDto permission, bool value) + { + if (permission.IsGranted == value) + { + return; + } + + permission.IsGranted = value; + } + + protected virtual PermissionGrantInfoDto GetParentPermission(PermissionGroupDto permissionGroup, PermissionGrantInfoDto permission) + { + return permissionGroup.Permissions.First(x => x.Name == permission.ParentName); + } + + protected virtual List GetChildPermissions(PermissionGroupDto permissionGroup, PermissionGrantInfoDto permission) + { + var childPermissions = new List(); + GetChildPermissions(childPermissions, permissionGroup.Permissions, permission); + return childPermissions; + } + + protected virtual void GetChildPermissions(List allChildPermissions, List permissions, PermissionGrantInfoDto permission) + { + var childPermissions = permissions.Where(x => x.ParentName == permission.Name).ToList(); + if (childPermissions.Count == 0) + { + return; + } + + allChildPermissions.AddRange(childPermissions); + + foreach (var childPermission in childPermissions) + { + GetChildPermissions(allChildPermissions, permissions, childPermission); + } + } + + protected virtual bool IsDisabledPermission(PermissionGrantInfoDto permissionGrantInfo) + { + if (!permissionGrantInfo.IsEditable) + { + return true; + } + + return permissionGrantInfo.IsGranted && + permissionGrantInfo.GrantedProviders.Any() && + permissionGrantInfo.GrantedProviders.All(p => p.ProviderName != _providerName); + } + + protected virtual string GetShownName(PermissionGrantInfoDto permissionGrantInfo) + { + if (permissionGrantInfo.GrantedProviders.All(p => p.ProviderName == _providerName)) + { + return permissionGrantInfo.DisplayName; + } + + var grantedByOtherProviders = permissionGrantInfo.GrantedProviders + .Where(p => p.ProviderName != _providerName) + .ToList(); + + if (!grantedByOtherProviders.Any()) + { + return permissionGrantInfo.DisplayName; + } + + return string.Format( + "{0} ({1})", + permissionGrantInfo.DisplayName, + grantedByOtherProviders + .Select(p => p.ProviderName) + .JoinAsString(", ") + ); + } + + protected virtual bool IsPermissionGroupDisabled(PermissionGroupDto group) + { + return group.Permissions.All(IsDisabledPermission); + } + + protected virtual async Task ResetSearchTextAsync() + { + _permissionGroupSearchText = string.Empty; + if (_allGroups != null) + { + _groups = _permissionGroupSearchText.IsNullOrWhiteSpace() + ? _allGroups.ToList() + : _allGroups.Where(x => x.DisplayName.Contains(_permissionGroupSearchText!, StringComparison.OrdinalIgnoreCase) || x.Permissions.Any(permission => permission.DisplayName.Contains(_permissionGroupSearchText!, StringComparison.OrdinalIgnoreCase))).ToList(); + + NormalizePermissionGroup(false); + } + + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task OnPermissionGroupSearchTextChangedAsync(string? value) + { + if (value == _permissionGroupSearchText) + { + return; + } + + _permissionGroupSearchText = value; + if (_allGroups != null) + { + _groups = _permissionGroupSearchText.IsNullOrWhiteSpace() + ? _allGroups.ToList() + : _allGroups.Where(x => x.DisplayName.Contains(_permissionGroupSearchText!, StringComparison.OrdinalIgnoreCase) || x.Permissions.Any(permission => permission.DisplayName.Contains(_permissionGroupSearchText!, StringComparison.OrdinalIgnoreCase))).ToList(); + + GrantAll = _allGroups.SelectMany(x => x.Permissions).All(p => p.IsGranted); + GrantAny = !GrantAll && _allGroups.SelectMany(x => x.Permissions).Any(p => p.IsGranted); + + NormalizePermissionGroup(false); + } + + await InvokeAsync(StateHasChanged); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor new file mode 100644 index 0000000000..14aa92d735 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor @@ -0,0 +1,154 @@ +@using global::MudBlazor +@inherits Volo.Abp.AspNetCore.Components.AbpComponentBase + + + + @L["ResourcePermissions"] - @ResourceDisplayName + + + @if (HasAnyResourcePermission && HasAnyResourceProviderKeyLookupService) + { +
+ + @L["AddResourcePermission"] + +
+ + + + + + + @L["Edit"] + + + @L["Delete"] + + + + + + + + + @context.Item.ProviderName + + + @context.Item.ProviderDisplayName + + + + + @foreach (var permission in context.Item.Permissions) + { + + @permission.DisplayName + + } + + + + + @L["NoDataAvailableInDatatable"] + + + } + else + { + + @if (!HasAnyResourcePermission) + { + @L["NoResourcePermissionFound"] + } + else if (!HasAnyResourceProviderKeyLookupService) + { + @L["NoResourceProviderKeyLookupServiceFound"] + } + + } +
+ + @L["Close"] + +
+ +@* Create Resource Permission Dialog *@ +@if (_createDialogVisible) +{ + + + @L["AddResourcePermission"] + + + + @L["ResourcePermissionTarget"] + + @foreach (var keyLookupService in ResourceProviderKeyLookupServices) + { + @keyLookupService.DisplayName + } + + + @L["ResourcePermissionPermissions"] + +
+ @foreach (var permission in CreateEntity.Permissions) + { + + } +
+
+
+ + @L["Cancel"] + @L["Save"] + +
+} + +@* Edit Resource Permission Dialog *@ +@if (_editDialogVisible) +{ + + + @L["UpdateResourcePermission"] + + + + @L["ResourcePermissionPermissions"] + +
+ @foreach (var permission in EditEntity.Permissions) + { + + } +
+
+
+ + @L["Cancel"] + @L["Save"] + +
+} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor.cs new file mode 100644 index 0000000000..e45473bb04 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Components/ResourcePermissionManagementModal.razor.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.PermissionManagement.Localization; + +namespace Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components; + +public partial class ResourcePermissionManagementModal +{ + [Inject] protected IPermissionAppService PermissionAppService { get; set; } = default!; + + [Inject] protected IUiMessageService UiMessageService { get; set; } = default!; + + [Inject] protected ISnackbar Snackbar { get; set; } = default!; + + [Inject] protected IDialogService DialogService { get; set; } = default!; + + protected bool _isVisible; + protected bool _createDialogVisible; + protected bool _editDialogVisible; + protected MudForm? _createFormRef; + protected MudForm? _editFormRef; + + public bool HasAnyResourcePermission { get; set; } + public bool HasAnyResourceProviderKeyLookupService { get; set; } + protected string? ResourceName { get; set; } + protected string? ResourceKey { get; set; } + protected string? ResourceDisplayName { get; set; } + protected int PageSize { get; set; } = 10; + + protected CreateModel CreateEntity { get; set; } = new CreateModel + { + Permissions = new List() + }; + + protected SearchProviderKeyInfo? _selectedProviderKey; + protected string? ProviderKey => _selectedProviderKey?.ProviderKey; + protected string? ProviderDisplayName => _selectedProviderKey?.ProviderDisplayName; + + public GetResourcePermissionDefinitionListResultDto ResourcePermissionDefinitions { get; set; } = new() + { + Permissions = new List() + }; + protected string? CurrentLookupService { get; set; } + protected List ResourceProviderKeyLookupServices { get; set; } = new(); + protected List ProviderKeys { get; set; } = new(); + protected GetResourcePermissionListResultDto ResourcePermissionList = new() + { + Permissions = new List() + }; + + protected EditModel EditEntity { get; set; } = new EditModel + { + Permissions = new List() + }; + + protected bool _grantAllCreate; + protected bool _grantAllEdit; + + public ResourcePermissionManagementModal() + { + LocalizationResource = typeof(AbpPermissionManagementResource); + } + + public virtual async Task OpenAsync(string resourceName, string resourceKey, string resourceDisplayName) + { + try + { + ResourceName = resourceName; + ResourceKey = resourceKey; + ResourceDisplayName = resourceDisplayName; + + ResourcePermissionDefinitions = await PermissionAppService.GetResourceDefinitionsAsync(ResourceName); + ResourceProviderKeyLookupServices = (await PermissionAppService.GetResourceProviderKeyLookupServicesAsync(ResourceName)).Providers; + + HasAnyResourcePermission = ResourcePermissionDefinitions.Permissions.Any(); + if (HasAnyResourcePermission) + { + HasAnyResourceProviderKeyLookupService = ResourceProviderKeyLookupServices.Count > 0; + } + + await InvokeAsync(StateHasChanged); + + ResourcePermissionList = await PermissionAppService.GetResourceAsync(ResourceName, ResourceKey); + + _isVisible = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual Task CloseModal() + { + _isVisible = false; + return InvokeAsync(StateHasChanged); + } + + protected virtual async Task OpenCreateDialogAsync() + { + CurrentLookupService = ResourceProviderKeyLookupServices.FirstOrDefault()?.Name; + + _selectedProviderKey = null; + ProviderKeys = new List(); + if (_createFormRef != null) + { + await _createFormRef.ResetAsync(); + } + + CreateEntity = new CreateModel + { + Permissions = ResourcePermissionDefinitions.Permissions.Select(x => new ResourcePermissionModel + { + Name = x.Name, + DisplayName = x.DisplayName, + IsGranted = false + }).ToList() + }; + + _grantAllCreate = false; + _createDialogVisible = true; + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task SelectedProviderKeyChanged(SearchProviderKeyInfo? value) + { + _selectedProviderKey = value; + + if (value != null && CurrentLookupService != null) + { + var permissionGrants = await PermissionAppService.GetResourceByProviderAsync(ResourceName!, ResourceKey!, CurrentLookupService, value.ProviderKey); + foreach (var permission in CreateEntity.Permissions) + { + permission.IsGranted = permissionGrants.Permissions.Any(p => p.Name == permission.Name && p.Providers.Contains(CurrentLookupService) && p.IsGranted); + } + + _grantAllCreate = CreateEntity.Permissions.All(x => x.IsGranted); + } + + await InvokeAsync(StateHasChanged); + } + + private async Task> SearchProviderKeyAsync(string value, CancellationToken token) + { + if (value.IsNullOrWhiteSpace() || CurrentLookupService == null) + { + ProviderKeys = new List(); + return ProviderKeys; + } + + ProviderKeys = (await PermissionAppService.SearchResourceProviderKeyAsync(ResourceName!, CurrentLookupService, value, 1)).Keys; + + return ProviderKeys; + } + + protected virtual async Task GrantAllCreateAsync(bool value) + { + _grantAllCreate = value; + foreach (var permission in CreateEntity.Permissions) + { + permission.IsGranted = value; + } + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task GrantAllEditAsync(bool value) + { + _grantAllEdit = value; + foreach (var permission in EditEntity.Permissions) + { + permission.IsGranted = value; + } + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task OpenEditDialogAsync(ResourcePermissionGrantInfoDto permission) + { + var resourcePermissions = await PermissionAppService.GetResourceByProviderAsync(ResourceName!, ResourceKey!, permission.ProviderName, permission.ProviderKey); + EditEntity = new EditModel + { + ProviderName = permission.ProviderName, + ProviderKey = permission.ProviderKey, + Permissions = resourcePermissions.Permissions.Select(x => new ResourcePermissionModel + { + Name = x.Name, + DisplayName = x.DisplayName, + IsGranted = x.IsGranted + }).ToList() + }; + + _grantAllEdit = EditEntity.Permissions.All(x => x.IsGranted); + _editDialogVisible = true; + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task CloseCreateDialogAsync() + { + _createDialogVisible = false; + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task CloseEditDialogAsync() + { + _editDialogVisible = false; + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task OnLookupServiceCheckedValueChanged(string? value) + { + CurrentLookupService = value; + _selectedProviderKey = null; + ProviderKeys = new List(); + if (_createFormRef != null) + { + await _createFormRef.ResetAsync(); + } + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task CreateResourcePermissionAsync() + { + if (_createFormRef == null) + { + return; + } + + await _createFormRef.ValidateAsync(); + if (!_createFormRef.IsValid) + { + return; + } + + if (ProviderKey == null || CurrentLookupService == null) + { + return; + } + + await PermissionAppService.UpdateResourceAsync( + ResourceName!, + ResourceKey!, + new UpdateResourcePermissionsDto + { + ProviderName = CurrentLookupService, + ProviderKey = ProviderKey, + Permissions = CreateEntity.Permissions.Where(p => p.IsGranted).Select(p => p.Name).ToList() + } + ); + + await CloseCreateDialogAsync(); + ResourcePermissionList = await PermissionAppService.GetResourceAsync(ResourceName!, ResourceKey!); + await InvokeAsync(StateHasChanged); + Snackbar.Add(L["SavedSuccessfully"], Severity.Success); + } + + protected virtual async Task UpdateResourcePermissionAsync() + { + if (_editFormRef == null) + { + return; + } + + await _editFormRef.ValidateAsync(); + if (!_editFormRef.IsValid) + { + return; + } + + await PermissionAppService.UpdateResourceAsync( + ResourceName!, + ResourceKey!, + new UpdateResourcePermissionsDto + { + ProviderName = EditEntity.ProviderName, + ProviderKey = EditEntity.ProviderKey, + Permissions = EditEntity.Permissions.Where(p => p.IsGranted).Select(p => p.Name).ToList() + } + ); + + await CloseEditDialogAsync(); + ResourcePermissionList = await PermissionAppService.GetResourceAsync(ResourceName!, ResourceKey!); + await InvokeAsync(StateHasChanged); + Snackbar.Add(L["SavedSuccessfully"], Severity.Success); + } + + protected virtual async Task DeleteResourcePermissionAsync(ResourcePermissionGrantInfoDto permission) + { + var confirmed = await DialogService.ShowMessageBoxAsync( + L["AreYouSure"], + L["ResourcePermissionDeletionConfirmationMessage"], + yesText: L["Yes"], + cancelText: L["Cancel"]); + + if (confirmed == true) + { + await PermissionAppService.DeleteResourceAsync( + ResourceName!, + ResourceKey!, + permission.ProviderName, + permission.ProviderKey + ); + + ResourcePermissionList = await PermissionAppService.GetResourceAsync(ResourceName!, ResourceKey!); + Snackbar.Add(L["DeletedSuccessfully"], Severity.Success); + await InvokeAsync(StateHasChanged); + } + } + + public class CreateModel + { + public List Permissions { get; set; } = new(); + } + + public class EditModel + { + public string ProviderName { get; set; } = string.Empty; + + public string ProviderKey { get; set; } = string.Empty; + + public List Permissions { get; set; } = new(); + } + + public class ResourcePermissionModel + { + public string Name { get; set; } = string.Empty; + + public string DisplayName { get; set; } = string.Empty; + + public bool IsGranted { get; set; } + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/FodyWeavers.xml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/FodyWeavers.xsd b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/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/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Volo.Abp.PermissionManagement.Blazor.MudBlazor.csproj b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Volo.Abp.PermissionManagement.Blazor.MudBlazor.csproj new file mode 100644 index 0000000000..d12603d05f --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/Volo.Abp.PermissionManagement.Blazor.MudBlazor.csproj @@ -0,0 +1,18 @@ + + + + + + + net10.0 + + + + + + + + + + + diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor new file mode 100644 index 0000000000..614bc218c7 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor.MudBlazor/_Imports.razor @@ -0,0 +1,4 @@ +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using MudBlazor diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs index 8eeea26a97..0bfdf1eba3 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs @@ -144,7 +144,7 @@ public class PermissionManager : IPermissionManager, ISingletonDependency if (permission.Providers.Any() && !permission.Providers.Contains(providerName)) { //TODO: BusinessException - throw new ApplicationException($"The permission named '{permission.Name}' has not compatible with the provider named '{providerName}'"); + throw new ApplicationException($"The permission named '{permission.Name}' is not compatible with the provider named '{providerName}'"); } if (!permission.MultiTenancySide.HasFlag(CurrentTenant.GetMultiTenancySide())) diff --git a/modules/setting-management/Volo.Abp.SettingManagement.slnx b/modules/setting-management/Volo.Abp.SettingManagement.slnx index f44b64c8e0..5b73c332f0 100644 --- a/modules/setting-management/Volo.Abp.SettingManagement.slnx +++ b/modules/setting-management/Volo.Abp.SettingManagement.slnx @@ -5,6 +5,9 @@ + + + diff --git a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json index 4de9410e3b..f5892fa17a 100644 --- a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json +++ b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json @@ -3,6 +3,6 @@ "name": "demo-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1" } } diff --git a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock index 2e8f03c042..1504b8a6e1 100644 --- a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock +++ b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.3.0.tgz#bf48e8fa571067d15bc19bdd3f13509acf58f9c7" - integrity sha512-t9pHqrXaCDYcsdrz0VPKT28ujnnU+EVxyVDogQSRRpKsDoLhNcOD8NI3swdAWBQZ46YSCDfeDR+gfwjC3yqpXQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.4.1.tgz#6691f4a120cef77ec0ba341efc45fa828cebc98a" + integrity sha512-nMLbBHFUk4eAmu5kj72k9Hf9FFXlt3QWQoW3CxXEEvdynTwjSEwHMvp8zgLalyelxvKuO4tKKoYVn3fU0zUBKA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.4.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.3.0.tgz#bcf72b5dd1dea285c3fa7cb1b6ce27c29b985ff7" - integrity sha512-X5xmMSxnIzNJPexvKR9y6jyn0v5n2wscN1SECG/bMI1YdKugMlkfkSuyUmLn+RX9/YdcK0jOsdMhfuzs5mgAOA== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.4.1.tgz#b12adabcb73a99d4597509ca2b0ff0475c81218e" + integrity sha512-tlb+b2OtPXzdRTMIPcoGuzH2glTAUSwJ+Pa3tDUZiwmnsZ1WTgwF54s4pwmQxSNHpFu1okdpeP1KHw0FEEFTzw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.3.0" - "@abp/bootstrap" "~10.3.0" - "@abp/bootstrap-datepicker" "~10.3.0" - "@abp/bootstrap-daterangepicker" "~10.3.0" - "@abp/datatables.net-bs5" "~10.3.0" - "@abp/font-awesome" "~10.3.0" - "@abp/jquery-validation-unobtrusive" "~10.3.0" - "@abp/lodash" "~10.3.0" - "@abp/luxon" "~10.3.0" - "@abp/malihu-custom-scrollbar-plugin" "~10.3.0" - "@abp/moment" "~10.3.0" - "@abp/select2" "~10.3.0" - "@abp/sweetalert2" "~10.3.0" - "@abp/timeago" "~10.3.0" - -"@abp/aspnetcore.mvc.ui@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.3.0.tgz#0d125efd172b6772c516eae82cfe35f144da73fc" - integrity sha512-kp0XdwrlNz+NeLdxSJ6xdUaB5PM7xdbiLsIyTUcwa7AUKBtcVU6yq6qttbk9VyO0KqDgLywSuEL+9PkFhthuPg== + "@abp/aspnetcore.mvc.ui" "~10.4.1" + "@abp/bootstrap" "~10.4.1" + "@abp/bootstrap-datepicker" "~10.4.1" + "@abp/bootstrap-daterangepicker" "~10.4.1" + "@abp/datatables.net-bs5" "~10.4.1" + "@abp/font-awesome" "~10.4.1" + "@abp/jquery-validation-unobtrusive" "~10.4.1" + "@abp/lodash" "~10.4.1" + "@abp/luxon" "~10.4.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.4.1" + "@abp/moment" "~10.4.1" + "@abp/select2" "~10.4.1" + "@abp/sweetalert2" "~10.4.1" + "@abp/timeago" "~10.4.1" + +"@abp/aspnetcore.mvc.ui@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.4.1.tgz#b39b2379da89fbadaec1279ba71f5e1a17cb3855" + integrity sha512-KhtMaMBGVqb+RyOyzS6ZpreGbiMZ1PU4tPJq9Eo80Cynki+E1ERoM/7aFQ30vvXchMXWHyCENAY34r47G81dOA== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.3.0.tgz#032aaf0e474f971b9f90ca7fb83ff7ca4d7be933" - integrity sha512-7mRuRRIE4R0yw1cZuoIskgrqZPUa5/SJzse0K6u+/QDqvyLjjOsyE51WuqAXxlVfKpLp/fBvXZwnCDeCRU9iZQ== +"@abp/bootstrap-datepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.4.1.tgz#b7ad625795df22396bd1cd3c74d458efd3b7d8f0" + integrity sha512-X3h3/1OJnm9o8PRyotxEkC4SZeVDAJ7VfN23OFAffraqOWj99kfhbygX6CziAPqTTbu/gUxgow9oqw15M89F2w== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.3.0.tgz#b2a9171595ee311c32f57460013870cde67c6b7f" - integrity sha512-ejLu3sWhfNDlsXeUXAQudZvPv0xM4McxIK65vuVEZiKIwFSnrLYil8jBi12EJJmAw5yYA/KC0EdyG82E+AWpmA== +"@abp/bootstrap-daterangepicker@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.4.1.tgz#a70aba1b0a0189bc8332f982289776abe86695c3" + integrity sha512-5z+7UkdfBteedbOPgq/i9aSYY6DeFIg9L97/Ema5t626+NQonG1tA1YVnrUWMi+K1di1BHbx5FEIWcPEH7rFmg== dependencies: - "@abp/moment" "~10.3.0" + "@abp/moment" "~10.4.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.3.0.tgz#23535c90613271b02fdad3eae1556b67844729e6" - integrity sha512-JnVOeJUyR78oo+QURvaNqa9xGZaVux0PRIpFpd4Qsqiiz9FvJrbCuSoBkkZoj/0eH23WoZ3bJxy0K9LDE19qvg== +"@abp/bootstrap@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.4.1.tgz#b44949abc6fa2d20b14044dd0010147caa5d9df9" + integrity sha512-9NZlHg0Xj/p1ffoUOKcxcfb6YXCjEdxWkmlqXx3qWM7r3yS06G5yHbQrYClnY+9SQuP2+/FtwRZ87HfH7zVg9w== dependencies: - "@abp/core" "~10.3.0" - "@abp/popper.js" "~10.3.0" + "@abp/core" "~10.4.1" + "@abp/popper.js" "~10.4.1" bootstrap "^5.3.8" -"@abp/core@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.3.0.tgz#9960f8179d1fdc42250d0bbba60e379590c8199c" - integrity sha512-DpL4qrsyCUolypUwxKVDDK8bPVhNbYKx6l2ytUjoOu73CwgPoCSGkvHWrmKIsEsmTfOC9J7+GDErh8jWR/5pVA== +"@abp/core@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.4.1.tgz#1a286e98b6217ae65e58bca7f65b57b86d298ae1" + integrity sha512-pOtbCwq7Vi244Vql3CjZ5VY4SqEUuR4VSDeZ8ZvJnP/aQWaHb229z1iZMcjg/L+Mu0J3eHQso7rrP7n+4ww0Jw== dependencies: - "@abp/utils" "~10.3.0" + "@abp/utils" "~10.4.1" -"@abp/datatables.net-bs5@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.3.0.tgz#d8711c52f7c03df51d14d1a5f0e6b00f6d9551fc" - integrity sha512-I8rEU98kEhsNZjVdpPvOcG6F4xr1CgIRb0bTf5bfd8ufzxEUKB+xF9bguJIB5kYwjme8QbMlcJ0V/c+N4iewtQ== +"@abp/datatables.net-bs5@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.4.1.tgz#0ee2cb1a2f22146fd3561eab6ed412ac4301b928" + integrity sha512-bXl2JUpa7/IGWq9IsBlp+OVRdeIiGaaXbyKONSdI1LGeY9L6W3cHF90E68c+DaCUlcTkRUgshWvEhxSRAEEDnQ== dependencies: - "@abp/datatables.net" "~10.3.0" + "@abp/datatables.net" "~10.4.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.3.0.tgz#f98319d8e226da5aced4d1d94d3ababb11327074" - integrity sha512-imM25WmO1V0hqeG5iPeLvYMID2HX7SG9drz47qilr74MhpY2i2J7HlsvZDonNkjpxzhK4DHpGeQd6mE2QzG7Hw== +"@abp/datatables.net@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.4.1.tgz#62b2ca692ee7b59317ac46a80e46fc2f8df576ef" + integrity sha512-Lz0DUwlICTZiz0C7F2s32ddFOmlTQmEK+bUuigh7miir/lTb0GuoDIeDRQxFM7ucdsc3wo+ImbDwPvRI35XF4A== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.3.0.tgz#43602605f5633132cc6fe21b6ce5ab19b86bdf27" - integrity sha512-RUOVHxDyT81hp01DhUuSshKvLM+NiTt9VP+O4/rSj+SOaSJAm3wX7eziCKrDi6H41FwhoZL75CifLhcZVyVO4A== +"@abp/font-awesome@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.4.1.tgz#936674af94d418eb4ebf5b42fbf8694e78139172" + integrity sha512-FEY5XWyjLFbcydJyguim/nd6T0CS4K3xKg0wdciUEz4Nb8dAvi4mUeMcQNrD5qEtuhOA1OuGKbiG5TtTN2X/bg== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.3.0.tgz#5bc14ed642d48f9eae54761ad39bd9f51beeb005" - integrity sha512-j/9fA7tAs1/t3P2U1kKUiDlHAImmseul6KzvV77sNpnms7g4+x6Fgsa+sMzicSRGOLz7lK5RHdBFyvfKxt7/nA== +"@abp/jquery-validation-unobtrusive@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.4.1.tgz#b0ea174c262659215b69c6a92612b219273a091a" + integrity sha512-HGb+yM26BWk1ne4iUAD2HqMbOk75xNYIA+V1ny469eh1JdWBzlI+gJOQCiFbwdxnEWDJ2QsWiN2DmLY/0SGmqQ== dependencies: - "@abp/jquery-validation" "~10.3.0" + "@abp/jquery-validation" "~10.4.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.3.0.tgz#e669a54d28f6c0b863a284322e527a5c65cb7dbf" - integrity sha512-Wo95ZJLS1ScKm7dm3zFlEFUKqXrqo3m9+82DX5mFJUVCHHuBPrhxvDjxNJ18z4gpoOUu08uqxX7LFbphtkRM3g== +"@abp/jquery-validation@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.4.1.tgz#21970617ca9286328b85b64501f00e444bd0ec0d" + integrity sha512-mRZdEMssYwCjZOLQEFcXQbM//jrTMCMp6P5E1acQBaRZfaTPg/mNCHmdgd9vTRRgXfaAhQhkpYSkwv3GzAXbgA== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.3.0.tgz#3b60bcdfe8f9a33be79db1bea2500837fe09c67d" - integrity sha512-TndX/8bJx5vGAAwIykx2CJUZIFFDkwpD+cUjE45WO2dSrY2M5vNfWiuw95OcLGngO8RaT/gTkFGzPuZqKn9ggg== +"@abp/jquery@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.4.1.tgz#9828be045b0ae767cc6d7c2a0b4affe2aea19bf7" + integrity sha512-m9Pg0kV3oCDh0fzcseKF4InM83Ryvrhf5iXioBklshqGAXodKgnk5YIwfsSjTIcj9wMuWdpznuxC5bMo+PP7ig== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" jquery "~3.7.1" -"@abp/lodash@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.3.0.tgz#bd091fc811d85d2f535802631af6165ac1c8a198" - integrity sha512-7JDU9UlbD+9odhFQjolvyJZ6EQKG5kiL2Pt9T1RMVchNh47iEuZwfi8c+CIxa3YxA3n6lAsZ+8Pmxq4yOku+RQ== +"@abp/lodash@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.4.1.tgz#f3d5532aef9772ccb38a6c7875a6b8b29db3ddc8" + integrity sha512-GcUNsy9rs2HET8v86PwCCwi9R0XOt5nklBjGhDyER+fzGSHHLFKX87WWjPulwBtxFCfEHMaYvCDAnkGO15tFwA== dependencies: - "@abp/core" "~10.3.0" - lodash "^4.17.21" + "@abp/core" "~10.4.1" + lodash "^4.18.1" -"@abp/luxon@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.3.0.tgz#acbd92289456a546607ed4506dc36f71e6da53c9" - integrity sha512-IIi2+odQzuFK4qV1KJ2HtlTewSPby2F9CJFNNtNRMzX/P30dQkAGWpNV5/FZzJwFIihR+zSei5wlu11WpPugsw== +"@abp/luxon@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.4.1.tgz#d279101a35f02e061eb08b0ac20285c17208ffc7" + integrity sha512-Tr8hhMzWPoiQlSHBvhxxy0viu8dw2y8JikvyDc2sQusm258vlHi4NMH3FAY7JsyeiZFH6fwlRTJjkEAsdRLlCQ== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.3.0.tgz#209931d02a994ef448371d6203e9ec74c72b61e6" - integrity sha512-hJjwEExP3hKE1MNDG+pcvoHRFUihh3t/0wCPNaoePFd8+QnJuyQ9U0VA+vdtykTxm8uFNE2uaCgyHffoHmzeZA== +"@abp/malihu-custom-scrollbar-plugin@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.4.1.tgz#fee2f4ffe0b6a56ec1afddc51217bd569be9163c" + integrity sha512-iCvuDqoRtM0HtWToyzbox14kE9Y9E3DFRbE+sISB6vCOL+3gW+xEHmmOMzESWQ3DX+p/q8xidQMxCZsyo2l0IA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.3.0.tgz#5a5c4ece14d1bc4dd33134f85d1d085733d3c975" - integrity sha512-8DKw2jgC/28nLrB/CR9BiyUSt0pUmWF+bEgAsduln6VrSCHn0aRLqgs+wi/9Ol+1bl7pO2wbJOYg1qbnkDhrAQ== +"@abp/moment@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.4.1.tgz#43e8e4ebc6a4da3877b0333d463362df46e57f83" + integrity sha512-RNx+5gXkI902rWlFbMSHEYd+FAoiXKxfKrmj1hWiK4GSwhY+vike3oAcdGjcpkFyAG1o+5MkL5p9951eHtn1RA== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.3.0.tgz#cb2415e45a905854980f4e864c00c8fe1156a6f9" - integrity sha512-Yfafs50t7DKnShxAl9WPpihZxG5cLAt99rJmIyXBYUQIpkHkxDyUIA5b4gCoVCqKgjFDOxG7i6L+Ovd/2N8k7g== +"@abp/popper.js@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.4.1.tgz#616691ccdd8632e84bf9419e59db841ef2ea610c" + integrity sha512-74kOjXTb4FRbg5t4dTsZ11io9iuEG38IjPyYue/1yIXVbc2hruIhcJC9V3HFZLirQv1s1rkRqmnf38kVjUiKHA== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.3.0.tgz#199777173ca3e43a5448f8b51b30c3aafe4a3333" - integrity sha512-6sEIYIakVI1DM3eOqOaxwdG8vkyVFBWvVZhoChPZkgIIQ0EH2JLerHv0pePJEl6mQSxOUrikPA8++B2y+sBnQA== +"@abp/select2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.4.1.tgz#8729f7648073ecedc93915d3c618d10c3c557cbb" + integrity sha512-BtjhMMYKcHl1ZN04L+iWhQWzzqKGbpHi8tMe6o75ZunIpVtIFlIPUpzVUI0AaH6t2sIhlrYFEoxvGxA31lcIXw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.3.0.tgz#515e0477574b54bc6650b9f5ee193bfc648c715a" - integrity sha512-bY5MXEiKja+vk71ujSn8rpzRC45Ge8zvcca1k1vBJs+oKyRCii//JBxa3H0lagZo5r/QwIqv5ihdyLE1xsuLSg== +"@abp/sweetalert2@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.4.1.tgz#68b4abe96c71b05d956c362edc19ccce8b9bbe42" + integrity sha512-6Ce7uR49sqJBkqRfR56cn6jI898+xXHbWgJV+VHN+Dm6w6WX5MbEiOnjF3VLwaq0pwI3FeoCALZAAgra6t9vVw== dependencies: - "@abp/core" "~10.3.0" + "@abp/core" "~10.4.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.3.0.tgz#3214a6d000c79504c9fa7cb4e004a967fe46459b" - integrity sha512-Rgcex+jvM9Z9/E0I5Q3wlC2bY5550/+6xRX407k5ho4gxCccYo3c0Gkpiy5pJRFhTwtBgcNzYxArh6NCez+tDg== +"@abp/timeago@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.4.1.tgz#a01c3840eb3c1e67b5c9fdd0ead020acb01d6676" + integrity sha512-sh1afPeBSsL1RNBhWRn4FOD1ZgRE8HQc0zOhCAl6Kb7WXWcDuP8RPKbGLolnpNHgXfz12lhE6TsbsNFAaCGYjg== dependencies: - "@abp/jquery" "~10.3.0" + "@abp/jquery" "~10.4.1" timeago "^1.6.7" -"@abp/utils@~10.3.0": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.3.0.tgz#d88c0d639d69d6210c3070892aac61d5e945c124" - integrity sha512-iNehxiqWJQl2wwE9EbsQANPv/ITMh0yUcPATUlG/McoLWdpPcqobx75NxXcHDqW8ZarecqSktjJDZPvaUaD+jA== +"@abp/utils@~10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.4.1.tgz#b5c7b57e3c2ef8f87c0c5780995d82a7baf2ee9d" + integrity sha512-kPp7PsaMni81vQK3J1F4j3Eem4dBlZuqIYR6QWbO+udgS5CzYGJagdw+HsOGer6YzJHhcGFfe0LcxS1z/VsFDQ== dependencies: just-compare "^2.3.0" @@ -268,10 +268,10 @@ just-compare@^2.3.0: resolved "https://registry.yarnpkg.com/just-compare/-/just-compare-2.3.0.tgz#a2adcc1d1940536263275f5a1ef1298bcacfeda7" integrity sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg== -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== luxon@^3.7.2: version "3.7.2" diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/AbpSettingManagementBlazorMudBlazorServerModule.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/AbpSettingManagementBlazorMudBlazorServerModule.cs new file mode 100644 index 0000000000..8a100a605e --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/AbpSettingManagementBlazorMudBlazorServerModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Server; + +[DependsOn( + typeof(AbpSettingManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsServerThemingMudBlazorModule) +)] +public class AbpSettingManagementBlazorMudBlazorServerModule : AbpModule +{ +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/FodyWeavers.xml b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/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/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server.csproj b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server.csproj new file mode 100644 index 0000000000..99c10ba6e0 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server/Volo.Abp.SettingManagement.Blazor.MudBlazor.Server.csproj @@ -0,0 +1,15 @@ + + + + + + + net10.0 + + + + + + + + diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/AbpSettingManagementBlazorMudBlazorWebAssemblyModule.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/AbpSettingManagementBlazorMudBlazorWebAssemblyModule.cs new file mode 100644 index 0000000000..6ecf4db202 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/AbpSettingManagementBlazorMudBlazorWebAssemblyModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; +using Volo.Abp.Modularity; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly; + +[DependsOn( + typeof(AbpSettingManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule), + typeof(AbpSettingManagementHttpApiClientModule) +)] +public class AbpSettingManagementBlazorMudBlazorWebAssemblyModule : AbpModule +{ +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/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/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly.csproj b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly.csproj new file mode 100644 index 0000000000..c219f565c9 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.SettingManagement.Blazor.MudBlazor.WebAssembly.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + + + + + + + + + diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/AbpSettingManagementBlazorMudBlazorModule.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/AbpSettingManagementBlazorMudBlazorModule.cs new file mode 100644 index 0000000000..2e5f322047 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/AbpSettingManagementBlazorMudBlazorModule.cs @@ -0,0 +1,51 @@ +using Localization.Resources.AbpUi; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor; +using Volo.Abp.Localization; +using Volo.Abp.Mapperly; +using Volo.Abp.Modularity; +using Volo.Abp.SettingManagement.Blazor.MudBlazor.Menus; +using Volo.Abp.SettingManagement.Blazor.MudBlazor.Settings; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; +using Volo.Abp.SettingManagement.Localization; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor; + +[DependsOn( + typeof(AbpMapperlyModule), + typeof(AbpAspNetCoreComponentsWebThemingMudBlazorModule), + typeof(AbpSettingManagementApplicationContractsModule) +)] +public class AbpSettingManagementBlazorMudBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + + Configure(options => + { + options.MenuContributors.Add(new SettingManagementMenuContributor()); + }); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpSettingManagementBlazorMudBlazorModule).Assembly); + }); + + Configure(options => + { + options.Contributors.Add(new EmailingPageContributor()); + options.Contributors.Add(new TimeZonePageContributor()); + }); + + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes( + typeof(AbpUiResource) + ); + }); + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/FodyWeavers.xml b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/FodyWeavers.xsd b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/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/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/ISettingComponentContributor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/ISettingComponentContributor.cs new file mode 100644 index 0000000000..b81cd7f78d --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/ISettingComponentContributor.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor; + +public interface ISettingComponentContributor +{ + Task ConfigureAsync(SettingComponentCreationContext context); + + Task CheckPermissionsAsync(SettingComponentCreationContext context); +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Menus/SettingManagementMenuContributor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Menus/SettingManagementMenuContributor.cs new file mode 100644 index 0000000000..477f719a21 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Menus/SettingManagementMenuContributor.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.Features; +using Volo.Abp.SettingManagement.Localization; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Menus; + +public class SettingManagementMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + var settingManagementPageOptions = context.ServiceProvider.GetRequiredService>().Value; + var settingPageCreationContext = new SettingComponentCreationContext(context.ServiceProvider); + if (!settingManagementPageOptions.Contributors.Any() || + !(await CheckAnyOfPagePermissionsGranted(settingManagementPageOptions, settingPageCreationContext)) + ) + { + return; + } + + var l = context.GetLocalizer(); + + /* This may happen if MVC UI is being used in the same application. + * In this case, we are removing the MVC setting management UI. */ + context.Menu.GetAdministration().TryRemoveMenuItem(SettingManagementMenus.GroupName); + + context.Menu + .GetAdministration() + .AddItem( + new ApplicationMenuItem( + SettingManagementMenus.GroupName, + l["Settings"], + "~/setting-management", + icon: "fa fa-cog" + ).RequireFeatures(SettingManagementFeatures.Enable) + ); + } + + protected virtual async Task CheckAnyOfPagePermissionsGranted( + SettingManagementComponentOptions settingManagementComponentOptions, + SettingComponentCreationContext settingComponentCreationContext) + { + foreach (var contributor in settingManagementComponentOptions.Contributors) + { + if (await contributor.CheckPermissionsAsync(settingComponentCreationContext)) + { + return true; + } + } + return false; + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Menus/SettingManagementMenus.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Menus/SettingManagementMenus.cs new file mode 100644 index 0000000000..8c773725e7 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Menus/SettingManagementMenus.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Menus; + +public class SettingManagementMenus +{ + public const string GroupName = "SettingManagement"; +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor new file mode 100644 index 0000000000..bec6df4d84 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor @@ -0,0 +1,106 @@ +@using Volo.Abp.SettingManagement.Localization +@using global::MudBlazor +@inherits Volo.Abp.AspNetCore.Components.AbpComponentBase + +@if (EmailSettings != null) +{ + + + + + + + + + + + + + + + + + + + + + @if (!EmailSettings.SmtpUseDefaultCredentials) + { + + + + + + + + + + } + + + +
+ @if (HasSendTestEmailPermission) + { + + @L["SendTestEmail"] + + } + + @L["Save"] + +
+ + @if (HasSendTestEmailPermission) + { + + + @L["SendTestEmail"] + + + + + + + + + + + @L["Cancel"] + @L["Send"] + + + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs new file mode 100644 index 0000000000..28c74ad285 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs @@ -0,0 +1,207 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.AspNetCore.Components.Web.Configuration; +using Volo.Abp.Auditing; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.SettingManagement.Localization; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Pages.SettingManagement.EmailSettingGroup; + +public partial class EmailSettingGroupViewComponent +{ + [Inject] + protected IEmailSettingsAppService EmailSettingsAppService { get; set; } = default!; + + [Inject] + protected IPermissionChecker PermissionChecker { get; set; } = default!; + + [Inject] + private ICurrentApplicationConfigurationCacheResetService CurrentApplicationConfigurationCacheResetService { get; set; } = default!; + + [Inject] + protected IUiMessageService UiMessageService { get; set; } = default!; + + [Inject] + protected ISnackbar Snackbar { get; set; } = default!; + + protected UpdateEmailSettingsViewModel? EmailSettings; + + protected SendTestEmailViewModel SendTestEmailInput = new(); + + protected MudForm? _emailFormRef; + protected MudForm? _testEmailFormRef; + + protected bool _sendTestEmailDialogVisible; + + protected bool HasSendTestEmailPermission { get; set; } + + public EmailSettingGroupViewComponent() + { + ObjectMapperContext = typeof(AbpSettingManagementBlazorMudBlazorModule); + LocalizationResource = typeof(AbpSettingManagementResource); + } + + protected async override Task OnInitializedAsync() + { + try + { + EmailSettings = ObjectMapper.Map(await EmailSettingsAppService.GetAsync()); + HasSendTestEmailPermission = await PermissionChecker.IsGrantedAsync(SettingManagementPermissions.EmailingTest); + SendTestEmailInput = new SendTestEmailViewModel(); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual async Task UpdateSettingsAsync() + { + try + { + if (_emailFormRef == null) + { + return; + } + + await _emailFormRef.ValidateAsync(); + if (!_emailFormRef.IsValid) + { + return; + } + + if (EmailSettings == null) + { + return; + } + + await EmailSettingsAppService.UpdateAsync(ObjectMapper.Map(EmailSettings)); + + await CurrentApplicationConfigurationCacheResetService.ResetAsync(); + + Snackbar.Add(L["SavedSuccessfully"], Severity.Success); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual async Task OpenSendTestEmailDialogAsync() + { + try + { + if (_testEmailFormRef != null) + { + await _testEmailFormRef.ResetAsync(); + } + var emailSettings = await EmailSettingsAppService.GetAsync(); + SendTestEmailInput = new SendTestEmailViewModel + { + SenderEmailAddress = emailSettings.DefaultFromAddress, + TargetEmailAddress = CurrentUser.Email, + Subject = L["TestEmailSubject", new Random().Next(1000, 9999)], + Body = L["TestEmailBody"] + }; + + _sendTestEmailDialogVisible = true; + await InvokeAsync(StateHasChanged); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + protected virtual Task CloseSendTestEmailDialogAsync() + { + _sendTestEmailDialogVisible = false; + return InvokeAsync(StateHasChanged); + } + + protected virtual async Task SendTestEmailAsync() + { + try + { + if (_testEmailFormRef == null) + { + return; + } + + await _testEmailFormRef.ValidateAsync(); + if (!_testEmailFormRef.IsValid) + { + return; + } + + await EmailSettingsAppService.SendTestEmailAsync(ObjectMapper.Map(SendTestEmailInput)); + + Snackbar.Add(L["SentSuccessfully"], Severity.Success); + + await CloseSendTestEmailDialogAsync(); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + public class UpdateEmailSettingsViewModel + { + [MaxLength(256)] + [Display(Name = "SmtpHost")] + public string? SmtpHost { get; set; } + + [Range(1, 65535)] + [Display(Name = "SmtpPort")] + public int SmtpPort { get; set; } + + [MaxLength(1024)] + [Display(Name = "SmtpUserName")] + public string? SmtpUserName { get; set; } + + [MaxLength(1024)] + [DataType(DataType.Password)] + [DisableAuditing] + [Display(Name = "SmtpPassword")] + public string? SmtpPassword { get; set; } + + [MaxLength(1024)] + [Display(Name = "SmtpDomain")] + public string? SmtpDomain { get; set; } + + [Display(Name = "SmtpEnableSsl")] + public bool SmtpEnableSsl { get; set; } + + [Display(Name = "SmtpUseDefaultCredentials")] + public bool SmtpUseDefaultCredentials { get; set; } + + [MaxLength(1024)] + [Required] + [Display(Name = "DefaultFromAddress")] + public string? DefaultFromAddress { get; set; } + + [MaxLength(1024)] + [Required] + [Display(Name = "DefaultFromDisplayName")] + public string? DefaultFromDisplayName { get; set; } + } + + public class SendTestEmailViewModel + { + [Required] + public string? SenderEmailAddress { get; set; } + + [Required] + public string? TargetEmailAddress { get; set; } + + [Required] + public string? Subject { get; set; } + + public string? Body { get; set; } + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/SettingManagement.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/SettingManagement.razor new file mode 100644 index 0000000000..a9ccb02a6a --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/SettingManagement.razor @@ -0,0 +1,51 @@ +@page "/setting-management" +@page "/{culture}/setting-management" +@using Microsoft.AspNetCore.Authorization +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Components +@using Volo.Abp.Features +@attribute [Authorize] +@attribute [RequiresFeature(SettingManagementFeatures.Enable)] +@using Microsoft.Extensions.Localization +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation +@using global::MudBlazor + +@* ************************* PAGE HEADER ************************* *@ + + + + + @if (_loading) + { +
+ +
+ } + else if (SettingComponentCreationContext != null && SettingComponentCreationContext.Groups.Any()) + { + + @foreach (var group in SettingComponentCreationContext.Groups) + { + +
+ @{ + SettingItemRenders.Add(builder => + { + builder.OpenComponent(0, group.ComponentType); + builder.CloseComponent(); + }); + } + + @SettingItemRenders.Last() +
+
+ } +
+ } +
+
+ diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/SettingManagement.razor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/SettingManagement.razor.cs new file mode 100644 index 0000000000..a6ebdd07e7 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/SettingManagement.razor.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using MudBlazor; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.SettingManagement.Localization; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Pages.SettingManagement; + +public partial class SettingManagement +{ + [Parameter] + public string? Culture { get; set; } + + [Inject] + protected IServiceProvider ServiceProvider { get; set; } = default!; + + protected SettingComponentCreationContext SettingComponentCreationContext { get; set; } = default!; + + [Inject] + protected IOptions _options { get; set; } = default!; + + [Inject] + protected IStringLocalizer L { get; set; } = default!; + + protected SettingManagementComponentOptions Options => _options.Value; + + protected List SettingItemRenders { get; set; } = new List(); + + protected string? SelectedGroup; + protected List BreadcrumbItems = new(); + + protected int _activeTabIndex = 0; + + protected bool _loading = true; + + protected async override Task OnInitializedAsync() + { + BreadcrumbItems.Add(new BreadcrumbItem(LUiNavigation["Menu:Administration"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Menu:Settings"].Value, null, disabled: true)); + + SettingComponentCreationContext = new SettingComponentCreationContext(ServiceProvider); + + foreach (var contributor in Options.Contributors) + { + await contributor.ConfigureAsync(SettingComponentCreationContext); + } + SettingComponentCreationContext.Normalize(); + SettingItemRenders.Clear(); + + if (SelectedGroup.IsNullOrEmpty() && SettingComponentCreationContext.Groups.Any()) + { + SelectedGroup = GetNormalizedString(SettingComponentCreationContext.Groups.First().Id); + _activeTabIndex = 0; + } + + _loading = false; + } + + protected virtual string GetNormalizedString(string value) + { + return value.Replace('.', '_'); + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/TimeZoneSettingGroup/TimeZoneSettingGroupViewComponent.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/TimeZoneSettingGroup/TimeZoneSettingGroupViewComponent.razor new file mode 100644 index 0000000000..f2689862c6 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/TimeZoneSettingGroup/TimeZoneSettingGroupViewComponent.razor @@ -0,0 +1,32 @@ +@using Volo.Abp.SettingManagement.Localization +@using global::MudBlazor +@inherits Volo.Abp.AspNetCore.Components.AbpComponentBase + +@if (TimezoneSettings != null) +{ + + + + + @foreach (var item in TimezoneSettings.TimeZoneItems) + { + @item.Name + } + + @L["TimezoneHelpText"] + + +
+ + @L["Save"] + +
+
+} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/TimeZoneSettingGroup/TimeZoneSettingGroupViewComponent.razor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/TimeZoneSettingGroup/TimeZoneSettingGroupViewComponent.razor.cs new file mode 100644 index 0000000000..90481994e7 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Pages/SettingManagement/TimeZoneSettingGroup/TimeZoneSettingGroupViewComponent.razor.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Messages; +using Volo.Abp.AspNetCore.Components.Web.Configuration; +using Volo.Abp.SettingManagement.Localization; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Pages.SettingManagement.TimeZoneSettingGroup; + +public partial class TimeZoneSettingGroupViewComponent +{ + [Inject] + protected ITimeZoneSettingsAppService TimeZoneSettingsAppService { get; set; } = default!; + + [Inject] + private ICurrentApplicationConfigurationCacheResetService CurrentApplicationConfigurationCacheResetService { get; set; } = default!; + + [Inject] + protected IUiMessageService UiMessageService { get; set; } = default!; + + [Inject] + protected ISnackbar Snackbar { get; set; } = default!; + + protected UpdateTimezoneSettingsViewModel? TimezoneSettings; + + public TimeZoneSettingGroupViewComponent() + { + ObjectMapperContext = typeof(AbpSettingManagementBlazorMudBlazorModule); + LocalizationResource = typeof(AbpSettingManagementResource); + } + + protected async override Task OnInitializedAsync() + { + TimezoneSettings = new UpdateTimezoneSettingsViewModel() + { + Timezone = await TimeZoneSettingsAppService.GetAsync(), + TimeZoneItems = await TimeZoneSettingsAppService.GetTimezonesAsync() + }; + } + + protected virtual async Task OnSelectedValueChangedAsync(string? timezone) + { + if (TimezoneSettings != null) + { + TimezoneSettings.Timezone = timezone; + } + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task UpdateSettingsAsync() + { + try + { + if (TimezoneSettings == null) + { + return; + } + + await TimeZoneSettingsAppService.UpdateAsync(TimezoneSettings.Timezone); + await CurrentApplicationConfigurationCacheResetService.ResetAsync(); + Snackbar.Add(L["SavedSuccessfully"], Severity.Success); + } + catch (Exception ex) + { + await HandleErrorAsync(ex); + } + } + + public class UpdateTimezoneSettingsViewModel + { + public string? Timezone { get; set; } + + public List? TimeZoneItems { get; set; } + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingComponentCreationContext.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingComponentCreationContext.cs new file mode 100644 index 0000000000..b032b09841 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingComponentCreationContext.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor; + +public class SettingComponentCreationContext : IServiceProviderAccessor +{ + public IServiceProvider ServiceProvider { get; } + + public List Groups { get; private set; } + + public SettingComponentCreationContext(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + + Groups = new List(); + } + + public void Normalize() + { + Order(); + } + + private void Order() + { + Groups = Groups.OrderBy(item => item.Order).ThenBy(item => item.DisplayName).ToList(); + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingComponentGroup.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingComponentGroup.cs new file mode 100644 index 0000000000..9755b410b1 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingComponentGroup.cs @@ -0,0 +1,40 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor; + +public class SettingComponentGroup +{ + public const int DefaultOrder = 1000; + + public string Id { + get => _id; + set => _id = Check.NotNullOrWhiteSpace(value, nameof(Id)); + } + private string _id = string.Empty; + + public string DisplayName { + get => _displayName; + set => _displayName = Check.NotNullOrWhiteSpace(value, nameof(DisplayName)); + } + private string _displayName = string.Empty; + + public Type ComponentType { + get => _componentType; + set => _componentType = Check.NotNull(value, nameof(ComponentType)); + } + private Type _componentType = null!; + + public object? Parameter { get; set; } + + public int Order { get; set; } + + public SettingComponentGroup([NotNull] string id, [NotNull] string displayName, [NotNull] Type componentType, object? parameter = null, int order = DefaultOrder) + { + Id = id; + DisplayName = displayName; + ComponentType = componentType; + Parameter = parameter; + Order = order; + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingManagementBlazorMudBlazorMappers.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingManagementBlazorMudBlazorMappers.cs new file mode 100644 index 0000000000..89b9b23de6 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingManagementBlazorMudBlazorMappers.cs @@ -0,0 +1,30 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using static Volo.Abp.SettingManagement.Blazor.MudBlazor.Pages.SettingManagement.EmailSettingGroup.EmailSettingGroupViewComponent; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor; + +[Mapper] +public partial class UpdateEmailSettingsViewModelToUpdateEmailSettingsDtoMapper : MapperBase +{ + public override partial UpdateEmailSettingsDto Map(UpdateEmailSettingsViewModel source); + + public override partial void Map(UpdateEmailSettingsViewModel source, UpdateEmailSettingsDto destination); +} + + +[Mapper] +public partial class EmailSettingsDtoToUpdateEmailSettingsViewModelMapper : MapperBase +{ + public override partial UpdateEmailSettingsViewModel Map(EmailSettingsDto source); + + public override partial void Map(EmailSettingsDto source, UpdateEmailSettingsViewModel destination); +} + +[Mapper] +public partial class SendTestEmailViewModelToSendTestEmailInputMapper : MapperBase +{ + public override partial SendTestEmailInput Map(SendTestEmailViewModel source); + + public override partial void Map(SendTestEmailViewModel source, SendTestEmailInput destination); +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingManagementComponentOptions.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingManagementComponentOptions.cs new file mode 100644 index 0000000000..7e5e473c3d --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/SettingManagementComponentOptions.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor; + +public class SettingManagementComponentOptions +{ + public List Contributors { get; } + + public SettingManagementComponentOptions() + { + Contributors = new List(); + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Settings/EmailingPageContributor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Settings/EmailingPageContributor.cs new file mode 100644 index 0000000000..bdb204e698 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Settings/EmailingPageContributor.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; +using Volo.Abp.SettingManagement.Blazor.MudBlazor.Pages.SettingManagement.EmailSettingGroup; +using Volo.Abp.SettingManagement.Localization; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Settings; + +public class EmailingPageContributor : ISettingComponentContributor +{ + public async Task ConfigureAsync(SettingComponentCreationContext context) + { + if (!await CheckPermissionsInternalAsync(context)) + { + return; + } + + var l = context.ServiceProvider.GetRequiredService>(); + context.Groups.Add( + new SettingComponentGroup( + "Volo.Abp.SettingManagement", + l["Menu:Emailing"], + typeof(EmailSettingGroupViewComponent) + ) + ); + } + + public async Task CheckPermissionsAsync(SettingComponentCreationContext context) + { + return await CheckPermissionsInternalAsync(context); + } + + private async Task CheckPermissionsInternalAsync(SettingComponentCreationContext context) + { + if (!await CheckFeatureAsync(context)) + { + return false; + } + + var authorizationService = context.ServiceProvider.GetRequiredService(); + + return await authorizationService.IsGrantedAsync(SettingManagementPermissions.Emailing); + } + + private async Task CheckFeatureAsync(SettingComponentCreationContext context) + { + var currentTenant = context.ServiceProvider.GetRequiredService(); + + if (!currentTenant.IsAvailable) + { + return true; + } + + var featureCheck = context.ServiceProvider.GetRequiredService(); + + return await featureCheck.IsEnabledAsync(SettingManagementFeatures.AllowChangingEmailSettings); + + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Settings/TimeZonePageContributor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Settings/TimeZonePageContributor.cs new file mode 100644 index 0000000000..6f812bd6e1 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Settings/TimeZonePageContributor.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp.Features; +using Volo.Abp.SettingManagement.Blazor.MudBlazor.Pages.SettingManagement.TimeZoneSettingGroup; +using Volo.Abp.SettingManagement.Localization; +using Volo.Abp.Timing; + +namespace Volo.Abp.SettingManagement.Blazor.MudBlazor.Settings; + +public class TimeZonePageContributor : ISettingComponentContributor +{ + public async Task ConfigureAsync(SettingComponentCreationContext context) + { + var l = context.ServiceProvider.GetRequiredService>(); + if (await CheckPermissionsAsync(context)) + { + context.Groups.Add( + new SettingComponentGroup( + "Volo.Abp.TimeZone", + l["Menu:TimeZone"], + typeof(TimeZoneSettingGroupViewComponent) + ) + ); + } + } + + public async Task CheckPermissionsAsync(SettingComponentCreationContext context) + { + var authorizationService = context.ServiceProvider.GetRequiredService(); + + return context.ServiceProvider.GetRequiredService().SupportsMultipleTimezone + && await authorizationService.IsGrantedAsync(SettingManagementPermissions.TimeZone); + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Volo.Abp.SettingManagement.Blazor.MudBlazor.csproj b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Volo.Abp.SettingManagement.Blazor.MudBlazor.csproj new file mode 100644 index 0000000000..93f85f21e9 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/Volo.Abp.SettingManagement.Blazor.MudBlazor.csproj @@ -0,0 +1,19 @@ + + + + + + + net10.0 + Volo.Abp.SettingManagement.Blazor.MudBlazor + + + + + + + + + + + diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/_Imports.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/_Imports.razor new file mode 100644 index 0000000000..b1be76fe73 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor.MudBlazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using MudBlazor +@using Volo.Abp.MudBlazorUI diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor index ad571fb448..6cbbe42423 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor @@ -1,4 +1,5 @@ @page "/setting-management" +@page "/{culture}/setting-management" @using Microsoft.AspNetCore.Authorization @using Volo.Abp.AspNetCore.Components.Web.Theming.Layout @using Volo.Abp.Features @@ -43,5 +44,12 @@ } + else + { +
+ +

@L["NoSettingsAvailable"]

+
+ } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs index 1b443fc827..3a1697ca86 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs @@ -12,6 +12,9 @@ namespace Volo.Abp.SettingManagement.Blazor.Pages.SettingManagement; public partial class SettingManagement { + [Parameter] + public string? Culture { get; set; } + [Inject] protected IServiceProvider ServiceProvider { get; set; } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json index 38464ed8c0..36ec3d5ef9 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "تفعيل إعداد نظام الإدارة في التطبيق.", "Feature:AllowChangingEmailSettings": "السماح لتغيير إعدادات البريد الإلكتروني", "Feature:AllowChangingEmailSettingsDescription": "السماح لتغيير إعدادات البريد الإلكتروني.", - "SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور" + "SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور", + "NoSettingsAvailable": "لا توجد إعدادات لعرضها." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json index 34a9f6dd2c..d28d30e5d2 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Povolit systém správy nastavení v aplikaci.", "Feature:AllowChangingEmailSettings": "Povolit změnu nastavení e-mailu", "Feature:AllowChangingEmailSettingsDescription": "Povolit změnu nastavení e-mailu.", - "SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla" + "SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla", + "NoSettingsAvailable": "Nejsou k dispozici žádná nastavení." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json index a2b2c9ab95..b9b5c15c70 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.", "Feature:AllowChangingEmailSettings": "Änderung der E-Mail-Einstellungen zulassen", "Feature:AllowChangingEmailSettingsDescription": "Änderung der E-Mail-Einstellungen zulassen.", - "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren" + "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren", + "NoSettingsAvailable": "Es sind keine Einstellungen anzuzeigen." } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json index fabd73852f..b711b4e3ad 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json @@ -38,6 +38,7 @@ "Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.", "Feature:AllowChangingEmailSettings": "Erlauben Sie das Ändern der E-Mail-Einstellungen", "Feature:AllowChangingEmailSettingsDescription": "Erlauben Sie das Ändern der E-Mail-Einstellungen.", - "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren" + "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren", + "NoSettingsAvailable": "Es sind keine Einstellungen anzuzeigen." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json index 118b1664fe..e97871d303 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json @@ -33,6 +33,7 @@ "Feature:SettingManagementEnableDescription": "Ενεργοποίηση συστήματος διαχείρισης ρυθμίσεων στην εφαρμογή.", "Feature:AllowChangingEmailSettings": "Επιτρέψτε την αλλαγή των ρυθμίσεων email", "Feature:AllowChangingEmailSettingsDescription": "Επιτρέψτε την αλλαγή των ρυθμίσεων email.", - "SmtpPasswordPlaceholder": "Εισαγάγετε μια τιμή για ενημέρωση κωδικού πρόσβασης" + "SmtpPasswordPlaceholder": "Εισαγάγετε μια τιμή για ενημέρωση κωδικού πρόσβασης", + "NoSettingsAvailable": "Δεν υπάρχουν ρυθμίσεις για εμφάνιση." } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en-GB.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en-GB.json index 6284af2e2f..5a7dfa2c20 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en-GB.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en-GB.json @@ -2,6 +2,7 @@ "culture": "en-GB", "texts": { "Settings": "Settings", - "SavedSuccessfully": "Saved successfully" + "SavedSuccessfully": "Saved successfully", + "NoSettingsAvailable": "There are no settings to display." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json index cffd6f27cd..0681e3a9cb 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Enable setting management system in the application.", "Feature:AllowChangingEmailSettings": "Allow changing email settings", "Feature:AllowChangingEmailSettingsDescription": "Allow changing email settings.", - "SmtpPasswordPlaceholder": "Enter a value to update password" + "SmtpPasswordPlaceholder": "Enter a value to update password", + "NoSettingsAvailable": "There are no settings to display." } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json index f158ec3b0d..b6f71d89ee 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Habilite el sistema de gestión de la configuración en la aplicación.", "Feature:AllowChangingEmailSettings": "Permitir cambiar la configuración de correo electrónico", "Feature:AllowChangingEmailSettingsDescription": "Permitir cambiar la configuración de correo electrónico.", - "SmtpPasswordPlaceholder": "Ingrese un valor para actualizar la contraseña" + "SmtpPasswordPlaceholder": "Ingrese un valor para actualizar la contraseña", + "NoSettingsAvailable": "No hay ajustes para mostrar." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json index 800ac04a66..af9e74cdb5 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json @@ -18,6 +18,7 @@ "DefaultFromDisplayName": "نام نمایشی ارسال کننده پیش فرض", "Feature:SettingManagementGroup": "تنظیمات مدیریت", "Feature:SettingManagementEnable": "فعال کردن مدیریت تنظیمات", - "Feature:SettingManagementEnableDescription": "سیستم مدیریت تنظیمات را در برنامه فعال کنید." + "Feature:SettingManagementEnableDescription": "سیستم مدیریت تنظیمات را در برنامه فعال کنید.", + "NoSettingsAvailable": "تنظیماتی برای نمایش وجود ندارد." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json index a2062743fd..a9cdd907cf 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Ota asetustenhallintajärjestelmä käyttöön sovelluksessa.", "Feature:AllowChangingEmailSettings": "Salli sähköpostiasetusten muuttaminen", "Feature:AllowChangingEmailSettingsDescription": "Salli sähköpostiasetusten muuttaminen.", - "SmtpPasswordPlaceholder": "Syötä arvo päivittääksesi salasana" + "SmtpPasswordPlaceholder": "Syötä arvo päivittääksesi salasana", + "NoSettingsAvailable": "Näytettäviä asetuksia ei ole." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json index 957ff62a19..e28808ac59 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Activer le système de gestion des paramètres dans l'application.", "Feature:AllowChangingEmailSettings": "Autoriser la modification des paramètres de messagerie", "Feature:AllowChangingEmailSettingsDescription": "Autoriser la modification des paramètres de messagerie.", - "SmtpPasswordPlaceholder": "Entrez une valeur pour mettre à jour le mot de passe" + "SmtpPasswordPlaceholder": "Entrez une valeur pour mettre à jour le mot de passe", + "NoSettingsAvailable": "Aucun paramètre à afficher." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json index 8c00ffca0d..e25c3710ec 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "एप्लिकेशन में सेटिंग प्रबंधन प्रणाली सक्षम करें।", "Feature:AllowChangingEmailSettings": "ईमेल सेटिंग्स बदलने की अनुमति दें।", "Feature:AllowChangingEmailSettingsDescription": "ईमेल सेटिंग्स बदलने की अनुमति दें।", - "SmtpPasswordPlaceholder": "पासवर्ड अपडेट करने के लिए एक मान दर्ज करें" + "SmtpPasswordPlaceholder": "पासवर्ड अपडेट करने के लिए एक मान दर्ज करें", + "NoSettingsAvailable": "दिखाने के लिए कोई सेटिंग नहीं है।" } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json index 1e68c9ad7e..69c3b931db 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json @@ -38,6 +38,7 @@ "Feature:SettingManagementEnableDescription": "Omogućite sustav upravljanja postavkama u aplikaciji.", "Feature:AllowChangingEmailSettings": "Dopusti promjenu postavki e-pošte", "Feature:AllowChangingEmailSettingsDescription": "Dopusti promjenu postavki e-pošte.", - "SmtpPasswordPlaceholder": "Unesite vrijednost za ažuriranje lozinke" + "SmtpPasswordPlaceholder": "Unesite vrijednost za ažuriranje lozinke", + "NoSettingsAvailable": "Nema postavki za prikaz." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json index 43b8a53593..7b4dd904ae 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "A beállításkezelő rendszer engedélyezése az alkalmazásban.", "Feature:AllowChangingEmailSettings": "Az e-mail beállítások módosításának engedélyezése", "Feature:AllowChangingEmailSettingsDescription": "Az e-mail beállítások módosításának engedélyezése.", - "SmtpPasswordPlaceholder": "Adjon meg egy értéket a jelszó frissítéséhez" + "SmtpPasswordPlaceholder": "Adjon meg egy értéket a jelszó frissítéséhez", + "NoSettingsAvailable": "Nincs megjeleníthető beállítás." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json index 69a437b66e..475546fc52 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Virkja stillingar í forritinu.", "Feature:AllowChangingEmailSettings": "Leyfa að breyta stillingum tölvupósts", "Feature:AllowChangingEmailSettingsDescription": "Leyfa að breyta stillingum tölvupósts.", - "SmtpPasswordPlaceholder": "Sláðu inn gildi til að uppfæra lykilorð" + "SmtpPasswordPlaceholder": "Sláðu inn gildi til að uppfæra lykilorð", + "NoSettingsAvailable": "Engar stillingar til að sýna." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json index 01f80cecfb..a1eaf443ff 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Abilita sistema gestione impostazioni nell'applicazione", "Feature:AllowChangingEmailSettings": "Consenti di modificare le loro impostazioni e-mail", "Feature:AllowChangingEmailSettingsDescription": "Consenti di modificare le loro impostazioni e-mail.", - "SmtpPasswordPlaceholder": "Inserisci un valore per aggiornare la password" + "SmtpPasswordPlaceholder": "Inserisci un valore per aggiornare la password", + "NoSettingsAvailable": "Non ci sono impostazioni da visualizzare." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json index 57a87f550b..d172a031d9 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Schakel het instellingsbeheersysteem in de toepassing in.", "Feature:AllowChangingEmailSettings": "Toestaan om e-mailinstellingen te wijzigen", "Feature:AllowChangingEmailSettingsDescription": "Toestaan om e-mailinstellingen te wijzigen.", - "SmtpPasswordPlaceholder": "Voer een waarde in om het wachtwoord bij te werken" + "SmtpPasswordPlaceholder": "Voer een waarde in om het wachtwoord bij te werken", + "NoSettingsAvailable": "Er zijn geen instellingen om weer te geven." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json index 28c8dd3689..e452b646f2 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Włącz system zarządzania ustawieniami w aplikacji.", "Feature:AllowChangingEmailSettings": "Zezwól na zmianę ustawień poczty e-mail", "Feature:AllowChangingEmailSettingsDescription": "Zezwól na zmianę ustawień poczty e-mail.", - "SmtpPasswordPlaceholder": "Wprowadź wartość, aby zaktualizować hasło" + "SmtpPasswordPlaceholder": "Wprowadź wartość, aby zaktualizować hasło", + "NoSettingsAvailable": "Brak ustawień do wyświetlenia." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json index 8b968bacfe..0485d4c9c2 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Habilite o sistema de gerenciamento de configuração no aplicativo.", "Feature:AllowChangingEmailSettings": "Permitir alterar as configurações de e-mail", "Feature:AllowChangingEmailSettingsDescription": "Permitir alterar as configurações de e-mail.", - "SmtpPasswordPlaceholder": "Digite um valor para atualizar a senha" + "SmtpPasswordPlaceholder": "Digite um valor para atualizar a senha", + "NoSettingsAvailable": "Não há configurações para exibir." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json index 83a9b3e2dc..a325164f39 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Activează sistemul de administrare a setărilor în aplicaţie.", "Feature:AllowChangingEmailSettings": "Permiteți modificarea setărilor de e-mail", "Feature:AllowChangingEmailSettingsDescription": "Permiteți modificarea setărilor de e-mail.", - "SmtpPasswordPlaceholder": "Introduceți o valoare pentru a actualiza parola" + "SmtpPasswordPlaceholder": "Introduceți o valoare pentru a actualiza parola", + "NoSettingsAvailable": "Nu există setări de afișat." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json index 7026026918..7764d3a320 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Включите систему управления настройками в приложении.", "Feature:AllowChangingEmailSettings": "Разрешить изменение настроек электронной почты", "Feature:AllowChangingEmailSettingsDescription": "Разрешить изменение настроек электронной почты.", - "SmtpPasswordPlaceholder": "Введите значение для обновления пароля" + "SmtpPasswordPlaceholder": "Введите значение для обновления пароля", + "NoSettingsAvailable": "Нет настроек для отображения." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json index 9e4ef99363..7d83c646f1 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Povoliť systém správy nastavení v aplikácii.", "Feature:AllowChangingEmailSettings": "Povoliť zmenu nastavení e-mailu", "Feature:AllowChangingEmailSettingsDescription": "Povoliť zmenu nastavení e-mailu.", - "SmtpPasswordPlaceholder": "Zadajte hodnotu pre aktualizáciu hesla" + "SmtpPasswordPlaceholder": "Zadajte hodnotu pre aktualizáciu hesla", + "NoSettingsAvailable": "Nie sú k dispozícii žiadne nastavenia." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json index e1cce43730..95bd7c7fe5 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Omogočite nastavitev sistema upravljanja v aplikaciji.", "Feature:AllowChangingEmailSettings": "Dovoli spreminjanje e-poštnih nastavitev", "Feature:AllowChangingEmailSettingsDescription": "Dovoli spreminjanje e-poštnih nastavitev.", - "SmtpPasswordPlaceholder": "Vnesite vrednost za posodobitev gesla" + "SmtpPasswordPlaceholder": "Vnesite vrednost za posodobitev gesla", + "NoSettingsAvailable": "Ni nastavitev za prikaz." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json index 95462e08b7..1ffdace684 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json @@ -36,6 +36,7 @@ "Feature:SettingManagementEnableDescription": "Aktivera inställningshanteringssystem i applikationen.", "Feature:AllowChangingEmailSettings": "Tillåt ändring av e-postinställningar", "Feature:AllowChangingEmailSettingsDescription": "Tillåt ändring av e-postinställningar.", - "SmtpPasswordPlaceholder": "Ange ett värde för att uppdatera lösenordet" + "SmtpPasswordPlaceholder": "Ange ett värde för att uppdatera lösenordet", + "NoSettingsAvailable": "Det finns inga inställningar att visa." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json index 8a2db80e58..ed30fc5937 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Uygulamada ayar yönetim sistemini etkinleştirin.", "Feature:AllowChangingEmailSettings": "E-posta ayarlarını değiştirmeye izin verin", "Feature:AllowChangingEmailSettingsDescription": "E-posta ayarlarını değiştirmeye izin verin.", - "SmtpPasswordPlaceholder": "Şifreyi güncellemek için bir değer girin" + "SmtpPasswordPlaceholder": "Şifreyi güncellemek için bir değer girin", + "NoSettingsAvailable": "Görüntülenecek ayar yok." } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json index 4c4f74ec2c..a9fb2f391f 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "Bật cài đặt hệ thống quản lý trong ứng dụng.", "Feature:AllowChangingEmailSettings": "Cho phép thay đổi cài đặt email", "Feature:AllowChangingEmailSettingsDescription": "Cho phép thay đổi cài đặt email.", - "SmtpPasswordPlaceholder": "Nhập một giá trị để cập nhật mật khẩu" + "SmtpPasswordPlaceholder": "Nhập một giá trị để cập nhật mật khẩu", + "NoSettingsAvailable": "Không có cài đặt nào để hiển thị." } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json index 392eb7e4e9..d78697e880 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "在应用程序中启用设置管理系统。", "Feature:AllowChangingEmailSettings": "允许更改邮件设置", "Feature:AllowChangingEmailSettingsDescription": "允许更改邮件设置。", - "SmtpPasswordPlaceholder": "输入一个值以更新密码" + "SmtpPasswordPlaceholder": "输入一个值以更新密码", + "NoSettingsAvailable": "没有可显示的设置。" } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json index 3dea507456..74b6dc03e9 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json @@ -37,6 +37,7 @@ "Feature:SettingManagementEnableDescription": "在應用程序中啟用設定管理系統.", "Feature:AllowChangingEmailSettings": "允許更改電子郵件設置", "Feature:AllowChangingEmailSettingsDescription": "允許更改電子郵件設置。", - "SmtpPasswordPlaceholder": "輸入一個值以更新密碼" + "SmtpPasswordPlaceholder": "輸入一個值以更新密碼", + "NoSettingsAvailable": "沒有可顯示的設定。" } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManager.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManager.cs index b3fd120f38..4e21039003 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManager.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManager.cs @@ -73,11 +73,20 @@ public class SettingManager : ISettingManager, ISingletonDependency foreach (var setting in settingDefinitions) { + var settingProviderList = setting.Providers.Any() + ? providerList.Where(p => setting.Providers.Contains(p.Name)).ToList() + : providerList; + + if (!settingProviderList.Any()) + { + continue; + } + string value = null; if (setting.IsInherited) { - foreach (var provider in providerList) + foreach (var provider in settingProviderList) { var providerValue = await provider.GetOrNullAsync( setting, @@ -91,7 +100,7 @@ public class SettingManager : ISettingManager, ISingletonDependency } else { - value = await providerList[0].GetOrNullAsync( + value = await settingProviderList[0].GetOrNullAsync( setting, providerKey ); @@ -118,6 +127,11 @@ public class SettingManager : ISettingManager, ISingletonDependency var setting = await SettingDefinitionManager.GetAsync(name); + if (setting.Providers.Any() && !setting.Providers.Contains(providerName)) + { + throw new AbpException($"The setting named '{name}' is not compatible with the provider named '{providerName}'"); + } + var providers = Enumerable .Reverse(Providers) .SkipWhile(p => p.Name != providerName) @@ -188,6 +202,11 @@ public class SettingManager : ISettingManager, ISingletonDependency providers = providers.TakeWhile(c => c.Name == providerName); } + if (setting.Providers.Any()) + { + providers = providers.Where(p => setting.Providers.Contains(p.Name)); + } + string value = null; foreach (var provider in providers) { diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml index b259131ec7..25eff5c498 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml @@ -25,24 +25,34 @@
-
-
- -
-
-
+ @if (Model.SettingPageCreationContext.Groups.Count > 0) + { +
+
+ +
+
+
+
-
+ } + else + { +
+ +

@L["NoSettingsAvailable"]

+
+ }
diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/TestSettingDefinitionProvider.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/TestSettingDefinitionProvider.cs index c19d044d8e..d881f29fb1 100644 --- a/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/TestSettingDefinitionProvider.cs +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/TestSettingDefinitionProvider.cs @@ -4,6 +4,9 @@ namespace Volo.Abp.SettingManagement; public class TestSettingDefinitionProvider : SettingDefinitionProvider { + public const string UserOnlySetting = "UserOnlySetting"; + public const string GlobalOnlySetting = "GlobalOnlySetting"; + public override void Define(ISettingDefinitionContext context) { context.Add(new SettingDefinition("MySetting1")); @@ -11,7 +14,9 @@ public class TestSettingDefinitionProvider : SettingDefinitionProvider context.Add(new SettingDefinition("MySetting3", "123")); context.Add(new SettingDefinition("MySettingWithoutInherit", isInherited: false)); context.Add(new SettingDefinition("SettingNotSetInStore", defaultValue: "default-value")); - - + context.Add(new SettingDefinition(UserOnlySetting) + .WithProviders(UserSettingValueProvider.ProviderName)); + context.Add(new SettingDefinition(GlobalOnlySetting) + .WithProviders(GlobalSettingValueProvider.ProviderName)); } } diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs index 95691f53a9..075d4e65f2 100644 --- a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs @@ -10,11 +10,13 @@ public class SettingManager_Basic_Tests : SettingsTestBase { private readonly ISettingManager _settingManager; private readonly ISettingProvider _settingProvider; + private readonly ISettingManagementStore _settingManagementStore; public SettingManager_Basic_Tests() { _settingManager = GetRequiredService(); _settingProvider = GetRequiredService(); + _settingManagementStore = GetRequiredService(); } [Fact] @@ -74,4 +76,101 @@ public class SettingManager_Basic_Tests : SettingsTestBase exception.Message.ShouldBe("Unknown setting value provider: UndefinedProvider"); } + + [Fact] + public async Task Set_Should_Throw_Exception_If_Provider_Not_In_Providers() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _settingManager.SetGlobalAsync(TestSettingDefinitionProvider.UserOnlySetting, "value"); + }); + + exception.Message.ShouldContain(TestSettingDefinitionProvider.UserOnlySetting); + exception.Message.ShouldContain(GlobalSettingValueProvider.ProviderName); + } + + [Fact] + public async Task Set_Should_Allow_Setting_For_Provider_In_Providers() + { + var userId = Guid.NewGuid(); + + await _settingManager.SetForUserAsync(userId, TestSettingDefinitionProvider.UserOnlySetting, "value"); + + (await _settingManager.GetOrNullForUserAsync( + TestSettingDefinitionProvider.UserOnlySetting, + userId)).ShouldBe("value"); + } + + [Fact] + public async Task GetAllAsync_Should_Not_Return_Settings_With_Disallowed_Provider() + { + var userId = Guid.NewGuid(); + await _settingManager.SetForUserAsync(userId, TestSettingDefinitionProvider.UserOnlySetting, "user-value"); + + var globalSettings = await _settingManager.GetAllGlobalAsync(); + globalSettings.ShouldNotContain(x => x.Name == TestSettingDefinitionProvider.UserOnlySetting); + + var userSettings = await _settingManager.GetAllForUserAsync(userId); + userSettings.ShouldContain(x => + x.Name == TestSettingDefinitionProvider.UserOnlySetting && x.Value == "user-value"); + } + + [Fact] + public async Task GetOrNullForGlobal_Should_Not_Read_From_Disallowed_Provider() + { + await _settingManagementStore.SetAsync( + TestSettingDefinitionProvider.UserOnlySetting, + "stale", + GlobalSettingValueProvider.ProviderName, + null); + + (await _settingManager.GetOrNullGlobalAsync( + TestSettingDefinitionProvider.UserOnlySetting)).ShouldBeNull(); + } + + [Fact] + public async Task GetAllForUser_Should_Inherit_Setting_From_Allowed_Upstream_Provider() + { + await _settingManager.SetGlobalAsync( + TestSettingDefinitionProvider.GlobalOnlySetting, + "global-value"); + + var userSettings = await _settingManager.GetAllForUserAsync(Guid.NewGuid()); + + userSettings.ShouldContain(x => + x.Name == TestSettingDefinitionProvider.GlobalOnlySetting && x.Value == "global-value"); + } + + [Fact] + public async Task GetOrNullForUser_Should_Inherit_Value_From_Allowed_Upstream_Provider() + { + await _settingManager.SetGlobalAsync( + TestSettingDefinitionProvider.GlobalOnlySetting, + "global-value"); + + (await _settingManager.GetOrNullForUserAsync( + TestSettingDefinitionProvider.GlobalOnlySetting, + Guid.NewGuid())).ShouldBe("global-value"); + } + + [Fact] + public async Task SetForUser_Should_Not_Be_Cleared_By_Stale_Disallowed_Provider_Fallback() + { + var userId = Guid.NewGuid(); + + await _settingManagementStore.SetAsync( + TestSettingDefinitionProvider.UserOnlySetting, + "user-value", + GlobalSettingValueProvider.ProviderName, + null); + + await _settingManager.SetForUserAsync( + userId, + TestSettingDefinitionProvider.UserOnlySetting, + "user-value"); + + (await _settingManager.GetOrNullForUserAsync( + TestSettingDefinitionProvider.UserOnlySetting, + userId)).ShouldBe("user-value"); + } } diff --git a/modules/tenant-management/Volo.Abp.TenantManagement.slnx b/modules/tenant-management/Volo.Abp.TenantManagement.slnx index ee9677787f..e44ab4110a 100644 --- a/modules/tenant-management/Volo.Abp.TenantManagement.slnx +++ b/modules/tenant-management/Volo.Abp.TenantManagement.slnx @@ -2,6 +2,9 @@ + + + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/AbpTenantManagementBlazorMudBlazorServerModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/AbpTenantManagementBlazorMudBlazorServerModule.cs new file mode 100644 index 0000000000..59c6cb267f --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/AbpTenantManagementBlazorMudBlazorServerModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.AspNetCore.Components.Server.Theming.MudBlazor; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor.Server; +using Volo.Abp.Modularity; + +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor.Server; + +[DependsOn( + typeof(AbpTenantManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsServerThemingMudBlazorModule), + typeof(AbpFeatureManagementBlazorMudBlazorServerModule) +)] +public class AbpTenantManagementBlazorMudBlazorServerModule : AbpModule +{ +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/FodyWeavers.xml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/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/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server.csproj new file mode 100644 index 0000000000..96cf8a86b0 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server/Volo.Abp.TenantManagement.Blazor.MudBlazor.Server.csproj @@ -0,0 +1,16 @@ + + + + + + + net10.0 + + + + + + + + + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/AbpTenantManagementBlazorMudBlazorWebAssemblyModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/AbpTenantManagementBlazorMudBlazorWebAssemblyModule.cs new file mode 100644 index 0000000000..4258f1a8c4 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/AbpTenantManagementBlazorMudBlazorWebAssemblyModule.cs @@ -0,0 +1,15 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.MudBlazor; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor.WebAssembly; +using Volo.Abp.Modularity; + +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly; + +[DependsOn( + typeof(AbpTenantManagementBlazorMudBlazorModule), + typeof(AbpAspNetCoreComponentsWebAssemblyThemingMudBlazorModule), + typeof(AbpFeatureManagementBlazorMudBlazorWebAssemblyModule), + typeof(AbpTenantManagementHttpApiClientModule) +)] +public class AbpTenantManagementBlazorMudBlazorWebAssemblyModule : AbpModule +{ +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/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/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly.csproj new file mode 100644 index 0000000000..f41f2e4ef0 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly/Volo.Abp.TenantManagement.Blazor.MudBlazor.WebAssembly.csproj @@ -0,0 +1,17 @@ + + + + + + + net10.0 + + + + + + + + + + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/AbpTenantManagementBlazorMudBlazorMapperlyMappers.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/AbpTenantManagementBlazorMudBlazorMapperlyMappers.cs new file mode 100644 index 0000000000..83b1474647 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/AbpTenantManagementBlazorMudBlazorMapperlyMappers.cs @@ -0,0 +1,16 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor; + +[Mapper] +[MapExtraProperties] +public partial class TenantDtoToTenantUpdateDtoMapper + : MapperBase +{ + [MapperIgnoreSource(nameof(TenantDto.Id))] + public override partial TenantUpdateDto Map(TenantDto source); + + [MapperIgnoreSource(nameof(TenantDto.Id))] + public override partial void Map(TenantDto source, TenantUpdateDto destination); +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/AbpTenantManagementBlazorMudBlazorModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/AbpTenantManagementBlazorMudBlazorModule.cs new file mode 100644 index 0000000000..dfee56fd12 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/AbpTenantManagementBlazorMudBlazorModule.cs @@ -0,0 +1,64 @@ +using Localization.Resources.AbpUi; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Mapperly; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor; +using Volo.Abp.FeatureManagement.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.Modularity; +using Volo.Abp.TenantManagement.Blazor.MudBlazor.Navigation; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Routing; +using Volo.Abp.TenantManagement.Localization; +using Volo.Abp.Threading; +using Volo.Abp.UI.Navigation; + +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor; + +[DependsOn( + typeof(AbpMapperlyModule), + typeof(AbpTenantManagementApplicationContractsModule), + typeof(AbpFeatureManagementBlazorMudBlazorModule) +)] +public class AbpTenantManagementBlazorMudBlazorModule : AbpModule +{ + private static readonly OneTimeRunner OneTimeRunner = new(); + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + + Configure(options => + { + options.MenuContributors.Add(new TenantManagementBlazorMenuContributor()); + }); + + Configure(options => + { + options.AdditionalAssemblies.Add(typeof(AbpTenantManagementBlazorMudBlazorModule).Assembly); + }); + + Configure(options => + { + options.Resources + .Get() + .AddBaseTypes( + typeof(AbpFeatureManagementResource), + typeof(AbpUiResource)); + }); + } + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + OneTimeRunner.Run(() => + { + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToUi( + TenantManagementModuleExtensionConsts.ModuleName, + TenantManagementModuleExtensionConsts.EntityNames.Tenant, + createFormTypes: new[] { typeof(TenantCreateDto) }, + editFormTypes: new[] { typeof(TenantUpdateDto) } + ); + }); + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/FodyWeavers.xml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/FodyWeavers.xml new file mode 100644 index 0000000000..00e1d9a1c1 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/FodyWeavers.xsd b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/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/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Navigation/TenantManagementBlazorMenuContributor.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Navigation/TenantManagementBlazorMenuContributor.cs new file mode 100644 index 0000000000..04540c85a6 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Navigation/TenantManagementBlazorMenuContributor.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Volo.Abp.TenantManagement.Localization; +using Volo.Abp.UI.Navigation; +using Volo.Abp.Authorization.Permissions; + +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor.Navigation; + +public class TenantManagementBlazorMenuContributor : IMenuContributor +{ + public virtual Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name != StandardMenus.Main) + { + return Task.CompletedTask; + } + + var administrationMenu = context.Menu.GetAdministration(); + + var l = context.GetLocalizer(); + + var tenantManagementMenuItem = new ApplicationMenuItem( + TenantManagementMenuNames.GroupName, + l["Menu:TenantManagement"], + icon: "fa fa-users" + ); + administrationMenu.AddItem(tenantManagementMenuItem); + + tenantManagementMenuItem.AddItem(new ApplicationMenuItem( + TenantManagementMenuNames.Tenants, + l["Tenants"], + url: "~/tenant-management/tenants" + ).RequirePermissions(TenantManagementPermissions.Tenants.Default)); + + return Task.CompletedTask; + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Navigation/TenantManagementMenuNames.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Navigation/TenantManagementMenuNames.cs new file mode 100644 index 0000000000..2f5dda85fa --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Navigation/TenantManagementMenuNames.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor.Navigation; + +public class TenantManagementMenuNames +{ + public const string GroupName = "TenantManagement"; + + public const string Tenants = GroupName + ".Tenants"; +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor new file mode 100644 index 0000000000..e2211914f4 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor @@ -0,0 +1,96 @@ +@page "/tenant-management/tenants" +@page "/{culture}/tenant-management/tenants" +@attribute [Authorize(TenantManagementPermissions.Tenants.Default)] +@using global::MudBlazor +@using Microsoft.AspNetCore.Authorization +@using Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components +@using Volo.Abp.MudBlazorUI.Components +@using Volo.Abp.MudBlazorUI.Components.ObjectExtending +@using Volo.Abp.TenantManagement.Localization +@using Microsoft.Extensions.Localization +@using Volo.Abp.UI.Navigation.Localization.Resource +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@inject IStringLocalizer LUiNavigation +@inherits AbpMudCrudPageBase + + + + + + + + + +@* ************************* CREATE MODAL ************************* *@ +@if (HasCreatePermission) +{ + + + @L["NewTenant"] + + + + + + + + + + + @L["Cancel"] + @L["Save"] + + +} + +@* ************************* EDIT MODAL ************************* *@ +@if (HasUpdatePermission) +{ + + + @L["Edit"] + + + + + + + + + @L["Cancel"] + @L["Save"] + + +} + +@if (HasManageFeaturesPermission) +{ + +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor.cs new file mode 100644 index 0000000000..dcfb0c3152 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; +using MudBlazor; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions; +using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; +using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.PageToolbars; +using Volo.Abp.FeatureManagement.Blazor.MudBlazor.Components; +using Volo.Abp.MudBlazorUI; +using Volo.Abp.ObjectExtending; +using Volo.Abp.TenantManagement.Localization; + +namespace Volo.Abp.TenantManagement.Blazor.MudBlazor.Pages.TenantManagement; + +public partial class TenantManagement +{ + [Parameter] + public string? Culture { get; set; } + + protected const string FeatureProviderName = "T"; + + protected bool HasManageFeaturesPermission; + protected string? ManageFeaturesPolicyName; + + protected FeatureManagementModal? FeatureManagementModal; + + protected bool ShowPassword { get; set; } + + protected PageToolbar Toolbar { get; } = new(); + + protected List TenantManagementTableColumns => TableColumns.Get(); + + private AbpMudBlazorMessageLocalizerHelper? _localizerHelper; + + [Inject] + protected IStringLocalizer Localizer { get; set; } = default!; + + private MudForm? _createFormRef; + private MudForm? _editFormRef; + + public TenantManagement() + { + LocalizationResource = typeof(AbpTenantManagementResource); + ObjectMapperContext = typeof(AbpTenantManagementBlazorMudBlazorModule); + + CreatePolicyName = TenantManagementPermissions.Tenants.Create; + UpdatePolicyName = TenantManagementPermissions.Tenants.Update; + DeletePolicyName = TenantManagementPermissions.Tenants.Delete; + + ManageFeaturesPolicyName = TenantManagementPermissions.Tenants.ManageFeatures; + } + + protected override ValueTask SetBreadcrumbItemsAsync() + { + BreadcrumbItems.Add(new BreadcrumbItem(LUiNavigation["Menu:Administration"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Menu:TenantManagement"].Value, null, disabled: true)); + BreadcrumbItems.Add(new BreadcrumbItem(L["Tenants"].Value, null, disabled: true)); + return base.SetBreadcrumbItemsAsync(); + } + + protected override async Task OnInitializedAsync() + { + _localizerHelper = new AbpMudBlazorMessageLocalizerHelper(Localizer); + await base.OnInitializedAsync(); + } + + protected override async Task SetPermissionsAsync() + { + await base.SetPermissionsAsync(); + + HasManageFeaturesPermission = await AuthorizationService.IsGrantedAsync(ManageFeaturesPolicyName!); + } + + protected override string GetDeleteConfirmationMessage(TenantDto entity) + { + return string.Format(L["TenantDeletionConfirmationMessage"], entity.Name); + } + + protected override ValueTask SetToolbarItemsAsync() + { + Toolbar.AddMudButton(L["NewTenant"], + OpenCreateDialogAsync, + icon: Icons.Material.Filled.Add.ToString(), + requiredPolicyName: CreatePolicyName); + + return base.SetToolbarItemsAsync(); + } + + protected override ValueTask SetEntityActionsAsync() + { + EntityActions + .Get() + .AddRange(new EntityAction[] + { + new EntityAction + { + Text = L["Edit"], + Visible = (data) => HasUpdatePermission, + Clicked = async (data) => { await OpenEditDialogAsync(data.As()); } + }, + new EntityAction + { + Text = L["Features"], + Visible = (data) => HasManageFeaturesPermission, + Clicked = async (data) => + { + var tenant = data.As(); + if (FeatureManagementModal != null) + { + await FeatureManagementModal.OpenAsync(FeatureProviderName, tenant.Id.ToString(), tenant.Name); + } + } + }, + new EntityAction + { + Text = L["Delete"], + Visible = (data) => HasDeletePermission, + Clicked = async (data) => await DeleteEntityAsync(data.As()), + ConfirmationMessage = (data) => GetDeleteConfirmationMessage(data.As()) + } + }); + + return base.SetEntityActionsAsync(); + } + + protected override async ValueTask SetTableColumnsAsync() + { + TenantManagementTableColumns + .AddRange(new TableColumn[] + { + new TableColumn + { + Title = L["Actions"], + Actions = EntityActions.Get(), + }, + new TableColumn + { + Title = L["TenantName"], + Sortable = true, + Data = nameof(TenantDto.Name), + }, + }); + + TenantManagementTableColumns.AddRange(await GetExtensionTableColumnsAsync( + TenantManagementModuleExtensionConsts.ModuleName, + TenantManagementModuleExtensionConsts.EntityNames.Tenant)); + + await base.SetTableColumnsAsync(); + } + + protected virtual void TogglePasswordVisibility() + { + ShowPassword = !ShowPassword; + } + + protected override async Task CreateEntityAsync() + { + if (_createFormRef == null) + { + return; + } + + await _createFormRef.ValidateAsync(); + if (!_createFormRef.IsValid) + { + return; + } + + await base.CreateEntityAsync(); + } + + protected override async Task UpdateEntityAsync() + { + if (_editFormRef == null) + { + return; + } + + await _editFormRef.ValidateAsync(); + if (!_editFormRef.IsValid) + { + return; + } + + await base.UpdateEntityAsync(); + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Volo.Abp.TenantManagement.Blazor.MudBlazor.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Volo.Abp.TenantManagement.Blazor.MudBlazor.csproj new file mode 100644 index 0000000000..d7748927ac --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Volo.Abp.TenantManagement.Blazor.MudBlazor.csproj @@ -0,0 +1,22 @@ + + + + + + + net10.0 + + + + + + + + + + + + + + + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/_Imports.razor b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/_Imports.razor new file mode 100644 index 0000000000..b1be76fe73 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/_Imports.razor @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Components.Web +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor +@using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout +@using MudBlazor +@using Volo.Abp.MudBlazorUI diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor index b7313b0c9a..49e4c64822 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor @@ -1,4 +1,5 @@ @page "/tenant-management/tenants" +@page "/{culture}/tenant-management/tenants" @attribute [Authorize(TenantManagementPermissions.Tenants.Default)] @using Microsoft.AspNetCore.Authorization @using Volo.Abp.FeatureManagement.Blazor.Components diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs index 272bd9cf7a..c1fdbce867 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Blazorise; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions; using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns; using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars; @@ -14,6 +15,9 @@ namespace Volo.Abp.TenantManagement.Blazor.Pages.TenantManagement; public partial class TenantManagement { + [Parameter] + public string? Culture { get; set; } + protected const string FeatureProviderName = "T"; protected bool HasManageFeaturesPermission; diff --git a/modules/virtual-file-explorer/.abpstudio/state.json b/modules/virtual-file-explorer/.abpstudio/state.json deleted file mode 100644 index b0ef88d2f2..0000000000 --- a/modules/virtual-file-explorer/.abpstudio/state.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "selectedKubernetesProfile": null, - "solutionRunner": { - "selectedProfile": null, - "targetFrameworks": [], - "applicationsStartingWithoutBuild": [], - "applicationsWithoutAutoRefreshBrowserOnRestart": [], - "applicationBatchStartStates": [], - "folderBatchStartStates": [] - } -} \ No newline at end of file diff --git a/modules/virtual-file-explorer/app/package.json b/modules/virtual-file-explorer/app/package.json index 681f89a5ba..04a3e3f5d9 100644 --- a/modules/virtual-file-explorer/app/package.json +++ b/modules/virtual-file-explorer/app/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.3.0", - "@abp/virtual-file-explorer": "~10.3.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.4.1", + "@abp/virtual-file-explorer": "~10.4.1" } } diff --git a/npm/lerna.json b/npm/lerna.json index acf6a49b45..9d723d4557 100644 --- a/npm/lerna.json +++ b/npm/lerna.json @@ -1,5 +1,5 @@ { - "version": "10.3.0", + "version": "10.4.1", "packages": [ "packs/*" ], diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 8feb2de24c..f857a0538a 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -48,8 +48,8 @@ }, "private": true, "devDependencies": { - "@abp/ng.theme.lepton-x": "~5.3.0", - "@abp/utils": "~10.3.0", + "@abp/ng.theme.lepton-x": "~5.4.1", + "@abp/utils": "~10.4.1", "@angular-devkit/build-angular": "~21.2.0", "@angular-devkit/core": "~21.2.0", "@angular-devkit/schematics": "~21.2.0", diff --git a/npm/ng-packs/packages/account-core/package.json b/npm/ng-packs/packages/account-core/package.json index cfbee9058d..961e33ceeb 100644 --- a/npm/ng-packs/packages/account-core/package.json +++ b/npm/ng-packs/packages/account-core/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.account.core", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.core": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.core": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/account/package.json b/npm/ng-packs/packages/account/package.json index bee5f39ff5..d88209dacb 100644 --- a/npm/ng-packs/packages/account/package.json +++ b/npm/ng-packs/packages/account/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.account", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.account.core": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.account.core": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/cms-kit/package.json b/npm/ng-packs/packages/cms-kit/package.json index d18df04cd3..9b3ba2e069 100644 --- a/npm/ng-packs/packages/cms-kit/package.json +++ b/npm/ng-packs/packages/cms-kit/package.json @@ -1,15 +1,15 @@ { "name": "@abp/ng.cms-kit", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.components": "~10.3.0", - "@abp/ng.setting-management": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.components": "~10.4.1", + "@abp/ng.setting-management": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "@toast-ui/editor": "~3.0.0", "codemirror": "~6.0.0", "tslib": "^2.0.0" diff --git a/npm/ng-packs/packages/components/package.json b/npm/ng-packs/packages/components/package.json index f730716d60..aa206e17ff 100644 --- a/npm/ng-packs/packages/components/package.json +++ b/npm/ng-packs/packages/components/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.components", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "peerDependencies": { - "@abp/ng.core": ">=10.3.0", - "@abp/ng.theme.shared": ">=10.3.0" + "@abp/ng.core": ">=10.4.1", + "@abp/ng.theme.shared": ">=10.4.1" }, "dependencies": { "chart.js": "^3.5.1", diff --git a/npm/ng-packs/packages/core/package.json b/npm/ng-packs/packages/core/package.json index afc9f2287c..fc67769e16 100644 --- a/npm/ng-packs/packages/core/package.json +++ b/npm/ng-packs/packages/core/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.core", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/utils": "~10.3.0", + "@abp/utils": "~10.4.1", "just-clone": "^6.0.0", "just-compare": "^2.0.0", "ts-toolbelt": "^9.0.0", diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index 81f9305f68..8d6b5f3992 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -14,7 +14,8 @@ import { ReplaceableComponentsService } from '../services/replaceable-components import { RouterEvents } from '../services/router-events.service'; import { RoutesService } from '../services/routes.service'; import { SubscriptionService } from '../services/subscription.service'; -import { findRoute, getRoutePath } from '../utils/route-utils'; +import { RouteBasedCultureUrlService } from '../services/route-based-culture-url.service'; +import { findRoute } from '../utils/route-utils'; import { TreeNode } from '../utils/tree-utils'; import { DYNAMIC_LAYOUTS_TOKEN } from '../tokens/dynamic-layout.token'; import { EnvironmentService } from '../services'; @@ -46,6 +47,7 @@ export class DynamicLayoutComponent { protected readonly subscription = inject(SubscriptionService); protected readonly routerEvents = inject(RouterEvents); protected readonly environment = inject(EnvironmentService); + protected readonly routeCultureUrl = inject(RouteBasedCultureUrlService); constructor() { const dynamicLayoutComponent = inject(DynamicLayoutComponent, { optional: true, skipSelf: true }); @@ -85,7 +87,7 @@ export class DynamicLayoutComponent { const routeData = this.route.snapshot.data || {}; let expectedLayout = routeData['layout'] as eLayoutType; - let node = findRoute(this.routes, getRoutePath(this.router)); + let node = findRoute(this.routes, this.routeCultureUrl.getRoutePathForMatching(this.router)); node = { parent: node } as TreeNode; while (node.parent) { diff --git a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts index 41e07f8d1b..a6ae040597 100644 --- a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts +++ b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts @@ -10,8 +10,14 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { AuthService, IAbpGuard } from '../abstracts'; -import { findRoute, getRoutePath } from '../utils/route-utils'; -import { RoutesService, PermissionService, HttpErrorReporterService, ConfigStateService } from '../services'; +import { findRoute } from '../utils/route-utils'; +import { + RoutesService, + PermissionService, + HttpErrorReporterService, + ConfigStateService, + RouteBasedCultureUrlService, +} from '../services'; import { isPlatformServer } from '@angular/common'; /** * @deprecated Use `permissionGuard` *function* instead. @@ -26,12 +32,16 @@ export class PermissionGuard implements IAbpGuard { protected readonly permissionService = inject(PermissionService); protected readonly httpErrorReporter = inject(HttpErrorReporterService); protected readonly configStateService = inject(ConfigStateService); + protected readonly routeCultureUrl = inject(RouteBasedCultureUrlService); canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { let { requiredPolicy } = route.data || {}; if (!requiredPolicy) { - const routeFound = findRoute(this.routesService, getRoutePath(this.router, state.url)); + const routeFound = findRoute( + this.routesService, + this.routeCultureUrl.getRoutePathForMatching(this.router, state.url), + ); requiredPolicy = routeFound?.requiredPolicy; } @@ -70,12 +80,13 @@ export const permissionGuard: CanActivateFn = ( const permissionService = inject(PermissionService); const httpErrorReporter = inject(HttpErrorReporterService); const configStateService = inject(ConfigStateService); + const routeCultureUrl = inject(RouteBasedCultureUrlService); const platformId = inject(PLATFORM_ID); let { requiredPolicy } = route.data || {}; if (!requiredPolicy) { - const routeFound = findRoute(routesService, getRoutePath(router, state.url)); + const routeFound = findRoute(routesService, routeCultureUrl.getRoutePathForMatching(router, state.url)); requiredPolicy = routeFound?.requiredPolicy; } diff --git a/npm/ng-packs/packages/core/src/lib/pipes/index.ts b/npm/ng-packs/packages/core/src/lib/pipes/index.ts index 0320350f55..0dfe63d39f 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/index.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/index.ts @@ -8,4 +8,5 @@ export * from './short-date-time.pipe'; export * from './utc-to-local.pipe'; export * from './async-localization.pipe'; export * from './lazy-localization.pipe'; +export * from './route-culture-url.pipe'; export * from './html-encode.pipe'; diff --git a/npm/ng-packs/packages/core/src/lib/pipes/route-culture-url.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/route-culture-url.pipe.ts new file mode 100644 index 0000000000..26e43153e1 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/pipes/route-culture-url.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { RouteBasedCultureUrlService } from '../services/route-based-culture-url.service'; + +/** + * Prefixes menu/navigation paths with the current culture when route-based culture is enabled. + * Impure so links update after language changes. + */ +@Pipe({ + name: 'abpRouteCultureUrl', + pure: false, +}) +export class AbpRouteCultureUrlPipe implements PipeTransform { + private readonly url = inject(RouteBasedCultureUrlService); + + transform(path: string | undefined | null): string | undefined | null { + return this.url.prefixPathWithCulture(path); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/models.ts b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/models.ts index 524c9a8c78..b2d7b67bea 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/models.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/models.ts @@ -42,6 +42,7 @@ export interface ApplicationLocalizationConfigurationDto { defaultResourceName?: string; languagesMap: Record; languageFilesMap: Record; + useRouteBasedCulture: boolean; } export interface ApplicationLocalizationDto { diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index 205fd2f0dc..cccac2ffb6 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -16,6 +16,8 @@ export * from './resource-wait.service'; export * from './rest.service'; export * from './router-events.service'; export * from './router-wait.service'; +export * from './route-based-culture.service'; +export * from './route-based-culture-url.service'; export * from './routes.service'; export * from './session-state.service'; export * from './subscription.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/route-based-culture-url.service.ts b/npm/ng-packs/packages/core/src/lib/services/route-based-culture-url.service.ts new file mode 100644 index 0000000000..cbd94f3803 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/route-based-culture-url.service.ts @@ -0,0 +1,170 @@ +import { Location } from '@angular/common'; +import { isPlatformBrowser } from '@angular/common'; +import { Injectable, PLATFORM_ID, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { LanguageInfo } from '../proxy/volo/abp/localization/models'; +import { + findMatchingCultureName, + getFirstPathSegment, + stripCultureSegmentFromPath, +} from '../utils/route-based-culture.utils'; +import { getRoutePath } from '../utils/route-utils'; +import { ConfigStateService } from './config-state.service'; +import { SessionStateService } from './session-state.service'; + +/** + * URL helpers for route-based culture: prefix menu links, strip culture for route matching, + * and navigate when the user picks a language so the URL stays in sync with the session. + */ +@Injectable({ + providedIn: 'root', +}) +export class RouteBasedCultureUrlService { + private readonly router = inject(Router); + private readonly location = inject(Location); + private readonly configState = inject(ConfigStateService); + private readonly sessionState = inject(SessionStateService); + private readonly platformId = inject(PLATFORM_ID); + + /** Cached from localization config; refreshed when application configuration updates. */ + private useRouteBasedCulture = false; + private languages: LanguageInfo[] | undefined; + + constructor() { + this.refreshRouteCultureCache(); + this.configState.getAll$().subscribe(() => this.refreshRouteCultureCache()); + } + + private refreshRouteCultureCache(): void { + const loc = this.configState.getOne('localization'); + if (!loc) { + this.useRouteBasedCulture = false; + this.languages = undefined; + return; + } + this.useRouteBasedCulture = !!loc.useRouteBasedCulture; + this.languages = loc.languages as LanguageInfo[] | undefined; + } + + private isRouteBasedCultureEnabled(): boolean { + return this.useRouteBasedCulture; + } + + /** + * Same as {@link getRoutePath} but removes the leading culture segment when route-based culture is enabled, + * so paths match `RoutesService` entries (e.g. `/identity/users`). + */ + getRoutePathForMatching(router: Router, url = router.url): string { + const raw = getRoutePath(router, url); + return this.stripCulturePrefixIfEnabled(raw); + } + + /** + * Strips the leading culture segment when it matches a configured language and route-based culture is enabled. + * Use for menu active state, breadcrumbs, and any comparison between `router.url` and `RoutesService` paths. + */ + stripCulturePrefixIfEnabled(path: string): string { + if (!this.isRouteBasedCultureEnabled() || !path) { + return path; + } + return this.stripCulturePrefix(path); + } + + /** + * Alias for {@link stripCulturePrefixIfEnabled}: normalizes the current URL for **menu highlighting** + * when menu `link` values omit the culture segment (e.g. Lepton `NavbarRoutesComponent`, `NavbarService.getRouteItem`). + */ + normalizeForMenuMatch(path: string): string { + return this.stripCulturePrefixIfEnabled(path); + } + + /** + * Removes the first segment when it is a known UI culture (for matching and normalization). + */ + stripCulturePrefix(path: string): string { + return stripCultureSegmentFromPath(path, this.languages); + } + + /** + * Prefixes an app path with the current session culture when route-based culture is enabled + * (e.g. `/identity/users` → `/en/identity/users`). Use for `routerLink`, `navigateByUrl`, etc. + */ + prefixPathWithCulture(path: string | undefined | null): string | undefined | null { + if (path == null || path === '') { + return path; + } + + if (!this.isRouteBasedCultureEnabled()) { + return path; + } + + if (/^https?:\/\//i.test(path)) { + return path; + } + + const lang = this.sessionState.getLanguage(); + if (!lang) { + return path; + } + + const stripped = this.stripCulturePrefix(path); + const normalized = stripped.startsWith('/') ? stripped : '/' + stripped; + const suffix = normalized === '/' ? '' : normalized; + return `/${lang}${suffix}`; + } + + /** + * Rewrites the current URL so the first segment is {@link cultureName} (or prepends it). + * Call this when the user selects a language in the UI instead of only {@link SessionStateService.setLanguage} + * so the address bar stays aligned with the session. + */ + navigateToUrlWithCulture(cultureName: string): Promise | void { + if (!cultureName || !isPlatformBrowser(this.platformId)) { + return Promise.resolve(false); + } + + if (!this.isRouteBasedCultureEnabled()) { + this.sessionState.setLanguage(cultureName); + return Promise.resolve(true); + } + + const path = this.location.path(); + const newPath = this.rewritePathToCulture(path, cultureName); + return this.router.navigateByUrl(newPath); + } + + /** + * Preferred entry point for language pickers (e.g. Lepton toolbar): same as {@link navigateToUrlWithCulture}. + */ + applyLanguageSelection(cultureName: string): Promise | void { + return this.navigateToUrlWithCulture(cultureName); + } + + /** + * Builds a new path (including query/hash) with the given culture as the first segment, + * replacing the culture segment when one is already present. + */ + rewritePathToCulture(urlPath: string, newCulture: string): string { + const languages = this.languages; + const pathEnd = urlPath.search(/[?#]/); + const pathOnly = pathEnd >= 0 ? urlPath.slice(0, pathEnd) : urlPath; + const suffix = pathEnd >= 0 ? urlPath.slice(pathEnd) : ''; + + const first = getFirstPathSegment(pathOnly); + const firstIsCulture = !!findMatchingCultureName(first, languages); + + if (firstIsCulture) { + const segments = pathOnly.split('/').filter(s => s.length > 0); + if (segments.length === 0) { + return `/${newCulture}${suffix}`; + } + segments[0] = newCulture; + const joined = '/' + segments.join('/'); + return joined + suffix; + } + + const normalized = pathOnly.startsWith('/') ? pathOnly : '/' + pathOnly; + const rest = normalized === '/' ? '' : normalized; + return `/${newCulture}${rest}${suffix}`; + } +} diff --git a/npm/ng-packs/packages/core/src/lib/services/route-based-culture.service.ts b/npm/ng-packs/packages/core/src/lib/services/route-based-culture.service.ts new file mode 100644 index 0000000000..f78b6130a9 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/route-based-culture.service.ts @@ -0,0 +1,59 @@ +import { isPlatformBrowser } from '@angular/common'; +import { Injectable, PLATFORM_ID, inject } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { LanguageInfo } from '../proxy/volo/abp/localization/models'; +import { findMatchingCultureName, getFirstPathSegment } from '../utils/route-based-culture.utils'; +import { ConfigStateService } from './config-state.service'; +import { SessionStateService } from './session-state.service'; + +/** + * When the backend enables URL-based localization (`localization.useRouteBasedCulture` from application configuration), + * keeps session language in sync with the first URL path segment (e.g. /en/..., /tr-TR/...). + * Works with nested routes because only the leading segment is interpreted as culture. + */ +@Injectable({ + providedIn: 'root', +}) +export class RouteBasedCultureService { + private readonly router = inject(Router); + private readonly configState = inject(ConfigStateService); + private readonly sessionState = inject(SessionStateService); + private readonly platformId = inject(PLATFORM_ID); + protected readonly localization = this.configState.getOne('localization'); + + constructor() { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + if (!this.localization?.useRouteBasedCulture) { + return; + } + + this.router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe(() => this.syncLanguageFromUrl()); + } + + /** + * Reads the culture from the current URL and updates session language when it matches a configured language. + * @param pathOverride Optional path (e.g. from `Location.path()` during app bootstrap before navigation settles). + */ + syncLanguageFromUrl(pathOverride?: string): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + const languages = this.localization?.languages as LanguageInfo[] | undefined; + const path = pathOverride ?? this.router.url; + const firstSegment = getFirstPathSegment(path); + const cultureName = findMatchingCultureName(firstSegment, languages); + + if (!cultureName) { + return; + } + + this.sessionState.setLanguage(cultureName); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/services/title-strategy.service.ts b/npm/ng-packs/packages/core/src/lib/services/title-strategy.service.ts index af961794f8..18d83290b1 100644 --- a/npm/ng-packs/packages/core/src/lib/services/title-strategy.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/title-strategy.service.ts @@ -27,6 +27,10 @@ export class AbpTitleStrategy extends TitleStrategy { override updateTitle(routerState: RouterStateSnapshot) { this.routerState = routerState; + if (!routerState?.root) { + return; + } + const title = this.buildTitle(routerState); const projectName = this.localizationService.instant({ diff --git a/npm/ng-packs/packages/core/src/lib/tests/route-based-culture-url.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/route-based-culture-url.service.spec.ts new file mode 100644 index 0000000000..eaaf94df1b --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/route-based-culture-url.service.spec.ts @@ -0,0 +1,81 @@ +import { Location } from '@angular/common'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { ConfigStateService } from '../services/config-state.service'; +import { RouteBasedCultureUrlService } from '../services/route-based-culture-url.service'; +import { SessionStateService } from '../services/session-state.service'; + +const languages = [ + { cultureName: 'en', displayName: 'English' }, + { cultureName: 'tr', displayName: 'Turkish' }, +]; + +describe('RouteBasedCultureUrlService', () => { + let service: RouteBasedCultureUrlService; + let configState: { getOne: ReturnType }; + let sessionState: { + getLanguage: ReturnType; + setLanguage: ReturnType; + }; + + function setup(useRouteBasedCulture: boolean) { + configState = { + getOne: vi.fn((key: string) => { + if (key === 'localization') { + return { useRouteBasedCulture, languages }; + } + return undefined; + }), + getAll$: () => of({}), + }; + sessionState = { + getLanguage: vi.fn(() => 'tr'), + setLanguage: vi.fn(), + }; + + TestBed.configureTestingModule({ + providers: [ + RouteBasedCultureUrlService, + { provide: ConfigStateService, useValue: configState }, + { provide: SessionStateService, useValue: sessionState }, + { + provide: Router, + useValue: { navigateByUrl: vi.fn(() => Promise.resolve(true)), url: '/tr' }, + }, + { provide: Location, useValue: { path: () => '/tr/home' } }, + ], + }); + + service = TestBed.inject(RouteBasedCultureUrlService); + } + + afterEach(() => { + TestBed.resetTestingModule(); + }); + + test('prefixPathWithCulture leaves path unchanged when useRouteBasedCulture is false', () => { + setup(false); + expect(service.prefixPathWithCulture('/identity/users')).toBe('/identity/users'); + }); + + test('prefixPathWithCulture prepends session culture when enabled', () => { + setup(true); + expect(service.prefixPathWithCulture('/identity/users')).toBe('/tr/identity/users'); + }); + + test('stripCulturePrefixIfEnabled removes leading culture segment', () => { + setup(true); + expect(service.stripCulturePrefixIfEnabled('/en/identity/users')).toBe('/identity/users'); + }); + + test('rewritePathToCulture replaces existing culture segment', () => { + setup(true); + expect(service.rewritePathToCulture('/en/home?x=1', 'tr')).toBe('/tr/home?x=1'); + }); + + test('rewritePathToCulture prepends culture when missing', () => { + setup(true); + expect(service.rewritePathToCulture('/identity/users', 'tr')).toBe('/tr/identity/users'); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/route-based-culture.utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/route-based-culture.utils.spec.ts new file mode 100644 index 0000000000..e9e2ea9b96 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/route-based-culture.utils.spec.ts @@ -0,0 +1,81 @@ +import { + findMatchingCultureName, + getFirstPathSegment, + normalizeUrlForRouteCultureMatch, + stripCultureSegmentFromPath, +} from '../utils/route-based-culture.utils'; +import type { LanguageInfo } from '../proxy/volo/abp/localization/models'; + +describe('route-based-culture.utils', () => { + describe('#getFirstPathSegment', () => { + test.each` + urlPath | expected + ${''} | ${''} + ${'/'} | ${''} + ${'/en'} | ${'en'} + ${'/en/account'} | ${'en'} + ${'en/account'} | ${'en'} + ${'/tr-TR/foo/bar'} | ${'tr-TR'} + ${'/zh-Hans/account?x=1'} | ${'zh-Hans'} + ${'tr/home#frag'} | ${'tr'} + `('should return $expected when urlPath is $urlPath', ({ urlPath, expected }) => { + expect(getFirstPathSegment(urlPath)).toBe(expected); + }); + }); + + describe('#findMatchingCultureName', () => { + const languages: LanguageInfo[] = [ + { cultureName: 'en' }, + { cultureName: 'tr-TR' }, + { cultureName: 'zh-Hans' }, + ]; + + test.each` + segment | expected + ${'en'} | ${'en'} + ${'EN'} | ${'en'} + ${'tr-tr'} | ${'tr-TR'} + ${'zh-hans'} | ${'zh-Hans'} + ${'account'} | ${undefined} + ${''} | ${undefined} + `('should return $expected when segment is $segment', ({ segment, expected }) => { + expect(findMatchingCultureName(segment, languages)).toBe(expected); + }); + + test('should return undefined when languages is empty', () => { + expect(findMatchingCultureName('en', [])).toBeUndefined(); + }); + }); + + describe('#stripCultureSegmentFromPath', () => { + const languages: LanguageInfo[] = [{ cultureName: 'en' }, { cultureName: 'tr' }]; + + test('should strip leading culture segment for menu matching', () => { + expect(stripCultureSegmentFromPath('/en/identity/users', languages)).toBe('/identity/users'); + }); + + test('should leave path unchanged when first segment is not a culture', () => { + expect(stripCultureSegmentFromPath('/identity/users', languages)).toBe('/identity/users'); + }); + + test('should preserve query string', () => { + expect(stripCultureSegmentFromPath('/en/home?x=1', languages)).toBe('/home?x=1'); + }); + }); + + describe('#normalizeUrlForRouteCultureMatch', () => { + const languages: LanguageInfo[] = [{ cultureName: 'en' }]; + + test('should no-op when useRouteBasedCulture is false', () => { + expect(normalizeUrlForRouteCultureMatch('/en/identity/users', false, languages)).toBe( + '/en/identity/users', + ); + }); + + test('should strip when enabled', () => { + expect(normalizeUrlForRouteCultureMatch('/en/identity/users', true, languages)).toBe( + '/identity/users', + ); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/route-culture-url-matcher.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/route-culture-url-matcher.spec.ts new file mode 100644 index 0000000000..bad77e0aac --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/route-culture-url-matcher.spec.ts @@ -0,0 +1,44 @@ +import { UrlSegment } from '@angular/router'; +import { + createRouteCultureUrlMatcher, + isLikelyCultureSegment, +} from '../utils/route-culture-url-matcher'; + +describe('route-culture-url-matcher', () => { + describe('#isLikelyCultureSegment', () => { + test.each` + segment | expected + ${''} | ${false} + ${'en'} | ${true} + ${'tr'} | ${true} + ${'zh-Hans'} | ${true} + ${'pt-BR'} | ${true} + ${'home'} | ${false} + ${'identity'} | ${false} + ${'cms-kit'} | ${false} + ${'account'} | ${false} + `('should return $expected when segment is $segment', ({ segment, expected }) => { + expect(isLikelyCultureSegment(segment)).toBe(expected); + }); + }); + + describe('#createRouteCultureUrlMatcher', () => { + const matcher = createRouteCultureUrlMatcher(); + + test('should consume first segment when it is a culture code', () => { + const segments = [new UrlSegment('en', {}), new UrlSegment('home', {})]; + const result = matcher(segments, null as any, {} as any); + expect(result?.consumed).toEqual([segments[0]]); + expect(result?.posParams?.culture).toBe(segments[0]); + }); + + test('should return null when first segment is not a culture code', () => { + const segments = [new UrlSegment('identity', {})]; + expect(matcher(segments, null as any, {} as any)).toBeNull(); + }); + + test('should return null when there are no segments', () => { + expect(matcher([], null as any, {} as any)).toBeNull(); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 9b542ec5b4..2a03a36c4e 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -15,6 +15,8 @@ export * from './multi-tenancy-utils'; export * from './number-utils'; export * from './object-utils'; export * from './queue'; +export * from './route-based-culture.utils'; +export * from './route-culture-url-matcher'; export * from './route-utils'; export * from './string-utils'; export * from './tree-utils'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts index 414f4d0a31..1534648a72 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts @@ -1,4 +1,4 @@ -import { registerLocaleData } from '@angular/common'; +import { Location, registerLocaleData } from '@angular/common'; import { inject, Injector } from '@angular/core'; import { tap, catchError } from 'rxjs/operators'; import { firstValueFrom, lastValueFrom, of, throwError, timeout } from 'rxjs'; @@ -7,6 +7,7 @@ import { Environment } from '../models/environment'; import { CurrentTenantDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models'; import { ConfigStateService } from '../services/config-state.service'; import { EnvironmentService } from '../services/environment.service'; +import { RouteBasedCultureService } from '../services/route-based-culture.service'; import { SessionStateService } from '../services/session-state.service'; import { CORE_OPTIONS } from '../tokens/options.token'; import { APP_INIT_ERROR_HANDLERS } from '../tokens/app-config.token'; @@ -63,6 +64,9 @@ export async function getInitialData() { await lastValueFrom(result$); } + const routeBasedCulture = injector.get(RouteBasedCultureService); + routeBasedCulture.syncLanguageFromUrl(injector.get(Location).path()); + await localeInitializer(injector); } diff --git a/npm/ng-packs/packages/core/src/lib/utils/route-based-culture.utils.ts b/npm/ng-packs/packages/core/src/lib/utils/route-based-culture.utils.ts new file mode 100644 index 0000000000..3c0f92b357 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/route-based-culture.utils.ts @@ -0,0 +1,82 @@ +import type { LanguageInfo } from '../proxy/volo/abp/localization/models'; + +/** + * Returns the first path segment of a URL path (leading slashes are ignored). + * Strips query string and fragment first (e.g. "/zh-Hans/account?x=1" → "zh-Hans"). + */ +export function getFirstPathSegment(urlPath: string): string { + if (!urlPath) { + return ''; + } + + const suffixIndex = urlPath.search(/[?#]/); + const pathPart = suffixIndex >= 0 ? urlPath.slice(0, suffixIndex) : urlPath; + const segments = pathPart.split('/').filter(s => s.length > 0); + + return segments[0] ?? ''; +} + +/** + * If the first segment matches a configured language culture name (case-insensitive), + * returns the canonical culture name from configuration; otherwise undefined. + */ +export function findMatchingCultureName( + firstSegment: string, + languages: LanguageInfo[] | undefined, +): string | undefined { + if (!firstSegment || !languages?.length) { + return undefined; + } + + const normalized = firstSegment.toLowerCase(); + const match = languages.find( + lang => lang.cultureName && lang.cultureName.toLowerCase() === normalized, + ); + + return match?.cultureName; +} + +/** + * Removes the first path segment when it matches a configured UI culture (e.g. `/en/identity/users` → `/identity/users`). + * Use when comparing the **browser URL** to **menu links** that omit the culture segment. + */ +export function stripCultureSegmentFromPath( + path: string, + languages: LanguageInfo[] | undefined, +): string { + if (!path || !languages?.length) { + return path; + } + + const pathEnd = path.search(/[?#]/); + const pathOnly = pathEnd >= 0 ? path.slice(0, pathEnd) : path; + const suffix = pathEnd >= 0 ? path.slice(pathEnd) : ''; + + const first = getFirstPathSegment(pathOnly); + if (!first || !findMatchingCultureName(first, languages)) { + return path; + } + + const segments = pathOnly.split('/').filter(s => s.length > 0); + if (segments.length === 0) { + return path; + } + + const rest = segments.slice(1); + const normalized = rest.length ? '/' + rest.join('/') : '/'; + return normalized + suffix; +} + +/** + * When route-based culture is enabled, normalizes URLs for comparison with menu routes that do not include a culture prefix. + */ +export function normalizeUrlForRouteCultureMatch( + path: string, + useRouteBasedCulture: boolean, + languages: LanguageInfo[] | undefined, +): string { + if (!useRouteBasedCulture || !path) { + return path; + } + return stripCultureSegmentFromPath(path, languages); +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/route-culture-url-matcher.ts b/npm/ng-packs/packages/core/src/lib/utils/route-culture-url-matcher.ts new file mode 100644 index 0000000000..0093c5cb67 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/route-culture-url-matcher.ts @@ -0,0 +1,59 @@ +import { Route, Routes, UrlMatchResult, UrlSegment, UrlSegmentGroup } from '@angular/router'; + +/** + * Heuristic: first path segment looks like a BCP 47-style culture code (e.g. en, tr, zh-Hans, pt-BR). + * Used by the optional culture URL matcher so real routes like `identity` or `cms-kit` are not mistaken for cultures. + */ +export function isLikelyCultureSegment(segment: string): boolean { + if (!segment) { + return false; + } + + return /^[a-z]{2}(-[a-zA-Z0-9]{2,8})?$/.test(segment); +} + +/** + * Matcher that consumes the first segment when it looks like a culture code. + * Exposes it as the route param `culture` on matched routes. + */ +export function createRouteCultureUrlMatcher(): ( + segments: UrlSegment[], + group: UrlSegmentGroup, + route: Route, +) => UrlMatchResult | null { + return (segments: UrlSegment[]) => { + if (segments.length < 1) { + return null; + } + + const first = segments[0].path; + if (!isLikelyCultureSegment(first)) { + return null; + } + + return { + consumed: [segments[0]], + posParams: { culture: segments[0] }, + }; + }; +} + +/** + * Wraps your app routes so the same URLs work with or without a leading culture segment. + * + * Examples (same components for both shapes): + * - `/` and `/en` + * - `/home` and `/en/home` + * - `/identity/users` and `/en/identity/users` + * + * The culture segment is matched only when it passes {@link isLikelyCultureSegment} (e.g. `en`, `tr-TR`, `zh-Hans`). + * Session language from the URL is still applied only when `localization.useRouteBasedCulture` is true (`RouteBasedCultureService`). + */ +export function withOptionalRouteCulturePrefix(routes: Routes): Routes { + const matcher = createRouteCultureUrlMatcher(); + + return [ + { matcher, children: [...routes] }, + { path: '', children: [...routes] }, + ]; +} diff --git a/npm/ng-packs/packages/feature-management/package.json b/npm/ng-packs/packages/feature-management/package.json index 867f5ec95c..b833c3c4dc 100644 --- a/npm/ng-packs/packages/feature-management/package.json +++ b/npm/ng-packs/packages/feature-management/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.feature-management", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "peerDependencies": { diff --git a/npm/ng-packs/packages/generators/package.json b/npm/ng-packs/packages/generators/package.json index ab07a3cb03..386dd493db 100644 --- a/npm/ng-packs/packages/generators/package.json +++ b/npm/ng-packs/packages/generators/package.json @@ -1,6 +1,6 @@ { "name": "@abp/nx.generators", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "generators": "./generators.json", "type": "commonjs", diff --git a/npm/ng-packs/packages/identity/package.json b/npm/ng-packs/packages/identity/package.json index 8826eafeec..6a7758da0a 100644 --- a/npm/ng-packs/packages/identity/package.json +++ b/npm/ng-packs/packages/identity/package.json @@ -1,15 +1,15 @@ { "name": "@abp/ng.identity", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.components": "~10.3.0", - "@abp/ng.permission-management": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.components": "~10.4.1", + "@abp/ng.permission-management": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/oauth/package.json b/npm/ng-packs/packages/oauth/package.json index d6f455c4cf..37f43a7e54 100644 --- a/npm/ng-packs/packages/oauth/package.json +++ b/npm/ng-packs/packages/oauth/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.oauth", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.core": "~10.3.0", - "@abp/utils": "~10.3.0", + "@abp/ng.core": "~10.4.1", + "@abp/utils": "~10.4.1", "angular-oauth2-oidc": "^20.0.0", "just-clone": "^6.0.0", "just-compare": "^2.0.0", diff --git a/npm/ng-packs/packages/permission-management/package.json b/npm/ng-packs/packages/permission-management/package.json index 5295a7fd9f..9cfb02939e 100644 --- a/npm/ng-packs/packages/permission-management/package.json +++ b/npm/ng-packs/packages/permission-management/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.permission-management", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "peerDependencies": { diff --git a/npm/ng-packs/packages/schematics/package.json b/npm/ng-packs/packages/schematics/package.json index dbbfe297f6..d4c3010388 100644 --- a/npm/ng-packs/packages/schematics/package.json +++ b/npm/ng-packs/packages/schematics/package.json @@ -1,6 +1,6 @@ { "name": "@abp/ng.schematics", - "version": "10.3.0", + "version": "10.4.1", "author": "", "schematics": "./collection.json", "dependencies": { diff --git a/npm/ng-packs/packages/setting-management/package.json b/npm/ng-packs/packages/setting-management/package.json index 2635bd99d0..7d8cede5e6 100644 --- a/npm/ng-packs/packages/setting-management/package.json +++ b/npm/ng-packs/packages/setting-management/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.setting-management", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.components": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.components": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "peerDependencies": { diff --git a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html index 07830f2d1f..35241930a3 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html +++ b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html @@ -2,28 +2,33 @@
-
-
-
diff --git a/npm/ng-packs/packages/tenant-management/package.json b/npm/ng-packs/packages/tenant-management/package.json index ba369306b3..12071c8b07 100644 --- a/npm/ng-packs/packages/tenant-management/package.json +++ b/npm/ng-packs/packages/tenant-management/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.tenant-management", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.feature-management": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.feature-management": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/theme-basic/package.json b/npm/ng-packs/packages/theme-basic/package.json index cb1cffc0e5..a8f7073b3c 100644 --- a/npm/ng-packs/packages/theme-basic/package.json +++ b/npm/ng-packs/packages/theme-basic/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.theme.basic", - "version": "10.3.0", + "version": "10.4.1", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.account.core": "~10.3.0", - "@abp/ng.theme.shared": "~10.3.0", + "@abp/ng.account.core": "~10.4.1", + "@abp/ng.theme.shared": "~10.4.1", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html index afc498d5ac..e6475affa1 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html @@ -6,7 +6,7 @@