diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..1eff521897 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(yarn nx g:*)", + "Bash(npx vitest:*)" + ] + } +} diff --git a/.gitignore b/.gitignore index a386e86320..d3bc52cfb8 100644 --- a/.gitignore +++ b/.gitignore @@ -328,4 +328,4 @@ deploy/_run_all_log.txt # No commit yarn.lock files in the subfolders of templates directory templates/**/yarn.lock templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Logs/logs.txt -templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json +templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index fa951f8d7d..bd5a55694f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,8 +30,9 @@ + - + @@ -56,63 +57,63 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -140,7 +141,7 @@ - + @@ -168,20 +169,20 @@ - + - - + + - - - + + + - + @@ -193,6 +194,6 @@ - + - + \ No newline at end of file diff --git a/ai-rules/README.md b/ai-rules/README.md new file mode 100644 index 0000000000..ed9370b591 --- /dev/null +++ b/ai-rules/README.md @@ -0,0 +1,151 @@ +# ABP AI Rules + +This folder contains AI rules (Cursor `.mdc` format) for ABP based solutions. These rules help AI assistants understand ABP-specific patterns, conventions, and best practices when working with ABP-based applications. + +## Purpose + +This folder serves as a central repository for ABP-specific AI rules. The community can contribute, improve, and maintain these rules collaboratively. + +When you create a new ABP solution, these rules are included in your project based on your configuration. This provides AI assistants with ABP-specific context, helping them generate code that follows ABP conventions. + +> **Important**: These rules are ABP-specific. They don't cover general .NET or ASP.NET Core patterns—AI assistants already know those. Instead, they focus on ABP's unique architecture, module system, and conventions. + +## How Rules Work + +Large language models don't retain memory between completions. Rules provide persistent, reusable context at the prompt level. + +When applied, rule contents are included at the start of the model context. This gives the AI consistent guidance for generating code, interpreting edits, or helping with workflows. + +## Mini Glossary (ABP Terms) + +- **Application service**: Use-case orchestration (ABP’s primary “business API” surface). Usually exposed remotely via Auto API Controllers or explicit controllers. +- **Auto API Controllers**: ABP can auto-generate HTTP endpoints from `IApplicationService` contracts. +- **Client proxy**: Generated client-side code (Angular/JS/C#) to call remote application services. +- **Integration service (microservices)**: Application-service-like contract intended for **service-to-service** communication; typically exposed separately and consumed via generated C# proxies. +- **Domain vs Application**: Domain holds business rules/invariants; Application coordinates domain + infrastructure and returns DTOs. + +## File Structure + +``` +ai-rules/ +├── README.md +├── common/ # Rules for all ABP projects +│ ├── abp-core.mdc # Core ABP conventions (alwaysApply: true) +│ ├── ddd-patterns.mdc # DDD patterns (Entity, AggregateRoot, Repository) +│ ├── application-layer.mdc # Application services, DTOs, validation +│ ├── authorization.mdc # Permissions and authorization +│ ├── multi-tenancy.mdc # Multi-tenant entities and data isolation +│ ├── infrastructure.mdc # Settings, Features, Caching, Events, Jobs +│ ├── dependency-rules.mdc # Layer dependencies and guardrails +│ ├── development-flow.mdc # Development workflow +│ └── cli-commands.mdc # ABP CLI commands reference +├── ui/ # UI-specific rules (applied by globs) +│ ├── blazor.mdc # Blazor UI patterns +│ ├── angular.mdc # Angular UI patterns +│ └── mvc.mdc # MVC/Razor Pages patterns +├── data/ # Data layer rules (applied by globs) +│ ├── ef-core.mdc # Entity Framework Core patterns +│ └── mongodb.mdc # MongoDB patterns +├── testing/ # Testing rules +│ └── patterns.mdc # Unit and integration test patterns +└── template-specific/ # Template-specific rules + ├── app-nolayers.mdc # Single-layer app template + ├── module.mdc # Module template + └── microservice.mdc # Microservice template +``` + +### Rule Format + +Each rule is a markdown file with frontmatter metadata: + +```markdown +--- +description: "Describes when this rule should apply - used by AI to decide relevance" +globs: "src/**/*.cs" +alwaysApply: false +--- + +# Rule Title + +Your rule content here... +``` + +### Frontmatter Properties + +| Property | Description | +|----------|-------------| +| `description` | Brief description of what the rule covers. Used by AI to determine relevance. | +| `globs` | File patterns that trigger this rule (e.g., `**/*.cs`, `*.Domain/**`). | +| `alwaysApply` | If `true`, rule is always included. If `false`, AI decides based on context. | + +### Rule Types + +| Type | When Applied | +|------|--------------| +| **Always Apply** | Every chat session (`alwaysApply: true`) | +| **Apply Intelligently** | When AI decides it's relevant based on `description` | +| **Apply to Specific Files** | When file matches `globs` pattern | +| **Apply Manually** | When @-mentioned in chat (e.g., `@my-rule`) | + +## Rule Categories + +### Common Rules +Core ABP patterns that apply to all DDD-based templates (app, module, microservice): +- `abp-core.mdc` - Always applied, covers module system, DI conventions, base classes +- `ddd-patterns.mdc` - Entity, AggregateRoot, Repository, Domain Services +- `application-layer.mdc` - Application services, DTOs, validation, error handling +- `authorization.mdc` - Permission system and authorization +- `infrastructure.mdc` - Settings, Features, Caching, Events, Background Jobs +- `dependency-rules.mdc` - Layer dependencies and project structure +- `development-flow.mdc` - Development workflow for adding features + +### UI Rules (Applied by Globs) +- `blazor.mdc` - Applied to `**/*.razor`, `**/Blazor/**/*.cs` +- `angular.mdc` - Applied to `**/angular/**/*.ts` +- `mvc.mdc` - Applied to `**/*.cshtml`, `**/Pages/**/*.cs` + +### Data Rules (Applied by Globs) +- `ef-core.mdc` - Applied to `**/*.EntityFrameworkCore/**/*.cs` +- `mongodb.mdc` - Applied to `**/*.MongoDB/**/*.cs` + +### Template-Specific Rules +- `app-nolayers.mdc` - For single-layer web application template +- `module.mdc` - For reusable module template +- `microservice.mdc` - For microservice template + +## Best Practices + +Good rules are focused, actionable, and scoped: + +- **Keep rules under 500 lines** - Split large rules into multiple, composable rules +- **Provide concrete examples** - Reference actual files or include code snippets +- **Be specific, not vague** - Write rules like clear internal documentation +- **Reference files instead of copying** - This keeps rules short and prevents staleness +- **Start simple** - Add rules only when you notice AI making the same mistake repeatedly + +## What to Avoid + +- **Copying entire style guides**: Use a linter instead. AI already knows common style conventions. +- **Documenting every possible command**: AI knows common tools like `dotnet` and `npm`. +- **Adding instructions for edge cases that rarely apply**: Keep rules focused on patterns you use frequently. +- **Duplicating what's already in your codebase**: Point to canonical examples instead of copying code. +- **Including non-ABP patterns**: Don't add generic .NET/ASP.NET Core guidance—focus on ABP-specific conventions. + +## Contributing + +We welcome community contributions to improve these rules! You can open a PR to add new rules or improve existing ones. + +Please review our [Contribution Guide](../CONTRIBUTING.md) and [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing. + +### Contribution Guidelines + +- Each rule should focus on a single ABP concept or pattern +- Use clear, actionable language +- Include examples where helpful +- Test your rules by using them in a real ABP project +- Keep ABP-specific focus—don't add general .NET patterns + +## Related Resources + +- [Cursor Rules Documentation](https://cursor.com/docs/context/rules) +- [ABP Framework Documentation](https://abp.io/docs) diff --git a/ai-rules/common/abp-core.mdc b/ai-rules/common/abp-core.mdc new file mode 100644 index 0000000000..673c4199e6 --- /dev/null +++ b/ai-rules/common/abp-core.mdc @@ -0,0 +1,182 @@ +--- +description: "Core ABP Framework conventions - module system, dependency injection, and base classes" +alwaysApply: true +--- + +# ABP Core Conventions + +> **Documentation**: https://abp.io/docs/latest +> **API Reference**: https://abp.io/docs/api/ + +## Module System +Every ABP application/module has a module class that configures services: + +```csharp +[DependsOn( + typeof(AbpDddDomainModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class MyAppModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Service registration and configuration + } +} +``` + +> **Note**: Middleware configuration (`OnApplicationInitialization`) should only be done in the final host application, not in reusable modules. + +## Dependency Injection Conventions + +### Automatic Registration +ABP automatically registers services implementing marker interfaces: +- `ITransientDependency` → Transient lifetime +- `ISingletonDependency` → Singleton lifetime +- `IScopedDependency` → Scoped lifetime + +Classes inheriting from `ApplicationService`, `DomainService`, `AbpController` are also auto-registered. + +### Repository Usage +You can use the generic `IRepository` for simple CRUD operations. Define custom repository interfaces only when you need custom query methods: + +```csharp +// Simple CRUD - Generic repository is fine +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; // ✅ OK for simple operations +} + +// Custom queries needed - Define custom interface +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); // Custom query +} + +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ Use custom when needed +} +``` + +### Exposing Services +```csharp +[ExposeServices(typeof(IMyService))] +public class MyService : IMyService, ITransientDependency { } +``` + +## Important Base Classes + +| Base Class | Purpose | +|------------|---------| +| `Entity` | Basic entity with ID | +| `AggregateRoot` | DDD aggregate root | +| `DomainService` | Domain business logic | +| `ApplicationService` | Use case orchestration | +| `AbpController` | REST API controller | + +ABP base classes already inject commonly used services as properties. Before injecting a service, check if it's already available: + +| Property | Available In | Description | +|----------|--------------|-------------| +| `GuidGenerator` | All base classes | Generate GUIDs | +| `Clock` | All base classes | Current time (use instead of `DateTime`) | +| `CurrentUser` | All base classes | Authenticated user info | +| `CurrentTenant` | All base classes | Multi-tenancy context | +| `L` (StringLocalizer) | `ApplicationService`, `AbpController` | Localization | +| `AuthorizationService` | `ApplicationService`, `AbpController` | Permission checks | +| `FeatureChecker` | `ApplicationService`, `AbpController` | Feature availability | +| `DataFilter` | All base classes | Data filtering (soft-delete, tenant) | +| `UnitOfWorkManager` | `ApplicationService`, `DomainService` | Unit of work management | +| `LoggerFactory` | All base classes | Create loggers | +| `Logger` | All base classes | Logging (auto-created) | +| `LazyServiceProvider` | All base classes | Lazy service resolution | + +**Useful methods from base classes:** +- `CheckPolicyAsync()` - Check permission and throw if not granted +- `IsGrantedAsync()` - Check permission without throwing + +## Async Best Practices +- Use async all the way - never use `.Result` or `.Wait()` +- All async methods should end with `Async` suffix +- ABP automatically handles `CancellationToken` in most cases (e.g., from `HttpContext.RequestAborted`) +- Only pass `CancellationToken` explicitly when implementing custom cancellation logic + +## Time Handling +Never use `DateTime.Now` or `DateTime.UtcNow` directly. Use ABP's `IClock` service: + +```csharp +// In classes inheriting from base classes (ApplicationService, DomainService, etc.) +public class BookAppService : ApplicationService +{ + public void DoSomething() + { + var now = Clock.Now; // ✅ Already available as property + } +} + +// In other services - inject IClock +public class MyService : ITransientDependency +{ + private readonly IClock _clock; + + public MyService(IClock clock) => _clock = clock; + + public void DoSomething() + { + var now = _clock.Now; // ✅ Correct + // var now = DateTime.Now; // ❌ Wrong - not testable, ignores timezone settings + } +} +``` + +> **Tip**: Before injecting a service, check if it's already available as a property in your base classes. + +## Business Exceptions +Use `BusinessException` for domain rule violations with namespaced error codes: + +```csharp +throw new BusinessException("MyModule:BookNameAlreadyExists") + .WithData("Name", bookName); +``` + +Configure localization mapping: +```csharp +Configure(options => +{ + options.MapCodeNamespace("MyModule", typeof(MyModuleResource)); +}); +``` + +## Localization +- In base classes (`ApplicationService`, `AbpController`, etc.): Use `L["Key"]` - this is the `IStringLocalizer` property +- In other services: Inject `IStringLocalizer` +- Always localize user-facing messages and exceptions + +**Localization file location**: `*.Domain.Shared/Localization/{ResourceName}/{lang}.json` + +```json +// Example: MyProject.Domain.Shared/Localization/MyProject/en.json +{ + "culture": "en", + "texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "BookName": "Book Name" + } +} +``` + +## ❌ Never Use (ABP Anti-Patterns) + +| Don't Use | Use Instead | +|-----------|-------------| +| Minimal APIs | ABP Controllers or Auto API Controllers | +| MediatR | Application Services | +| `DbContext` directly in App Services | `IRepository` | +| `AddScoped/AddTransient/AddSingleton` | `ITransientDependency`, `ISingletonDependency` | +| `DateTime.Now` | `IClock` / `Clock.Now` | +| Custom UnitOfWork | ABP's `IUnitOfWorkManager` | +| Manual HTTP calls from UI | ABP client proxies (`generate-proxy`) | +| Hardcoded role checks | Permission-based authorization | +| Business logic in Controllers | Application Services | diff --git a/ai-rules/common/application-layer.mdc b/ai-rules/common/application-layer.mdc new file mode 100644 index 0000000000..7df134c590 --- /dev/null +++ b/ai-rules/common/application-layer.mdc @@ -0,0 +1,232 @@ +--- +description: "ABP Application Services, DTOs, validation, and error handling patterns" +globs: "**/*.Application/**/*.cs,**/Application/**/*.cs,**/*AppService*.cs,**/*Dto*.cs" +alwaysApply: false +--- + +# ABP Application Layer Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services + +## Application Service Structure + +### Interface (Application.Contracts) +```csharp +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(GetBookListInput input); + Task CreateAsync(CreateBookDto input); + Task UpdateAsync(Guid id, UpdateBookDto input); + Task DeleteAsync(Guid id); +} +``` + +### Implementation (Application) +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IBookRepository _bookRepository; + private readonly BookManager _bookManager; + private readonly BookMapper _bookMapper; + + public BookAppService( + IBookRepository bookRepository, + BookManager bookManager, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookManager = bookManager; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = await _bookManager.CreateAsync(input.Name, input.Price); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + [Authorize(BookStorePermissions.Books.Edit)] + public async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await _bookRepository.GetAsync(id); + await _bookManager.ChangeNameAsync(book, input.Name); + book.SetPrice(input.Price); + await _bookRepository.UpdateAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +## Application Service Best Practices +- Don't repeat entity name in method names (`GetAsync` not `GetBookAsync`) +- Accept/return DTOs only, never entities +- ID not inside UpdateDto - pass separately +- Use custom repositories when you need custom queries, generic repository is fine for simple CRUD +- Call `UpdateAsync` explicitly (don't assume change tracking) +- Don't call other app services in same module +- Don't use `IFormFile`/`Stream` - pass `byte[]` from controllers +- Use base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) instead of injecting these services + +## DTO Naming Conventions + +| Purpose | Convention | Example | +|---------|------------|---------| +| Query input | `Get{Entity}Input` | `GetBookInput` | +| List query input | `Get{Entity}ListInput` | `GetBookListInput` | +| Create input | `Create{Entity}Dto` | `CreateBookDto` | +| Update input | `Update{Entity}Dto` | `UpdateBookDto` | +| Single entity output | `{Entity}Dto` | `BookDto` | +| List item output | `{Entity}ListItemDto` | `BookListItemDto` | + +## DTO Location +- Define DTOs in `*.Application.Contracts` project +- This allows sharing with clients (Blazor, HttpApi.Client) + +## Validation + +### Data Annotations +```csharp +public class CreateBookDto +{ + [Required] + [StringLength(100, MinimumLength = 3)] + public string Name { get; set; } + + [Range(0, 999.99)] + public decimal Price { get; set; } +} +``` + +### Custom Validation with IValidatableObject +Before adding custom validation, decide if it's a **domain rule** or **application rule**: +- **Domain rule**: Put validation in entity constructor or domain service (enforces business invariants) +- **Application rule**: Use DTO validation (input format, required fields) + +Only use `IValidatableObject` for application-level validation that can't be expressed with data annotations: + +```csharp +public class CreateBookDto : IValidatableObject +{ + public string Name { get; set; } + public string Description { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Name == Description) + { + yield return new ValidationResult( + "Name and Description cannot be the same!", + new[] { nameof(Name), nameof(Description) } + ); + } + } +} +``` + +### FluentValidation +```csharp +public class CreateBookDtoValidator : AbstractValidator +{ + public CreateBookDtoValidator() + { + RuleFor(x => x.Name).NotEmpty().Length(3, 100); + RuleFor(x => x.Price).GreaterThan(0); + } +} +``` + +## Error Handling + +### Business Exceptions +```csharp +throw new BusinessException("BookStore:010001") + .WithData("BookName", name); +``` + +### Entity Not Found +```csharp +var book = await _bookRepository.FindAsync(id); +if (book == null) +{ + throw new EntityNotFoundException(typeof(Book), id); +} +``` + +### User-Friendly Exceptions +```csharp +throw new UserFriendlyException(L["BookNotAvailable"]); +``` + +### HTTP Status Code Mapping +Status code mapping is **configurable** in ABP (do not rely on a fixed mapping in business logic). + +| Exception | Typical HTTP Status | +|-----------|-------------| +| `AbpValidationException` | 400 | +| `AbpAuthorizationException` | 401/403 | +| `EntityNotFoundException` | 404 | +| `BusinessException` | 403 (but configurable) | +| Other exceptions | 500 | + +## Auto API Controllers +ABP automatically generates API controllers for application services: +- Interface must inherit `IApplicationService` (which already has `[RemoteService]` attribute) +- HTTP methods determined by method name prefix (Get, Create, Update, Delete) +- Use `[RemoteService(false)]` to disable auto API generation for specific methods + +## Object Mapping (Mapperly / AutoMapper) +ABP supports **both Mapperly and AutoMapper** integrations. But the default mapping library is Mapperly. You need to first check the project's active mapping library. +- Prefer the mapping provider already used in the solution (check existing mapping files / loaded modules). +- In mixed solutions, explicitly setting the default provider may be required (see `docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md`). + +### Mapperly (compile-time) +Define mappers as partial classes: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddSingleton(); +} +``` + +Usage in application service: +```csharp +public class BookAppService : ApplicationService +{ + private readonly BookMapper _bookMapper; + + public BookAppService(BookMapper bookMapper) + { + _bookMapper = bookMapper; + } + + public BookDto GetBook(Book book) + { + return _bookMapper.MapToDto(book); + } +} +``` + +> **Note**: Mapperly generates mapping code at compile-time, providing better performance than runtime mappers. + +### AutoMapper (runtime) +If the solution uses AutoMapper, mappings are typically defined in `Profile` classes and registered via ABP's AutoMapper integration. diff --git a/ai-rules/common/authorization.mdc b/ai-rules/common/authorization.mdc new file mode 100644 index 0000000000..b7885271c1 --- /dev/null +++ b/ai-rules/common/authorization.mdc @@ -0,0 +1,183 @@ +--- +description: "ABP permission system and authorization patterns" +globs: "**/*Permission*.cs,**/*AppService*.cs,**/*Controller*.cs" +alwaysApply: false +--- + +# ABP Authorization + +> **Docs**: https://abp.io/docs/latest/framework/fundamentals/authorization + +## Permission Definition +Define permissions in `*.Application.Contracts` project: + +```csharp +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } +} +``` + +Register in provider: +```csharp +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); + + var booksPermission = bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books")); + + booksPermission.AddChild( + BookStorePermissions.Books.Create, + L("Permission:Books.Create")); + + booksPermission.AddChild( + BookStorePermissions.Books.Edit, + L("Permission:Books.Edit")); + + booksPermission.AddChild( + BookStorePermissions.Books.Delete, + L("Permission:Books.Delete")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} +``` + +## Using Permissions + +### Declarative (Attribute) +```csharp +[Authorize(BookStorePermissions.Books.Create)] +public virtual async Task CreateAsync(CreateBookDto input) +{ + // Only users with Books.Create permission can execute +} +``` + +### Programmatic Check +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Check and throw if not granted + await CheckPolicyAsync(BookStorePermissions.Books.Edit); + + // Or check without throwing + if (await IsGrantedAsync(BookStorePermissions.Books.Delete)) + { + // Has permission + } + } +} +``` + +### Allow Anonymous Access +```csharp +[AllowAnonymous] +public virtual async Task GetPublicBookAsync(Guid id) +{ + // No authentication required +} +``` + +## Current User +Access authenticated user info via `CurrentUser` property (available in base classes like `ApplicationService`, `DomainService`, `AbpController`): + +```csharp +public class BookAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // CurrentUser is available from base class - no injection needed + var userId = CurrentUser.Id; + var userName = CurrentUser.UserName; + var email = CurrentUser.Email; + var isAuthenticated = CurrentUser.IsAuthenticated; + var roles = CurrentUser.Roles; + var tenantId = CurrentUser.TenantId; + } +} + +// In other services, inject ICurrentUser +public class MyService : ITransientDependency +{ + private readonly ICurrentUser _currentUser; + public MyService(ICurrentUser currentUser) => _currentUser = currentUser; +} +``` + +### Ownership Validation +```csharp +public async Task UpdateMyBookAsync(Guid bookId, UpdateBookDto input) +{ + var book = await _bookRepository.GetAsync(bookId); + + if (book.CreatorId != CurrentUser.Id) + { + throw new AbpAuthorizationException(); + } + + // Update book... +} +``` + +## Multi-Tenancy Permissions +Control permission availability per tenant side: + +```csharp +bookStoreGroup.AddPermission( + BookStorePermissions.Books.Default, + L("Permission:Books"), + multiTenancySide: MultiTenancySides.Tenant // Only for tenants +); +``` + +Options: `MultiTenancySides.Host`, `Tenant`, or `Both` + +## Feature-Dependent Permissions +```csharp +booksPermission.RequireFeatures("BookStore.PremiumFeature"); +``` + +## Permission Management +Grant/revoke permissions programmatically: + +```csharp +public class MyService : ITransientDependency +{ + private readonly IPermissionManager _permissionManager; + + public async Task GrantPermissionToUserAsync(Guid userId, string permissionName) + { + await _permissionManager.SetForUserAsync(userId, permissionName, true); + } + + public async Task GrantPermissionToRoleAsync(string roleName, string permissionName) + { + await _permissionManager.SetForRoleAsync(roleName, permissionName, true); + } +} +``` + +## Security Best Practices +- Never trust client input for user identity +- Use `CurrentUser` property (from base class) or inject `ICurrentUser` +- Validate ownership in application service methods +- Filter queries by current user when appropriate +- Don't expose sensitive fields in DTOs diff --git a/ai-rules/common/cli-commands.mdc b/ai-rules/common/cli-commands.mdc new file mode 100644 index 0000000000..3e406898a1 --- /dev/null +++ b/ai-rules/common/cli-commands.mdc @@ -0,0 +1,90 @@ +--- +description: "ABP CLI commands: generate-proxy, install-libs, add-package-ref, new-module, install-module, update, clean, suite generate (CRUD pages)" +globs: "**/*.csproj,**/appsettings*.json" +alwaysApply: false +--- + +# ABP CLI Commands + +> **Full documentation**: https://abp.io/docs/latest/cli +> Use `abp help [command]` for detailed options. + +## Generate Client Proxies + +```bash +# URL flag: `-u` (short) or `--url` (long). Use whichever your team prefers, but keep it consistent. +# +# Angular (host must be running) +abp generate-proxy -t ng + +# C# client proxies +abp generate-proxy -t csharp -u https://localhost:44300 + +# Integration services only (microservices) +abp generate-proxy -t csharp -u https://localhost:44300 -st integration + +# JavaScript +abp generate-proxy -t js -u https://localhost:44300 +``` + +## Install Client-Side Libraries + +```bash +# Install NPM packages for MVC/Blazor Server +abp install-libs +``` + +## Add Package Reference + +```bash +# Add project reference with module dependency +abp add-package-ref Acme.BookStore.Domain +abp add-package-ref Acme.BookStore.Domain -t Acme.BookStore.Application +``` + +## Module Operations + +```bash +# Create new module in solution +abp new-module Acme.OrderManagement -t module:ddd + +# Install published module +abp install-module Volo.Blogging + +# Add ABP NuGet package +abp add-package Volo.Abp.Caching.StackExchangeRedis +``` + +## Update & Clean + +```bash +abp update # Update all ABP packages +abp update --version 8.0.0 # Specific version +abp clean # Delete bin/obj folders +``` + +## ABP Suite (CRUD Generation) + +Generate CRUD pages from entity JSON (created via Suite UI): + +```bash +abp suite generate --entity .suite/entities/Book.json --solution ./Acme.BookStore.sln +``` + +> **Note**: Entity JSON files are created when you generate an entity via ABP Suite UI. They are stored in `.suite/entities/` folder. +> **Suite docs**: https://abp.io/docs/latest/suite + +## Quick Reference + +| Task | Command | +|------|---------| +| Angular proxies | `abp generate-proxy -t ng` | +| C# proxies | `abp generate-proxy -t csharp -u URL` | +| Install JS libs | `abp install-libs` | +| Add reference | `abp add-package-ref PackageName` | +| Create module | `abp new-module ModuleName` | +| Install module | `abp install-module ModuleName` | +| Update packages | `abp update` | +| Clean solution | `abp clean` | +| Suite CRUD | `abp suite generate -e entity.json -s solution.sln` | +| Get help | `abp help [command]` | diff --git a/ai-rules/common/ddd-patterns.mdc b/ai-rules/common/ddd-patterns.mdc new file mode 100644 index 0000000000..7eccbdbcbe --- /dev/null +++ b/ai-rules/common/ddd-patterns.mdc @@ -0,0 +1,241 @@ +--- +description: "ABP DDD patterns - Entities, Aggregate Roots, Repositories, Domain Services" +globs: "**/*.Domain/**/*.cs,**/Domain/**/*.cs,**/Entities/**/*.cs" +alwaysApply: false +--- + +# ABP DDD Patterns + +> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design + +## Rich Domain Model vs Anemic Domain Model + +ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior: + +| Anemic (Anti-pattern) | Rich (Recommended) | +|----------------------|-------------------| +| Entity = data only | Entity = data + behavior | +| Logic in services | Logic in entity methods | +| Public setters | Private setters with methods | +| No validation in entity | Entity enforces invariants | + +**Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods. + +## Entities + +### Entity Example (Rich Model) +```csharp +public class OrderLine : Entity +{ + public Guid ProductId { get; private set; } + public int Count { get; private set; } + public decimal Price { get; private set; } + + protected OrderLine() { } // For ORM + + internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id) + { + ProductId = productId; + SetCount(count); // Validates through method + Price = price; + } + + public void SetCount(int count) + { + if (count <= 0) + throw new BusinessException("Orders:InvalidCount"); + Count = count; + } +} +``` + +## Aggregate Roots + +Aggregate roots are consistency boundaries that: +- Own their child entities +- Enforce business rules +- Publish domain events + +```csharp +public class Order : AggregateRoot +{ + public string OrderNumber { get; private set; } + public Guid CustomerId { get; private set; } + public OrderStatus Status { get; private set; } + public ICollection Lines { get; private set; } + + protected Order() { } // For ORM + + public Order(Guid id, string orderNumber, Guid customerId) : base(id) + { + OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber)); + CustomerId = customerId; + Status = OrderStatus.Created; + Lines = new List(); + } + + public void AddLine(Guid lineId, Guid productId, int count, decimal price) + { + // Business rule: Can only add lines to created orders + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotModifyOrder"); + + Lines.Add(new OrderLine(lineId, productId, count, price)); + } + + public void Complete() + { + if (Status != OrderStatus.Created) + throw new BusinessException("Orders:CannotCompleteOrder"); + + Status = OrderStatus.Completed; + + // Publish events for side effects + AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction + AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service + } +} +``` + +### Domain Events +- `AddLocalEvent()` - Handled within same transaction, can access full entity +- `AddDistributedEvent()` - Handled asynchronously, use ETOs (Event Transfer Objects) + +### Entity Best Practices +- **Encapsulation**: Private setters, public methods that enforce rules +- **Primary constructor**: Enforce invariants, accept `id` parameter +- **Protected parameterless constructor**: Required for ORM +- **Initialize collections**: In primary constructor +- **Virtual members**: For ORM proxy compatibility +- **Reference by Id**: Don't add navigation properties to other aggregates +- **Don't generate GUID in constructor**: Use `IGuidGenerator` externally + +## Repository Pattern + +### When to Use Custom Repository +- **Generic repository** (`IRepository`): Sufficient for simple CRUD operations +- **Custom repository**: Only when you need custom query methods + +### Interface (Domain Layer) +```csharp +// Define custom interface only when custom queries are needed +public interface IOrderRepository : IRepository +{ + Task FindByOrderNumberAsync(string orderNumber, bool includeDetails = false); + Task> GetListByCustomerAsync(Guid customerId, bool includeDetails = false); +} +``` + +### Repository Best Practices +- **One repository per aggregate root only** - Never create repositories for child entities +- Child entities must be accessed/modified only through their aggregate root +- Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules) +- In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this +- Define custom repository only when custom queries are needed +- ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control +- Single entity methods: `includeDetails = true` by default +- List methods: `includeDetails = false` by default +- Don't return projection classes +- Interface in Domain, implementation in data layer + +```csharp +// ✅ Correct: Repository for aggregate root (Order) +public interface IOrderRepository : IRepository { } + +// ❌ Wrong: Repository for child entity (OrderLine) +// OrderLine should only be accessed through Order aggregate +public interface IOrderLineRepository : IRepository { } // Don't do this! +``` + +## Domain Services + +Use domain services for business logic that: +- Spans multiple aggregates +- Requires repository queries to enforce rules + +```csharp +public class OrderManager : DomainService +{ + private readonly IOrderRepository _orderRepository; + private readonly IProductRepository _productRepository; + + public OrderManager( + IOrderRepository orderRepository, + IProductRepository productRepository) + { + _orderRepository = orderRepository; + _productRepository = productRepository; + } + + public async Task CreateAsync(string orderNumber, Guid customerId) + { + // Business rule: Order number must be unique + var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber); + if (existing != null) + { + throw new BusinessException("Orders:OrderNumberAlreadyExists") + .WithData("OrderNumber", orderNumber); + } + + return new Order(GuidGenerator.Create(), orderNumber, customerId); + } + + public async Task AddProductAsync(Order order, Guid productId, int count) + { + var product = await _productRepository.GetAsync(productId); + order.AddLine(productId, count, product.Price); + } +} +``` + +### Domain Service Best Practices +- Use `*Manager` suffix naming +- No interface by default (create only if needed) +- Accept/return domain objects, not DTOs +- Don't depend on authenticated user - pass values from application layer +- Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services + +## Domain Events + +### Local Events +```csharp +// In aggregate +AddLocalEvent(new OrderCompletedEvent(Id)); + +// Handler +public class OrderCompletedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCompletedEvent eventData) + { + // Handle within same transaction + } +} +``` + +### Distributed Events (ETO) +For inter-module/microservice communication: +```csharp +// In Domain.Shared +[EventName("Orders.OrderCompleted")] +public class OrderCompletedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} +``` + +## Specifications + +Reusable query conditions: +```csharp +public class CompletedOrdersSpec : Specification +{ + public override Expression> ToExpression() + { + return o => o.Status == OrderStatus.Completed; + } +} + +// Usage +var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec()); +``` diff --git a/ai-rules/common/dependency-rules.mdc b/ai-rules/common/dependency-rules.mdc new file mode 100644 index 0000000000..3210b7436a --- /dev/null +++ b/ai-rules/common/dependency-rules.mdc @@ -0,0 +1,151 @@ +--- +description: "ABP layer dependency rules and project structure guardrails" +globs: "**/*.csproj,**/*Module*.cs" +alwaysApply: false +--- + +# ABP Dependency Rules + +## Core Principles (All Templates) + +These principles apply regardless of solution structure: + +1. **Domain logic never depends on infrastructure** (no DbContext in domain/application) +2. **Use abstractions** (interfaces) for dependencies +3. **Higher layers depend on lower layers**, never the reverse +4. **Data access through repositories**, not direct DbContext + +## Layered Template Structure + +> **Note**: This section applies to layered templates (app, module). Single-layer and microservice templates have different structures. + +``` +Domain.Shared → Constants, enums, localization keys + ↑ + Domain → Entities, repository interfaces, domain services + ↑ +Application.Contracts → App service interfaces, DTOs + ↑ + Application → App service implementations + ↑ + HttpApi → REST controllers (optional) + ↑ + Host → Final application with DI and middleware +``` + +### Layered Dependency Direction + +| Project | Can Reference | Referenced By | +|---------|---------------|---------------| +| Domain.Shared | Nothing | All | +| Domain | Domain.Shared | Application, Data layer | +| Application.Contracts | Domain.Shared | Application, HttpApi, Clients | +| Application | Domain, Contracts | Host | +| EntityFrameworkCore/MongoDB | Domain | Host only | +| HttpApi | Contracts only | Host | + +## Critical Rules + +### ❌ Never Do +```csharp +// Application layer accessing DbContext directly +public class BookAppService : ApplicationService +{ + private readonly MyDbContext _dbContext; // ❌ WRONG +} + +// Domain depending on application layer +public class BookManager : DomainService +{ + private readonly IBookAppService _appService; // ❌ WRONG +} + +// HttpApi depending on Application implementation +public class BookController : AbpController +{ + private readonly BookAppService _bookAppService; // ❌ WRONG - Use interface +} +``` + +### ✅ Always Do +```csharp +// Application layer using repository abstraction +public class BookAppService : ApplicationService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// Domain service using domain abstractions +public class BookManager : DomainService +{ + private readonly IBookRepository _bookRepository; // ✅ CORRECT +} + +// HttpApi depending on contracts only +public class BookController : AbpController +{ + private readonly IBookAppService _bookAppService; // ✅ CORRECT +} +``` + +## Repository Pattern Enforcement + +### Interface Location +```csharp +// In Domain project +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### Implementation Location +```csharp +// In EntityFrameworkCore project +public class BookRepository : EfCoreRepository, IBookRepository +{ + // Implementation +} + +// In MongoDB project +public class BookRepository : MongoDbRepository, IBookRepository +{ + // Implementation +} +``` + +## Multi-Application Scenarios + +When you have multiple applications (e.g., Admin + Public API): + +### Vertical Separation +``` +MyProject.Admin.Application - Admin-specific services +MyProject.Public.Application - Public-specific services +MyProject.Domain - Shared domain (both reference this) +``` + +### Rules +- Admin and Public application layers **MUST NOT** reference each other +- Share domain logic, not application logic +- Each vertical can have its own DTOs even if similar + +## Enforcement Checklist (Layered Templates) + +When adding a new feature: +1. **Entity changes?** → Domain project +2. **Constants/enums?** → Domain.Shared project +3. **Repository interface?** → Domain project (only if custom queries needed) +4. **Repository implementation?** → EntityFrameworkCore/MongoDB project +5. **DTOs and service interface?** → Application.Contracts project +6. **Service implementation?** → Application project +7. **API endpoint?** → HttpApi project (if not using auto API controllers) + +## Common Violations to Watch + +| Violation | Impact | Fix | +|-----------|--------|-----| +| DbContext in Application | Breaks DB independence | Use repository | +| Entity in DTO | Exposes internals | Map to DTO | +| IQueryable in interface | Breaks abstraction | Return concrete types | +| Cross-module app service call | Tight coupling | Use events or domain | diff --git a/ai-rules/common/development-flow.mdc b/ai-rules/common/development-flow.mdc new file mode 100644 index 0000000000..22ee69687a --- /dev/null +++ b/ai-rules/common/development-flow.mdc @@ -0,0 +1,291 @@ +--- +description: "ABP development workflow - adding features, entities, and migrations" +globs: "**/*AppService*.cs,**/*Application*/**/*.cs,**/*Application.Contracts*/**/*.cs,**/*Dto*.cs,**/*DbContext*.cs,**/*.EntityFrameworkCore/**/*.cs,**/*.MongoDB/**/*.cs,**/*Permission*.cs" +alwaysApply: false +--- + +# ABP Development Workflow + +> **Tutorials**: https://abp.io/docs/latest/tutorials + +## Adding a New Entity (Full Flow) + +### 1. Domain Layer +Create entity (location varies by template: `*.Domain/Entities/` for layered, `Entities/` for single-layer/microservice): + +```csharp +public class Book : AggregateRoot +{ + public string Name { get; private set; } + public decimal Price { get; private set; } + public Guid AuthorId { get; private set; } + + protected Book() { } + + public Book(Guid id, string name, decimal price, Guid authorId) : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + SetPrice(price); + AuthorId = authorId; + } + + public void SetPrice(decimal price) + { + Price = Check.Range(price, nameof(price), 0, 9999); + } +} +``` + +### 2. Domain.Shared +Add constants and enums in `*.Domain.Shared/`: + +```csharp +public static class BookConsts +{ + public const int MaxNameLength = 128; +} + +public enum BookType +{ + Novel, + Science, + Biography +} +``` + +### 3. Repository Interface (Optional) +Define custom repository in `*.Domain/` only if you need custom query methods. For simple CRUD, use generic `IRepository` directly: + +```csharp +// Only if custom queries are needed +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); +} +``` + +### 4. EF Core Configuration +In `*.EntityFrameworkCore/`: + +**DbContext:** +```csharp +public DbSet Books { get; set; } +``` + +**OnModelCreating:** +```csharp +builder.Entity(b => +{ + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); + b.Property(x => x.Name).IsRequired().HasMaxLength(BookConsts.MaxNameLength); + b.HasIndex(x => x.Name); +}); +``` + +**Repository Implementation (only if custom interface defined):** +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + return await (await GetDbSetAsync()) + .FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### 5. Run Migration +```bash +cd src/MyProject.EntityFrameworkCore + +# Add migration +dotnet ef migrations add Added_Book + +# Apply migration (choose one): +dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data +# OR +dotnet ef database update # EF Core command only +``` + +### 6. Application.Contracts +Create DTOs and service interface: + +```csharp +// DTOs +public class BookDto : EntityDto +{ + public string Name { get; set; } + public decimal Price { get; set; } + public Guid AuthorId { get; set; } +} + +public class CreateBookDto +{ + [Required] + [StringLength(BookConsts.MaxNameLength)] + public string Name { get; set; } + + [Range(0, 9999)] + public decimal Price { get; set; } + + [Required] + public Guid AuthorId { get; set; } +} + +// Service Interface +public interface IBookAppService : IApplicationService +{ + Task GetAsync(Guid id); + Task> GetListAsync(PagedAndSortedResultRequestDto input); + Task CreateAsync(CreateBookDto input); +} +``` + +### 7. Object Mapping (Mapperly / AutoMapper) +ABP supports both Mapperly and AutoMapper. Prefer the provider already used in the solution. + +If the solution uses **Mapperly**, create a mapper in the Application project: + +```csharp +[Mapper] +public partial class BookMapper +{ + public partial BookDto MapToDto(Book book); + public partial List MapToDtoList(List books); +} +``` + +Register in module: +```csharp +context.Services.AddSingleton(); +``` + +### 8. Application Service +Implement service (using generic repository - use `IBookRepository` if you defined custom interface in step 3): + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; // Or IBookRepository + private readonly BookMapper _bookMapper; + + public BookAppService( + IRepository bookRepository, + BookMapper bookMapper) + { + _bookRepository = bookRepository; + _bookMapper = bookMapper; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return _bookMapper.MapToDto(book); + } + + [Authorize(MyProjectPermissions.Books.Create)] + public async Task CreateAsync(CreateBookDto input) + { + var book = new Book( + GuidGenerator.Create(), + input.Name, + input.Price, + input.AuthorId + ); + + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } +} +``` + +### 9. Add Localization +In `*.Domain.Shared/Localization/*/en.json`: + +```json +{ + "Book": "Book", + "Books": "Books", + "BookName": "Name", + "BookPrice": "Price" +} +``` + +### 10. Add Permissions (if needed) +```csharp +public static class MyProjectPermissions +{ + public static class Books + { + public const string Default = "MyProject.Books"; + public const string Create = Default + ".Create"; + } +} +``` + +### 11. Add Tests +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + var result = await _bookAppService.CreateAsync(new CreateBookDto + { + Name = "Test Book", + Price = 19.99m + }); + + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("Test Book"); + } +} +``` + +## Quick Reference Commands + +### Build Solution +```bash +dotnet build +``` + +### Run Migrations +```bash +cd src/MyProject.EntityFrameworkCore +dotnet ef migrations add MigrationName +dotnet run --project ../MyProject.DbMigrator # Apply migration + seed data +``` + +### Generate Angular Proxies +```bash +abp generate-proxy -t ng +``` + +## Checklist for New Features + +- [ ] Entity created with proper constructors +- [ ] Constants in Domain.Shared +- [ ] Custom repository interface in Domain (only if custom queries needed) +- [ ] EF Core configuration added +- [ ] Custom repository implementation (only if interface defined) +- [ ] Migration generated and applied (use DbMigrator) +- [ ] Mapperly mapper created and registered +- [ ] DTOs created in Application.Contracts +- [ ] Service interface defined +- [ ] Service implementation with authorization +- [ ] Localization keys added +- [ ] Permissions defined (if applicable) +- [ ] Tests written diff --git a/ai-rules/common/infrastructure.mdc b/ai-rules/common/infrastructure.mdc new file mode 100644 index 0000000000..1a2c555581 --- /dev/null +++ b/ai-rules/common/infrastructure.mdc @@ -0,0 +1,244 @@ +--- +description: "ABP infrastructure services - Settings, Features, Caching, Events, Background Jobs" +globs: "**/*Setting*.cs,**/*Feature*.cs,**/*Cache*.cs,**/*Event*.cs,**/*Job*.cs" +alwaysApply: false +--- + +# ABP Infrastructure Services + +> **Docs**: https://abp.io/docs/latest/framework/infrastructure + +## Settings + +### Define Settings +```csharp +public class MySettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add( + new SettingDefinition("MyApp.MaxItemCount", "10"), + new SettingDefinition("MyApp.EnableFeature", "false"), + new SettingDefinition("MyApp.SecretKey", isEncrypted: true) + ); + } +} +``` + +### Read Settings +```csharp +public class MyService : ITransientDependency +{ + private readonly ISettingProvider _settingProvider; + + public async Task DoSomethingAsync() + { + var maxCount = await _settingProvider.GetAsync("MyApp.MaxItemCount"); + var isEnabled = await _settingProvider.IsTrueAsync("MyApp.EnableFeature"); + } +} +``` + +### Setting Value Providers (Priority Order) +1. User settings (highest) +2. Tenant settings +3. Global settings +4. Configuration (appsettings.json) +5. Default value (lowest) + +## Features + +### Define Features +```csharp +public class MyFeatureDefinitionProvider : FeatureDefinitionProvider +{ + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + myGroup.AddFeature( + "MyApp.PdfReporting", + defaultValue: "false", + valueType: new ToggleStringValueType() + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)) + ); + } +} +``` + +### Check Features +```csharp +[RequiresFeature("MyApp.PdfReporting")] +public async Task GetPdfReportAsync() +{ + // Only executes if feature is enabled +} + +// Or programmatically +if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) +{ + // Feature is enabled for current tenant +} + +var maxCount = await _featureChecker.GetAsync("MyApp.MaxProductCount"); +``` + +## Distributed Caching + +### Typed Cache +```csharp +public class BookService : ITransientDependency +{ + private readonly IDistributedCache _cache; + private readonly IClock _clock; + + public BookService(IDistributedCache cache, IClock clock) + { + _cache = cache; + _clock = clock; + } + + public async Task GetAsync(Guid bookId) + { + return await _cache.GetOrAddAsync( + bookId.ToString(), + async () => await GetBookFromDatabaseAsync(bookId), + () => new DistributedCacheEntryOptions + { + AbsoluteExpiration = _clock.Now.AddHours(1) + } + ); + } +} + +[CacheName("Books")] +public class BookCacheItem +{ + public string Name { get; set; } + public decimal Price { get; set; } +} +``` + +## Event Bus + +### Local Events (Same Process) +```csharp +// Event class +public class OrderCreatedEvent +{ + public Order Order { get; set; } +} + +// Handler +public class OrderCreatedEventHandler : ILocalEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEvent eventData) + { + // Handle within same transaction + } +} + +// Publish +await _localEventBus.PublishAsync(new OrderCreatedEvent { Order = order }); +``` + +### Distributed Events (Cross-Service) +```csharp +// Event Transfer Object (in Domain.Shared) +[EventName("MyApp.Order.Created")] +public class OrderCreatedEto +{ + public Guid OrderId { get; set; } + public string OrderNumber { get; set; } +} + +// Handler +public class OrderCreatedEtoHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(OrderCreatedEto eventData) + { + // Handle distributed event + } +} + +// Publish +await _distributedEventBus.PublishAsync(new OrderCreatedEto { ... }); +``` + +### When to Use Which +- **Local**: Within same module/bounded context +- **Distributed**: Cross-module or microservice communication + +## Background Jobs + +### Define Job +```csharp +public class EmailSendingArgs +{ + public string EmailAddress { get; set; } + public string Subject { get; set; } + public string Body { get; set; } +} + +public class EmailSendingJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly IEmailSender _emailSender; + + public EmailSendingJob(IEmailSender emailSender) + { + _emailSender = emailSender; + } + + public override async Task ExecuteAsync(EmailSendingArgs args) + { + await _emailSender.SendAsync(args.EmailAddress, args.Subject, args.Body); + } +} +``` + +### Enqueue Job +```csharp +await _backgroundJobManager.EnqueueAsync( + new EmailSendingArgs + { + EmailAddress = "user@example.com", + Subject = "Hello", + Body = "..." + }, + delay: TimeSpan.FromMinutes(5) // Optional delay +); +``` + +## Localization + +### Define Resource +```csharp +[LocalizationResourceName("MyModule")] +public class MyModuleResource { } +``` + +### JSON Structure +```json +{ + "culture": "en", + "texts": { + "HelloWorld": "Hello World!", + "Menu:Books": "Books" + } +} +``` + +### Usage +- In `ApplicationService`: Use `L["Key"]` property (already available from base class) +- In other services: Inject `IStringLocalizer` + +> **Tip**: ABP base classes already provide commonly used services as properties. Check before injecting: +> - `StringLocalizer` (L), `Clock`, `CurrentUser`, `CurrentTenant`, `GuidGenerator` +> - `AuthorizationService`, `FeatureChecker`, `DataFilter` +> - `LoggerFactory`, `Logger` +> - Methods like `CheckPolicyAsync()` for authorization checks diff --git a/ai-rules/common/multi-tenancy.mdc b/ai-rules/common/multi-tenancy.mdc new file mode 100644 index 0000000000..54574a1c31 --- /dev/null +++ b/ai-rules/common/multi-tenancy.mdc @@ -0,0 +1,162 @@ +--- +description: "ABP Multi-Tenancy patterns - tenant-aware entities, data isolation, and tenant switching" +globs: "**/*Tenant*.cs,**/*MultiTenant*.cs,**/Entities/**/*.cs" +alwaysApply: false +--- + +# ABP Multi-Tenancy + +> **Docs**: https://abp.io/docs/latest/framework/architecture/multi-tenancy + +## Making Entities Multi-Tenant + +Implement `IMultiTenant` interface to make entities tenant-aware: + +```csharp +public class Product : AggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; set; } // Required by IMultiTenant + + public string Name { get; private set; } + public decimal Price { get; private set; } + + protected Product() { } + + public Product(Guid id, string name, decimal price) : base(id) + { + Name = name; + Price = price; + // TenantId is automatically set from CurrentTenant.Id + } +} +``` + +**Key points:** +- `TenantId` is **nullable** - `null` means entity belongs to Host +- ABP **automatically filters** queries by current tenant +- ABP **automatically sets** `TenantId` when creating entities + +## Accessing Current Tenant + +Use `CurrentTenant` property (available in base classes) or inject `ICurrentTenant`: + +```csharp +public class ProductAppService : ApplicationService +{ + public async Task DoSomethingAsync() + { + // Available from base class + var tenantId = CurrentTenant.Id; // Guid? - null for host + var tenantName = CurrentTenant.Name; // string? + var isAvailable = CurrentTenant.IsAvailable; // true if Id is not null + } +} + +// In other services +public class MyService : ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant; +} +``` + +## Switching Tenant Context + +Use `CurrentTenant.Change()` to temporarily switch tenant (useful in host context): + +```csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public async Task GetProductCountAsync(Guid? tenantId) + { + // Switch to specific tenant + using (CurrentTenant.Change(tenantId)) + { + return await _productRepository.GetCountAsync(); + } + // Automatically restored to previous tenant after using block + } + + public async Task DoHostOperationAsync() + { + // Switch to host context + using (CurrentTenant.Change(null)) + { + // Operations here are in host context + } + } +} +``` + +> **Important**: Always use `Change()` with a `using` statement. + +## Disabling Multi-Tenant Filter + +To query all tenants' data (only works with single database): + +```csharp +public class ProductManager : DomainService +{ + public async Task GetAllProductCountAsync() + { + // DataFilter is available from base class + using (DataFilter.Disable()) + { + return await _productRepository.GetCountAsync(); + // Returns count from ALL tenants + } + } +} +``` + +> **Note**: This doesn't work with separate databases per tenant. + +## Database Architecture Options + +| Approach | Description | Use Case | +|----------|-------------|----------| +| Single Database | All tenants share one database | Simple, cost-effective | +| Database per Tenant | Each tenant has dedicated database | Data isolation, compliance | +| Hybrid | Mix of shared and dedicated | Flexible, premium tenants | + +Connection strings are configured per tenant in Tenant Management module. + +## Best Practices + +1. **Always implement `IMultiTenant`** for tenant-specific entities +2. **Never manually filter by `TenantId`** - ABP does it automatically +3. **Don't change `TenantId` after creation** - it moves entity between tenants +4. **Use `Change()` scope carefully** - nested scopes are supported +5. **Test both host and tenant contexts** - ensure proper data isolation +6. **Consider nullable `TenantId`** - entity may be host-only or shared + +## Enabling Multi-Tenancy + +```csharp +Configure(options => +{ + options.IsEnabled = true; // Enabled by default in ABP templates +}); +``` + +Check `MultiTenancyConsts.IsEnabled` in your solution for centralized control. + +## Tenant Resolution + +ABP resolves current tenant from (in order): +1. Current user's claims +2. Query string (`?__tenant=...`) +3. Route (`/{__tenant}/...`) +4. HTTP header (`__tenant`) +5. Cookie (`__tenant`) +6. Domain/subdomain (if configured) + +For subdomain-based resolution: +```csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.mydomain.com"); +}); +``` diff --git a/ai-rules/data/ef-core.mdc b/ai-rules/data/ef-core.mdc new file mode 100644 index 0000000000..6ca6a423d2 --- /dev/null +++ b/ai-rules/data/ef-core.mdc @@ -0,0 +1,254 @@ +--- +description: "ABP Entity Framework Core patterns - DbContext, migrations, repositories" +globs: "**/*.EntityFrameworkCore/**/*.cs,**/EntityFrameworkCore/**/*.cs,**/*DbContext*.cs" +alwaysApply: false +--- + +# ABP Entity Framework Core + +> **Docs**: https://abp.io/docs/latest/framework/data/entity-framework-core + +## DbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectDbContext : AbpDbContext +{ + public DbSet Books { get; set; } + public DbSet Authors { get; set; } + + public MyProjectDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Configure all entities + builder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectDbContextModelCreatingExtensions +{ + public static void ConfigureMyProject(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema); + b.ConfigureByConvention(); // ABP conventions (audit, soft-delete, etc.) + + // Property configurations + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(BookConsts.MaxNameLength); + + b.Property(x => x.Price) + .HasColumnType("decimal(18,2)"); + + // Indexes + b.HasIndex(x => x.Name); + + // Relationships + b.HasOne() + .WithMany() + .HasForeignKey(x => x.AuthorId) + .OnDelete(DeleteBehavior.Restrict); + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var dbSet = await GetDbSetAsync(); + + return await dbSet + .IncludeDetails(includeDetails) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public override async Task> WithDetailsAsync() + { + return (await GetQueryableAsync()) + .Include(b => b.Reviews); + } +} +``` + +## Extension Method for Include +```csharp +public static class BookEfCoreQueryableExtensions +{ + public static IQueryable IncludeDetails( + this IQueryable queryable, + bool include = true) + { + if (!include) + { + return queryable; + } + + return queryable + .Include(b => b.Reviews); + } +} +``` + +## Migration Commands + +```bash +# Navigate to EF Core project +cd src/MyProject.EntityFrameworkCore + +# Add migration +dotnet ef migrations add MigrationName + +# Apply migration (choose one): +dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data +dotnet ef database update # EF Core command only + +# Remove last migration (if not applied) +dotnet ef migrations remove + +# Generate SQL script +dotnet ef migrations script +``` + +> **Note**: ABP templates include `IDesignTimeDbContextFactory` in the EF Core project, so `-s` (startup project) parameter is not needed. + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpEntityFrameworkCoreModule))] +public class MyProjectEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - it creates repositories for child entities, + // allowing them to be modified without going through the aggregate root, + // which breaks data consistency + }); + + Configure(options => + { + options.UseSqlServer(); // or UseNpgsql(), UseMySql(), etc. + }); + } +} +``` + +## Best Practices + +### Repositories for Aggregate Roots Only +Don't use `includeAllEntities: true` in `AddDefaultRepositories()`. This creates repositories for child entities, allowing direct modification without going through the aggregate root - breaking DDD data consistency rules. + +```csharp +// ✅ Correct - Only aggregate roots get repositories +options.AddDefaultRepositories(); + +// ❌ Avoid - Creates repositories for ALL entities including child entities +options.AddDefaultRepositories(includeAllEntities: true); +``` + +### Always Call ConfigureByConvention +```csharp +builder.Entity(b => +{ + b.ConfigureByConvention(); // Don't forget this! + // Other configurations... +}); +``` + +### Use Table Prefix +```csharp +public static class MyProjectConsts +{ + public const string DbTablePrefix = "App"; + public const string DbSchema = null; // Or "myschema" +} +``` + +### Performance Tips +- Add explicit indexes for frequently queried fields +- Use `AsNoTracking()` for read-only queries +- Avoid N+1 queries with `.Include()` or specifications +- ABP handles cancellation automatically; use `GetCancellationToken(cancellationToken)` only in custom repository methods +- Consider query splitting for complex queries with multiple collections + +### Accessing Raw DbContext +```csharp +public async Task CustomOperationAsync() +{ + var dbContext = await GetDbContextAsync(); + + // Raw SQL + await dbContext.Database.ExecuteSqlRawAsync( + "UPDATE Books SET IsPublished = 1 WHERE AuthorId = {0}", + authorId + ); +} +``` + +## Data Seeding + +```csharp +public class MyProjectDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book(_guidGenerator.Create(), "Sample Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` diff --git a/ai-rules/data/mongodb.mdc b/ai-rules/data/mongodb.mdc new file mode 100644 index 0000000000..671d2052c4 --- /dev/null +++ b/ai-rules/data/mongodb.mdc @@ -0,0 +1,203 @@ +--- +description: "ABP MongoDB patterns - MongoDbContext and repositories" +globs: "**/*.MongoDB/**/*.cs,**/MongoDB/**/*.cs,**/*MongoDb*.cs" +alwaysApply: false +--- + +# ABP MongoDB + +> **Docs**: https://abp.io/docs/latest/framework/data/mongodb + +## MongoDbContext Configuration + +```csharp +[ConnectionStringName("Default")] +public class MyProjectMongoDbContext : AbpMongoDbContext +{ + public IMongoCollection Books => Collection(); + public IMongoCollection Authors => Collection(); + + protected override void CreateModel(IMongoModelBuilder modelBuilder) + { + base.CreateModel(modelBuilder); + + modelBuilder.ConfigureMyProject(); + } +} +``` + +## Entity Configuration + +```csharp +public static class MyProjectMongoDbContextExtensions +{ + public static void ConfigureMyProject(this IMongoModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + b.CollectionName = MyProjectConsts.DbTablePrefix + "Books"; + }); + + builder.Entity(b => + { + b.CollectionName = MyProjectConsts.DbTablePrefix + "Authors"; + }); + } +} +``` + +## Repository Implementation + +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public BookRepository(IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync( + string name, + bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .FirstOrDefaultAsync( + b => b.Name == name, + GetCancellationToken(cancellationToken)); + } + + public async Task> GetListByAuthorAsync( + Guid authorId, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await (await GetQueryableAsync()) + .Where(b => b.AuthorId == authorId) + .ToListAsync(GetCancellationToken(cancellationToken)); + } +} +``` + +## Module Configuration + +```csharp +[DependsOn(typeof(AbpMongoDbModule))] +public class MyProjectMongoDbModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMongoDbContext(options => + { + // Add default repositories for aggregate roots only (DDD best practice) + options.AddDefaultRepositories(); + // ⚠️ Avoid includeAllEntities: true - breaks DDD data consistency + }); + } +} +``` + +## Connection String + +In `appsettings.json`: +```json +{ + "ConnectionStrings": { + "Default": "mongodb://localhost:27017/MyProjectDb" + } +} +``` + +## Key Differences from EF Core + +### No Migrations +MongoDB is schema-less; no migrations needed. Changes to entity structure are handled automatically. + +### includeDetails Parameter +Often ignored in MongoDB because documents typically embed related data: + +```csharp +public async Task> GetListAsync( + bool includeDetails = false, // Usually ignored + CancellationToken cancellationToken = default) +{ + // MongoDB documents already include nested data + return await (await GetQueryableAsync()) + .ToListAsync(GetCancellationToken(cancellationToken)); +} +``` + +### Embedded Documents vs References +```csharp +// Embedded (stored in same document) +public class Order : AggregateRoot +{ + public List Lines { get; set; } // Embedded +} + +// Reference (separate collection, store ID only) +public class Order : AggregateRoot +{ + public Guid CustomerId { get; set; } // Reference by ID +} +``` + +### No Change Tracking +MongoDB doesn't track entity changes automatically: + +```csharp +public async Task UpdateBookAsync(Guid id, string newName) +{ + var book = await _bookRepository.GetAsync(id); + book.SetName(newName); + + // Must explicitly update + await _bookRepository.UpdateAsync(book); +} +``` + +## Direct Collection Access + +```csharp +public async Task CustomOperationAsync() +{ + var collection = await GetCollectionAsync(); + + // Use MongoDB driver directly + var filter = Builders.Filter.Eq(b => b.AuthorId, authorId); + var update = Builders.Update.Set(b => b.IsPublished, true); + + await collection.UpdateManyAsync(filter, update); +} +``` + +## Indexing + +Configure indexes in repository or via MongoDB driver: + +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public override async Task> GetQueryableAsync() + { + var collection = await GetCollectionAsync(); + + // Ensure index exists + var indexKeys = Builders.IndexKeys.Ascending(b => b.Name); + await collection.Indexes.CreateOneAsync(new CreateIndexModel(indexKeys)); + + return await base.GetQueryableAsync(); + } +} +``` + +## Best Practices + +- Design documents for query patterns (denormalize when needed) +- Use references for frequently changing data +- Use embedding for data that's always accessed together +- Add indexes for frequently queried fields +- Use `GetCancellationToken(cancellationToken)` for proper cancellation +- Remember: ABP data filters (soft-delete, multi-tenancy) work with MongoDB too diff --git a/ai-rules/template-specific/app-nolayers.mdc b/ai-rules/template-specific/app-nolayers.mdc new file mode 100644 index 0000000000..4d9a00458f --- /dev/null +++ b/ai-rules/template-specific/app-nolayers.mdc @@ -0,0 +1,79 @@ +--- +description: "ABP Single-Layer (No-Layers) application template specific patterns" +globs: "**/src/*/*Module.cs,**/src/*/Entities/**/*.cs,**/src/*/Services/**/*.cs,**/src/*/Data/**/*.cs" +alwaysApply: false +--- + +# ABP Single-Layer Application Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/single-layer-web-application + +## Solution Structure + +Single project containing everything: + +``` +MyProject/ +├── src/ +│ └── MyProject/ +│ ├── Data/ # DbContext, migrations +│ ├── Entities/ # Domain entities +│ ├── Services/ # Application services + DTOs +│ ├── Pages/ # Razor pages / Blazor components +│ └── MyProjectModule.cs +└── test/ + └── MyProject.Tests/ +``` + +## Key Differences from Layered + +| Layered Template | Single-Layer Template | +|------------------|----------------------| +| DTOs in Application.Contracts | DTOs in Services folder (same project) | +| Repository interfaces in Domain | Use generic `IRepository` directly | +| Separate Domain.Shared for constants | Constants in same project | +| Multiple module classes | Single module class | + +## File Organization + +Group related files by feature: + +``` +Services/ +├── Books/ +│ ├── BookAppService.cs +│ ├── BookDto.cs +│ ├── CreateBookDto.cs +│ └── IBookAppService.cs +└── Authors/ + ├── AuthorAppService.cs + └── ... +``` + +## Simplified Entity (Still keep invariants) + +Single-layer templates are structurally simpler, but you may still have real business invariants. + +- For **trivial CRUD** entities, public setters can be acceptable. +- For **non-trivial business rules**, still prefer encapsulation (private setters + methods) to prevent invalid states. + +```csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } // OK for trivial CRUD only + public decimal Price { get; set; } +} +``` + +## No Custom Repository Needed + +Use generic repository directly - no need to define custom interfaces: + +```csharp +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; + + // Generic repository is sufficient for single-layer apps +} +``` diff --git a/ai-rules/template-specific/microservice.mdc b/ai-rules/template-specific/microservice.mdc new file mode 100644 index 0000000000..749dfca572 --- /dev/null +++ b/ai-rules/template-specific/microservice.mdc @@ -0,0 +1,209 @@ +--- +description: "ABP Microservice solution template specific patterns" +alwaysApply: false +--- + +# ABP Microservice Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/microservice + +## Solution Structure + +``` +MyMicroservice/ +├── apps/ # UI applications +│ ├── web/ # Web application +│ ├── public-web/ # Public website +│ └── auth-server/ # Authentication server (OpenIddict) +├── gateways/ # BFF pattern - one gateway per UI +│ └── web-gateway/ # YARP reverse proxy +├── services/ # Microservices +│ ├── administration/ # Permissions, settings, features +│ ├── identity/ # Users, roles +│ └── [your-services]/ # Your business services +└── etc/ + ├── docker/ # Docker compose for local infra + └── helm/ # Kubernetes deployment +``` + +## Microservice Structure (NOT Layered!) + +Each microservice has simplified structure - everything in one project: + +``` +services/ordering/ +├── OrderingService/ # Main project +│ ├── Entities/ +│ ├── Services/ +│ ├── IntegrationServices/ # For inter-service communication +│ ├── Data/ # DbContext (implements IHasEventInbox, IHasEventOutbox) +│ └── OrderingServiceModule.cs +├── OrderingService.Contracts/ # Interfaces, DTOs, ETOs (shared) +└── OrderingService.Tests/ +``` + +## Inter-Service Communication + +### 1. Integration Services (Synchronous HTTP) + +For synchronous calls, use **Integration Services** - NOT regular application services. + +#### Step 1: Provider Service - Create Integration Service + +```csharp +// In CatalogService.Contracts project +[IntegrationService] +public interface IProductIntegrationService : IApplicationService +{ + Task> GetProductsByIdsAsync(List ids); +} + +// In CatalogService project +[IntegrationService] +public class ProductIntegrationService : ApplicationService, IProductIntegrationService +{ + public async Task> GetProductsByIdsAsync(List ids) + { + var products = await _productRepository.GetListAsync(p => ids.Contains(p.Id)); + return ObjectMapper.Map, List>(products); + } +} +``` + +#### Step 2: Provider Service - Expose Integration Services + +```csharp +// In CatalogServiceModule.cs +Configure(options => +{ + options.ExposeIntegrationServices = true; +}); +``` + +#### Step 3: Consumer Service - Add Package Reference + +Add reference to provider's Contracts project (via ABP Studio or manually): +- Right-click OrderingService → Add Package Reference → Select `CatalogService.Contracts` + +#### Step 4: Consumer Service - Generate Proxies + +```bash +# Run ABP CLI in consumer service folder +abp generate-proxy -t csharp -u http://localhost:44361 -m catalog --without-contracts +``` + +Or use ABP Studio: Right-click service → ABP CLI → Generate Proxy → C# + +#### Step 5: Consumer Service - Register HTTP Client Proxies + +```csharp +// In OrderingServiceModule.cs +[DependsOn(typeof(CatalogServiceContractsModule))] // Add module dependency +public class OrderingServiceModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Register static HTTP client proxies + context.Services.AddStaticHttpClientProxies( + typeof(CatalogServiceContractsModule).Assembly, + "CatalogService"); + } +} +``` + +#### Step 6: Consumer Service - Configure Remote Service URL + +```json +// appsettings.json +"RemoteServices": { + "CatalogService": { + "BaseUrl": "http://localhost:44361" + } +} +``` + +#### Step 7: Use Integration Service + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IProductIntegrationService _productIntegrationService; + + public async Task> GetListAsync() + { + var orders = await _orderRepository.GetListAsync(); + var productIds = orders.Select(o => o.ProductId).Distinct().ToList(); + + // Call remote service via generated proxy + var products = await _productIntegrationService.GetProductsByIdsAsync(productIds); + // ... + } +} +``` + +> **Why Integration Services?** Application services are for UI - they have different authorization, validation, and optimization needs. Integration services are designed specifically for inter-service communication. + +**When to use:** Need immediate response, data required to complete current operation (e.g., get product details to display in order list). + +### 2. Distributed Events (Asynchronous) + +Use RabbitMQ-based events for loose coupling. + +**When to use:** +- Notifying other services about state changes (e.g., "order placed", "stock updated") +- Operations that don't need immediate response +- When services should remain independent and decoupled + +```csharp +// Define ETO in Contracts project +[EventName("Product.StockChanged")] +public class StockCountChangedEto +{ + public Guid ProductId { get; set; } + public int NewCount { get; set; } +} + +// Publish +await _distributedEventBus.PublishAsync(new StockCountChangedEto { ... }); + +// Subscribe in another service +public class StockChangedHandler : IDistributedEventHandler, ITransientDependency +{ + public async Task HandleEventAsync(StockCountChangedEto eventData) { ... } +} +``` + +DbContext must implement `IHasEventInbox`, `IHasEventOutbox` for Outbox/Inbox pattern. + +## Performance: Entity Cache + +For frequently accessed data from other services, use Entity Cache: + +```csharp +// Register +context.Services.AddEntityCache(); + +// Use - auto-invalidates on entity changes +private readonly IEntityCache _productCache; + +public async Task GetProductAsync(Guid id) +{ + return await _productCache.GetAsync(id); +} +``` + +## Pre-Configured Infrastructure + +- **RabbitMQ** - Distributed events with Outbox/Inbox +- **Redis** - Distributed cache and locking +- **YARP** - API Gateway +- **OpenIddict** - Auth server + +## Best Practices + +- **Choose communication wisely** - Synchronous for queries needing immediate data, asynchronous for notifications and state changes +- **Use Integration Services** - Not application services for inter-service calls +- **Cache remote data** - Use Entity Cache or IDistributedCache for frequently accessed data +- **Share only Contracts** - Never share implementations +- **Idempotent handlers** - Events may be delivered multiple times +- **Database per service** - Each service owns its database diff --git a/ai-rules/template-specific/module.mdc b/ai-rules/template-specific/module.mdc new file mode 100644 index 0000000000..c60f54239e --- /dev/null +++ b/ai-rules/template-specific/module.mdc @@ -0,0 +1,234 @@ +--- +description: "ABP Module solution template specific patterns" +alwaysApply: false +--- + +# ABP Module Solution Template + +> **Docs**: https://abp.io/docs/latest/solution-templates/application-module + +This template is for developing reusable ABP modules. Key requirement: **extensibility** - consumers must be able to override and customize module behavior. + +## Solution Structure + +``` +MyModule/ +├── src/ +│ ├── MyModule.Domain.Shared/ # Constants, enums, localization +│ ├── MyModule.Domain/ # Entities, repository interfaces, domain services +│ ├── MyModule.Application.Contracts/ # DTOs, service interfaces +│ ├── MyModule.Application/ # Service implementations +│ ├── MyModule.EntityFrameworkCore/ # EF Core implementation +│ ├── MyModule.MongoDB/ # MongoDB implementation +│ ├── MyModule.HttpApi/ # REST controllers +│ ├── MyModule.HttpApi.Client/ # Client proxies +│ ├── MyModule.Web/ # MVC/Razor Pages UI +│ └── MyModule.Blazor/ # Blazor UI +├── test/ +│ └── MyModule.Tests/ +└── host/ + └── MyModule.HttpApi.Host/ # Test host application +``` + +## Database Independence + +Support both EF Core and MongoDB: + +### Repository Interface (Domain) +```csharp +public interface IBookRepository : IRepository +{ + Task FindByNameAsync(string name); + Task> GetListByAuthorAsync(Guid authorId); +} +``` + +### EF Core Implementation +```csharp +public class BookRepository : EfCoreRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +### MongoDB Implementation +```csharp +public class BookRepository : MongoDbRepository, IBookRepository +{ + public async Task FindByNameAsync(string name) + { + var queryable = await GetQueryableAsync(); + return await queryable.FirstOrDefaultAsync(b => b.Name == name); + } +} +``` + +## Table/Collection Prefix + +Allow customization to avoid naming conflicts: + +```csharp +// Domain.Shared +public static class MyModuleDbProperties +{ + public static string DbTablePrefix { get; set; } = "MyModule"; + public static string DbSchema { get; set; } = null; + + public const string ConnectionStringName = "MyModule"; +} +``` + +Usage: +```csharp +builder.Entity(b => +{ + b.ToTable(MyModuleDbProperties.DbTablePrefix + "Books", MyModuleDbProperties.DbSchema); +}); +``` + +## Module Options + +Provide configuration options: + +```csharp +// Domain +public class MyModuleOptions +{ + public bool EnableFeatureX { get; set; } = true; + public int MaxItemCount { get; set; } = 100; +} +``` + +Usage in module: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.EnableFeatureX = true; + }); +} +``` + +Usage in service: +```csharp +public class MyService : ITransientDependency +{ + private readonly MyModuleOptions _options; + + public MyService(IOptions options) + { + _options = options.Value; + } +} +``` + +## Extensibility Points + +### Virtual Methods (Critical for Modules!) +When developing a reusable module, **all public and protected methods must be virtual** to allow consumers to override behavior: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + // ✅ Public methods MUST be virtual + public virtual async Task CreateAsync(CreateBookDto input) + { + var book = await CreateBookEntityAsync(input); + await _bookRepository.InsertAsync(book); + return _bookMapper.MapToDto(book); + } + + // ✅ Use protected virtual for helper methods (not private) + protected virtual Task CreateBookEntityAsync(CreateBookDto input) + { + return Task.FromResult(new Book( + GuidGenerator.Create(), + input.Name, + input.Price + )); + } + + // ❌ WRONG for modules - private methods cannot be overridden + // private Book CreateBook(CreateBookDto input) { ... } +} +``` + +This allows module consumers to: +- Override specific methods without copying entire class +- Extend functionality while preserving base behavior +- Customize module behavior for their needs + +### Entity Extension +Support object extension system: +```csharp +public class MyModuleModuleExtensionConfigurator +{ + public static void Configure() + { + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance.Modules() + .ConfigureMyModule(module => + { + module.ConfigureBook(book => + { + book.AddOrUpdateProperty("CustomProperty"); + }); + }); + }); + } +} +``` + +## Localization + +```csharp +// Domain.Shared +[LocalizationResourceName("MyModule")] +public class MyModuleResource +{ +} + +// Module configuration +Configure(options => +{ + options.Resources + .Add("en") + .AddVirtualJson("/Localization/MyModule"); +}); +``` + +## Permission Definition + +```csharp +public class MyModulePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var myGroup = context.AddGroup( + MyModulePermissions.GroupName, + L("Permission:MyModule")); + + myGroup.AddPermission( + MyModulePermissions.Books.Default, + L("Permission:Books")); + } +} +``` + +## Best Practices + +1. **Virtual methods** - All public/protected methods must be `virtual` for extensibility +2. **Protected virtual helpers** - Use `protected virtual` instead of `private` for helper methods +3. **Database agnostic** - Support both EF Core and MongoDB +4. **Configurable** - Use options pattern for customization +5. **Localizable** - Use localization for all user-facing text +6. **Table prefix** - Allow customization to avoid conflicts +7. **Separate connection string** - Support dedicated database +8. **No dependencies on host** - Module should be self-contained +9. **Test with host app** - Include a host application for testing diff --git a/ai-rules/testing/patterns.mdc b/ai-rules/testing/patterns.mdc new file mode 100644 index 0000000000..07c9307448 --- /dev/null +++ b/ai-rules/testing/patterns.mdc @@ -0,0 +1,270 @@ +--- +description: "ABP testing patterns - unit tests and integration tests" +globs: "test/**/*.cs,tests/**/*.cs,**/*Tests*/**/*.cs,**/*Test*.cs" +alwaysApply: false +--- + +# ABP Testing Patterns + +> **Docs**: https://abp.io/docs/latest/testing + +## Test Project Structure + +| Project | Purpose | Base Class | +|---------|---------|------------| +| `*.Domain.Tests` | Domain logic, entities, domain services | `*DomainTestBase` | +| `*.Application.Tests` | Application services | `*ApplicationTestBase` | +| `*.EntityFrameworkCore.Tests` | Repository implementations | `*EntityFrameworkCoreTestBase` | + +## Integration Test Approach + +ABP recommends integration tests over unit tests: +- Tests run with real services and database (SQLite in-memory) +- No mocking of internal services +- Each test gets a fresh database instance + +## Application Service Test + +```csharp +public class BookAppService_Tests : MyProjectApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + // Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + // Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test Book"); + } + + [Fact] + public async Task Should_Create_Book() + { + // Arrange + var input = new CreateBookDto + { + Name = "New Book", + Price = 19.99m + }; + + // Act + var result = await _bookAppService.CreateAsync(input); + + // Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New Book"); + result.Price.ShouldBe(19.99m); + } + + [Fact] + public async Task Should_Not_Create_Book_With_Invalid_Name() + { + // Arrange + var input = new CreateBookDto + { + Name = "", // Invalid + Price = 10m + }; + + // Act & Assert + await Should.ThrowAsync(async () => + { + await _bookAppService.CreateAsync(input); + }); + } +} +``` + +## Domain Service Test + +```csharp +public class BookManager_Tests : MyProjectDomainTestBase +{ + private readonly BookManager _bookManager; + private readonly IBookRepository _bookRepository; + + public BookManager_Tests() + { + _bookManager = GetRequiredService(); + _bookRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_Book() + { + // Act + var book = await _bookManager.CreateAsync("Test Book", 29.99m); + + // Assert + book.ShouldNotBeNull(); + book.Name.ShouldBe("Test Book"); + book.Price.ShouldBe(29.99m); + } + + [Fact] + public async Task Should_Not_Allow_Duplicate_Book_Name() + { + // Arrange + await _bookManager.CreateAsync("Existing Book", 10m); + + // Act & Assert + var exception = await Should.ThrowAsync(async () => + { + await _bookManager.CreateAsync("Existing Book", 20m); + }); + + exception.Code.ShouldBe("MyProject:BookNameAlreadyExists"); + } +} +``` + +## Test Naming Convention + +Use descriptive names: +```csharp +// Pattern: Should_ExpectedBehavior_When_Condition +public async Task Should_Create_Book_When_Input_Is_Valid() +public async Task Should_Throw_BusinessException_When_Name_Already_Exists() +public async Task Should_Return_Empty_List_When_No_Books_Exist() +``` + +## Arrange-Act-Assert (AAA) + +```csharp +[Fact] +public async Task Should_Update_Book_Price() +{ + // Arrange + var bookId = await CreateTestBookAsync(); + var newPrice = 39.99m; + + // Act + var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto + { + Price = newPrice + }); + + // Assert + result.Price.ShouldBe(newPrice); +} +``` + +## Assertions with Shouldly + +ABP uses Shouldly library: +```csharp +result.ShouldNotBeNull(); +result.Name.ShouldBe("Expected Name"); +result.Price.ShouldBeGreaterThan(0); +result.Items.ShouldContain(x => x.Id == expectedId); +result.Items.ShouldBeEmpty(); +result.Items.Count.ShouldBe(5); + +// Exception assertions +await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); + +var ex = await Should.ThrowAsync(async () => +{ + await _service.DoSomethingAsync(); +}); +ex.Code.ShouldBe("MyProject:ErrorCode"); +``` + +## Test Data Seeding + +```csharp +public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + public static readonly Guid TestBookId = Guid.Parse("..."); + + private readonly IBookRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(TestBookId, "Test Book", 19.99m, Guid.Empty), + autoSave: true + ); + } +} +``` + +## Disabling Authorization in Tests + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAlwaysAllowAuthorization(); +} +``` + +## Mocking External Services + +Use NSubstitute when needed: +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var emailSender = Substitute.For(); + emailSender.SendAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + context.Services.AddSingleton(emailSender); +} +``` + +## Testing with Specific User + +```csharp +[Fact] +public async Task Should_Get_Current_User_Books() +{ + // Login as specific user + await WithUnitOfWorkAsync(async () => + { + using (CurrentUser.Change(TestData.UserId)) + { + var result = await _bookAppService.GetMyBooksAsync(); + result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId); + } + }); +} +``` + +## Testing Multi-Tenancy + +```csharp +[Fact] +public async Task Should_Filter_Books_By_Tenant() +{ + using (CurrentTenant.Change(TestData.TenantId)) + { + var result = await _bookAppService.GetListAsync(new GetBookListDto()); + // Results should be filtered by tenant + } +} +``` + +## Best Practices + +- Each test should be independent +- Don't share state between tests +- Use meaningful test data +- Test edge cases and error conditions +- Keep tests focused on single behavior +- Use test data seeders for common data +- Avoid testing framework internals diff --git a/ai-rules/ui/angular.mdc b/ai-rules/ui/angular.mdc new file mode 100644 index 0000000000..e61881fb28 --- /dev/null +++ b/ai-rules/ui/angular.mdc @@ -0,0 +1,221 @@ +--- +description: "ABP Angular UI patterns and best practices" +globs: "**/angular/**/*.ts,**/angular/**/*.html,**/*.component.ts" +alwaysApply: false +--- + +# ABP Angular UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/angular/overview + +## Project Structure +``` +src/app/ +├── proxy/ # Auto-generated service proxies +├── shared/ # Shared components, pipes, directives +├── book/ # Feature module +│ ├── book.module.ts +│ ├── book-routing.module.ts +│ ├── book-list/ +│ │ ├── book-list.component.ts +│ │ ├── book-list.component.html +│ │ └── book-list.component.scss +│ └── book-detail/ +``` + +## Generate Service Proxies +```bash +abp generate-proxy -t ng +``` + +This generates typed service classes in `src/app/proxy/`. + +## List Component Pattern +```typescript +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html' +}) +export class BookListComponent implements OnInit { + books = { items: [], totalCount: 0 } as PagedResultDto; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private confirmation: ConfirmationService + ) {} + + ngOnInit(): void { + this.hookToQuery(); + } + + private hookToQuery(): void { + this.list.hookToQuery(query => + this.bookService.getList(query) + ).subscribe(response => { + this.books = response; + }); + } + + create(): void { + // Open create modal + } + + delete(book: BookDto): void { + this.confirmation + .warn('::AreYouSureToDelete', '::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.bookService.delete(book.id).subscribe(() => this.list.get()); + } + }); + } +} +``` + +## Localization +```typescript +// In component +constructor(private localizationService: LocalizationService) {} + +getText(): string { + return this.localizationService.instant('::Books'); +} +``` + +```html + +

{{ '::Books' | abpLocalization }}

+ + +

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

+``` + +## Authorization + +### Permission Directive +```html + +``` + +### Permission Guard +```typescript +const routes: Routes = [ + { + path: '', + component: BookListComponent, + canActivate: [PermissionGuard], + data: { + requiredPolicy: 'BookStore.Books' + } + } +]; +``` + +### Programmatic Check +```typescript +constructor(private permissionService: PermissionService) {} + +canCreate(): boolean { + return this.permissionService.getGrantedPolicy('BookStore.Books.Create'); +} +``` + +## Forms with Validation +```typescript +@Component({...}) +export class BookFormComponent { + form: FormGroup; + + constructor(private fb: FormBuilder) { + this.buildForm(); + } + + buildForm(): void { + this.form = this.fb.group({ + name: ['', [Validators.required, Validators.maxLength(128)]], + price: [0, [Validators.required, Validators.min(0)]] + }); + } + + save(): void { + if (this.form.invalid) return; + + this.bookService.create(this.form.value).subscribe(() => { + // Handle success + }); + } +} +``` + +```html +
+
+ + +
+ + +
+``` + +## Configuration API +```typescript +constructor(private configService: ConfigStateService) {} + +getCurrentUser(): CurrentUserDto { + return this.configService.getOne('currentUser'); +} + +getSettings(): void { + const setting = this.configService.getSetting('MyApp.MaxItemCount'); +} +``` + +## Modal Service +```typescript +constructor(private modalService: ModalService) {} + +openCreateModal(): void { + const modalRef = this.modalService.open(BookFormComponent, { + size: 'lg' + }); + + modalRef.result.then(result => { + if (result) { + this.list.get(); + } + }); +} +``` + +## Toast Notifications +```typescript +constructor(private toaster: ToasterService) {} + +showSuccess(): void { + this.toaster.success('::BookCreatedSuccessfully', '::Success'); +} + +showError(error: string): void { + this.toaster.error(error, '::Error'); +} +``` + +## Lazy Loading Modules +```typescript +// app-routing.module.ts +const routes: Routes = [ + { + path: 'books', + loadChildren: () => import('./book/book.module').then(m => m.BookModule) + } +]; +``` + +## Theme & Styling +- Use Bootstrap classes +- ABP provides theme variables via CSS custom properties +- Component-specific styles in `.component.scss` diff --git a/ai-rules/ui/blazor.mdc b/ai-rules/ui/blazor.mdc new file mode 100644 index 0000000000..68b8051109 --- /dev/null +++ b/ai-rules/ui/blazor.mdc @@ -0,0 +1,207 @@ +--- +description: "ABP Blazor UI patterns and components" +globs: "**/*.razor,**/Blazor/**/*.cs,**/*.Blazor*/**/*.cs" +alwaysApply: false +--- + +# ABP Blazor UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/blazor/overall + +## Component Base Classes + +### Basic Component +```razor +@inherits AbpComponentBase + +

@L["Books"]

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

@L["Books"]

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

@L["PageTitle"]

+ +@* With parameters *@ +

@L["WelcomeMessage", CurrentUser.UserName]

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

You have access!

+
+
+``` + +## Navigation & Menu +Configure in `*MenuContributor.cs`: + +```csharp +public class MyMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + var bookMenu = new ApplicationMenuItem( + "Books", + l["Menu:Books"], + "/books", + icon: "fa fa-book" + ); + + if (await context.IsGrantedAsync(MyPermissions.Books.Default)) + { + context.Menu.AddItem(bookMenu); + } + } + } +} +``` + +## Notifications & Messages +```csharp +// Success message +await Message.Success(L["BookCreatedSuccessfully"]); + +// Confirmation dialog +if (await Message.Confirm(L["AreYouSure"])) +{ + // User confirmed +} + +// Toast notification +await Notify.Success(L["OperationCompleted"]); +``` + +## Forms & Validation +```razor +
+ + + + @L["Name"] + + + + + + + + +
+``` + +## JavaScript Interop +```csharp +@inject IJSRuntime JsRuntime + +@code { + private async Task CallJavaScript() + { + await JsRuntime.InvokeVoidAsync("myFunction", arg1, arg2); + var result = await JsRuntime.InvokeAsync("myFunctionWithReturn"); + } +} +``` + +## State Management +```csharp +// Inject service proxy from HttpApi.Client +@inject IBookAppService BookAppService + +@code { + private List Books { get; set; } + + protected override async Task OnInitializedAsync() + { + var result = await BookAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + Books = result.Items.ToList(); + } +} +``` + +## Code-Behind Pattern +**Books.razor:** +```razor +@page "/books" +@inherits BooksBase +``` + +**Books.razor.cs:** +```csharp +public partial class Books : BooksBase +{ + // Component logic here +} +``` + +**BooksBase.cs:** +```csharp +public abstract class BooksBase : AbpComponentBase +{ + [Inject] + protected IBookAppService BookAppService { get; set; } +} +``` diff --git a/ai-rules/ui/mvc.mdc b/ai-rules/ui/mvc.mdc new file mode 100644 index 0000000000..1bc423010d --- /dev/null +++ b/ai-rules/ui/mvc.mdc @@ -0,0 +1,258 @@ +--- +description: "ABP MVC and Razor Pages UI patterns" +globs: "**/*.cshtml,**/Pages/**/*.cs,**/Views/**/*.cs,**/Controllers/**/*.cs" +alwaysApply: false +--- + +# ABP MVC / Razor Pages UI + +> **Docs**: https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall + +## Razor Page Model +```csharp +public class IndexModel : AbpPageModel +{ + private readonly IBookAppService _bookAppService; + + public List Books { get; set; } + + public IndexModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + Books = result.Items.ToList(); + } +} +``` + +## Razor Page View +```html +@page +@model IndexModel + + + + + +

@L["Books"]

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

@L["Books"]

+ +@* With parameters *@ +

@L["WelcomeMessage", Model.UserName]

+``` + +## JavaScript API +```javascript +// Localization +var text = abp.localization.getResource('BookStore')('Books'); + +// Authorization +if (abp.auth.isGranted('BookStore.Books.Create')) { + // Show create button +} + +// Settings +var maxCount = abp.setting.get('BookStore.MaxItemCount'); + +// Ajax with automatic error handling +abp.ajax({ + url: '/api/app/book', + type: 'POST', + data: JSON.stringify(bookData) +}).then(function(result) { + // Success +}); + +// Notifications +abp.notify.success('Book created successfully!'); +abp.notify.error('An error occurred!'); + +// Confirmation +abp.message.confirm('Are you sure?').then(function(confirmed) { + if (confirmed) { + // User confirmed + } +}); +``` + +## DataTables Integration +```javascript +var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + ajax: abp.libs.datatables.createAjax(bookService.getList), + columnDefs: [ + { + title: l('Name'), + data: 'name' + }, + { + title: l('Price'), + data: 'price', + render: function(data) { + return data.toFixed(2); + } + }, + { + title: l('Actions'), + rowAction: { + items: [ + { + text: l('Edit'), + visible: abp.auth.isGranted('BookStore.Books.Edit'), + action: function(data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + visible: abp.auth.isGranted('BookStore.Books.Delete'), + confirmMessage: function(data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function(data) { + bookService.delete(data.record.id).then(function() { + abp.notify.success(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + } + ] + }) +); +``` + +## Modal Pages +**CreateModal.cshtml:** +```html +@page +@model CreateModalModel + + + + + + + + + + +``` + +**CreateModal.cshtml.cs:** +```csharp +public class CreateModalModel : AbpPageModel +{ + [BindProperty] + public CreateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } +} +``` + +## Bundle & Minification +```csharp +Configure(options => +{ + options.StyleBundles.Configure( + StandardBundles.Styles.Global, + bundle => bundle.AddFiles("/styles/my-styles.css") + ); +}); +``` diff --git a/common.props b/common.props index 42e230791c..9df5767555 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 10.1.0-preview - 5.1.0-preview + 10.1.0-rc.2 + 5.1.0-rc.2 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 1cce28fa3f..0fc452a7b2 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -347,6 +347,10 @@ { "text": "Working with ABP Suite", "path": "studio/working-with-suite.md" + }, + { + "text": "Custom Commands", + "path": "studio/custom-commands.md" } ] }, @@ -1284,7 +1288,7 @@ }, { "text": "LeptonX Lite", - "path": "ui-themes/lepton-x-lite/mvc.md" + "path": "ui-themes/lepton-x-lite/asp-net-core.md" }, { "text": "LeptonX", diff --git a/docs/en/framework/architecture/microservices/index.md b/docs/en/framework/architecture/microservices/index.md index c014190437..9d8643eedf 100644 --- a/docs/en/framework/architecture/microservices/index.md +++ b/docs/en/framework/architecture/microservices/index.md @@ -7,6 +7,16 @@ # Microservice Architecture +````json +//[doc-nav] +{ + "Next": { + "Name": "Microservice Solution Template", + "Path": "solution-templates/microservice/index" + } +} +```` + *"Microservices are a software development technique—a variant of the **service-oriented architecture** (SOA) architectural style that structures an application as a collection of **loosely coupled services**. In a microservices architecture, services are **fine-grained** and the protocols are **lightweight**. The benefit of decomposing an application into different smaller services is that it improves **modularity**. This makes the application easier to understand, develop, test, and become more resilient to architecture erosion. It **parallelizes development** by enabling small autonomous teams to **develop, deploy and scale** their respective services independently. It also allows the architecture of an individual service to emerge through **continuous refactoring**. Microservices-based architectures enable **continuous delivery and deployment**."* — [Wikipedia](https://en.wikipedia.org/wiki/Microservices) @@ -24,11 +34,49 @@ One of the major goals of the ABP is to provide a convenient infrastructure to c * Provides a [distributed event bus](../../infrastructure/event-bus) to communicate your services. * Provides many other services to make your daily development easier. +## ABP Studio for Microservice Development + +[ABP Studio](../../../studio/overview.md) is a comprehensive desktop application that significantly simplifies microservice solution development and management. It provides powerful tools specifically designed for distributed systems: + +### Solution Runner + +The [Solution Runner](../../../studio/running-applications.md) allows you to run all your microservices with a single click. You can create different profiles to organize services based on your team's needs. For example, `team-1` might only need to run the *Administration* and *Identity* services, while `team-2` works with *SaaS* and *Audit Logging* services. This approach saves resources and speeds up development by allowing each team to run only the services they need. + +### Kubernetes Integration + +The [Kubernetes Integration](../../../studio/kubernetes.md) panel enables you to deploy your microservices to a Kubernetes cluster and manage them directly from ABP Studio. Key features include: + +* **Deploy to Kubernetes**: Build Docker images and install Helm charts with a few clicks. +* **Intercept Services**: Debug and develop specific services locally while the rest of the system runs in Kubernetes. This eliminates the need to run all microservices on your local machine. +* **Redeploy Charts**: Quickly redeploy individual services after making changes. +* **Connect to Cluster Resources**: Access databases, message queues, and other infrastructure services running in the cluster. + +### Application Monitoring + +The [Application Monitoring](../../../studio/monitoring-applications.md) area provides a centralized view of all your running microservices: + +* **HTTP Requests**: View all HTTP requests across services with detailed information including headers, payloads, and response times. +* **Distributed Events**: Monitor all distributed events sent and received by your services, making it easy to debug inter-service communication. +* **Exceptions**: Track exceptions thrown by any service in real-time. +* **Logs**: Access logs from all services in a single place with filtering capabilities. +* **Built-in Browser**: Browse and test your APIs without leaving ABP Studio. + +### Creating New Microservices + +ABP Studio's [Solution Explorer](../../../studio/solution-explorer.md) makes it easy to [add new microservices to your solution](../../../solution-templates/microservice/adding-new-microservices). Right-click on the `services` folder and select *Add* -> *New Module* -> *Microservice*. ABP Studio will: + +* Create the microservice with proper project structure. +* Configure database connections and migrations. +* Set up authentication and authorization. +* Integrate with API gateways. +* Configure distributed event bus connections. +* Add the service to Kubernetes Helm charts. + ## Microservice for New Applications -One common advise to start a new solution is **always to start with a monolith**, keep it modular and split into microservices once the monolith becomes a problem. This makes your progress fast in the beginning especially if your team is small and you don't want to deal with challenges of the microservice architecture. +One common advice to start a new solution is **always to start with a monolith**, keep it modular and split into microservices once the monolith becomes a problem. This makes your progress fast in the beginning especially if your team is small and you don't want to deal with challenges of the microservice architecture. -However, developing such a well-modular application can be a problem since it is **hard to keep modules isolated** from each other as you would do it for microservices (see [Stefan Tilkov's article](https://martinfowler.com/articles/dont-start-monolith.html) about that). Microservice architecture naturally forces you to develop well isolated services, but in a modular monolithic application it's easy to tight couple modules to each other and design **weak module boundaries** and API contracts. +However, developing such a well-modular application can be a problem since it is **hard to keep modules isolated** from each other as you would do it for microservices (see [Stefan Tilkov's article](https://martinfowler.com/articles/dont-start-monolith.html) about that). Microservice architecture naturally forces you to develop well isolated services, but in a modular monolithic application it's easy to tightly couple modules to each other and design **weak module boundaries** and API contracts. ABP can help you in that point by offering a **microservice-compatible, strict module architecture** where your module is split into multiple layers/projects and developed in its own VS solution completely isolated and independent from other modules. Such a developed module is a natural microservice yet it can be easily plugged-in a monolithic application. See the [module development best practice guide](../best-practices) that offers a **microservice-first module design**. All [standard ABP modules](https://github.com/abpframework/abp/tree/master/modules) are developed based on this guide. So, you can use these modules by embedding into your monolithic solution or deploy them separately and use via remote APIs. They can share a single database or can have their own database based on your simple configuration. @@ -37,3 +85,20 @@ ABP can help you in that point by offering a **microservice-compatible, strict m ABP provides a pre-architected and production-ready microservice solution template that includes multiple services, API gateways and applications well integrated with each other. This template helps you quickly start building distributed systems with common microservice patterns. See the [Microservice Solution Template](../../../solution-templates/microservice/index.md) documentation for details. + +## Tutorials + +For a hands-on experience, follow the [Microservice Development Tutorial](../../../tutorials/microservice/index.md) that guides you through: + +* Creating the initial microservice solution +* Adding new microservices (Catalog and Ordering services) +* Building CRUD functionality +* Implementing HTTP API calls between services +* Using distributed events for asynchronous communication + +## See Also + +* [Get Started: Microservice Solution](../../../get-started/microservice.md) +* [Microservice Solution Template](../../../solution-templates/microservice/index.md) +* [Microservice Development Tutorial](../../../tutorials/microservice/index.md) +* [ABP Studio Overview](../../../studio/overview.md) diff --git a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md index b44572838a..6c13f5d61d 100644 --- a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md +++ b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md @@ -1,3 +1,10 @@ +```json +//[doc-seo] +{ + "Description": "Explore how to integrate AI services into your ABP Framework applications using the Microsoft.Extensions.AI library for seamless functionality." +} +``` + # Microsoft.Extensions.AI [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai) is a library that provides a unified API for integrating AI services. It is a part of the Microsoft AI Extensions Library. It is used to integrate AI services into your application. This documentation is about the usage of this library with ABP Framework. Make sure you have read the [Artificial Intelligence](./index.md) documentation before reading this documentation. @@ -64,7 +71,7 @@ public class CommentSummarization > [!NOTE] > If you don't specify the workspace name, the full name of the class will be used as the workspace name. -You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, you will get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client. +You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, the default workspace's chat client is returned. Only if both the workspace-specific and default chat clients are not configured will you get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client. `IChatClient` or `IChatClientAccessor` can be resolved to access a specific workspace's chat client. This is a typed chat client and can be configured separately from the default chat client. @@ -92,7 +99,7 @@ Example of resolving a typed chat client accessor: public class MyService { private readonly IChatClientAccessor _chatClientAccessor; -} + public async Task GetResponseAsync(string prompt) { var chatClient = _chatClientAccessor.ChatClient; @@ -174,4 +181,4 @@ public class MyProjectModule : AbpModule - [Usage of Agent Framework](./microsoft-agent-framework.md) - [Usage of Semantic Kernel](./microsoft-semantic-kernel.md) -- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/) \ No newline at end of file +- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/) diff --git a/docs/en/framework/ui/angular/checkbox-component.md b/docs/en/framework/ui/angular/checkbox-component.md index 2e5cc44ad3..fa3824027d 100644 --- a/docs/en/framework/ui/angular/checkbox-component.md +++ b/docs/en/framework/ui/angular/checkbox-component.md @@ -25,26 +25,21 @@ The ABP Checkbox Component is a reusable form input component for the checkbox t # Usage -The ABP Checkbox component is a part of the `ThemeSharedModule` module. If you've imported that module into your module, there's no need to import it again. If not, then first import it as shown below: +The ABP Checkbox component (`AbpCheckboxComponent`) is a standalone component. You can import it directly in your component: ```ts -// my-feature.module.ts - -import { ThemeSharedModule } from "@abp/ng.theme.shared"; -import { CheckboxDemoComponent } from "./CheckboxDemoComponent.component"; - -@NgModule({ - imports: [ - ThemeSharedModule, - // ... - ], - declarations: [CheckboxDemoComponent], - // ... +import { Component } from "@angular/core"; +import { AbpCheckboxComponent } from "@abp/ng.theme.shared"; + +@Component({ + selector: 'app-checkbox-demo', + imports: [AbpCheckboxComponent], + templateUrl: './checkbox-demo.component.html', }) -export class MyFeatureModule {} +export class CheckboxDemoComponent {} ``` -Then, the `abp-checkbox` component can be used. See the example below: +Then, the `abp-checkbox` component can be used in your template. See the example below: ```html
diff --git a/docs/en/framework/ui/angular/form-input-component.md b/docs/en/framework/ui/angular/form-input-component.md index aaf3b6f5be..4f860fe36d 100644 --- a/docs/en/framework/ui/angular/form-input-component.md +++ b/docs/en/framework/ui/angular/form-input-component.md @@ -22,23 +22,21 @@ The ABP FormInput Component is a reusable form input component for the text type # Usage -The ABP FormInput component is a part of the `ThemeSharedModule` module. If you've imported that module into your module, there's no need to import it again. If not, then first import it as shown below: +The ABP FormInput component (`AbpFormInputComponent`) is a standalone component. You can import it directly in your component: ```ts -import { ThemeSharedModule } from "@abp/ng.theme.shared"; -import { FormInputDemoComponent } from "./FomrInputDemoComponent.component"; - -@NgModule({ - imports: [ - ThemeSharedModule, - // ... - ], - declarations: [FormInputDemoComponent], +import { Component } from "@angular/core"; +import { AbpFormInputComponent } from "@abp/ng.theme.shared"; + +@Component({ + selector: 'app-form-input-demo', + imports: [AbpFormInputComponent], + templateUrl: './form-input-demo.component.html', }) -export class MyFeatureModule {} +export class FormInputDemoComponent {} ``` -Then, the `abp-form-input` component can be used. See the example below: +Then, the `abp-form-input` component can be used in your template. See the example below: ```html
diff --git a/docs/en/framework/ui/angular/form-validation.md b/docs/en/framework/ui/angular/form-validation.md index b4ed31842d..8047767279 100644 --- a/docs/en/framework/ui/angular/form-validation.md +++ b/docs/en/framework/ui/angular/form-validation.md @@ -303,7 +303,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; @Component({ selector: 'app-nested-form', templateUrl: './nested-form.component.html', - standalone: true, imports: [NgxValidateCoreModule], }) export class NestedFormComponent implements OnInit { 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 5d70510e51..6db3c95fda 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,38 +9,45 @@ 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. -Create a new module called `MyRolesModule`: - -```bash -yarn ng generate module my-roles --module app -``` - Create a new component called `MyRolesComponent`: ```bash -yarn ng generate component my-roles/my-roles --flat --export +yarn ng generate component my-roles/my-roles --flat ``` Open the generated `src/app/my-roles/my-roles.component.ts` file and replace its content with the following: ```js import { Component, Injector, inject, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { finalize } from 'rxjs/operators'; -import { ListService, PagedAndSortedResultRequestDto, PagedResultDto } from '@abp/ng.core'; +import { ListService, PagedAndSortedResultRequestDto, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; import { eIdentityComponents, RolesComponent } from '@abp/ng.identity'; import { IdentityRoleDto, IdentityRoleService } from '@abp/ng.identity/proxy'; -import { ePermissionManagementComponents } from '@abp/ng.permission-management'; -import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared'; +import { ePermissionManagementComponents, PermissionManagementComponent } from '@abp/ng.permission-management'; +import { Confirmation, ConfirmationService, ModalComponent, ButtonComponent } from '@abp/ng.theme.shared'; import { EXTENSIONS_IDENTIFIER, FormPropData, - generateFormFromProps + generateFormFromProps, + PageToolbarComponent, + ExtensibleTableComponent, + ExtensibleFormComponent } from '@abp/ng.components/extensible'; @Component({ selector: 'app-my-roles', + imports: [ + ReactiveFormsModule, + LocalizationPipe, + ModalComponent, + ButtonComponent, + PageToolbarComponent, + ExtensibleTableComponent, + ExtensibleFormComponent, + PermissionManagementComponent + ], templateUrl: './my-roles.component.html', providers: [ ListService, @@ -236,25 +243,12 @@ Open the generated `src/app/my-role/my-role.component.html` file and replace its We have added the `abp-page-toolbar`, `abp-extensible-table`, and `abp-extensible-form` extension components to template of the `MyRolesComponent`. -You should import the required modules for the `MyRolesComponent` to `MyRolesModule`. Open the `src/my-roles/my-roles.module.ts` file and replace the content with the following: - -```js -import { ExtensibleModule } from '@abp/ng.components/extensible'; -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { MyRolesComponent } from './my-roles.component'; -import { PermissionManagementModule } from '@abp/ng.permission-management'; - -@NgModule({ - declarations: [MyRolesComponent], - imports: [SharedModule, ExtensibleModule, PermissionManagementModule], - exports: [MyRolesComponent], -}) -export class MyRolesModule {} -``` - -- `ExtensionsModule` imported to be able to use the extension components in your component. -- `PermissionManagementModule` imported to be able to use the `abp-permission-*management` in your component. +Since we are using standalone components, all required imports are already defined in the component's `imports` array: +- `PageToolbarComponent`, `ExtensibleTableComponent`, `ExtensibleFormComponent` - Extension components +- `PermissionManagementComponent` - Permission management component +- `ModalComponent`, `ButtonComponent` - Theme shared components +- `LocalizationPipe` - For localization +- `ReactiveFormsModule` - For form handling As the last step, it is needs to be replaced the `RolesComponent` with the `MyRolesComponent`. Open the `app.component.ts` and modify its content as shown below: diff --git a/docs/en/framework/ui/angular/quick-start.md b/docs/en/framework/ui/angular/quick-start.md index 0e48f0190e..2417b187b1 100644 --- a/docs/en/framework/ui/angular/quick-start.md +++ b/docs/en/framework/ui/angular/quick-start.md @@ -1,13 +1,13 @@ ```json //[doc-seo] { - "Description": "Learn how to set up your development environment for ABP Angular 17.3.x with this quick start guide, ensuring a smooth development experience." + "Description": "Learn how to set up your development environment for ABP Angular 21.x with this quick start guide, ensuring a smooth development experience." } ``` # ABP Angular Quick Start -**In this version ABP uses Angular [20.0.x](https://github.com/angular/angular/tree/20.0.x) version. You don't have to install Angular CLI globally** +**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** ## How to Prepare Development Environment @@ -18,13 +18,13 @@ Please follow the steps below to prepare your development environment for Angula 3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to Angular projects. ABP project templates even contain plugin recommendations for VS Code users, which VS Code will ask you to install when you open the Angular project folder. Here is a list of recommended extensions: - [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=angular.ng-template) - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - - [TSLint](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-typescript-tslint-plugin) + - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=visualstudioexptteam.vscodeintellicode) - [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) - [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - [Angular 10 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout](https://marketplace.visualstudio.com/items?itemName=Mikael.Angular-BeastCode) - [JavaScript (ES6) code snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets) - - [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) + - [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) - [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) diff --git a/docs/en/framework/ui/angular/ssr-configuration.md b/docs/en/framework/ui/angular/ssr-configuration.md index e058beb861..4d889377fd 100644 --- a/docs/en/framework/ui/angular/ssr-configuration.md +++ b/docs/en/framework/ui/angular/ssr-configuration.md @@ -239,7 +239,7 @@ The schematic installs `openid-client` to handle authentication on the server si ## 5. Render Modes & Hybrid Rendering -Angular 20 provides different rendering modes that you can configure per route in the `app.routes.server.ts` file to optimize performance and SEO. +Angular 21 provides different rendering modes that you can configure per route in the `app.routes.server.ts` file to optimize performance and SEO. ### 5.1. Available Render Modes @@ -352,13 +352,17 @@ currentTime = new Date(); // ✅ Good - use TransferState for consistent data import { TransferState, makeStateKey } from '@angular/core'; -const TIME_KEY = makeStateKey('time'); +TIME_KEY = makeStateKey('time'); +transferState = inject(TransferState); +time: string; -constructor(private transferState: TransferState) { +constructor() { if (isPlatformServer(this.platformId)) { - this.transferState.set(TIME_KEY, new Date().toISOString()); + this.time = new Date().toISOString(); + this.transferState.set(this.TIME_KEY, this.time); } else { - this.time = this.transferState.get(TIME_KEY, new Date().toISOString()); + const timeFromCache = this.transferState.get(this.TIME_KEY, new Date().toISOString()); + this.time = timeFromCache; } } ``` diff --git a/docs/en/framework/ui/angular/theming.md b/docs/en/framework/ui/angular/theming.md index 1351a02f33..bad673716a 100644 --- a/docs/en/framework/ui/angular/theming.md +++ b/docs/en/framework/ui/angular/theming.md @@ -229,13 +229,14 @@ All of the options are shown below. You can choose either of them. ````ts import { eUserMenuItems } from '@abp/ng.theme.basic'; -import { UserMenuService } from '@abp/ng.theme.shared'; +import { UserMenuService, UserMenu } from '@abp/ng.theme.shared'; +import { LocalizationPipe, INJECTOR_PIPE_DATA_TOKEN } from '@abp/ng.core'; import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -// make sure that you import this component in a NgModule @Component({ selector: 'abp-current-user-test', + imports: [LocalizationPipe], template: ` @if (data.textTemplate.icon){ diff --git a/docs/en/framework/ui/mvc-razor-pages/modals.md b/docs/en/framework/ui/mvc-razor-pages/modals.md index d37575cc26..6ad7ad421b 100644 --- a/docs/en/framework/ui/mvc-razor-pages/modals.md +++ b/docs/en/framework/ui/mvc-razor-pages/modals.md @@ -207,7 +207,7 @@ namespace MyProject.Web.Pages.Products public class ProductCreateModalModel : AbpPageModel { [BindProperty] - public PoductCreationDto Product { get; set; } + public ProductCreationDto Product { get; set; } public async Task OnGetAsync() { @@ -227,9 +227,9 @@ namespace MyProject.Web.Pages.Products * This is a simple `PageModal` class. The `[BindProperty]` make the form binding to the model when you post (submit) the form; The standard ASP.NET Core system. * `OnPostAsync` returns `NoContent` (this method is defined by the base `AbpPageModel` class). Because we don't need to a return value in the client side, after the form post operation. -**PoductCreationDto:** +**ProductCreationDto:** -`ProductCreateModalModel` uses a `PoductCreationDto` class defined as shown below: +`ProductCreateModalModel` uses a `ProductCreationDto` class defined as shown below: ````csharp using System; @@ -238,7 +238,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; namespace MyProject.Web.Pages.Products { - public class PoductCreationDto + public class ProductCreationDto { [Required] [StringLength(128)] diff --git a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md index d986d65b43..e0be666b2c 100644 --- a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md +++ b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/index.md @@ -7,7 +7,7 @@ # ABP Tag Helpers -ABP defines a set of **tag helper components** to simply the user interface development for ASP.NET Core (MVC / Razor Pages) applications. +ABP defines a set of **tag helper components** to simplify the user interface development for ASP.NET Core (MVC / Razor Pages) applications. ## Bootstrap Component Wrappers @@ -45,7 +45,7 @@ Here, the list of components those are wrapped by the ABP: ## Form Elements -**Abp Tag Helpers** add new features to standard **Asp.Net Core MVC input & select Tag Helpers** and wrap them with **Bootstrap** form controls. See [Form Elements documentation](form-elements.md) . +**Abp Tag Helpers** add new features to standard **ASP.NET Core MVC input & select Tag Helpers** and wrap them with **Bootstrap** form controls. See [Form Elements documentation](form-elements.md) . ## Dynamic Forms diff --git a/docs/en/framework/ui/mvc-razor-pages/theming.md b/docs/en/framework/ui/mvc-razor-pages/theming.md index ff73b3ad1b..138b52e3d2 100644 --- a/docs/en/framework/ui/mvc-razor-pages/theming.md +++ b/docs/en/framework/ui/mvc-razor-pages/theming.md @@ -437,7 +437,7 @@ In this way, applications or modules can have selectors based on the current lay ### RTL -To support Right-To-Left languages, the Layout should check the current culture and add `dir="rtl"` to the `html` tag and `rtl` CSS class the the `body` tag. +To support Right-To-Left languages, the Layout should check the current culture and add `dir="rtl"` to the `html` tag and `rtl` CSS class to the `body` tag. You can check `CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft` to understand if the current language is a RTL language. diff --git a/docs/en/framework/ui/mvc-razor-pages/widgets.md b/docs/en/framework/ui/mvc-razor-pages/widgets.md index ac9901733b..7e894027f6 100644 --- a/docs/en/framework/ui/mvc-razor-pages/widgets.md +++ b/docs/en/framework/ui/mvc-razor-pages/widgets.md @@ -42,7 +42,7 @@ namespace DashboardDemo.Web.Pages.Components.MySimpleWidget Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. -You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. +You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document for all different usages. **Default.cshtml**: diff --git a/docs/en/framework/ui/react-native/index.md b/docs/en/framework/ui/react-native/index.md index 6496b0a640..862d836ac4 100644 --- a/docs/en/framework/ui/react-native/index.md +++ b/docs/en/framework/ui/react-native/index.md @@ -12,11 +12,11 @@ } ```` -# Getting Started with the React Native +# Getting Started with React Native -> React Native mobile option is *available for* ***Team*** *or higher licenses* +> The React Native mobile option is *available for* ***Team*** *or higher licenses* -ABP platform provide basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated to your ABP based backends**. +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) @@ -24,12 +24,14 @@ ABP platform provide basic [React Native](https://reactnative.dev/) startup temp Please follow the steps below to prepare your development environment for React Native. -1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js v20.11+ installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. -2. **[Optional] Install Yarn:** You may install Yarn v1 (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v6 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. -3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to React Native projects. -4. **Install an Emulator/Simulator:** React Native applications need an Android emulator or an iOS simulator to run on your OS. If you do not have Android Studio installed and configured on your system, we recommend [setting up android emulator without android studio](setting-up-android-emulator.md). +1. **Install Node.js:** Visit the [Node.js downloads page](https://nodejs.org/en/download/) and download the appropriate Node.js v20.11+ installer for your operating system. Alternatively, you can install [NVM](https://github.com/nvm-sh/nvm) to manage multiple versions of Node.js on your system. +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. -If you prefer the other way, you can check the [Android Studio Emulator](https://docs.expo.dev/workflow/android-studio-emulator/) or [iOS Simulator](https://docs.expo.dev/workflow/ios-simulator/) on expo.io documentation to learn how to set up an emulator. + > **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 @@ -37,34 +39,143 @@ You have multiple options to initiate a new React Native project that works with ### 1. Using ABP Studio -ABP Studio application is the most convenient and flexible way to initiate a React Native application based on ABP framework. You can follow the [tool documentation](../../../studio) and select the option below: +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) ### 2. Using ABP CLI -ABP CLI is another way of creating an ABP solution with a React Native application. Simply [install the ABP CLI](../../../cli) and run the following command in your terminal: +The ABP CLI is another way to create an ABP solution with a React Native application. [Install the ABP CLI](../../../cli) and run the following command in your terminal: ```shell abp new MyCompanyName.MyProjectName -csf -u -m react-native ``` -> To see further options in the CLI, please visit the [CLI manual](../../../cli). +> For more options, visit the [CLI manual](../../../cli). -This command will prepare a solution with an **Angular** or an **MVC** (depends on your choice), a **.NET Core**, and a **React Native** project in it. +This command creates a solution containing an **Angular** or **MVC** project (depending on your choice), a **.NET Core** project, and a **React Native** project. -## How to Configure & Run the Backend +## Running the React Native Application + +> **Recommended:** For faster development and testing, we recommend using the **Web View** option first, as it requires fewer backend modifications. The backend configuration described in the next section is only needed if you want to test on Android emulators or iOS simulators. + +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. + +Follow these steps to set up the web view: + +1. Navigate to the `react-native` directory and start the application by running: + ```bash + yarn web + ``` + +2. Generate SSL certificates by running the following command in a separate directory: + ```bash + mkcert localhost + ``` + +3. Set up the local proxy by running: + ```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 + "OpenIddict": { + "Applications": { + "MyApplication_Mobile": { + "ClientId": "MyApplication_Mobile", + "RootUrl": "https://localhost" + } + } + } + ``` + +### Running on Emulator/Simulator + +If you prefer to test on an Android emulator or iOS simulator, you'll need to configure the backend as described in the section below. Follow these steps: + +1. Make sure the [database migration is complete](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](../../../get-started?UI=NG&DB=EF&Tiered=No#run-the-application). +2. Open `react-native` folder and run `yarn install` or `npm install` if you have not already. +3. Open the `Environment.ts` file in the `react-native` folder and replace the `localhost` address in the `apiUrl` and `issuer` properties with your local IP address as shown below: + +{{ if Architecture == "Monolith" }} + +![react native monolith environment local IP](../../../images/react-native-monolith-environment-local-ip.png) + +{{ else if Architecture == "Tiered" }} + +![react native tiered environment local IP](../../../images/react-native-tiered-environment-local-ip.png) + +> 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) + +> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.AuthServer` project. + +{{ end }} + +1. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the options. + +> 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) + +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) + +### 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) + +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. + +## How to Configure & Run the Backend (Required for Emulator/Simulator Testing) > React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development. -A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application using the `Kestrel` configuration. +To disable the HTTPS-only settings of OpenIddict, open the {{ if Architecture == "Monolith" }}`MyProjectNameHttpApiHostModule`{{ else if Architecture == "Tiered" }}`MyProjectNameAuthServerModule`{{ end }} project and add the following code block to the `PreConfigureServices` method: + +```csharp +#if DEBUG + PreConfigure(options => + { + options.UseAspNetCore() + .DisableTransportSecurityRequirement(); + }); +#endif +``` + +> **Important:** Before running the backend application, make sure you have completed the [database migration](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) if you are starting with a fresh database. The backend application requires the database to be properly initialized. + +A React Native application running on an Android emulator or a physical phone **cannot connect to the backend** on `localhost`. To resolve this, you need to run the backend application using the `Kestrel` configuration. {{ if Architecture == "Monolith" }} ![React Native monolith host project configuration](../../../images/react-native-monolith-be-config.png) -- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address on 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 just to test the react native application on the development environment. +- 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 { @@ -82,8 +193,8 @@ A React Native application running on an Android emulator or a physical phone ** ![React Native tiered project configuration](../../../images/react-native-tiered-be-config.png) -- Open the `appsettings.json` file in the `.DbMigrator` folder. Replace the `localhost` address on 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 just to test the react native application on the development environment. +- 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 { @@ -97,7 +208,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 again. In addition, you will need to introduce the authentication server as mentioned above. +- 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 { @@ -121,7 +232,7 @@ A React Native application running on an Android emulator or a physical phone ** ![React Native microservice project configuration](../../../images/react-native-microservice-be-config.png) -- Open the `appsettings.Development.json` file in the `.AuthServer` folder. Add this configuration to accept global requests just to test the react native application on the development environment. +- 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 { @@ -138,7 +249,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 just to test the react native application on the development environment. You should also provide the authentication server configuration. In addition, you need to apply the same process for all the services you would use in the react native application. +- 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 { @@ -161,7 +272,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 +- Update the `appsettings.json` file in the `.IdentityService` folder. Replace the `localhost` configuration with your local IP address for the React Native application. ```json { @@ -182,7 +293,7 @@ A React Native application running on an Android emulator or a physical phone ** } ``` -- Lastly, update the mobile gateway configurations as following: +- Finally, update the mobile gateway configurations as follows: ```json //gateways/mobile/MyMicroserviceProject.MobileGateway/Properties/launchSettings.json @@ -259,71 +370,4 @@ A React Native application running on an Android emulator or a physical phone ** {{ end }} -Run the backend application as described in the [getting started document](../../../get-started). - -> You should turn off the "Https Restriction" if you're using OpenIddict as a central identity management solution. Because the IOS Simulator doesn't support self-signed certificates and OpenIddict is set to only work with HTTPS by default. - -## How to disable the Https-only settings of OpenIddict - -Open the {{ if Architecture == "Monolith" }}`MyProjectNameHttpApiHostModule`{{ else if Architecture == "Tiered" }}`MyProjectNameAuthServerModule`{{ end }} project and copy-paste the below code-block to the `PreConfigureServices` method: - -```csharp -#if DEBUG - PreConfigure(options => - { - options.UseAspNetCore() - .DisableTransportSecurityRequirement(); - }); -#endif -``` - -## How to Configure & Run the React Native Application - -1. Make sure the [database migration is complete](../../../get-started?UI=NG&DB=EF&Tiered=No#create-the-database) and the [API is up and running](../../../get-started?UI=NG&DB=EF&Tiered=No#run-the-application). -2. Open `react-native` folder and run `yarn` or `npm install` if you have not already. -3. Open the `Environment.ts` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below: - -{{ if Architecture == "Monolith" }} - -![react native monolith environment local IP](../../../images/react-native-monolith-environment-local-ip.png) - -{{ else if Architecture == "Tiered" }} - -![react native tiered environment local IP](../../../images/react-native-tiered-environment-local-ip.png) - -> 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-microservice-environment-local-ip.png) - -> Make sure that `issuer` matches the running address of the `.AuthServer` project, `apiUrl` matches the running address of the `.AuthServer` project. - -{{ end }} - -1. Run `yarn start` or `npm start`. Wait for the Expo CLI to print the opitons. - -> The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. - -![expo-cli-options](../../../images/rn-options.png) - -In the above image, you can start the application with 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 choosing the option. - -### Expo - -![React Native login screen on iPhone 16](../../../images/rn-login-iphone.png) - -### 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) - -Enter **admin** as the username and **1q2w3E\*** as the password to login to the application. - -The application is up and running. You can continue to develop your application based on this startup template. - -## See Also - -- [React Native project structure](../../../solution-templates/application-module#react-native) +Run the backend application(s) as described in the [getting started document](../../../get-started). diff --git a/docs/en/framework/ui/react-native/setting-up-android-emulator.md b/docs/en/framework/ui/react-native/setting-up-android-emulator.md index e5d14de32a..495197a0da 100644 --- a/docs/en/framework/ui/react-native/setting-up-android-emulator.md +++ b/docs/en/framework/ui/react-native/setting-up-android-emulator.md @@ -1,58 +1,121 @@ -```json -//[doc-seo] -{ - "Description": "Learn how to set up an Android emulator without Android Studio using command line tools on Windows, macOS, and Linux." -} -``` - +```json +//[doc-seo] +{ + "Description": "Learn how to set up an Android emulator without Android Studio using command line tools on Windows, macOS, and Linux." +} +``` + # Setting Up Android Emulator Without Android Studio (Windows, macOS, Linux) -This guide explains how to install and run an Android emulator **without Android Studio**, using only **Command Line Tools**. +This guide walks you through installing and running an Android emulator **without Android Studio**, using only the **Android Command Line Tools**. --- ## 1. Download Required Tools -Go to: [https://developer.android.com/studio#command-tools](https://developer.android.com/studio#command-tools) -Download the "Command line tools only" package for your OS: +Visit the [Android Command Line Tools download page](https://developer.android.com/studio#command-line-tools-only) and download the "Command line tools only" package for your operating system: - **Windows:** `commandlinetools-win-*.zip` - **macOS:** `commandlinetools-mac-*.zip` - **Linux:** `commandlinetools-linux-*.zip` +> **Alternative for Windows:** If you prefer using Windows Package Manager, you can install Android Studio (which includes the command-line tools) using: +> ```powershell +> winget install --id=Google.AndroidStudio -e +> ``` +> However, this guide focuses on installing only the command-line tools without the full IDE. + --- -## 2. Create the Required Directory Structure +## 2. Create Directory Structure and Extract Files + +The Android SDK tools require a specific directory structure. Follow the steps below for your operating system. ### Windows: -``` -C:\Android\ -└── cmdline-tools\ - └── latest\ - └── [extract all files from the zip here] -``` + +1. **Create the directory structure:** + ``` + C:\Android\ + └── cmdline-tools\ + └── latest\ + ``` + +2. **Extract the downloaded zip file.** The archive contains a `cmdline-tools` folder with `bin`, `lib`, and other files. + +3. **Move all contents** from the extracted `cmdline-tools` folder into `C:\Android\cmdline-tools\latest\` + + Your final directory structure should look like this: + ``` + C:\Android\ + └── cmdline-tools\ + └── latest\ + ├── bin\ + ├── lib\ + └── [other files] + ``` ### macOS / Linux: -``` -~/Android/ -└── cmdline-tools/ - └── latest/ - └── [extract all files from the zip here] -``` -> You need to create the `latest` folder manually. +1. **Create the directory structure:** + ```bash + mkdir -p ~/Android/cmdline-tools + ``` + +2. **Extract the downloaded zip file:** + ```bash + cd ~/Downloads + unzip commandlinetools-*.zip + ``` + +3. **Move the extracted folder to the correct location:** + ```bash + mv cmdline-tools ~/Android/cmdline-tools/latest + ``` + + Your final directory structure should look like this: + ``` + ~/Android/ + └── cmdline-tools/ + └── latest/ + ├── bin/ + ├── lib/ + └── [other files] + ``` + +> **Important:** The `latest` folder must be created manually (Windows) or by renaming the extracted folder (macOS/Linux). The Android SDK tools require this exact directory structure to function properly. --- -## 3. Set Environment Variables +## 3. Configure Environment Variables + +Set up environment variables so your system can locate the Android SDK tools. + +### Windows (PowerShell - permanent): + +Run these commands in PowerShell to set environment variables permanently: + +```powershell +[System.Environment]::SetEnvironmentVariable('ANDROID_HOME', 'C:\Android', 'User') +$currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') +[System.Environment]::SetEnvironmentVariable('Path', "$currentPath;C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator", 'User') +``` + +> **Note:** You may need to restart your terminal or PowerShell session for the changes to take effect. + +### Windows (CMD - temporary for current session): + +If you only need the environment variables for the current session, use these commands: -### Windows (temporary for CMD session): ```cmd -set PATH=C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator;%PATH% +set ANDROID_HOME=C:\Android +set PATH=%PATH%;C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools;C:\Android\emulator ``` +> **Note:** These settings will be lost when you close the command prompt window. + ### macOS / Linux: -Add the following to your `.bashrc`, `.zshrc`, or `.bash_profile` file: + +Add the following environment variables to your shell configuration file (`.bashrc`, `.zshrc`, or `.bash_profile`): ```bash export ANDROID_HOME=$HOME/Android @@ -61,62 +124,164 @@ export PATH=$PATH:$ANDROID_HOME/platform-tools export PATH=$PATH:$ANDROID_HOME/emulator ``` -> Apply the changes: +After saving the file, reload your shell configuration: + ```bash source ~/.zshrc # or ~/.bashrc if you're using bash ``` +**Verify Environment Variables:** + +After reloading, verify the variables are set correctly: + +```bash +echo $ANDROID_HOME +which sdkmanager +``` + --- -## 4. Install SDK Components +## 4. Accept Android SDK Licenses (macOS/Linux) -Install platform tools, emulator, and a system image: +**macOS / Linux users only:** Before installing SDK components, you must accept the Android SDK licenses: ```bash -sdkmanager --sdk_root=$ANDROID_HOME "platform-tools" "platforms;android-34" "system-images;android-34;google_apis;x86_64" "emulator" +yes | sdkmanager --licenses ``` -> On Windows, replace `$ANDROID_HOME` with `--sdk_root=C:\Android`. +This command automatically accepts all licenses. Without this step, the installation will fail. + +> **Note:** Windows users will be prompted to accept licenses during the installation in the next step. --- -## 5. Create an AVD (Android Virtual Device) +## 5. Install SDK Components + +Use `sdkmanager` to install the required Android SDK components: platform tools, an Android platform, a system image, and the emulator. + +### Windows: + +```cmd +sdkmanager --sdk_root=C:\Android "platform-tools" "platforms;android-35" "system-images;android-35;google_apis;x86_64" "emulator" +``` + +### macOS / Linux: + +**For Apple Silicon Macs (M1/M2/M3/M4):** +```bash +sdkmanager "platform-tools" "platforms;android-35" "system-images;android-35;google_apis;arm64-v8a" "emulator" +``` + +**For Intel-based Macs and Linux:** +```bash +sdkmanager "platform-tools" "platforms;android-35" "system-images;android-35;google_apis;x86_64" "emulator" +``` + +> **Note:** +> - This command installs Android 15 (API level 35), which is the current stable version required by Google Play as of 2025. +> - To see all available versions, run `sdkmanager --list` and replace `android-35` with your preferred API level if needed. +> - Use `arm64-v8a` for Apple Silicon Macs (M1/M2/M3/M4) and `x86_64` for Intel-based Macs, Windows, and most Linux systems. The architecture must match your system's processor. + +--- + +## 6. Create an Android Virtual Device (AVD) + +An AVD is a device configuration that defines the characteristics of an Android device you want to simulate. + +### List Available Device Profiles (Optional): + +Before creating an AVD, you can view all available device profiles (e.g., Pixel, Nexus) to choose from: -### List available devices: ```bash avdmanager list devices ``` -### Create your AVD: +### Create Your AVD: + +Create a new AVD with the following command: + +**Windows:** +```cmd +avdmanager create avd -n myEmu -k "system-images;android-35;google_apis;x86_64" --device "pixel_8" +``` + +**macOS / Linux (Apple Silicon):** ```bash -avdmanager create avd -n myEmu -k "system-images;android-34;google_apis;x86_64" --device "pixel" +avdmanager create avd -n myEmu -k "system-images;android-35;google_apis;arm64-v8a" --device "pixel_8" ``` +**macOS / Linux (Intel-based):** +```bash +avdmanager create avd -n myEmu -k "system-images;android-35;google_apis;x86_64" --device "pixel_8" +``` + +When prompted "Do you wish to create a custom hardware profile?" type `no` and press Enter. + +> **Note:** +> - Replace `myEmu` with your preferred AVD name +> - Replace `pixel_8` with a device profile from the list above if you want a different device configuration +> - The `-k` parameter specifies the system image you installed in the previous step. Make sure it matches the architecture you installed (x86_64 for Windows and Intel-based systems, arm64-v8a for Apple Silicon Macs) + --- -## 6. Start the Emulator +## 7. Start the Emulator + +Launch your emulator using the AVD name you created: ```bash emulator -avd myEmu ``` -The emulator window should open +Replace `myEmu` with the name you used when creating your AVD. The emulator window should open and boot up the Android system. + +### Common Startup Issues (macOS/Linux): + +**If you get "command not found":** +```bash +$ANDROID_HOME/emulator/emulator -avd myEmu +``` + +**For better performance on Apple Silicon:** +```bash +emulator -avd myEmu -gpu host +``` + +**For headless mode (no GUI window):** +```bash +emulator -avd myEmu -no-window +``` --- -## Extra Tools and Commands +## 8. Additional Tools and Commands + +### List All AVDs: + +View all created virtual devices: + +```bash +avdmanager list avd +``` + +### List Connected Devices: + +Use ADB (Android Debug Bridge) to view all connected Android devices and running emulators: -### List connected devices with ADB: ```bash adb devices ``` +This is useful for verifying that your emulator is running and properly connected. + ### Install an APK: + +Install an Android application package (APK) to your emulator or connected device: + ```bash adb install myApp.apk ``` ---- +Replace `myApp.apk` with the path to your APK file. ## How to Enable Fast Refresh in React Native @@ -146,17 +311,66 @@ Focus the emulator window and press: --- -## Troubleshooting +## 9. Troubleshooting + +Common issues and their solutions: -| Problem | Explanation | -|--------|-------------| -| `sdkmanager not found` | Make sure `PATH` includes the `latest/bin` directory | -| `x86_64 system image not found` | Make sure you've downloaded it using `sdkmanager` | -| `emulator not found` | Add the `emulator` directory to `PATH` | -| `setx` truncates path (Windows) | Use GUI to update environment variables manually | +| Problem | Solution | +|--------|----------| +| `sdkmanager not found` | **Windows:** Verify that your `PATH` environment variable includes `C:\Android\cmdline-tools\latest\bin`. **macOS/Linux:** Verify `PATH` includes `$ANDROID_HOME/cmdline-tools/latest/bin`. Run `source ~/.zshrc` or `source ~/.bashrc` to reload environment variables | +| `Warning: Could not create settings` | **macOS/Linux:** Run `yes \| sdkmanager --licenses` to accept all Android SDK licenses. **Windows:** Accept licenses when prompted during installation | +| System image not found | Ensure you've installed the system image using `sdkmanager` in step 5. The architecture must match: `x86_64` for Windows and Intel-based systems, `arm64-v8a` for Apple Silicon Macs. Run `sdkmanager --list` to verify available images, then run the installation command again if needed | +| `emulator not found` | **Windows:** Add the `emulator` directory to your `PATH`: `C:\Android\emulator`. **macOS/Linux:** Use full path: `$ANDROID_HOME/emulator/emulator -avd myEmu` or verify `$ANDROID_HOME/emulator` is in your PATH | +| `setx` truncates path (Windows) | If `setx` truncates your PATH variable, use the PowerShell method from step 3, or update environment variables manually through the Windows GUI (System Properties → Environment Variables) | +| Emulator won't start | **Windows:** Ensure hardware acceleration is enabled. Enable Hyper-V or use Intel HAXM. **macOS (Apple Silicon):** Try `emulator -avd myEmu -gpu host`. **Linux:** Ensure KVM is enabled: `sudo apt install qemu-kvm` and add user to kvm group | +| Permission denied errors (macOS/Linux) | Run `chmod +x $ANDROID_HOME/cmdline-tools/latest/bin/*` to make tools executable | +| Emulator extremely slow | **Windows:** Enable Hyper-V or Intel HAXM. **Linux:** Install and enable KVM (see below). **Apple Silicon:** Ensure you're using `arm64-v8a` system images for optimal performance | + +### Enable Hardware Acceleration (Linux): + +For better performance on Linux, install and enable KVM: + +```bash +sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils +sudo adduser $USER kvm +``` + +Log out and log back in for group changes to take effect. + +--- + +## 10. Summary + +You've successfully set up an Android emulator without installing Android Studio, using only the command-line tools. This emulator can be used for React Native development or any other mobile development framework that requires Android emulation. + +The emulator is now ready to use. You can start it anytime with `emulator -avd myEmu` (using your AVD name) and begin developing and testing your Android applications. + +### Quick Reference Commands: + +**Start the emulator:** +```bash +emulator -avd myEmu +``` + +**List running devices:** +```bash +adb devices +``` + +**Install an app:** +```bash +adb install myApp.apk +``` + +**List all AVDs:** +```bash +avdmanager list avd +``` --- -## Summary +## Additional Notes -You can now run an Android emulator without installing Android Studio, entirely through the command line. This emulator can be used for React Native or any mobile development framework. +- **Android API Updates:** As of January 2026, Android 15 (API 35) is the current requirement for Google Play Store submissions. This guide uses API 35, but you can install older versions if needed for legacy app testing. +- **Storage Location:** All Android SDK files are stored in `C:\Android\` (Windows) or `~/Android/` (macOS/Linux) and can consume several GB of disk space. +- **Updating SDK Components:** Run `sdkmanager --update` periodically to keep your SDK tools up to date. \ No newline at end of file diff --git a/docs/en/get-started/images/abp-studio-background-tasks.png b/docs/en/get-started/images/abp-studio-background-tasks.png index cf63394600..c3d020a90f 100644 Binary files a/docs/en/get-started/images/abp-studio-background-tasks.png and b/docs/en/get-started/images/abp-studio-background-tasks.png differ diff --git a/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png b/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png index 97936d018e..398be32048 100644 Binary files a/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png and b/docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png differ diff --git a/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png b/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png index 7b13736509..28fa473185 100644 Binary files a/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png and b/docs/en/get-started/images/abp-studio-created-new-microservice-solution.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png b/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png index b2c3115f4b..a4730d1f61 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png and b/docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png b/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png index c4e4f6787f..a4f65d9214 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png and b/docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png b/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png index 16ff718210..400a4d1789 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png and b/docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png index 2bd1a2fe40..497ad5d065 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png index 6b15ce18b3..49abf55666 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png index da82711582..b633385243 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png index 1d953a9552..aba03aedf4 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png index 53152f1243..d3275453ab 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png index 35fc51899e..d02ac37d39 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png index 796d183ddc..d3539efffb 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner.png index 3e06813a46..4a856a62e5 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner.png differ diff --git a/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png b/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png index 42009900b3..de4476a9f1 100644 Binary files a/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png and b/docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png differ diff --git a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png index 3b1a4abe68..0ecc98f3ee 100644 Binary files a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png and b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png differ diff --git a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png index 5f2a9b7938..13d0e679a8 100644 Binary files a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png and b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png index cb85c3a2b8..ae89059631 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png index 6bbf3fb22a..40f2037ac5 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png new file mode 100644 index 0000000000..8040b491a3 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png index 74cd5066bb..507fdf0a7f 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png index e41580ad1e..12f0003333 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png index ed52bac62c..97255f2464 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png index 455a6bd574..3564685c5c 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png index 705fee9325..1797a4553c 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png index 9b9210b269..d1ec05e4c8 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png index 04e1af7ebe..004a473579 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png index 3c06ae001d..3311e76a76 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png index 40325a5358..edad8a63db 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png index 27e39f3776..08e30001e6 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png index 53692d03de..8755073d1d 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png differ diff --git a/docs/en/get-started/images/abp-studio-open-module-folder.png b/docs/en/get-started/images/abp-studio-open-module-folder.png index b32f046d94..d945377143 100644 Binary files a/docs/en/get-started/images/abp-studio-open-module-folder.png and b/docs/en/get-started/images/abp-studio-open-module-folder.png differ diff --git a/docs/en/get-started/images/abp-studio-welcome-screen.png b/docs/en/get-started/images/abp-studio-welcome-screen.png index 938be4a010..0607762677 100644 Binary files a/docs/en/get-started/images/abp-studio-welcome-screen.png and b/docs/en/get-started/images/abp-studio-welcome-screen.png differ diff --git a/docs/en/get-started/layered-web-application.md b/docs/en/get-started/layered-web-application.md index b2bd05454a..f4a383ff0b 100644 --- a/docs/en/get-started/layered-web-application.md +++ b/docs/en/get-started/layered-web-application.md @@ -142,11 +142,11 @@ In this step, you can choose which languages your application will support. * Click Add Custom Language if you want to add a language that is not listed. -You can change these settings later if needed. Thenk click the *Next* button for the *Additional Options* page: +You can change these settings later if needed. Then click the *Next* button for the *Additional Options* page: ![abp-studio-new-solution-dialog-additional-options](images/abp-studio-new-solution-dialog-additional-options_dark.png) -If you uncheck the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files, such as Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include or exclude the test projects from the solution. +If you uncheck the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files, which includes the Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include or exclude the test projects from the solution. On the next screen, you can configure the modularity options for your solution: diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index ee7e31bf85..d30e3926fc 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -118,7 +118,7 @@ Click the Next button to see *Additional Options* selection: ![abp-studio-new-solution-dialog-additional-options](images/abp-studio-new-solution-dialog-additional-options-microservice.png) -If you unchecked the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files which include the Helm charts and other Kubernetes related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include the test projects in the solution. +If you unchecked the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files which includes the Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include the test projects in the solution. Click the Next button to see *Additional Services* screen: @@ -126,6 +126,12 @@ Click the Next button to see *Additional Services* screen: On that screen, allows you to include extra microservices in your ABP solution during the creation process. This feature lets you extend your solution with business-specific services right from the start. +Click the Next button to see *Admin Password* screen: + +![abp-studio-new-solution-dialog-admin-password](images/abp-studio-new-solution-dialog-admin-password.png) + +Here, you can set the initial password for the `admin` user of your application. By default, it is set to `1q2w3E*`, but you can change it to a more secure password of your choice. + Now, we are ready to allow ABP Studio to create our solution. Just click the *Create* button and let the ABP Studio do the rest for you. After clicking the *Create* button, the dialog is closed and your solution is loaded into ABP Studio: ![abp-studio-created-new-microservice-solution](images/abp-studio-created-new-microservice-solution.png) @@ -297,7 +303,7 @@ Clicking the *Connect* button will start a process that establishes the VPN conn ![abp-studio-microservice-kubernetes-services](images/abp-studio-microservice-kubernetes-services.png) -Now, you can access all the services inside the Kubernetes cluster, including the services those are not exposed out of the cluster. You can use the service name as DNS. For example, you can directly visit `http://cloudcrm-local-identity` in your Browser. You can also right-click to a service or application and select the Browse command to open it's UI in the built-in browser of ABP Studio: +Now, you can access all the services inside the Kubernetes cluster, including the services those are not exposed out of the cluster. You can use the service name as DNS. For example, you can directly visit `http://cloudcrm-local-identity` in your Browser. You can also right-click to a service or application and select the Browse command to open its UI in the built-in browser of ABP Studio: ![abp-studio-microservice-kubernetes-services-browse](images/abp-studio-microservice-kubernetes-services-browse.png) diff --git a/docs/en/get-started/pre-requirements.md b/docs/en/get-started/pre-requirements.md index 8aad4818ab..e17b9cd227 100644 --- a/docs/en/get-started/pre-requirements.md +++ b/docs/en/get-started/pre-requirements.md @@ -36,7 +36,7 @@ Visual Studio Code is a **free and cross-platform** lightweight code editor that ## .NET SDK -ABP is based on NET, so you need to install the .NET SDK. You can download the .NET SDK from the [.NET official website](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). +ABP is based on .NET, so you need to install the .NET SDK. You can download the .NET SDK from the [.NET official website](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). > Installing Visual Studio or JetBrains Rider may automatically install the .NET SDK. @@ -56,7 +56,7 @@ dotnet tool update --global dotnet-ef ## Node.js -ABP projects include some frontend resource packages, so you need to install Node.js/NPM manage these resource packages. You can download Node.js from the [official Node.js website](https://nodejs.org/). We recommend installing version v20.11+. +ABP projects include some frontend resource packages, so you need to install Node.js/NPM to manage these resource packages. You can download Node.js from the [official Node.js website](https://nodejs.org/). We recommend installing version v20.11+. ## Yarn (Required Only for Angular Projects) @@ -89,7 +89,7 @@ ABP startup solution templates and tools use some PowerShell scripts (`*.ps1`) t * [Install PowerShell on macOS](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos) * [Install PowerShell on Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux) -## MicroService Solution +## Microservice Solution The following tools are only required to develop ABP's [microservice solution](../solution-templates/microservice/index.md) diff --git a/docs/en/get-started/single-layer-web-application.md b/docs/en/get-started/single-layer-web-application.md index 9ee87fd439..ef657722b0 100644 --- a/docs/en/get-started/single-layer-web-application.md +++ b/docs/en/get-started/single-layer-web-application.md @@ -66,7 +66,7 @@ Once your configuration is done, click the *Next* button to navigate to the *UI Here, you see all the possible UI options supported by that startup solution template. Pick the **{{ UI_Value }}**. -Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: +Notice that: Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: {{ if DB == "EF" }} ![abp-studio-new-solution-dialog-database-provider](images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore_dark.png) @@ -110,7 +110,7 @@ In this step, you can choose which languages your application will support. * Click Add Custom Language if you want to add a language that is not listed. -You can change these settings later if needed. Thenk click the *Next* button for the *Additional Options* page: +You can change these settings later if needed. Then click the *Next* button for the *Additional Options* page: ![abp-studio-no-layers-new-solution-additional-options](images/abp-studio-no-layers-new-solution-additional-options_dark.png) diff --git a/docs/en/modules/payment.md b/docs/en/modules/payment.md index 29654a2d42..487e0dc64d 100644 --- a/docs/en/modules/payment.md +++ b/docs/en/modules/payment.md @@ -15,9 +15,9 @@ Payment module implements payment gateway integration of an application. It prov See [the module description page](https://abp.io/modules/Volo.Payment) for an overview of the module features. -## How to install +## How to Install -Payment module is not installed in [the startup templates](../solution-templates/layered-web-application). So, it needs to be installed manually. There are two ways of installing a module into your application. +The Payment module is not installed in [the startup templates](../solution-templates/layered-web-application). So, it needs to be installed manually. There are two ways of installing a module into your application. ### Using ABP CLI @@ -30,7 +30,7 @@ abp add-module Volo.Payment ### Manual Installation -If you modified your solution structure, adding module using ABP CLI might not work for you. In such cases, payment module can be added to a solution manually. +If you modified your solution structure, adding a module using ABP CLI might not work for you. In such cases, the Payment module can be added to a solution manually. In order to do that, add packages listed below to matching project on your solution. For example, ```Volo.Payment.Application``` package to your **{ProjectName}.Application.csproj** like below; @@ -47,7 +47,9 @@ After adding the package reference, open the module class of the project (eg: `{ )] ``` -> If you are using Blazor Web App, you need to add the `Volo.Payment.Admin.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and ad the `Volo.Payment.Admin.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project. +> If you are using Blazor Web App, you need to add the `Volo.Payment.Admin.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and add the `Volo.Payment.Admin.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project. + +> For Blazor UI public pages (payment gateway selection, pre-payment, and post-payment pages), see the [Blazor UI](#blazor-ui) section below for detailed installation and configuration instructions. ### Supported Gateway Packages @@ -55,7 +57,8 @@ In order to use a Payment Gateway, you need to add related NuGet packages to you After adding packages of a payment gateway to your application, you also need to configure global payment module options and options for the payment modules you have added. See the Options section below. -### Creating a custom payment gateway +### Creating Custom Payment Gateways + If you require a different payment gateway than existing ones, you can create a custom payment gateway by your own. 2 steps are required to create a custom payment gateway. First is creating a payment gateway object that implements `IPaymentGateway`. This interface exposes core payment operations without any UI. Second step is creating UI for the payment gateway. This UI is used to redirect user to payment gateway and validate payment. Follow the [instructions here](payment-custom-gateway) to create a custom payment gateway. @@ -66,27 +69,291 @@ This module follows the [module development best practices guide](../framework/a You can visit [Payment module package list page](https://abp.io/packages?moduleName=Volo.Payment) to see list of packages related with this module. -## User interface +## User Interface -### Public Pages +The Payment module provides both **public pages** (for payment processing) and **admin pages** (for managing payment plans and requests). The UI is available for **MVC/Razor Pages**, **Blazor**, and **Angular** applications. See the UI-specific sections below for installation and configuration details. -#### Payment gateway selection +### MVC / Razor Pages UI -This page allows selecting a payment gateway. If there is one payment gateway configured for final application, this page will be skipped. +For MVC/Razor Pages applications, the `abp add-module Volo.Payment` command automatically adds the required packages (`Volo.Payment.Web` and gateway-specific Web packages) and the necessary `DependsOn` statements to your module. The only thing you need to do is configure `PaymentWebOptions` as explained in the [PaymentWebOptions](#paymentweboptions) section. -![payment-gateway-selection](../images/payment-gateway-selection.png) +### Blazor UI -#### PayU prepayment page +For Blazor applications, the `abp add-module Volo.Payment` command automatically adds the required packages (`Volo.Payment.Blazor.Server` or `Volo.Payment.Blazor.WebAssembly` and gateway-specific Blazor packages) and the necessary `DependsOn` statements to your module. The only thing you need to do is configure `PaymentBlazorOptions` as explained below. -This page is used to send Name, Surname and Email Address of user to PayU. +#### Installation -![payment-payu-prepayment-page](../images/payment-payu-prepayment-page.png) +> **Note:** If you used the `abp add-module Volo.Payment` command to install the Payment module, the following packages and module dependencies are automatically added to your project. You can skip to the [Gateway-Specific Blazor Packages](#gateway-specific-blazor-packages) section. The information below is provided for reference or manual installation scenarios. + +To use the Payment module's public pages in a Blazor application, you need to install the core Blazor packages and the gateway-specific Blazor packages for each payment gateway you want to support. + +##### Core Blazor Packages + +For **Blazor Server** applications, add the following package to your **{ProjectName}.Blazor.Server.csproj** (or **{ProjectName}.Blazor.csproj** for Blazor Web App): + +```json + +``` + +For **Blazor WebAssembly** applications, add the following package to your **{ProjectName}.Blazor.csproj** (or **{ProjectName}.Blazor.Client.csproj** for Blazor Web App): + +```json + +``` + +#### Gateway-Specific Blazor Packages + +Each payment gateway requires its own Blazor package. Add the packages for the gateways you want to support: + +**Stripe:** +- Blazor Server: `Volo.Payment.Stripe.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Stripe.Blazor.WebAssembly` + +**PayPal:** +- Blazor Server: `Volo.Payment.PayPal.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.PayPal.Blazor.WebAssembly` + +**PayU:** +- Blazor Server: `Volo.Payment.PayU.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.PayU.Blazor.WebAssembly` + +**Iyzico:** +- Blazor Server: `Volo.Payment.Iyzico.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Iyzico.Blazor.WebAssembly` +- HttpApi (required for callbacks): `Volo.Payment.Iyzico.HttpApi` + +> **Important:** Iyzico requires the `Volo.Payment.Iyzico.HttpApi` package because Blazor cannot directly handle POST requests from external payment gateways. This package provides an API endpoint to receive the POST callback from Iyzico and redirect to the Blazor post-payment page. + +**Alipay:** +- Blazor Server: `Volo.Payment.Alipay.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Alipay.Blazor.WebAssembly` + +**TwoCheckout:** +- Blazor Server: `Volo.Payment.TwoCheckout.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.TwoCheckout.Blazor.WebAssembly` + +##### Module Dependencies + +After adding the package references, add the module dependencies to your Blazor module class. For example, for a Blazor Server application with Stripe and PayPal: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpPaymentBlazorServerModule), + typeof(AbpPaymentStripeBlazorServerModule), + typeof(AbpPaymentPayPalBlazorServerModule) +)] +public class YourBlazorModule : AbpModule +{ + // ... +} +``` + +For Blazor WebAssembly: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpPaymentBlazorWebAssemblyModule), + typeof(AbpPaymentStripeBlazorWebAssemblyModule), + typeof(AbpPaymentPayPalBlazorWebAssemblyModule) +)] +public class YourBlazorModule : AbpModule +{ + // ... +} +``` + +#### Configuration + +Configure `PaymentBlazorOptions` in your Blazor module's `ConfigureServices` method: + +```csharp +Configure(options => +{ + options.RootUrl = configuration["App:SelfUrl"]; + options.CallbackUrl = configuration["App:SelfUrl"] + "/PaymentSucceed"; + options.GatewaySelectionCheckoutButtonStyle = "btn btn-primary"; // Optional CSS class +}); +``` + +You can also configure these options in your `appsettings.json` file: + +```json +{ + "Payment": { + "Blazor": { + "RootUrl": "https://localhost:44300", + "CallbackUrl": "https://localhost:44300/PaymentSucceed", + "GatewaySelectionCheckoutButtonStyle": "btn btn-primary" + } + } +} +``` + +##### Gateway-Specific Blazor Options + +Each payment gateway has its own Blazor options for customizing the UI. These options can be configured in `appsettings.json`: + +```json +{ + "Payment": { + "Blazor": { + "Payu": { + "PrePaymentCheckoutButtonStyle": "btn btn-success", + "Recommended": true, + "ExtraInfos": ["Fast checkout", "Secure payment"] + }, + "TwoCheckout": { + "Recommended": false, + "ExtraInfos": ["International payments"] + }, + "PayPal": { + "Recommended": true, + "ExtraInfos": ["Pay with PayPal balance", "Buyer protection"] + }, + "Stripe": { + "Recommended": true, + "ExtraInfos": ["Credit/Debit cards", "Apple Pay", "Google Pay"] + }, + "Iyzico": { + "PrePaymentCheckoutButtonStyle": "btn btn-primary", + "Recommended": false, + "ExtraInfos": ["Turkish payment gateway"] + }, + "Alipay": { + "PrePaymentCheckoutButtonStyle": "btn btn-info", + "Recommended": false, + "ExtraInfos": ["Chinese payment gateway", "CNY only"] + } + } + } +} +``` -### Admin Pages +#### Creating Payments in Blazor + +To initiate a payment in a Blazor component, inject `IPaymentRequestAppService` and `NavigationManager`, create a payment request, and navigate to the gateway selection page: + +```csharp +@page "/" +@using Microsoft.AspNetCore.Components +@using Volo.Payment +@using Volo.Payment.Requests +@inject IPaymentRequestAppService PaymentRequestAppService +@inject NavigationManager NavigationManager + + + +@code { + private async Task StartPaymentAsync() + { + var paymentRequest = await PaymentRequestAppService.CreateAsync( + new PaymentRequestCreateDto + { + Currency = "USD", + Products = new List + { + new PaymentRequestProductCreateDto + { + Code = "Product_01", + Name = "LEGO Super Mario", + Count = 2, + UnitPrice = 60, + TotalPrice = 120 + } + } + }); + + NavigationManager.NavigateTo($"/Payment/GatewaySelection?paymentRequestId={paymentRequest.Id}"); + } +} +``` + +##### Passing Extra Parameters + +Some payment gateways require additional parameters. You can pass these as extra properties when creating the payment request: + +```csharp +var paymentRequest = await PaymentRequestAppService.CreateAsync( + new PaymentRequestCreateDto + { + Currency = "USD", + Products = new List + { + new PaymentRequestProductCreateDto + { + Code = "Product_01", + Name = "LEGO Super Mario", + Count = 1, + UnitPrice = 60, + TotalPrice = 60 + } + }, + ExtraProperties = new ExtraPropertyDictionary + { + // For Iyzico - Customer information + { "Name", "John" }, + { "Surname", "Doe" }, + { "Email", "john.doe@example.com" }, + { "Address", "123 Main St" }, + { "City", "Istanbul" }, + { "Country", "Turkey" }, + { "ZipCode", "34000" }, + + // For PayU - Customer information + { "BuyerName", "John" }, + { "BuyerSurname", "Doe" }, + { "BuyerEmail", "john.doe@example.com" } + } + }); +``` + +#### Handling the Callback (Optional) + +When a user completes a payment on the external payment gateway, the following flow occurs: + +1. The user is redirected to the **PostPayment page** (handled internally by the payment module) +2. The PostPayment page validates the payment with the gateway and updates the payment request status to **Completed** +3. If a `CallbackUrl` is configured in `PaymentBlazorOptions`, the user is then redirected to that URL with the `paymentRequestId` as a query parameter + +Create a page to handle this callback and perform any application-specific actions: + +```csharp +@page "/PaymentSucceed" +@using Microsoft.AspNetCore.WebUtilities + +

Payment Successful!

+

Thank you for your purchase.

+

Payment Request ID: @PaymentRequestId

+ +@code { + [Parameter] + [SupplyParameterFromQuery] + public Guid? PaymentRequestId { get; set; } + + protected override async Task OnInitializedAsync() + { + if (PaymentRequestId.HasValue) + { + // The payment is already completed at this point. + // Perform application-specific actions here: + // e.g., activate subscription, send confirmation email, + // update order status, grant access to purchased content, etc. + } + } +} +``` + +> **Note:** By the time the user reaches your callback page, the payment request status has already been set to **Completed** by the PostPayment page. Your callback page is for performing additional application-specific logic. It is also your responsibility to handle if a payment request is used more than once. If you have already delivered your product for a given `PaymentRequestId`, you should not deliver it again when the callback URL is visited a second time. ### Angular UI -#### Installation +For Angular applications, you need to read and apply the steps explained in the following sections: + +#### Configurations In order to configure the application to use the payment module, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root configuration. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration: @@ -120,15 +387,35 @@ const APP_ROUTES: Routes = [ ]; ``` -#### Payment plans page +### Pages + +#### Public Pages + +##### Payment Gateway Selection + +This page allows selecting a payment gateway. If there is only one payment gateway configured for the application, this page will be skipped. + +![payment-gateway-selection](../images/payment-gateway-selection.png) + +##### PrePayment Page + +Some payment gateways require additional information before redirecting to the payment gateway. For example, PayU and Iyzico require customer information (Name, Surname, Email Address, etc.) before processing the payment. + +![payment-payu-prepayment-page](../images/payment-payu-prepayment-page.png) + +#### Admin Pages + +##### Payment Plans Page + Payment plans for subscriptions can be managed on this page. You can connect external subscriptions for each gateway to a plan. ![payment plans](../images/payment-plans.png) ![payment plans gateway plans](../images/payment-plans-gateway-plans.png) -#### Payment request list -This page lists all the payment request operations in application. +##### Payment Request List + +This page lists all the payment request operations in the application. ![payment request list](../images/payment-request-list.png) @@ -171,7 +458,22 @@ Configure(options => * ```PrePaymentUrl```: URL of the page before redirecting user to payment gateway for payment. * ```PostPaymentUrl```: URL of the page when user redirected back from payment gateway to your website. This page is used to validate the payment mostly. * ```Order```: Order of payment gateway for gateway selection page. - * ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. + * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. + * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### PaymentBlazorOptions + +```PaymentBlazorOptions``` is used to configure Blazor application related configurations. This is the Blazor equivalent of `PaymentWebOptions`. + +* ```CallbackUrl```: Final callback URL for internal payment gateway modules to return. User will be redirected to this URL on your website after a successful payment. +* ```RootUrl```: Root URL of your Blazor application. +* ```GatewaySelectionCheckoutButtonStyle```: CSS style to add to the Checkout button on the gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PaymentGatewayBlazorConfigurationDictionary```: Used to store Blazor related payment gateway configuration. + * ```Name```: Name of payment gateway. + * ```PrePaymentUrl```: URL of the Blazor page before redirecting user to payment gateway for payment. + * ```PostPaymentUrl```: URL of the Blazor page when user is redirected back from payment gateway to your website. + * ```Order```: Order of payment gateway for gateway selection page. + * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayuOptions @@ -193,9 +495,17 @@ Configure(options => ```PayuWebOptions``` is used to configure PayU payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: Css style to add Checkout button on PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. + +### PayuBlazorOptions + +```PayuBlazorOptions``` is used to configure PayU payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. ### TwoCheckoutOptions @@ -210,7 +520,14 @@ Configure(options => ```TwoCheckoutWebOptions``` is used to configure TwoCheckout payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### TwoCheckoutBlazorOptions + +```TwoCheckoutBlazorOptions``` is used to configure TwoCheckout payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### StripeOptions @@ -228,7 +545,14 @@ Configure(options => ```StripeWebOptions``` is used to configure Stripe payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### StripeBlazorOptions + +```StripeBlazorOptions``` is used to configure Stripe payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayPalOptions @@ -245,7 +569,14 @@ Configure(options => ```PayPalWebOptions``` is used to configure PayPal payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. + +### PayPalBlazorOptions + +```PayPalBlazorOptions``` is used to configure PayPal payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### IyzicoOptions @@ -263,9 +594,17 @@ Configure(options => ```IyzicoWebOptions``` is used to configure Iyzico payment gateway web options. -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. + +### IyzicoBlazorOptions + +```IyzicoBlazorOptions``` is used to configure Iyzico payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add Checkout button on Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. ### AlipayOptions @@ -285,9 +624,17 @@ Configure(options => #### AlipayWebOptions -* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page. +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. + +#### AlipayBlazorOptions + +```AlipayBlazorOptions``` is used to configure Alipay payment gateway Blazor options. + +* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add Checkout button on Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. > You can check the [Alipay document](https://opendocs.alipay.com/open/02np97) for more details. @@ -541,7 +888,7 @@ PrePayment page asks users for extra information if requested by the external pa PostPayment page is responsible for validation of the response of the external payment gateway. When a user completes the payment, user is redirected to PostPayment page for that payment gateway and PostPayment page validates the status of the payment. If the payment is succeeded, status of the payment request is updated and user is redirected to main application. -Note: It is main application's responsibility to handle if a payment request is used more than one time. For example if PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than onec manually by end users. If you already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited second time. +Note: It is the main application's responsibility to handle if a payment request is used more than once. For example, if the PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than once manually by end users. If you have already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited a second time. ### Creating One-Time Payment @@ -563,7 +910,7 @@ public class IndexModel: PageModel { var paymentRequest = await _paymentRequestAppService.CreateAsync(new PaymentRequestCreateDto() { - Currency= "USD", + Currency = "USD", Products = new List() { new PaymentRequestProductCreateDto @@ -582,7 +929,7 @@ public class IndexModel: PageModel } ``` -If the payment is successful, payment module will return to configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (Activating a user account, triggering a shipment start process etc...). +If the payment is successful, payment module will return to the configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (activating a user account, triggering a shipment start process, etc.). ## Subscriptions diff --git a/docs/en/release-info/index.md b/docs/en/release-info/index.md index e3d3aa56cd..b7da966e43 100644 --- a/docs/en/release-info/index.md +++ b/docs/en/release-info/index.md @@ -1,16 +1,135 @@ ```json //[doc-seo] { - "Description": "Explore the latest ABP Framework release information, including notes, migration guides, and upgrading tips to enhance your development experience." + "Description": "Understand ABP Platform's versioning, release schedule, LTS support policy, and how we handle breaking changes to ensure smooth upgrades for your applications." } ``` -# Release Information +# Versioning & Releases -* [Release Notes](./release-notes.md) -* [Migration Guides](./migration-guides/index.md) -* [Road Map](./road-map.md) -* [Upgrading](./upgrading.md) -* [Preview Releases](./previews.md) -* [Nightly Releases](./nightly-builds.md) -* [Official Packages](https://abp.io/packages) \ No newline at end of file +ABP Platform follows a predictable release cycle aligned with .NET releases. This document explains our versioning strategy, release schedule, support policy, and how we handle breaking changes. + +## Our Commitment + +As a framework you build upon, ABP must be both reliable and evolving — stable enough to trust for long-term projects, yet continuously improving with new features and the latest .NET advancements. To achieve this balance, we commit to: + +* **Predictable releases**: Major versions annually, aligned with .NET releases +* **Long-term support**: Every major version receives 2 years of support +* **Smooth upgrades**: Comprehensive migration guides and tooling for version updates +* **Transparent communication**: Clear documentation of breaking changes and deprecations + +## ABP Versioning + +ABP version numbers indicate the level of changes that are introduced by the release. This use of [semantic versioning](https://semver.org/) helps you understand the potential impact of updating to a new version. + +ABP version numbers have three parts: `major.minor.patch`. For example, version 10.1.2 indicates major version 10, minor version 1, and patch level 2. + +The version number is incremented based on the level of change included in the release. + +| Level of change | Details | +| --- | --- | +| **Major release** | Contains significant new features. Aligned with the new major .NET release. Some developer assistance is expected. You should check the [migration guide](migration-guides/index.md) and possibly refactor code to adapt to new APIs. | +| **Minor release** | Contains new features and improvements. Minor releases are generally backward-compatible; minimal developer assistance is expected, but you can optionally modify your applications to begin using new APIs and features. | +| **Patch release** | Low risk, bug fix and security patch release. No developer assistance is expected. | + +> **Note:** ABP version is aligned with .NET version. For example, ABP 10.x runs on .NET 10, ABP 9.x runs on .NET 9. + +### Preview Releases + +We provide preview releases for each major and minor release so you can try new features before the stable release: + +| Pre-release type | Details | +| --- | --- | +| **Release Candidate (RC)** | A release that is feature complete and in final testing. RC releases are indicated by a release tag appended with the `-rc` identifier, such as `10.1.0-rc.1`. | +| **Nightly builds** | The latest development builds published every weekday night. Nightly builds allow you to try the previous day's development. | + +See the [Preview Releases](previews.md) and [Nightly Builds](nightly-builds.md) documents for more information. + +## Release Frequency + +We work toward a regular schedule of releases, so that you can plan and coordinate your updates with the continuing evolution of ABP and the .NET platform. + +> **Note:** Dates are offered as general guidance and are subject to change. + +In general, expect the following release cycle: + +* **A major release once a year**, typically in November, following the new major .NET release +* **2-4 minor releases** for each major version, released every ~3 months after the major release +* **Patch releases** as needed, typically every 2-4 weeks for the latest minor version + +This cadence of releases gives eager developers access to new features as soon as they are fully developed and tested, while maintaining the stability and reliability of the platform for production users. + +### Release Schedule + +| Version | Status | Released | Active Ends | LTS Ends | +| --- | --- | --- | --- | --- | +| ^10.0.0 | Active | 2025-11 | 2026-11 | 2027-11 | +| ^9.0.0 | LTS | 2024-11 | 2025-11 | 2026-11 | + + +See the [Release Notes](release-notes.md) for detailed information about each release. + +## Support Policy and Schedule (LTS) + +ABP Platform follows a **Long-Term Support (LTS)** policy to ensure your applications remain secure and stable over time. + +### Support Window + +Every major version has a **2-year lifecycle** with two distinct phases: + +| Support Stage | Duration | Details | +| --- | --- | --- | +| **Active** | ~1 year | The version is under active development. Regularly-scheduled updates and patches are released. New features and improvements are added in minor versions. | +| **Long-Term Support (LTS)** | ~1 year | Only critical fixes and security patches are released. No new features are added. | + +This means we actively develop a major version for about 1 year (until the next major .NET release), then provide LTS support for another year. + +### LTS Fixes + +As a general rule, a fix is considered for an LTS version if it resolves one of: + +* A newly identified security vulnerability +* A critical bug that significantly impacts production applications +* A regression caused by a 3rd party change, such as a new browser version or dependency update + +## Deprecation Policy + +When the ABP team intends to remove an API or feature, it will be marked as *deprecated*. This occurs when an API is obsolete, superseded by another API, or otherwise discontinued. Deprecated APIs remain available through their deprecated phase, which lasts a minimum of one major version (approximately one year). + +To help ensure that you have sufficient time and a clear path to update, this is our deprecation policy: + +| Deprecation Stage | Details | +| --- | --- | +| **Announcement** | We announce deprecated APIs and features in the [release notes](release-notes.md) and [migration guides](migration-guides/index.md). Deprecated APIs are typically marked with `[Obsolete]` attribute in the code, which enables IDEs to provide warnings if your project depends on them. We also announce a recommended update path. | +| **Deprecation period** | When an API or feature is deprecated, it is still present in at least the next major release. After that, deprecated APIs and features are candidates for removal. A deprecation can be announced in any release, but the removal of a deprecated API or feature happens only in major releases. | +| **NuGet/NPM dependencies** | We typically make dependency updates that require changes to your applications in major releases. In minor releases, we may update dependencies by expanding the supported versions, but we try not to require projects to update these dependencies until the next major version. | + +## Breaking Changes Policy + +Breaking changes require you to do work because the state after the change is not backward compatible with the state before it. Examples of breaking changes include the removal of public APIs, changes to method signatures, changing the timing of events, or updating to a new version of a dependency that includes breaking changes itself. + +### How We Handle Breaking Changes + +To support you in case of breaking changes: + +* We follow our [deprecation policy](#deprecation-policy) before we remove a public API +* We provide detailed [migration guides](migration-guides/index.md) when a version includes breaking changes + +### Update Path + +We recommend updating one major version at a time for a smoother upgrade experience. For example, to update from version 8.x to version 10.x: + +1. Update from version 8.x to version 9.x +2. Update from version 9.x to version 10.x + +See the [upgrading](upgrading.md) document for detailed instructions on how to upgrade your solutions. + +## Related Documents + +* [Release Notes](release-notes.md) - Detailed release notes for each version +* [Migration Guides](migration-guides/index.md) - Step-by-step guides for upgrading between versions +* [Road Map](road-map.md) - Upcoming features and planned releases +* [Upgrading](upgrading.md) - How to upgrade your ABP-based solutions +* [Preview Releases](previews.md) - Information about preview/RC releases +* [Nightly Builds](nightly-builds.md) - How to use nightly builds +* [Official Packages](https://abp.io/packages) - Browse all ABP packages diff --git a/docs/en/release-info/migration-guides/abp-10-1.md b/docs/en/release-info/migration-guides/abp-10-1.md new file mode 100644 index 0000000000..dce45aff64 --- /dev/null +++ b/docs/en/release-info/migration-guides/abp-10-1.md @@ -0,0 +1,52 @@ +```json +//[doc-seo] +{ + "Description": "Upgrade your ABP solutions from v10.0 to v10.1 with this comprehensive migration guide, ensuring compatibility and new features with ABP v10.1." +} +``` + +# ABP Version 10.1 Migration Guide + +This document is a guide for upgrading ABP v10.0 solutions to ABP v10.1. There are some changes in this version that may affect your applications. Please read them carefully and apply the necessary changes to your application. + +## Open-Source (Framework) + +### Add New EF Core Migrations for Password History/User Passkey Entities + +In this version, we added password history/ user passkeys support to the [Identity PRO Module](../../modules/identity-pro.md) to enhance security compliance. A new `IdentityUserPasswordHistory` entity has been added to store previous password hashes, preventing users from reusing recent passwords. Additionally, we have introduced an `IdentityUserPasskey `entity to support passkey-based authentication. + +**You need to create a new EF Core migration and apply it to your database** after upgrading to ABP 10.1. + +> See [#23894](https://github.com/abpframework/abp/pull/23894) for more details. + +## PRO + +> Please check the **Open-Source (Framework)** section before reading this section. The listed topics might affect your application and you might need to take care of them. + +If you are a paid-license owner and using the ABP's paid version, then please follow the following sections to get informed about the breaking changes and apply the necessary ones: + +### AI Management Module: `Workspace` Entity Base Class Changed + +In this version, the `Workspace` entity in the [AI Management Module](../../modules/ai-management/index.md) has been changed from `FullAuditedAggregateRoot` to `AuditedAggregateRoot` as the base class. + +**This change removes support for soft deletion and related auditing features. If you are using the AI Management module, you need to create a new EF Core migration and apply it to your database.** + +> **Important:** If you have soft-deleted Workspaces in your database, they will become visible after this update. You may need to create a migration script to clean up already deleted records before applying the migration. + +### CMS Kit Pro Module: Dynamic FAQ Group Management + +In this version, the FAQ group system in the [CMS Kit Pro Module](../../modules/cms-kit-pro/faq.md) has been redesigned to support dynamic group management. FAQ groups are now **first-class entities** stored in the database, replacing the previous static configuration approach. + +**Key Changes:** + +- A new `FaqGroup` entity has been introduced with unique names for FAQ groups +- The `FaqSection` entity now uses `GroupId` (Guid) instead of `GroupName` (string) _(GroupName is deprecated and will be removed soon. Use GroupId instead.)_ +- Static FAQ group configuration (`FaqOptions.SetGroups`) has been removed + +**Migration Steps:** + +1. **Remove static group configuration** from your code (e.g., `Configure(options => { options.SetGroups([...]); })`) +2. **Create a new EF Core migration and apply it to your database** +3. **Run the one-time data migration seeder** to migrate existing FAQ sections to the new group entity model + +> **Note:** If you have existing FAQ data, you may need to create a data seeder to migrate your existing group associations to the new entity-based model. diff --git a/docs/en/release-info/migration-guides/index.md b/docs/en/release-info/migration-guides/index.md index f0d8a78b84..61bb095434 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.0 to 10.1](abp-10-1.md) - [9.x to 10.0](abp-10-0.md) - [9.2 to 9.3](abp-9-3.md) - [9.x to 9.2](abp-9-2.md) diff --git a/docs/en/release-info/release-notes.md b/docs/en/release-info/release-notes.md index bf335fa7a2..08d0bcde31 100644 --- a/docs/en/release-info/release-notes.md +++ b/docs/en/release-info/release-notes.md @@ -14,8 +14,23 @@ 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.1 (2026-01-06) + +> This is currently a RC (release-candidate) and you can see the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-1-release-candidate-cyqui19d)** for the v10.1 release. + +* Resource-Based Authorization +* Introducing the [TickerQ Background Worker Provider](../framework/infrastructure/background-workers/tickerq.md) +* Angular UI: Version Upgrade to **v21** +* [File Management Module](../modules/file-management.md): Public File Sharing Support +* [Payment Module](../modules/payment.md): Public Page Implementation for Blazor & Angular UIs +* [AI Management Module](../modules/ai-management/index.md) for Blazor & Angular UIs +* [Identity PRO Module](../modules/identity-pro.md): Password History Support +* [Account PRO Module](../modules/account-pro.md): Introducing WebAuthn Passkeys + ## 10.0 (2025-11-18) +> **Note**: ABP has upgraded to .NET 10.0, so if you plan to use ABP 10.0, you’ll need to migrate your solutions to .NET 10.0. You can refer to the [Migrate from ASP.NET Core 9.0 to 10.0](https://learn.microsoft.com/en-us/aspnet/core/migration/90-to-100) documentation for guidance. However, ABP’s NuGet packages are compatible with both .NET 9 and .NET 10, allowing developers to continue using .NET 9 while still enjoying the latest features and improvements of the ABP Framework without upgrading their SDK. + See the detailed **[blog post / announcement](https://abp.io/community/announcements/abp.io-platform-10.0-final-has-been-released-spknn925)** for the v10.0 release. * Upgraded to .NET 10.0 diff --git a/docs/en/release-info/road-map.md b/docs/en/release-info/road-map.md index 0df94ec157..70a9b392c4 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 9.1, launching January 2025." + "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.1, launching January 2026." } ``` @@ -11,33 +11,35 @@ This document provides a road map, release schedule, and planned features for th ## Next Versions -### v10.1 +### v10.2 -The next version will be 10.1 and planned to release the stable 10.1 version in January 2026. We will be mostly working on the following topics: +The next version will be 10.2 and planned to release the stable 10.2 version in April 2026. We will be mostly working on the following topics: * Framework - * OpenTelemetry Protocol Support for 3rd-party Integrations - * Resource Based Authorization Integration + * Resource-Based Authorization Improvements + * Handle datetime/timezon in `AbpExtensibleDataGrid` Component * Upgrading 3rd-party Dependencies * Enhancements in the Core Points * ABP Suite - * Define Navigation Properties Without Target String Property Dependency + * Creating enums on-the-fly (without needing to create manually on the code side) + * Improvements on the generated codes for nullability * Improvements on Master-Detail Page Desing (making it more compact) * Improvements One-To-Many Scenarios * File Upload Modal Enhancements * ABP Studio * Allow to Directly Create New Solutions with ABP's RC (Release Candidate) Versions + * Integrate AI Management Module with all solution templates and UIs * Automate More Details on New Service Creation for a Microservice Solution * Allow to Download ABP Samples from ABP Studio - * Task Panel Enhancements (and Documentation) + * Task Panel Documentation * Support Multiple Concurrent Kubernetes Deployment/Integration Scenarios * Improve the Module Installation Experience / Installation Guides * Application Modules - * Payment Module: Public Page Implementation (for Blazor & Angular UIs) - * AI Management Module: UI Implementation for Blazor & Angular UIs + * AI Management: MCP & RAG Supports + * File Management: Using Resource-Based Permission (on file-sharing and more...) * CMS Kit: Enhancements for Some Features (Rating, Dynamic Widgets, FAQ and more...) * UI/UX Improvements on Existing Application Modules diff --git a/docs/en/release-info/upgrading.md b/docs/en/release-info/upgrading.md index 88ce1249d8..974d7ef2d5 100644 --- a/docs/en/release-info/upgrading.md +++ b/docs/en/release-info/upgrading.md @@ -21,6 +21,8 @@ Run this command in the terminal while you are in the root folder of your soluti > If your solution has the Angular UI, you probably have `aspnet-core` and `angular` folders in the solution. Run this command in the parent folder of these two folders. +You can also specify a target version with `--version` parameter. See the [ABP CLI update command](../cli/index.md#update) for all available options. + ### Database Migrations > Warning: Be careful if you are migrating your database since you may have data loss in some cases. Carefully check the generated migration code before executing it. It is suggested to take a backup of your current database. diff --git a/docs/en/studio/concepts.md b/docs/en/studio/concepts.md index 1e6afb4fab..1530f3a029 100644 --- a/docs/en/studio/concepts.md +++ b/docs/en/studio/concepts.md @@ -43,6 +43,45 @@ An ABP Studio module is a sub-solution that contains zero, one or multiple packa An ABP Studio Package typically matches to a .NET project (`csproj`). +### Metadata + +Metadata is a collection of key-value pairs that provide additional information for various ABP Studio features. Metadata follows a hierarchical structure where values defined at lower levels override those at higher levels: + +**Hierarchy (from highest to lowest priority):** +1. **Helm Chart Metadata** - Defined in chart properties (Kubernetes context only) +2. **Kubernetes Profile Metadata** / **Run Profile Metadata** - Defined in profile settings (context-dependent) +3. **Solution Metadata** - Defined via *Solution Explorer* → right-click solution → *Manage Metadata* +4. **Global Metadata** - Defined via *Tools* → *Global Metadata* + +**Common Metadata Keys:** + +| Key | Description | Used By | +|-----|-------------|---------| +| `k8ssuffix` | Appends a suffix to Kubernetes namespace (e.g., for multi-developer scenarios) | Kubernetes integration | +| `dotnetEnvironment` | Specifies the .NET environment (e.g., `Development`, `Staging`) | Helm chart installation | +| `projectPath` | Path to the project for Docker image building | Docker image build | +| `imageName` | Docker image name | Docker image build | +| `projectType` | Project type (`dotnet` or `angular`) | Docker image build | + +> Metadata defined in *Global Metadata* is available for all solutions but will not be shared with team members. Metadata defined at *Solution* or *Profile* level will be shared through solution files. + +### Secrets + +Secrets are key-value pairs designed for storing sensitive information such as passwords, API keys, and connection strings. Unlike metadata, secrets are stored in the local file system and are not included in solution files for security reasons. + +**Hierarchy (from highest to lowest priority):** +1. **Kubernetes Profile Secrets** / **Run Profile Secrets** - Defined in profile settings (context-dependent) +2. **Solution Secrets** - Defined via *Solution Explorer* → right-click solution → *Manage Secrets* +3. **Global Secrets** - Defined via *Tools* → *Global Secrets* + +**Common Secret Keys:** + +| Key | Description | Used By | +|-----|-------------|---------| +| `wireGuardPassword` | Password for WireGuard VPN connection to Kubernetes cluster | Kubernetes integration | + +> Secrets are stored locally and are not shared with team members by default. Each developer needs to configure their own secrets. + ## ABP Studio vs .NET Terms Some ABP Studio terms may seem conflict with .NET and Visual Studio. To make them even more clear, you can use the following table. diff --git a/docs/en/studio/custom-commands.md b/docs/en/studio/custom-commands.md new file mode 100644 index 0000000000..af56492f37 --- /dev/null +++ b/docs/en/studio/custom-commands.md @@ -0,0 +1,128 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to create and manage custom commands in ABP Studio to automate build, deployment, and other workflows." +} +``` + +# Custom Commands + +````json +//[doc-nav] +{ + "Next": { + "Name": "Working with ABP Suite", + "Path": "studio/working-with-suite" + } +} +```` + +Custom commands allow you to define reusable terminal commands that appear in context menus throughout ABP Studio. You can use them to automate repetitive tasks such as building Docker images, installing Helm charts, running deployment scripts, or executing any custom workflow. + +> **Note:** This is an advanced feature primarily intended for teams working with Kubernetes deployments or complex build/deployment workflows. If you're developing a standard application without custom DevOps requirements, you may not need this feature. + +## Opening the Management Window + +To manage custom commands, right-click on the solution root in *Solution Explorer* and select *Manage Custom Commands*. + +![Custom Commands Management Window](images/custom-commands/management-window.png) + +The management window displays all defined commands with options to add, edit, or delete them. + +## Creating a New Command + +Click the *Add New Command* button to open the command editor dialog. + +![Create/Edit Custom Command](images/custom-commands/create-edit-command.png) + +## Command Properties + +| Property | Description | +|----------|-------------| +| **Command Name** | A unique identifier for the command (used internally) | +| **Display Name** | The text shown in context menus | +| **Terminal Command** | The PowerShell command to execute. Use `&&&` to chain multiple commands | +| **Working Directory** | Optional. The directory where the command runs (relative to solution path) | +| **Condition** | Optional. A [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) expression that determines when the command is visible | +| **Require Confirmation** | When enabled, shows a confirmation dialog before execution | +| **Confirmation Text** | The message shown in the confirmation dialog | + +## Trigger Targets + +Trigger targets determine where your command appears in context menus. You can select multiple targets for a single command. + +| Target | Location | +|--------|----------| +| **Helm Charts Root** | *Kubernetes* panel > *Helm* tab > root node | +| **Helm Main Chart** | *Kubernetes* panel > *Helm* tab > main chart | +| **Helm Sub Chart** | *Kubernetes* panel > *Helm* tab > sub chart | +| **Kubernetes Service** | *Kubernetes* panel > *Kubernetes* tab > service | +| **Solution Runner Root** | *Solution Runner* panel > profile root | +| **Solution Runner Folder** | *Solution Runner* panel > folder | +| **Solution Runner Application** | *Solution Runner* panel > application | + +## Execution Targets + +Execution targets define where the command actually runs. This enables cascading execution: + +- When you trigger a command from a **root or parent item**, it can recursively execute on all matching children +- For example: trigger from *Helm Charts Root* with execution target *Helm Sub Chart* → the command runs on each sub chart + +## Template Variables + +Commands support [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) template syntax for dynamic values. Use `{%{{{variable}}}%}` to insert context-specific data. + +### Available Variables by Context + +**Helm Charts:** + +| Variable | Description | +|----------|-------------| +| `profile.name` | Kubernetes profile name | +| `profile.namespace` | Kubernetes namespace | +| `chart.name` | Current chart name | +| `chart.path` | Chart directory path | +| `metadata.*` | Hierarchical metadata values (e.g., `metadata.imageName`) | +| `secrets.*` | Secret values (e.g., `secrets.registryPassword`) | + +**Kubernetes Service:** + +| Variable | Description | +|----------|-------------| +| `name` | Service name | +| `profile.name` | Kubernetes profile name | +| `profile.namespace` | Kubernetes namespace | +| `mainChart.name` | Parent main chart name | +| `chart.name` | Related sub chart name | +| `chart.metadata.*` | Chart-specific metadata | + +**Solution Runner (Root, Folder, Application):** + +| Variable | Description | +|----------|-------------| +| `profile.name` | Run profile name | +| `profile.path` | Profile file path | +| `application.name` | Application name (Application context only) | +| `application.baseUrl` | Application URL (Application context only) | +| `folder.name` | Folder name (Folder/Application context) | +| `metadata.*` | Profile metadata values | +| `secrets.*` | Profile secret values | + +## Example: Build Docker Image + +Here's an example command that builds a Docker image for Helm charts: + +**Command Properties:** +- **Command Name:** `buildDockerImage` +- **Display Name:** `Build Docker Image` +- **Terminal Command:** `./build-image.ps1 -ProjectPath {%{{{metadata.projectPath}}}%} -ImageName {%{{{metadata.imageName}}}%}` +- **Working Directory:** `etc/helm` +- **Trigger Targets:** Helm Charts Root, Helm Main Chart, Helm Sub Chart +- **Execution Targets:** Helm Main Chart, Helm Sub Chart +- **Condition:** `{%{{{metadata.projectPath}}}%}` + +This command: +1. Appears in the context menu of Helm charts root and all chart nodes +2. Executes on main charts and sub charts (cascading from root if triggered there) +3. Only shows for charts that have `projectPath` metadata defined +4. Runs the `build-image.ps1` script with dynamic parameters from metadata diff --git a/docs/en/studio/images/custom-commands/create-edit-command.png b/docs/en/studio/images/custom-commands/create-edit-command.png new file mode 100644 index 0000000000..7091516a8f Binary files /dev/null and b/docs/en/studio/images/custom-commands/create-edit-command.png differ diff --git a/docs/en/studio/images/custom-commands/management-window.png b/docs/en/studio/images/custom-commands/management-window.png new file mode 100644 index 0000000000..3a24ac8dc4 Binary files /dev/null and b/docs/en/studio/images/custom-commands/management-window.png differ diff --git a/docs/en/studio/images/monitoring-applications/tools-create.png b/docs/en/studio/images/monitoring-applications/tools-create.png index ac473d50da..7d7e7c8825 100644 Binary files a/docs/en/studio/images/monitoring-applications/tools-create.png and b/docs/en/studio/images/monitoring-applications/tools-create.png differ diff --git a/docs/en/studio/kubernetes.md b/docs/en/studio/kubernetes.md index ae23989f6c..eca8d83c5b 100644 --- a/docs/en/studio/kubernetes.md +++ b/docs/en/studio/kubernetes.md @@ -11,8 +11,8 @@ //[doc-nav] { "Next": { - "Name": "Working with ABP Suite", - "Path": "studio/working-with-suite" + "Name": "Custom Commands", + "Path": "studio/custom-commands" } } ```` @@ -100,7 +100,7 @@ It is the root of all subcharts. When you add a new main chart to the root, it i - `Install Chart(s)`: Installs the selected chart to the current profile. - `Uninstall Chart(s)`: Uninstalls the selected chart from the current profile. - `Properties`: It opens the *Chart Properties* window. You can see the chart information in the *Chart Info* tab. In the *Metadata* tab, you can add metadata for the selected main chart. It overrides the metadata in the profile. In the *Kubernetes Services* tab, you can relate a Kubernetes service with the main chart; however, since the main chart usually doesn't create kubernetes service, we can leave it empty. -- `Refrest Sub Charts`: Refreshes the subcharts of the selected main chart. +- `Refresh Sub Charts`: Refreshes the subcharts of the selected main chart. - `Open With`: You can open the selected chart with *Visual Studio Code* or *File Explorer*. - `Remove`: Removes the selected main chart from the solution. @@ -137,6 +137,11 @@ While connected, changing the current profile is not possible. Existing applicat ![connected](./images/kubernetes/connected.png) +When connected, you can right-click on a Kubernetes service to see the following context menu options: + +- `Browse`: Opens the [browser](./monitoring-applications.md#browse) and navigates to the Kubernetes service URL. This option is only visible if the service has a related Helm chart with matching *Kubernetes Services* regex pattern. +- `Enable Interception` / `Disable Interception`: Enables or disables traffic interception for the selected service. See the [Intercept a Service](#intercept-a-service) section for more details. + When you are connecting to a Kubernetes cluster, it automatically installs the WireGuard VPN to the Kubernetes cluster for a safe connection. You can specify the *wireGuardPassword* in the *Kubernetes Profile* -> *Secrets* tab or at a higher level such as *Solution Secrets* or *Global Secrets*. If you don't provide a password, it generates a random password and stores it in the *Kubernetes Profile* -> *Secrets*. However, if you try to connect to a cluster that already installed WireGuard VPN, then you should give the same password; otherwise, it won't connect. To see the random password, you can click the *eye* icon in the *Kubernetes Profile* -> *Secrets* tab. ![wireGuardPassword](./images/kubernetes/wireGuardPassword.png) @@ -205,46 +210,12 @@ When you connect to a Kubernetes cluster, it uses the selected profile for Kuber ## Advanced Topics -### Adding a Custom Command - -Custom commands can be added to both the *Helm* and *Kubernetes* tabs within the *Kubernetes* panel. For instance, when [redeploy](#redeploy-a-chart) a chart, it involves building the Docker image and reinstalling it. However, if you are working with a different Kubernetes cluster than Docker Desktop, you'll need to push the Docker image to the registry before the installation process. This can be achieved by incorporating a custom command into the *Kubernetes services*. Custom commands can be added to the *Chart Root*, *Main Chart*, and *Subchart* in the *Helm* tab, as well as to the *Service* in the *Kubernetes* tab. - -To do that, open the ABP Solution (*.abpsln*) file with *Visual Studio Code* it's a JSON file and you'll see the existing commands in the `commands` section. Before adding a new command, create a powershell script in the `abp-solution-path/etc/helm` folder. For example, we create a `push-image.ps1` script to push the docker image to the registry. Then, add the following command to the `commands` section. - -```JSON - "kubernetesRedeployWithPushImage": { - "triggerTargets": [ - "KUBERNETES_SERVICE" - ], - "executionTargets": [ - "KUBERNETES_SERVICE" - ], - "displayName": " Redeploy with Push Image", - "workingDirectory": "etc/helm", - "terminalCommand": "./build-image.ps1 -ProjectPath {%{{{chart.metadata.projectPath}}}%} -ImageName {%{{{chart.metadata.imageName}}}%} -ProjectType {%{{{chart.metadata.projectType}}}%} &&& ./push-image.ps1 -ImageName {%{{{chart.metadata.imageName}}}%} &&& ./install.ps1 -ChartName {%{{{mainChart.name}}}%} -Namespace {%{{{profile.namespace}}}%} -ReleaseName {%{{{mainChart.name}}}%}-{%{{{profile.name}}}%} -DotnetEnvironment {%{{{mainChart.metadata.dotnetEnvironment}}}%}", - "requireConfirmation": "true", - "confirmationText": "Are you sure to redeploy with push image the related chart '{%{{{chart.name}}}%}' for the service '{%{{{name}}}%}'?", - "condition": "{%{{{chart != null && chart.metadata.projectPath != null && chart.metadata.imageName != null && chart.metadata.projectType != null}}}%}" - } -``` +### Custom Commands + +You can add custom commands to context menus in both the *Helm* and *Kubernetes* tabs. This is useful for automating workflows like pushing Docker images to a registry before installation, or running custom deployment scripts. -Once the command is added, reload the solution from *File* -> *Reload Solution* in the toolbar. After reloading, you will find the *Redeploy with Push Image* command in the context-menu of the service. +Custom commands can be added to the *Chart Root*, *Main Chart*, and *Sub Chart* in the *Helm* tab, as well as to the *Service* in the *Kubernetes* tab. ![redeploy-push-image](./images/kubernetes/redeploy-push-image.png) -The JSON object has the following properties: - -- `triggerTargets`: Specifies the trigger targets for the command. The added command will appear in these targets. You can add one or more trigger targets, accepting values such as *HELM_CHARTS_ROOT*, *HELM_MAIN_CHART*, *HELM_SUB_CHART* and *KUBERNETES_SERVICE*. -- `executionTargets`: Specifies the execution targets for the command. When executing the command on a root item, it will recursively execute the command for all children. Acceptable values include *HELM_CHARTS_ROOT*, *HELM_MAIN_CHART*, *HELM_SUB_CHART*, and *KUBERNETES_SERVICE*. -- `displayName`: Specifies the display name of the command. -- `workingDirectory`: Specifies the working directory of the command. It's relative to the solution path. -- `terminalCommand`: Specifies the terminal command for the custom command. The `&&&` operator can be used to run multiple commands in the terminal. Utilize the [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) syntax to access input data, which varies based on the execution target. -- `requireConfirmation`: Specifies whether the command requires confirmation message before execution. Acceptable values include *true* and *false*. -- `confirmationText`: Specifies the confirmation text for the command. Utilize the [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) syntax to access input data, which varies based on the execution target. -- `condition`: Specifies the condition for the command. If the condition returns *false*, it skips the current item and attempts to execute the command for the next item or child item. Utilize the [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) syntax to access input data, which varies based on the execution target. - -You can use the following variables in the scriban syntax based on the execution target: - - `HELM_CHARTS_ROOT`: *profile*, *metadata*, *secrets* - - `HELM_MAIN_CHART`: *profile*, *chart*, *metadata*, *secret* - - `HELM_SUB_CHART`: *profile*, *chart*, *metadata*, *secret* - - `KUBERNETES_SERVICE`: *name*, *profile*, *mainChart*, *chart*, *metadata*, *secret* +For detailed information on creating and managing custom commands, see the [Custom Commands](custom-commands.md) documentation. diff --git a/docs/en/studio/monitoring-applications.md b/docs/en/studio/monitoring-applications.md index 75390bf904..083a0d384f 100644 --- a/docs/en/studio/monitoring-applications.md +++ b/docs/en/studio/monitoring-applications.md @@ -66,91 +66,318 @@ In this tab, you can view comprehensive overall information. You have the option ![overall](./images/monitoring-applications/overall.png) -In the data grid, details for each application are displayed. It's possible to sort rows by columns. When selecting a row, you can right-click to access the context menu, offering various actions. This menu allows for opening related tabs that are filtered by the selected application. +In the data grid, details for each application are displayed. It's possible to sort rows by columns. - `Name`: The name of the application. -- `State`: The state of the application. It can take on several values such as *Scheduled*, *Starting*, *Started*, *Stopping* and *Stopped*. In the event of an application crash during its starting, the state is mark as *Scheduled*, we can cancel the starting process at that stage. -- `Health` : The health state of the application. Clicking on the icon shows the latest health check response. Displays `N/A` if the application is not running or health check is not configured for the application. -- `Instances`: Indicates the count of running instances for the application. This value is particularly helpful when scaling the application within a Kubernetes, providing visibility into the number of currently active instances. +- `State`: The state of the application. It can take on several values such as *Scheduled*, *Starting*, *Started*, *Stopping* and *Stopped*. The *Scheduled* state indicates the application is waiting for an automatic restart (e.g., after a crash or when watch mode detects changes). You can cancel the scheduled restart at this stage. +- `Health`: The health state of the application. The icon indicates the current health status: *Healthy* (green), *Unhealthy* (red), *Degraded* (yellow), or *Unknown* (gray). Clicking on the icon shows the latest health check response in JSON format. Displays `N/A` if the application is not running or health check is not configured for the application. +- `Instances`: Indicates the count of running instances for the application. This value is particularly helpful when scaling the application within a Kubernetes cluster, providing visibility into the number of currently active pods. - `Uptime`: The time elapsed since the application started. - `Requests`: The number of HTTP requests received by the application. -- `Events (R/S)`: The number of [Distributed Event](../framework/infrastructure/event-bus/distributed) received or sent by the application. +- `Events (R/S)`: The number of [Distributed Events](../framework/infrastructure/event-bus/distributed) received or sent by the application. - `Exceptions`: The number of exceptions thrown by the application. - `Actions`: The actions that can be performed on the application. You can start and stop the application. -> For the events system, you can exclusively view the [Distributed Events](../framework/infrastructure/event-bus/distributed). Generally, the [Local Events](../framework/infrastructure/event-bus/distributed) is not included. +### Context Menu Actions + +When selecting a row, you can right-click to access the context menu with the following actions: + +| Action | Description | +|--------|-------------| +| **Start / Stop** | Start or stop the selected application. | +| **Restart** | Restart the application (available when the application is running). | +| **Build** | Build options including *Build*, *Graph Build*, *Restore*, and *Clean* (available for C# applications when stopped). | +| **Browse** | Open the application in the [Browse](#browse) tab (available when a Launch URL is configured). | +| **Health Status** | Submenu with *Browse Health UI* and *Show Latest Health Check Response* options. | +| **Requests** | Open the [HTTP Requests](#http-requests) tab filtered by this application. | +| **Exceptions** | Open the [Exceptions](#exceptions) tab filtered by this application. | +| **Logs** | Open the [Logs](#logs) tab with this application selected. | +| **Copy Url** | Copy the application's URL to the clipboard. | +| **Properties** | Open the application properties dialog. | + +> For the events system, you can exclusively view the [Distributed Events](../framework/infrastructure/event-bus/distributed). Generally, the [Local Events](../framework/infrastructure/event-bus/local) are not included. ## Browse -ABP Studio includes a browser tool that allows access to websites and running applications. You can open new tabs to browse different websites or view active applications. It's a convenient utility to access websites and applications without leaving ABP Studio. Clicking the *Browse* tab displays the running applications and an *Open new tab* button. +ABP Studio includes a built-in browser that allows access to websites and running applications. You can open new tabs to browse different websites or view active applications. It's a convenient utility to access websites and applications without leaving ABP Studio. Clicking the *Browse* tab displays the running applications and an *Open new tab* button. ![browse](./images/monitoring-applications/browse.png) -You can open the *Browse* tabs as many times as you want. It's possible to open the same application in several tabs simultaneously. To open an application, navigate through *Solution Runner* -> *C# or CLI Application* -> *Browse*. This option is only visible when there is a [Launch URL](./running-applications.md#properties). Additionally, you can access any URL by entering it into the address bar. +You can open the *Browse* tabs as many times as you want. It's possible to open the same application in several tabs simultaneously. To open an application, you can: + +- Double-click on an application in the *Solution Runner* tree. +- Right-click on an application and select *Browse* from the context menu. +- Click on a running application in the application list shown in the *Browse* tab. + +These options are only available when the application has a [Launch URL](./running-applications.md#properties) configured. Additionally, you can access any URL by entering it into the address bar. ![browse-2](./images/monitoring-applications/browse-2.png) -When you click the *Dev Tools* button it opens the [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools) for the selected tab. +### Browser Toolbar + +The browser toolbar provides the following controls: + +| Control | Description | +|---------|-------------| +| **Back / Forward** | Navigate through your browsing history within the tab. | +| **Refresh** | Reload the current page. | +| **Address Bar** | Enter any URL to navigate directly. The address bar shows the current URL and allows you to navigate to any website. | +| **Dev Tools** | Opens the [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools) for the selected tab, allowing you to inspect elements, debug JavaScript, and analyze network requests. | +| **Clear Cookies** | Clears all cookies for the currently selected tab, useful for testing authentication flows or resetting session state. | +| **Open in External Browser** | Opens the current URL in your system's default web browser. | ![dev-tools](./images/monitoring-applications/dev-tools.png) +### Default Credentials Notification + +When browsing certain applications (such as AppHost dashboards), ABP Studio displays a notification bar with default credentials. This helps you quickly log in during development. You can dismiss this notification permanently by clicking the *Don't show again* option. + ## HTTP Requests Within this tab, you can view all *HTTP Requests* received by your C# applications. You have the option to filter requests based on URLs by using the search textbox or by selecting a particular application from the combobox. The *Clear Requests* button removes all received requests. Moreover, you have the ability to sort requests by columns. ![http-requests](./images/monitoring-applications/http-requests.png) -Clicking on a row enables you to view the details of each HTTP request; `URL`, `Method`, `Status Code`, `Timestamp`, `Headers (Request, Response)`, `Request (Payload)` and `Response`. +### Request List Columns + +The request list displays the following information: + +| Column | Description | +|--------|-------------| +| **Timestamp** | When the request was received (displayed as HH:mm:ss). | +| **Application** | The name of the application that received the request. | +| **Path** | The request URL path and query string. | +| **Method** | The HTTP method (GET, POST, PUT, DELETE, etc.). | +| **Status Code** | The HTTP response status code. | +| **Duration** | The time taken to process the request in milliseconds. | +| **Request Body Size** | The size of the request payload. | +| **Response Body Size** | The size of the response payload. | + +### Request Details + +Clicking on a row enables you to view the details of each HTTP request: + +- **URL**: The full request URL. +- **Method**: The HTTP method used. +- **Status Code**: The response status code. +- **Duration**: Request processing time in milliseconds. +- **Timestamp**: When the request was received. +- **Headers (Request)**: All request headers sent by the client. +- **Headers (Response)**: All response headers returned by the server. +- **Request (Payload)**: The request body with size information. +- **Response**: The response body with size information. ![http-requests-details](./images/monitoring-applications/http-requests-details.png) -You can format the JSON content by clicking the *Format* button. +### Formatting JSON Content + +Both request and response payloads have a *Format* button that formats JSON content for better readability. This is available when the content type is `application/json`. ![http-requests-details-json](./images/monitoring-applications/http-requests-details-json.png) -Furthermore, by clicking the gear icon in the *HTTP Requests* tab, you can access the *Solution Runner HTTP Requests Options* window. Within the *Ignored URLs* tab, you have the ability to exclude particular URLs by applying a regex pattern. Excluded URLs won't be visible in the *HTTP Requests* tab. By default, the metrics URL is already ignored. You can add or remove items as needed. +### Quick URL Filter + +When viewing request details, you can right-click and select *Filter Selected URL* to quickly apply the current URL as a filter. This is useful when you want to see all requests to a specific endpoint. + +### Configuring Ignored URLs + +By clicking the gear icon in the *HTTP Requests* tab, you can access the *Solution Runner HTTP Requests Options* window. Within the *Ignored URLs* tab, you have the ability to exclude particular URLs by applying a regex pattern. Excluded URLs won't be visible in the *HTTP Requests* tab. By default, the metrics URL is already ignored. You can add or remove items as needed. ![http-requests-options](./images/monitoring-applications/http-requests-options.png) -> After adding a new URL, it will only affect subsequent requests. +> After adding a new URL pattern, it will only affect subsequent requests. Existing requests will remain visible. ## Events -In this tab, you can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by your C# applications. You can filter them by [Event Name](../framework/infrastructure/event-bus/distributed#event-name) using the search textbox or by selecting a specific application. Additionally, you can choose the *Direction* (Received/Send) and *Source* (Direct/Inbox/Outbox) of events. The *Clear Events* button removes all events. +In this tab, you can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by your C# applications. You can filter them by [Event Name](../framework/infrastructure/event-bus/distributed#event-name) using the search textbox or by selecting a specific application. Additionally, you can choose the *Direction* (Received/Sent) and *Source* (Direct/Inbox/Outbox) of events. The *Clear Events* button removes all events. ![events](./images/monitoring-applications/events.png) -> In the *Direction* section, there are two options: *Received*, indicating events received by the application, and *Sent*, indicating events sent by the application. Within the *Source* section, three options are available, and their significance comes into play when utilizing the [Inbox/Outbox pattern](../framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events). *Inbox* refers to events received by the application, *Outbox* refers to events sent by the application and *Direct* signifies events sent or received by the application without involving Inbox/Outbox pattern. +### Direction and Source Filters + +| Filter | Options | Description | +|--------|---------|-------------| +| **Direction** | *Received*, *Sent* | Filter by whether events were received or sent by the application. | +| **Source** | *Direct*, *Inbox*, *Outbox* | Filter by event source. Relevant when using the [Inbox/Outbox pattern](../framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events). | + +- **Direct**: Events sent or received without using the Inbox/Outbox pattern. +- **Inbox**: Events received through the transactional inbox. +- **Outbox**: Events sent through the transactional outbox. -Clicking on a row enables you to view the details of each event; `Application`, `Event Name`, `Direction`, `Source`, `Timestamp` and `Event Data`. +### Event Details + +Clicking on a row enables you to view the details of each event: + +- **Application**: The application that sent or received the event. +- **Event Name**: The full event type name. +- **Direction**: Whether the event was received or sent. +- **Source**: The event source (Direct, Inbox, or Outbox). +- **Timestamp**: When the event was processed. +- **Event Data**: The event payload in JSON format. ![event-details](./images/monitoring-applications/event-details.png) +### Formatting Event Data + +The *Event Data* section includes a *Format* button that formats the JSON content for better readability. This makes it easier to inspect complex event payloads. + +> ABP Studio automatically decodes Base64-encoded event data. If your event bus uses Base64 encoding for message transport, the data will be displayed in its decoded, readable form. + ## Exceptions -This tab displays all exceptions by your C# applications. You can apply filters using the search textbox based on *Message*, *Source*, *ExceptionType*, and *StackTrace* or by choosing a specific application. Additionally, you have the option to select the [Log Level](../framework/fundamentals/exception-handling.md#log-level) for adding a filter. To clear all exceptions, use the *Clear Exceptions* button. +This tab displays all exceptions thrown by your C# applications. You can apply filters using the search textbox based on *Message*, *Source*, *ExceptionType*, and *StackTrace* or by choosing a specific application. Additionally, you have the option to select the [Log Level](../framework/fundamentals/exception-handling.md#log-level) for filtering. To clear all exceptions, use the *Clear Exceptions* button. ![exceptions](./images/monitoring-applications/exceptions.png) -Click on a row to inspect the details of each exception; `Application`, `Exception Type`, `Source`, `Timestamp`, `Level`, `Message` and `StackTrace`. +### Exception List + +The exception list shows a summary of each exception: + +- **Exception Type**: The short exception type name (e.g., `NullReferenceException` instead of `System.NullReferenceException`). +- **Message**: A truncated version of the exception message. +- **Timestamp**: When the exception occurred. +- **Level**: The log level (Error, Warning, etc.). + +### Exception Details + +Click on a row to inspect the full details of each exception: + +- **Application**: The application where the exception occurred. +- **Exception Type**: The full exception type name including namespace. +- **Source**: The source file or component where the exception originated. +- **Timestamp**: When the exception was thrown. +- **Level**: The log level associated with this exception. +- **Message**: The complete exception message. +- **StackTrace**: The full stack trace showing the call hierarchy. ![exception-details](./images/monitoring-applications/exception-details.png) +### Inner Exceptions + +If an exception contains inner exceptions, they are displayed hierarchically in the details panel. This allows you to trace the root cause of wrapped exceptions, which is common in scenarios like database errors wrapped in application-level exceptions. + ## Logs -The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*. When you select a *Log Level* it shows selected log level and higher log levels. For example, if you select *Warning* it shows *Warning*, *Error* and *Critical* logs. To clear selected application logs, use the *Clear Logs* button. If *Auto Scroll* is checked, the display automatically scrolls when new logs are received. +The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application from the dropdown. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*. ![logs](./images/monitoring-applications/logs.png) +### Log Level Filtering + +When you select a *Log Level*, it shows the selected level and all higher severity levels: + +| Selected Level | Shows | +|---------------|-------| +| **Trace** | Trace, Debug, Information, Warning, Error, Critical | +| **Debug** | Debug, Information, Warning, Error, Critical | +| **Information** | Information, Warning, Error, Critical | +| **Warning** | Warning, Error, Critical | +| **Error** | Error, Critical | +| **Critical** | Critical only | +| **None** | All logs (no filtering) | + +### Log Display Features + +- **Color Coding**: Log entries are color-coded by level for quick visual identification. Errors and critical logs stand out with distinct colors. +- **Auto Scroll**: When enabled, the display automatically scrolls to show new logs as they arrive. This is useful for real-time monitoring. +- **Clear Logs**: Clears the logs for the currently selected application only. Other applications' logs remain intact. +- **Text Filter**: Search within log messages using the search textbox. The filter is applied in real-time with a slight delay for performance. + ## Tools -The *Tools* tab allows you to easily access to the user interfaces of the tools you are using. A *tool* may be related with a docker container, or independent. If it is related with a container (ex: *grafana*), the tool is opened when the container is up. If the tool is independent, it will be always opened. +The *Tools* tab provides quick access to web-based management interfaces for infrastructure services like Grafana, RabbitMQ, pgAdmin, and Redis Commander. Each tool opens in a dedicated browser tab within ABP Studio, eliminating the need to switch between external browser windows. ![tools](./images/monitoring-applications/tools-overview.png) -The microservice template comes with pre-defined tools to display related container user interfaces. You can edit existing tools, add new tools or delete existing tools. +The microservice template includes pre-configured tools for common infrastructure services. You can customize these tools or add new ones based on your project requirements. -In the example below, a new tool named `My Application Status` will be added to the tools and it will display the URL in the input: +### Adding a New Tool + +To add a new tool, click the *+* button in the *Tools* tab. This opens the *Create Tool* dialog where you can configure the tool properties. ![tools-create](./images/monitoring-applications/tools-create.png) +### Tool Properties + +Each tool has the following configurable properties: + +| Property | Required | Description | +|----------|----------|-------------| +| **Name** | Yes | A unique identifier displayed as the tab header. | +| **URL** | Yes | The web interface URL (e.g., `http://localhost:3000`). | +| **Related Container** | No | Docker container name. When set, the tool activates only when this container is running. | +| **Related Kubernetes Service** | No | A regex pattern to match Kubernetes service names for automatic URL switching. | +| **Related Kubernetes Service Port** | No | The port to use when connecting via Kubernetes service. | + +### Editing and Removing Tools + +- **Edit**: Right-click on a tool tab and select *Edit* to modify its properties. +- **Remove**: Right-click on a tool tab and select *Close* to remove it from the profile. +- **Clear Cookies**: Right-click on a tool tab and select *Clear Cookies* to reset the browser session for that tool. + +### Tool Activation States + +Tools can be in different activation states depending on their configuration: + +| State | Condition | Behavior | +|-------|-----------|----------| +| **Always Active** | No *Related Container* specified | Tool is always accessible regardless of container state. | +| **Container-Dependent** | *Related Container* specified | Tool activates only when the specified Docker container is running. | +| **Kubernetes-Aware** | *Related Kubernetes Service* specified | Tool URL switches between local and Kubernetes endpoints automatically. | + +### Kubernetes Integration + +When you specify a *Related Kubernetes Service*, the tool gains the ability to seamlessly switch between local and Kubernetes environments. This is particularly useful for microservice development where you run some services locally while others remain in a Kubernetes cluster. + +**Automatic URL Switching:** + +1. **Local Mode**: When the *Related Container* is running, the tool uses the configured *URL* (e.g., `http://localhost:3000`). +2. **Kubernetes Mode**: When the container stops and you're [connected to a Kubernetes cluster](./kubernetes.md#connecting-to-a-kubernetes-cluster), the tool automatically redirects to the matching Kubernetes service. +3. **Pattern Matching**: The *Related Kubernetes Service* accepts regex patterns. For example, `.*-grafana` matches any service name ending with `-grafana`. + +> This automatic switching eliminates the need to manually update URLs when transitioning between local development and Kubernetes-based testing. + +### Run Profile Configuration + +Tools are persisted in the Run Profile file (`.abprun.json`). Below is an example configuration with common infrastructure tools: + +```json +{ + "tools": { + "grafana": { + "url": "http://localhost:3000", + "relatedContainer": "grafana", + "relatedKubernetesService": ".*-grafana", + "relatedKubernetesServicePort": 3000 + }, + "rabbitmq": { + "url": "http://localhost:15672", + "relatedContainer": "rabbitmq", + "relatedKubernetesService": ".*-rabbitmq", + "relatedKubernetesServicePort": 15672 + }, + "redis-commander": { + "url": "http://localhost:8081", + "relatedContainer": "redis-commander" + }, + "pgadmin": { + "url": "http://localhost:5050", + "relatedContainer": "pgadmin" + }, + "seq": { + "url": "http://localhost:5341" + } + } +} +``` + +### Default Credentials + +Some tools display a notification bar with default credentials when opened for the first time: + +| Tool | Username | Password | +|------|----------|----------| +| Grafana | `admin` | `admin` | +| RabbitMQ | `guest` | `guest` | + +> You can dismiss this notification permanently by clicking the *Don't show again* option. diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index d2eba88e0f..13e206127e 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -9,7 +9,14 @@ 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. -## 2.1.3 (2025-12-15) Latest +## 2.1.4 (2025-12-30) Latest + +* Fixed books sample for blazor-webapp tiered solution. +* Fixed K8s cluster deployment issues for microservices. +* Fixed docker build problem on microservice template. +* Showed logs of the executed tasks. + +## 2.1.3 (2025-12-15) * Updated `createCommand` and CLI help for multi-tenancy. * Fixed `BookController` templating problem. diff --git a/docs/en/studio/running-applications.md b/docs/en/studio/running-applications.md index c922c1b501..1a0b22ae3c 100644 --- a/docs/en/studio/running-applications.md +++ b/docs/en/studio/running-applications.md @@ -77,7 +77,9 @@ We can use common [dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools) ### Add -We can add 3 different item type to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application` and `Folder`. +We can add 4 different item types to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application`, `Docker Container`, and `Folder`. + +> Note: The `Docker Container` option is only available when right-clicking the profile root. When right-clicking a folder, only `C# Application`, `CLI Application`, and `Folder` options are available. ![profile-root-context-menu-add](images/solution-runner/profile-root-context-menu-add.png) @@ -181,13 +183,15 @@ You can see the context menu by right-clicking *Folder*. It will start/stop all ## C# Application -The .NET icon indicates that the application is a C# project. After we [add](#c-application) the C# applications to the root of the tree or folder, we can go to any C# application and right-click to view the context menu; `Start`, `Build`, `Browse`, `Requests`, `Exceptions`, `Logs`, `Copy URL`, `Properties`, `Remove`. +The .NET icon indicates that the application is a C# project. After we [add](#c-application) the C# applications to the root of the tree or folder, we can go to any C# application and right-click to view the context menu; `Start`, `Stop`, `Restart`, `Build`, `Browse`, `Health Status`, `Requests`, `Exceptions`, `Logs`, `Copy URL`, `Properties`, `Remove`, and `Open with`. ![csharp-application-context-menu](images/solution-runner/csharp-application-context-menu.png) -### Start +### Start / Stop / Restart -Starts the selected application. Once it is started, *Stop* and *Restart* options will be available. +- **Start**: Starts the selected application. +- **Stop**: Stops the running application. +- **Restart**: Restarts the running application. This option is only available when the application is started. > When you start the C# application, you should see a *chain* icon next to the application name, that means the started application connected to ABP Studio. C# applications can connect to ABP Studio even when running from outside the ABP Studio environment, for example debugging with Visual Studio. If the application is run from outside the ABP Studio environment, it will display *(external)* information next to the chain icon. @@ -199,14 +203,17 @@ It's the [similar](#build) options like root of the tree options. The only diffe ### Monitoring -When the C# application is connected to ABP Studio, it starts sending telemetry information to see in one place. We can easily click these options to see the detail; `Browse`, `Requests`, `Exceptions` and `Logs`. +When the C# application is connected to ABP Studio, it starts sending telemetry information to see in one place. We can easily click these options to see the detail; `Browse`, `Health Status`, `Requests`, `Exceptions`, `Events` and `Logs`. ![csharp-application-context-menu-monitor](images/solution-runner/csharp-application-context-menu-monitor.png) - `Browse`: ABP Studio includes a browser tool for accessing websites and running applications. You can click this option to view the application in the ABP Studio browser. However, this option is only accessible if the application is started. -- Health Status : If Health Check endpoints are defined, it allows you to browse Health UI and see the latest health check response. +- `Health Status`: A submenu that provides health monitoring options when Health Check endpoints are defined: + - **Browse Health UI**: Opens the Health UI page of the application in the built-in browser. + - **Show Latest Health Check Response**: Displays the latest health check response in JSON format. - `Requests`: It opens the *HTTP Requests* tab with adding the selected application filter. You can view all *HTTP Requests* received by your applications. - `Exceptions`: We can display all exceptions on this tab. It opens the *Exceptions* tab with selected application. +- `Events`: Opens the *Events* tab filtered by this application. You can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by this application. - `Logs`: Clicking this option opens the *Logs* tab with adding the selected application filter. ### Properties @@ -215,16 +222,35 @@ We can open the *Application Properties* window to change *Launch url*, *Health ![solutioın-runner-properties](images/solution-runner/solutioın-runner-properties.png) -- **Health check endpoint**: Endpoint for controlling the health status of the application periodically. If the application doesn't have a endpoint for health check, you can enter `/` to use the home page of the application as health check endpoint. +- **Launch URL**: The URL used when browsing the application. This is the address where the application is accessible. +- **Kubernetes service**: A regex pattern to match Kubernetes service names. When connected to a Kubernetes cluster, ABP Studio uses this pattern to find the corresponding Kubernetes service and uses its URL instead of the Launch URL. This applies to *Browse*, *Copy URL*, and *Health UI* features. For example, if your Helm chart creates a service named `bookstore-identity-service`, you can use `.*-identity-service` or `bookstore-identity.*` as the pattern. For [microservice](../get-started/microservice.md) templates, this is pre-configured. +- **Health check endpoint**: Endpoint for controlling the health status of the application periodically. If the application doesn't have an endpoint for health check, you can enter `/` to use the home page of the application as health check endpoint. - **Health UI endpoint**: Endpoint of the Health UI page of the application. -- **Skip build before starting**: When enabled, application is started without build and it makes starting faster. This is useful when you are working on a single application out of multiple, so you don't need to build others everytime they start. -- **Watch changes while running**: When enabled, you should see an *eye* icon next to the application name. +- **Skip build before starting**: When enabled, the application is started without building, making startup faster. This is useful when you are working on a single application out of multiple, so you don't need to build others every time they start. +- **Watch changes while running**: When enabled, changes in your code are watched and dotnet hot-reloads the application or restarts it if needed. You should see an *eye* icon next to the application name when this is enabled. +- **Open browser on start**: When enabled, the application is automatically opened in the Browse tab when it starts. +- **Auto refresh browser on restart**: When enabled, browser tabs showing this application are automatically refreshed when the application restarts. +- **Runnable**: Controls whether the application can be started from Solution Runner. When disabled, the application will not be included in batch start operations. ![csharp-application-context-menu-run-connection](images/solution-runner/csharp-application-context-menu-run-connection.png) +### Open with + +The *Open with* submenu provides options to open the application project in external tools: + +- **Visual Studio**: Opens the project in Visual Studio (available if installed). +- **Visual Studio Code**: Opens the project folder in Visual Studio Code (available if installed). +- **JetBrains Rider**: Opens the project in JetBrains Rider (available if installed). +- **Terminal**: Opens a terminal window in the project directory. +- **Explorer / Finder**: Opens the project folder in the system file explorer. + +### Custom Commands + +You can add custom commands that appear in the context menu of Solution Runner items (root, folders, and applications). These commands allow you to automate custom workflows and scripts. For details on creating and managing custom commands, see the [Custom Commands](custom-commands.md) documentation. + ### Miscellaneous -- We can copy the selected application *Browse* URL with *Copy URL*. It copies the *Browse* URL instead *Launch URL* since we could connected to *Kubernetes* service. +- We can copy the selected application *Browse* URL with *Copy URL*. It copies the *Browse* URL instead of *Launch URL* since we could be connected to a *Kubernetes* service. - You can change the target framework by right-click the selected application and change the *Target Framework* option. This option visible if the project has multiple target framework such as MAUI applications. - To remove an application from the tree, open the context menu by right-clicking the application and selecting *Remove*. diff --git a/docs/en/studio/solution-explorer.md b/docs/en/studio/solution-explorer.md index 21dde505e5..edbac3b471 100644 --- a/docs/en/studio/solution-explorer.md +++ b/docs/en/studio/solution-explorer.md @@ -33,6 +33,9 @@ It is the main solution that you can open with ABP Studio, an ABP solution can c - `New Folder`: Creates a new folder within the ABP Solution, allowing you to organize your modules. - `New Module`: Allows you to create a new module. - `Existing Module`: You can add existing module to your solution. +- `Analyze`: Analyzes all modules and packages in the solution to extract information like aggregate roots, application services, permissions, etc. + - `Analyze`: Analyzes all modules and packages using existing build outputs. + - `Build & Analyze`: Builds the solution first, then analyzes all modules and packages. - `Rename`: Renames the solution. - `Manage Secrets`: You can edit your solution secrets. - `Manage Metadata`: You can edit your solution metadata. @@ -52,6 +55,9 @@ It is the main solution that you can open with ABP Studio, an ABP solution can c - `Clean`: Cleans the output of the previous build for modules. - `Restore`: Restores the dependencies for modules. - `Open With` + - `Visual Studio`: Opens the solution in Visual Studio. This option is only available if you have Visual Studio installed. + - `Visual Studio Code`: Opens the solution in Visual Studio Code. This option is only available if you have Visual Studio Code installed. + - `JetBrains Rider`: Opens the solution in JetBrains Rider. This option is only available if you have JetBrains Rider installed. - `Terminal`: Opens the terminal in the solution directory. - `Explorer`: Opens the file explorer in the solution directory. - `Solution Configuration`: You can see the project creation options in this menu. It opens the *Solution Configuration* window. @@ -73,6 +79,9 @@ You can click the *OK* button to add the folder to the solution. When you right- - `New Folder`: Creates a new nested folder within the selected folder, allowing you to organize your modules. - `New Module`: Allows you to create a new module to selected folder. - `Existing Module`: You can add existing module to your selected folder. +- `Analyze`: Analyzes all modules and packages in the selected folder. + - `Analyze`: Analyzes all modules and packages using existing build outputs. + - `Build & Analyze`: Builds first, then analyzes all modules and packages in the folder. - `Rename`: Renames the selected folder. - `Delete`: Deletes the selected folder and all child items from the solution. - `ABP CLI` @@ -102,6 +111,9 @@ A [module](./concepts.md#module) is a sub-solution that can contains zero, one o - `New Package`: Creates a new package within the selected module. - `Existing Package`: You can add existing package to your selected module. - `Folder`: Creates a new folder within the selected module, allowing you to organize your packages. +- `Analyze`: Analyzes all packages in the selected module. + - `Analyze`: Analyzes all packages using existing build outputs. + - `Build & Analyze`: Builds first, then analyzes all packages in the module. - `Import Module`: This option allows you to import an existing module from *Solution*, *Local*, or *NuGet* into your selected module. - `Rename`: Renames the selected module. - `Remove`: Removes the selected module and all child items from the solution. @@ -126,6 +138,7 @@ A [module](./concepts.md#module) is a sub-solution that can contains zero, one o - `JetBrains Rider`: Opens the module in JetBrains Rider. This option is only available if you have JetBrains Rider installed. - `Terminal`: Opens the terminal in the module directory. - `Explorer`: Opens the file explorer in the module directory. +- `Open Readme`: Opens the README file in the module if available. This option is only visible if the module has a README file. - `Upgrade to Pro`: This will be visible only when you purchased a license but still using the modules came with open-source (free) license. For more details, check out [Migrating from Open Source Templates](../guides/migrating-from-open-source.md) document. This is not shown in the screenshot above. ### Adding a New Empty Module @@ -238,10 +251,18 @@ A [package](./concepts.md#package) is a project that can be added to a module, a ![package-context-menu](./images/solution-explorer/package-context-menu.png) -- `Add Package Reference`: This option allows you to add a package reference to the selected package. +- `Add`: This menu is only visible for Host projects (except Blazor WebAssembly). + - `NPM Package`: Adds an NPM package reference to the selected package. +- `Add Package Reference`: This option allows you to add a package reference to the selected package. This option is only visible for non-Host projects. +- `Analyze`: Analyzes the selected package to extract information like aggregate roots, application services, permissions, etc. This menu is only visible for analyzable package types. + - `Analyze`: Analyzes the package using existing build outputs. + - `Build & Analyze`: Builds first, then analyzes the package. - `Reload`: Reloads the selected package. - `Remove`: Removes the selected package from the module. - `ABP CLI` + - `Generate Proxy`: Generates client-side proxy code for the selected package. + - `Javascript`: Generates JavaScript proxy code. + - `C#`: Generates C# proxy code. - `Install Libs`: Install NPM Packages for UI projects in your selected package. - `Upgrade ABP Packages`: Update all the ABP related NuGet and NPM packages in your selected package. - `Switch to`: It switches your selected package to the specified version of the ABP. @@ -264,6 +285,7 @@ A [package](./concepts.md#package) is a project that can be added to a module, a - `JetBrains Rider`: Opens the package in JetBrains Rider. This option is only available if you have JetBrains Rider installed. - `Terminal`: Opens the terminal in the package directory. - `Explorer`: Opens the file explorer in the package directory. +- `Open Readme`: Opens the README file in the package if available. This option is only visible if the package has a README file. ### Adding a New Package diff --git a/docs/en/studio/working-with-suite.md b/docs/en/studio/working-with-suite.md index b436b7b0f8..2dc3a4bcc0 100644 --- a/docs/en/studio/working-with-suite.md +++ b/docs/en/studio/working-with-suite.md @@ -39,7 +39,10 @@ If There are more than one module which is openable via Suite, Studio will ask y ### Opening from context menu -Alternatively, you can right click to a module in [Solution Explorer](solution-explorer.md) and click `ABP Suite` to open it with Suite. Suite will automatically open `Crud Page Generation` screen with the selected module. +You can right-click on the solution root or a module in [Solution Explorer](solution-explorer.md) and click `ABP Suite` to open it with Suite. + +- **Solution root**: If you right-click on the solution root and select `ABP Suite`, Studio will ask you to pick a module if there are multiple modules. Suite will then open with the selected module. +- **Module**: If you right-click on a specific module and select `ABP Suite`, Suite will automatically open `Crud Page Generation` screen with that module. ![suite-context-menu](./images/suite/suite-context-menu.png) @@ -51,10 +54,14 @@ Standard application solutions (`App` & `App-nolayers`) generated by Studio are You can generate code on the services of the microservice solution. UI code generation is not supported at the moment. It is on the roadmap. -## Managing Installed Version +## Managing Installed Version You can update or downgrade the version of `ABP Suite` by `Change Version` button in `ABP Suite` menu. +### Automatic Version Matching + +When you open ABP Suite for a solution, Studio checks if the installed Suite version matches the solution's ABP version. If they differ, Studio will prompt you to update or downgrade Suite to match your solution's version. This ensures compatibility between Suite and your project. + ![suite-change-version-button-main-page](./images/suite/suite-change-version-button-main-page.png) ![suite-change-version-window](./images/suite/suite-change-version-window.png) diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md index afc2536709..160f89216f 100644 --- a/docs/en/suite/generating-crud-page.md +++ b/docs/en/suite/generating-crud-page.md @@ -270,6 +270,29 @@ In the example above, the `IdentityUser` entity is selected as the navigation pr > **Note:** Ensure that your solution is built properly before establishing relationship between your own entity and a module entity because ABP Suite scans assemblies and finds which ABP modules you are using and lists their entities in the navigation property model if you have checked the **Include entities from ABP modules** checkbox. +#### Extending with Custom Module Entities + +If you want to extend ABP Suite's system to list entities from your own custom modules (not just ABP's built-in modules), you can configure the `module-entity-extension.json` file. This file is located in the `.suite` folder at the root of your solution (`/.suite/module-entity-extension.json`). + +Here is the default sample file content: + +```json +{ + "Modules": [ + { + "DomainProjectDllFileName": "MySampleModule.MyProject.Domain.dll" + } + ] +} +``` + +By defining the `DomainProjectDllFileName` property, ABP Suite will scan the specified module's **.dll** and list its entities in the navigation property model. This allows you to create navigation properties that reference entities from your custom modules. + +> **Important:** When extending with custom module entities, ensure that: +> - Your current solution properly depends on the related module. +> - All module references are correctly configured. +> - The solution is built successfully before attempting to establish relationships. + #### Adding An Existing Entity as a Navigation Property Alternatively, you can add `IdentityUser` entity (or any other entity) as a navigation property to an entity by manually entering the required information. See the screenshot below: diff --git a/docs/en/testing/integration-tests.md b/docs/en/testing/integration-tests.md index c87d270747..d28ed5ca5b 100644 --- a/docs/en/testing/integration-tests.md +++ b/docs/en/testing/integration-tests.md @@ -34,13 +34,13 @@ The startup template is configured to use **in-memory SQLite** database for the Using in-memory SQLite database has two main advantages; * It is faster compared to an external DBMS. -* It create a **new fresh database** for each test case, so tests doesn't affect each other. +* It creates a **new fresh database** for each test case, so tests don't affect each other. > **Tip**: Do not use EF Core's In-Memory database for advanced integration tests. It is not a real DBMS and has many differences in details. For example, it doesn't support transaction and rollback scenarios, so you can't truly test the failing scenarios. On the other hand, In-Memory SQLite is a real DBMS and supports the fundamental SQL database features. ## The Seed Data -Writing tests against an empty database is not practical. In most cases, you need to some initial data in the database. For example, if you write a test class that query, update and delete the products, it would be helpful to have a few products in the database before executing the test case. +Writing tests against an empty database is not practical. In most cases, you need some initial data in the database. For example, if you write a test class that queries, updates and deletes the products, it would be helpful to have a few products in the database before executing the test case. ABP's [Data Seeding](../framework/infrastructure/data-seeding.md) system is a powerful way to seed the initial data. The application startup template has a *YourProject*TestDataSeedContributor class in the `.TestBase` project. You can fill it to have an initial data that you can use for each test method. @@ -401,7 +401,7 @@ public class EfCoreIssueAppService_Tests : IssueAppService_Tests By deriving from the related abstract classes, now we can see the all tests in the test explorers and run them. +> By deriving from the related abstract classes, now we can see all the tests in the test explorers and run them. ![unitest-efcore-mongodb](../images/unitest-efcore-mongodb.png) diff --git a/docs/en/testing/overall.md b/docs/en/testing/overall.md index 154c169d5b..15e9abf725 100644 --- a/docs/en/testing/overall.md +++ b/docs/en/testing/overall.md @@ -69,7 +69,7 @@ The startup solution has the following libraries already installed; * [NSubstitute](https://nsubstitute.github.io/) as the mocking library. * [Shouldly](https://github.com/shouldly/shouldly) as the assertion library. -While you are free to replace them with your favorite tools, this document and examples will be base on these tooling. +While you are free to replace them with your favorite tools, this document and examples will be based on these tooling. ## The Test Explorer diff --git a/docs/en/testing/unit-tests.md b/docs/en/testing/unit-tests.md index cfc63a4848..2e06a44efb 100644 --- a/docs/en/testing/unit-tests.md +++ b/docs/en/testing/unit-tests.md @@ -73,7 +73,7 @@ namespace MyProject.Issues Notice that the `IsClosed` and `CloseDate` properties have private setters to force some business rules by using the `Open()` and `Close()` methods: -* Whenever you close an issue, the `CloseDate` should be set to the [current time](../framework/infrastructure/virtual-file-system.md). +* Whenever you close an issue, the `CloseDate` should be set to the current time. * An issue can not be re-opened if it is locked. And if it is re-opened, the `CloseDate` should be set to `null`. Since the `Issue` entity is a part of the Domain Layer, we should test it in the `Domain.Tests` project. Create an `Issue_Tests` class inside the `Domain.Tests` project: @@ -160,7 +160,7 @@ public void Should_Not_Allow_To_ReOpen_A_Locked_Issue() `Assert.Throws` checks if the executed code throws a matching exception. -> See the [xUnit](https://xunit.net/#documentation) & [Shoudly](https://docs.shouldly.org/) documentation to learn more about these libraries. +> See the [xUnit](https://xunit.net/#documentation) & [Shouldly](https://docs.shouldly.org/) documentation to learn more about these libraries. ## Classes With Dependencies diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs index 247e6e354e..dfe79d7e01 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs @@ -29,5 +29,13 @@ public class ChatClientAccessor : IChatClientAccessor ChatClient = serviceProvider.GetKeyedService( AbpAIWorkspaceOptions.GetChatClientServiceKeyName( WorkspaceNameAttribute.GetWorkspaceName())); + + // Fallback to default chat client if not configured for the workspace. + if (ChatClient is null) + { + ChatClient = serviceProvider.GetKeyedService( + AbpAIWorkspaceOptions.GetChatClientServiceKeyName( + AbpAIModule.DefaultWorkspaceName)); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs index 72ccfdaf38..9a52f7edc9 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs @@ -9,9 +9,13 @@ public class TypedChatClient : DelegatingChatClient, IChatClient( + serviceProvider.GetKeyedService( AbpAIWorkspaceOptions.GetChatClientServiceKeyName( WorkspaceNameAttribute.GetWorkspaceName())) + ?? + serviceProvider.GetRequiredKeyedService( + AbpAIWorkspaceOptions.GetChatClientServiceKeyName( + AbpAIModule.DefaultWorkspaceName)) ) { } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs index fc7d372b37..dc51022956 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs @@ -31,6 +31,8 @@ public class PageLayout : IScopedDependency, INotifyPropertyChanged } } + public bool ShowToolbar { get; set; } = true; + public virtual ObservableCollection BreadcrumbItems { get; } = new(); public virtual ObservableCollection ToolbarItems { get; } = new(); diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs index 647538c121..6e47281a70 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs @@ -16,6 +16,8 @@ public class SimplePageToolbarContributor : IPageToolbarContributor public string? RequiredPolicyName { get; } + private bool? _shouldAddComponent; + public SimplePageToolbarContributor( Type componentType, Dictionary? arguments = null, @@ -38,15 +40,19 @@ public class SimplePageToolbarContributor : IPageToolbarContributor protected virtual async Task ShouldAddComponentAsync(PageToolbarContributionContext context) { - if (RequiredPolicyName != null) + if (_shouldAddComponent.HasValue) + { + return _shouldAddComponent.Value; + } + + if (RequiredPolicyName == null) { - var authorizationService = context.ServiceProvider.GetRequiredService(); - if (!await authorizationService.IsGrantedAsync(RequiredPolicyName)) - { - return false; - } + _shouldAddComponent = true; + return _shouldAddComponent.Value; } - return true; + var authorizationService = context.ServiceProvider.GetRequiredService(); + _shouldAddComponent = await authorizationService.IsGrantedAsync(RequiredPolicyName); + return _shouldAddComponent.Value; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs index 1e5abce611..533a5b9187 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs @@ -11,6 +11,8 @@ public class ContentLayout public string? MenuItemName { get; set; } + public bool ShowToolbar { get; set; } = true; + public ContentLayout() { BreadCrumb = new BreadCrumb(); @@ -23,11 +25,6 @@ public class ContentLayout return true; } - if (BreadCrumb.ShowCurrent && !Title.IsNullOrEmpty()) - { - return true; - } - - return false; + return BreadCrumb.ShowCurrent || BreadCrumb.ShowHome; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs new file mode 100644 index 0000000000..a0c9570fc6 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; + +public class AbpNoContentApiDescriptionProvider : IApiDescriptionProvider, ITransientDependency +{ + public virtual void OnProvidersExecuted(ApiDescriptionProviderContext context) + { + } + + /// + /// The order -999 ensures that this provider is executed right after the + /// Microsoft.AspNetCore.Mvc.ApiExplorer.DefaultApiDescriptionProvider. + /// + public int Order => -999; + + public virtual void OnProvidersExecuting(ApiDescriptionProviderContext context) + { + foreach (var result in context.Results.Where(x => x.IsRemoteService())) + { + var actionProducesResponseTypeAttributes = + ReflectionHelper.GetAttributesOfMemberOrDeclaringType( + result.ActionDescriptor.GetMethodInfo()); + if (actionProducesResponseTypeAttributes.Any(x => x.StatusCode == (int) HttpStatusCode.NoContent)) + { + continue; + } + + var returnType = result.ActionDescriptor.GetReturnType(); + if (returnType == typeof(Task) || returnType == typeof(void)) + { + result.SupportedResponseTypes.Add(new ApiResponseType + { + // If the return type is Task, then we should treat it as a void return type since we can't infer anything without additional metadata or requiring unreferenced code. + Type = typeof(void), + StatusCode = (int) HttpStatusCode.NoContent + }); + } + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs index c37e75ef32..8f78e3292a 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs @@ -46,11 +46,14 @@ public static class CookieAuthenticationOptionsExtensions { var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme); + var clientId = principalContext.Properties.GetString("client_id"); + var clientSecret = principalContext.Properties.GetString("client_secret"); + var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest { Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect", - ClientId = openIdConnectOptions.ClientId!, - ClientSecret = openIdConnectOptions.ClientSecret, + ClientId = clientId ?? openIdConnectOptions.ClientId!, + ClientSecret = clientSecret ?? openIdConnectOptions.ClientSecret, Token = accessToken }); @@ -82,7 +85,7 @@ public static class CookieAuthenticationOptionsExtensions return options; } - private async static Task GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme) + private static async Task GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme) { var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService>().Get(oidcAuthenticationScheme); var cancellationTokenProvider = principalContext.HttpContext.RequestServices.GetRequiredService(); diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs new file mode 100644 index 0000000000..2df335abd8 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.BackgroundWorkers.Hangfire; + +public class AbpHangfirePeriodicBackgroundWorkerAdapterOptions +{ + public TimeZoneInfo TimeZone { get; set; } = TimeZoneInfo.Utc; + + public string Queue { get; set; } = default!; +} diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs index fe9a8ad983..64a4a1be64 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs @@ -5,7 +5,9 @@ using System.Threading; using System.Threading.Tasks; using Hangfire; using Hangfire.Common; +using Hangfire.Storage; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; @@ -30,8 +32,9 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet BackgroundJobServer = ServiceProvider.GetRequiredService(); } - public async override Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default) + public override async Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default) { + var logger = ServiceProvider.GetRequiredService>(); var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value; var defaultQueuePrefix = abpHangfireOptions.DefaultQueuePrefix; var defaultQueue = abpHangfireOptions.DefaultQueue; @@ -42,54 +45,90 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet { var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker); - RecurringJob.AddOrUpdate( - hangfireBackgroundWorker.RecurringJobId, - hangfireBackgroundWorker.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + hangfireBackgroundWorker.Queue, - () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken), - hangfireBackgroundWorker.CronExpression, - new RecurringJobOptions - { - TimeZone = hangfireBackgroundWorker.TimeZone - }); + var queueName = hangfireBackgroundWorker.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + hangfireBackgroundWorker.Queue; + if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) + { + logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead."); + RecurringJob.AddOrUpdate( + hangfireBackgroundWorker.RecurringJobId, + () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken), + hangfireBackgroundWorker.CronExpression, + new RecurringJobOptions + { + TimeZone = hangfireBackgroundWorker.TimeZone + }); + } + else + { + RecurringJob.AddOrUpdate( + hangfireBackgroundWorker.RecurringJobId, + queueName, + () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken), + hangfireBackgroundWorker.CronExpression, + new RecurringJobOptions + { + TimeZone = hangfireBackgroundWorker.TimeZone + }); + } break; } case AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase: { int? period = null; - string? CronExpression = null; + string? cronExpression = null; - if (worker is AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorkerBase) + switch (worker) { + case AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorkerBase: period = asyncPeriodicBackgroundWorkerBase.Period; - CronExpression = asyncPeriodicBackgroundWorkerBase.CronExpression; - } - else if (worker is PeriodicBackgroundWorkerBase periodicBackgroundWorkerBase) - { + cronExpression = asyncPeriodicBackgroundWorkerBase.CronExpression; + break; + case PeriodicBackgroundWorkerBase periodicBackgroundWorkerBase: period = periodicBackgroundWorkerBase.Period; - CronExpression = periodicBackgroundWorkerBase.CronExpression; + cronExpression = periodicBackgroundWorkerBase.CronExpression; + break; } - if (period == null && CronExpression.IsNullOrWhiteSpace()) + if (period == null && cronExpression.IsNullOrWhiteSpace()) { + logger.LogError( + $"Cannot add periodic background worker {worker.GetType().FullName} to Hangfire scheduler, because both Period and CronExpression are not set. " + + "You can either set Period or CronExpression property of the worker." + ); return; } - var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker)); - var workerAdapter = (Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker)!; - + var workerAdapter = (ServiceProvider.GetRequiredService(typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker))) as IHangfireBackgroundWorker)!; Expression> methodCall = () => workerAdapter.DoWorkAsync(cancellationToken); var recurringJobId = !workerAdapter.RecurringJobId.IsNullOrWhiteSpace() ? workerAdapter.RecurringJobId : GetRecurringJobId(worker, methodCall); - RecurringJob.AddOrUpdate( - recurringJobId, - workerAdapter.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + workerAdapter.Queue, - methodCall, - CronExpression ?? GetCron(period!.Value), - new RecurringJobOptions - { - TimeZone = workerAdapter.TimeZone - }); + var queueName = workerAdapter.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + workerAdapter.Queue; + if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty)) + { + logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead."); + RecurringJob.AddOrUpdate( + recurringJobId, + methodCall, + cronExpression ?? GetCron(period!.Value), + new RecurringJobOptions + { + TimeZone = workerAdapter.TimeZone + }); + } + else + { + RecurringJob.AddOrUpdate( + recurringJobId, + queueName, + methodCall, + cronExpression ?? GetCron(period!.Value), + new RecurringJobOptions + { + TimeZone = workerAdapter.TimeZone + }); + } + break; } default: @@ -98,7 +137,7 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet } } - private readonly static MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static); protected virtual string? GetRecurringJobId(IBackgroundWorker worker, Expression> methodCall) { string? recurringJobId = null; diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs index 43e9b4a95c..cf5945aaf1 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs @@ -1,7 +1,9 @@ -using System.Reflection; +using System; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Volo.Abp.BackgroundWorkers.Hangfire; @@ -11,14 +13,17 @@ public class HangfirePeriodicBackgroundWorkerAdapter : HangfireBackgrou private readonly MethodInfo _doWorkAsyncMethod; private readonly MethodInfo _doWorkMethod; - public HangfirePeriodicBackgroundWorkerAdapter() + public HangfirePeriodicBackgroundWorkerAdapter(IOptions options) { + TimeZone = options.Value.TimeZone; + Queue = options.Value.Queue; + RecurringJobId = BackgroundWorkerNameAttribute.GetNameOrNull(); + _doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; _doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic)!; - RecurringJobId = BackgroundWorkerNameAttribute.GetNameOrNull(); } - public async override Task DoWorkAsync(CancellationToken cancellationToken = default) + public override async Task DoWorkAsync(CancellationToken cancellationToken = default) { var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider, cancellationToken); var worker = ServiceProvider.GetRequiredService(); @@ -26,13 +31,11 @@ public class HangfirePeriodicBackgroundWorkerAdapter : HangfireBackgrou switch (worker) { case AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorker: - await (Task)(_doWorkAsyncMethod.Invoke(asyncPeriodicBackgroundWorker, new object[] { workerContext })!); + await (Task)(_doWorkAsyncMethod.Invoke(asyncPeriodicBackgroundWorker, [workerContext])!); break; case PeriodicBackgroundWorkerBase periodicBackgroundWorker: - _doWorkMethod.Invoke(periodicBackgroundWorker, new object[] { workerContext }); + _doWorkMethod.Invoke(periodicBackgroundWorker, [workerContext]); break; } } - - } diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index ae571cb5f9..6ffaa96ecf 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -19,7 +19,7 @@ namespace Volo.Abp.Caching; /// Represents a distributed cache of type. /// /// The type of cache item being cached. -public class DistributedCache : +public class DistributedCache : IDistributedCache where TCacheItem : class { @@ -683,35 +683,30 @@ public class DistributedCache : IDistributedCache x.Value != null)) - { - return result!; - } + var resultMap = result + .Where(x => x.Value != null) + .ToDictionary(x => x.Key, x => x.Value); - var missingKeys = new List(); - var missingValuesIndex = new List(); - for (var i = 0; i < keyArray.Length; i++) + if (resultMap.Count == keyArray.Length) { - if (result[i].Value != null) - { - continue; - } - - missingKeys.Add(keyArray[i]); - missingValuesIndex.Add(i); + return keyArray + .Select(key => new KeyValuePair(key, resultMap[key])) + .ToArray(); } + var missingKeys = keyArray.Where(key => !resultMap.ContainsKey(key)).ToList(); var missingValues = factory.Invoke(missingKeys).ToArray(); - var valueQueue = new Queue>(missingValues); SetMany(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow); - foreach (var index in missingValuesIndex) + foreach (var pair in missingValues) { - result[index] = valueQueue.Dequeue()!; + resultMap[pair.Key] = pair.Value; } - return result; + return keyArray + .Select(key => new KeyValuePair(key, resultMap.GetOrDefault(key))) + .ToArray(); } @@ -779,35 +774,30 @@ public class DistributedCache : IDistributedCache x.Value != null)) - { - return result; - } + var resultMap = result + .Where(x => x.Value != null) + .ToDictionary(x => x.Key, x => x.Value); - var missingKeys = new List(); - var missingValuesIndex = new List(); - for (var i = 0; i < keyArray.Length; i++) + if (resultMap.Count == keyArray.Length) { - if (result[i].Value != null) - { - continue; - } - - missingKeys.Add(keyArray[i]); - missingValuesIndex.Add(i); + return keyArray + .Select(key => new KeyValuePair(key, resultMap[key])) + .ToArray(); } + var missingKeys = keyArray.Where(key => !resultMap.ContainsKey(key)).ToList(); var missingValues = (await factory.Invoke(missingKeys)).ToArray(); - var valueQueue = new Queue>(missingValues); await SetManyAsync(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow, token); - foreach (var index in missingValuesIndex) + foreach (var pair in missingValues) { - result[index] = valueQueue.Dequeue()!; + resultMap[pair.Key] = pair.Value; } - return result; + return keyArray + .Select(key => new KeyValuePair(key, resultMap.GetOrDefault(key))) + .ToArray(); } /// diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj index 078d1aa6cc..23cb5c2b33 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj +++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs index 8ff8ad3206..a188137ea2 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs @@ -79,6 +79,7 @@ public class AbpCliCoreModule : AbpModule options.Commands[ClearDownloadCacheCommand.Name] = typeof(ClearDownloadCacheCommand); options.Commands[RecreateInitialMigrationCommand.Name] = typeof(RecreateInitialMigrationCommand); options.Commands[GenerateRazorPage.Name] = typeof(GenerateRazorPage); + options.Commands[McpCommand.Name] = typeof(McpCommand); options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Pro"); options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Lite"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs new file mode 100644 index 0000000000..e9ae5ba77c --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Cli.Commands; + +namespace Volo.Abp.Cli.Args; + +public static class CommandLineArgsExtensions +{ + public static bool IsMcpCommand(this CommandLineArgs args) + { + return args.IsCommand(McpCommand.Name); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs index 78a36fe329..43436329fb 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs @@ -1,4 +1,4 @@ -namespace Volo.Abp.Cli; +namespace Volo.Abp.Cli; public static class CliConsts { @@ -20,8 +20,12 @@ public static class CliConsts public static string AppSettingsSecretJsonFileName = "appsettings.secrets.json"; + public const string McpLogLevelEnvironmentVariable = "ABP_MCP_LOG_LEVEL"; + public const string DefaultMcpServerUrl = "https://mcp.abp.io"; + public static class MemoryKeys { public const string LatestCliVersionCheckDate = "LatestCliVersionCheckDate"; + public const string McpToolsLastFetchDate = "McpToolsLastFetchDate"; } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs index d47987b220..537c794c15 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; @@ -14,6 +14,9 @@ public static class CliPaths public static string Memory => Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "memory.bin"); public static string Build => Path.Combine(AbpRootPath, "build"); public static string Lic => Path.Combine(Path.GetTempPath(), Encoding.ASCII.GetString(new byte[] { 65, 98, 112, 76, 105, 99, 101, 110, 115, 101, 46, 98, 105, 110 })); + public static string McpToolsCache => Path.Combine(Root, "mcp-tools.json"); + public static string McpLog => Path.Combine(Log, "mcp.log"); + public static string McpConfig => Path.Combine(Root, "mcp-config.json"); public static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp"); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs index 063ceddebf..52bbac8e43 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using NuGet.Versioning; @@ -10,6 +10,7 @@ using System.Reflection; using System.Threading.Tasks; using Volo.Abp.Cli.Args; using Volo.Abp.Cli.Commands; +using Volo.Abp.Cli.Commands.Services; using Volo.Abp.Cli.Memory; using Volo.Abp.Cli.Version; using Volo.Abp.Cli.Utils; @@ -21,8 +22,11 @@ namespace Volo.Abp.Cli; public class CliService : ITransientDependency { + private const string McpLogSource = nameof(CliService); + private readonly MemoryService _memoryService; private readonly ITelemetryService _telemetryService; + private readonly IMcpLogger _mcpLogger; public ILogger Logger { get; set; } protected ICommandLineArgumentParser CommandLineArgumentParser { get; } protected ICommandSelector CommandSelector { get; } @@ -39,7 +43,8 @@ public class CliService : ITransientDependency ICmdHelper cmdHelper, MemoryService memoryService, CliVersionService cliVersionService, - ITelemetryService telemetryService) + ITelemetryService telemetryService, + IMcpLogger mcpLogger) { _memoryService = memoryService; CommandLineArgumentParser = commandLineArgumentParser; @@ -49,19 +54,27 @@ public class CliService : ITransientDependency CmdHelper = cmdHelper; CliVersionService = cliVersionService; _telemetryService = telemetryService; + _mcpLogger = mcpLogger; Logger = NullLogger.Instance; } public async Task RunAsync(string[] args) { - var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync(); - Logger.LogInformation($"ABP CLI {currentCliVersion}"); - var commandLineArgs = CommandLineArgumentParser.Parse(args); + var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync(); + + var isMcpCommand = commandLineArgs.IsMcpCommand(); + + // Don't print banner for MCP command to avoid corrupting stdout JSON-RPC stream + if (!isMcpCommand) + { + Logger.LogInformation($"ABP CLI {currentCliVersion}"); + } #if !DEBUG - if (!commandLineArgs.Options.ContainsKey("skip-cli-version-check")) + // Skip version check for MCP command to avoid corrupting stdout JSON-RPC stream + if (!isMcpCommand && !commandLineArgs.Options.ContainsKey("skip-cli-version-check")) { await CheckCliVersionAsync(currentCliVersion); } @@ -85,13 +98,29 @@ public class CliService : ITransientDependency } catch (CliUsageException usageException) { - Logger.LogWarning(usageException.Message); + // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream + if (commandLineArgs.IsMcpCommand()) + { + _mcpLogger.Error(McpLogSource, usageException.Message); + } + else + { + Logger.LogWarning(usageException.Message); + } Environment.ExitCode = 1; } catch (Exception ex) { await _telemetryService.AddErrorActivityAsync(ex.Message); - Logger.LogException(ex); + // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream + if (commandLineArgs.IsMcpCommand()) + { + _mcpLogger.Error(McpLogSource, "Fatal error", ex); + } + else + { + Logger.LogException(ex); + } throw; } finally diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs index 9bfdcfac5c..a409614712 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using Volo.Abp.Cli.Args; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs index cc13e9d187..c051f6d455 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs index 2b65b9d00b..c3641cac15 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs @@ -54,6 +54,14 @@ public class RecreateInitialMigrationCommand : IConsoleCommand, ITransientDepend Directory.Delete(Path.Combine(projectDir, "TenantMigrations"), true); separateDbContext = true; } + + CmdHelper.RunCmd("dotnet build", workingDirectory: projectDir, exitCode: out var exitCode); + if (exitCode != 0) + { + Logger.LogError("Build failed for project {Project}. Skipping migration recreation.", csprojFile); + continue; + } + if (!separateDbContext) { CmdHelper.RunCmd($"dotnet ef migrations add Initial", workingDirectory: projectDir); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs new file mode 100644 index 0000000000..ebf59e2ab6 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.Auth; +using Volo.Abp.Cli.Commands.Models; +using Volo.Abp.Cli.Commands.Services; +using Volo.Abp.Cli.Licensing; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Internal.Telemetry; +using Volo.Abp.Internal.Telemetry.Constants; + +namespace Volo.Abp.Cli.Commands; + +public class McpCommand : IConsoleCommand, ITransientDependency +{ + private const string LogSource = nameof(McpCommand); + public const string Name = "mcp"; + + private readonly AuthService _authService; + private readonly IApiKeyService _apiKeyService; + private readonly McpServerService _mcpServerService; + private readonly McpHttpClientService _mcpHttpClient; + private readonly IMcpLogger _mcpLogger; + private readonly ITelemetryService _telemetryService; + + public ILogger Logger { get; set; } + + public McpCommand( + IApiKeyService apiKeyService, + AuthService authService, + McpServerService mcpServerService, + McpHttpClientService mcpHttpClient, + IMcpLogger mcpLogger, + ITelemetryService telemetryService) + { + _apiKeyService = apiKeyService; + _authService = authService; + _mcpServerService = mcpServerService; + _mcpHttpClient = mcpHttpClient; + _mcpLogger = mcpLogger; + _telemetryService = telemetryService; + Logger = NullLogger.Instance; + } + + public async Task ExecuteAsync(CommandLineArgs commandLineArgs) + { + await ValidateLicenseAsync(); + + var option = commandLineArgs.Target; + + if (!string.IsNullOrEmpty(option) && option.Equals("get-config", StringComparison.OrdinalIgnoreCase)) + { + await PrintConfigurationAsync(); + return; + } + + await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsMcp); + + // Check server health before starting - fail if not reachable + _mcpLogger.Info(LogSource, "Checking ABP.IO MCP Server connection..."); + var isHealthy = await _mcpHttpClient.CheckServerHealthAsync(); + + if (!isHealthy) + { + throw new CliUsageException( + "Could not connect to ABP.IO MCP Server. " + + "The MCP server requires a connection to fetch tool definitions. " + + "Please check your internet connection and try again."); + } + + _mcpLogger.Info(LogSource, "Starting ABP MCP Server..."); + + var cts = new CancellationTokenSource(); + + ConsoleCancelEventHandler cancelHandler = (sender, e) => + { + e.Cancel = true; + _mcpLogger.Info(LogSource, "Shutting down ABP MCP Server..."); + + try + { + cts.Cancel(); + } + catch (ObjectDisposedException) + { + // CTS already disposed + } + }; + + Console.CancelKeyPress += cancelHandler; + + try + { + await _mcpServerService.RunAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Expected when Ctrl+C is pressed + } + catch (Exception ex) + { + _mcpLogger.Error(LogSource, "Error running MCP server", ex); + throw; + } + finally + { + Console.CancelKeyPress -= cancelHandler; + cts.Dispose(); + } + } + + private async Task ValidateLicenseAsync() + { + var loginInfo = await _authService.GetLoginInfoAsync(); + + if (string.IsNullOrEmpty(loginInfo?.Organization)) + { + throw new CliUsageException("Please log in with your account!"); + } + + var licenseResult = await _apiKeyService.GetApiKeyOrNullAsync(); + + if (licenseResult == null || !licenseResult.HasActiveLicense) + { + var errorMessage = licenseResult?.ErrorMessage ?? "No active license found."; + throw new CliUsageException(errorMessage); + } + + if (licenseResult.LicenseEndTime.HasValue && licenseResult.LicenseEndTime.Value < DateTime.UtcNow) + { + throw new CliUsageException("Your license has expired. Please renew your license to use the MCP server."); + } + } + + private Task PrintConfigurationAsync() + { + var config = new McpClientConfiguration + { + McpServers = new Dictionary + { + ["abp"] = new McpServerConfig + { + Command = "abp", + Args = new List { "mcp" }, + Env = new Dictionary() + } + } + }; + + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + Console.WriteLine(json); + + return Task.CompletedTask; + } + + public string GetUsageInfo() + { + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine("Usage:"); + sb.AppendLine(""); + sb.AppendLine(" abp mcp [options]"); + sb.AppendLine(""); + sb.AppendLine("Options:"); + sb.AppendLine(""); + sb.AppendLine(" (start the local MCP server)"); + sb.AppendLine("get-config (print MCP client configuration as JSON)"); + sb.AppendLine(""); + sb.AppendLine("Examples:"); + sb.AppendLine(""); + sb.AppendLine(" abp mcp"); + sb.AppendLine(" abp mcp get-config"); + sb.AppendLine(""); + + return sb.ToString(); + } + + public static string GetShortDescription() + { + return "Runs the local MCP server and outputs client configuration for AI tool integration."; + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpClientConfiguration.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpClientConfiguration.cs new file mode 100644 index 0000000000..60b21cbb33 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpClientConfiguration.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Volo.Abp.Cli.Commands.Models; + +public class McpClientConfiguration +{ + [JsonPropertyName("mcpServers")] + public Dictionary McpServers { get; set; } = new(); +} + +public class McpServerConfig +{ + [JsonPropertyName("command")] + public string Command { get; set; } + + [JsonPropertyName("args")] + public List Args { get; set; } = new(); + + [JsonPropertyName("env")] + public Dictionary Env { get; set; } +} + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpToolDefinition.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpToolDefinition.cs new file mode 100644 index 0000000000..f59776bc0a --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Models/McpToolDefinition.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Volo.Abp.Cli.Commands.Models; + +public class McpToolDefinition +{ + public string Name { get; set; } + public string Description { get; set; } + public McpToolInputSchema InputSchema { get; set; } + public JsonElement? OutputSchema { get; set; } +} + +public class McpToolInputSchema +{ + public string Type { get; set; } = "object"; + public Dictionary Properties { get; set; } + public List Required { get; set; } +} + +public class McpToolProperty +{ + public string Type { get; set; } + public string Description { get; set; } +} + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpMcpServerTool.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpMcpServerTool.cs new file mode 100644 index 0000000000..6858cf7571 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/AbpMcpServerTool.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace Volo.Abp.Cli.Commands.Services; + +internal class AbpMcpServerTool : McpServerTool +{ + private readonly string _name; + private readonly string _description; + private readonly JsonElement _inputSchema; + private readonly JsonElement? _outputSchema; + private readonly Func, CancellationToken, ValueTask> _handler; + + public AbpMcpServerTool( + string name, + string description, + JsonElement inputSchema, + JsonElement? outputSchema, + Func, CancellationToken, ValueTask> handler) + { + _name = name; + _description = description; + _inputSchema = inputSchema; + _outputSchema = outputSchema; + _handler = handler; + } + + public override Tool ProtocolTool => new Tool + { + Name = _name, + Description = _description, + InputSchema = _inputSchema, + OutputSchema = _outputSchema + }; + + public override IReadOnlyList Metadata => Array.Empty(); + + public override ValueTask InvokeAsync(RequestContext context, CancellationToken cancellationToken) + { + return _handler(context, cancellationToken); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs new file mode 100644 index 0000000000..f579420a1e --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/IMcpLogger.cs @@ -0,0 +1,36 @@ +using System; + +namespace Volo.Abp.Cli.Commands.Services; + +/// +/// Logger interface for MCP operations. +/// Writes detailed logs to file and critical messages (Warning/Error) to stderr. +/// Log level is controlled via ABP_MCP_LOG_LEVEL environment variable. +/// +public interface IMcpLogger +{ + /// + /// Logs a debug message. Only written to file when log level is Debug. + /// + void Debug(string source, string message); + + /// + /// Logs an informational message. Written to file when log level is Debug or Info. + /// + void Info(string source, string message); + + /// + /// Logs a warning message. Written to file and stderr. + /// + void Warning(string source, string message); + + /// + /// Logs an error message. Written to file and stderr. + /// + void Error(string source, string message); + + /// + /// Logs an error message with exception details. Written to file and stderr. + /// + void Error(string source, string message, Exception exception); +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs index 860fcef60f..2288eeccf7 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/InitialMigrationCreator.cs @@ -14,7 +14,7 @@ public class InitialMigrationCreator : ITransientDependency public ICmdHelper CmdHelper { get; } public DotnetEfToolManager DotnetEfToolManager { get; } public ILogger Logger { get; set; } - + public InitialMigrationCreator(ICmdHelper cmdHelper, DotnetEfToolManager dotnetEfToolManager) { CmdHelper = cmdHelper; @@ -30,11 +30,11 @@ public class InitialMigrationCreator : ITransientDependency Logger.LogError($"This path doesn't exist: {targetProjectFolder}"); return false; } - + Logger.LogInformation("Creating initial migrations..."); await DotnetEfToolManager.BeSureInstalledAsync(); - + var tenantDbContextName = FindTenantDbContextName(targetProjectFolder); var dbContextName = tenantDbContextName != null ? FindDbContextName(targetProjectFolder) @@ -60,7 +60,7 @@ public class InitialMigrationCreator : ITransientDependency return migrationSuccess; } - + private string FindTenantDbContextName(string projectFolder) { var tenantDbContext = Directory.GetFiles(projectFolder, "*TenantMigrationsDbContext.cs", SearchOption.AllDirectories) @@ -93,6 +93,12 @@ public class InitialMigrationCreator : ITransientDependency private string AddMigrationAndGetOutput(string dbMigrationsFolder, string dbContext, string outputDirectory) { + var output = CmdHelper.RunCmdAndGetOutput("dotnet build", out int buildExitCode, dbMigrationsFolder); + if (buildExitCode != 0) + { + return output; + } + var dbContextOption = string.IsNullOrWhiteSpace(dbContext) ? string.Empty : $"--context {dbContext}"; @@ -108,4 +114,4 @@ public class InitialMigrationCreator : ITransientDependency output.Contains("To undo this action") && output.Contains("ef migrations remove")); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs new file mode 100644 index 0000000000..c15a6c06b6 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpHttpClientService.cs @@ -0,0 +1,280 @@ +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.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.Cli.Commands.Models; +using Volo.Abp.Cli.Http; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IO; + +namespace Volo.Abp.Cli.Commands.Services; + +public class McpHttpClientService : ISingletonDependency +{ + private static readonly JsonSerializerOptions JsonSerializerOptionsWeb = new(JsonSerializerDefaults.Web); + + private const string LogSource = nameof(McpHttpClientService); + + private readonly CliHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IMcpLogger _mcpLogger; + private readonly Lazy> _cachedServerUrlLazy; + private List _validToolNames; + private bool _toolDefinitionsLoaded; + + public McpHttpClientService( + CliHttpClientFactory httpClientFactory, + ILogger logger, + IMcpLogger mcpLogger) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + _mcpLogger = mcpLogger; + _cachedServerUrlLazy = new Lazy>(GetMcpServerUrlInternalAsync); + } + + public void InitializeToolNames(List tools) + { + _validToolNames = tools.Select(t => t.Name).ToList(); + _toolDefinitionsLoaded = true; + _mcpLogger.Debug(LogSource, $"Initialized tool names from cache. Count={tools.Count}, Instance={GetHashCode()}"); + } + + public async Task CallToolAsync(string toolName, JsonElement arguments) + { + _mcpLogger.Debug(LogSource, $"CallToolAsync called for '{toolName}'. _toolDefinitionsLoaded={_toolDefinitionsLoaded}, Instance={GetHashCode()}"); + + if (!_toolDefinitionsLoaded) + { + throw new CliUsageException("Tool definitions have not been loaded yet. This is an internal error."); + } + + // Validate toolName against whitelist to prevent malicious input + if (_validToolNames != null && !_validToolNames.Contains(toolName)) + { + _mcpLogger.Warning(LogSource, $"Attempted to call unknown tool: {toolName}"); + return CreateErrorResponse($"Unknown tool: {toolName}"); + } + + var baseUrl = await GetMcpServerUrlAsync(); + var url = $"{baseUrl}/tools/call"; + + try + { + using var httpClient = _httpClientFactory.CreateClient(needsAuthentication: true); + + var jsonContent = JsonSerializer.Serialize( + new { name = toolName, arguments }, + JsonSerializerOptionsWeb); + + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync(url, content); + + if (!response.IsSuccessStatusCode) + { + _mcpLogger.Error(LogSource, $"API call failed with status: {response.StatusCode}"); + + // Return sanitized error message to client + var errorMessage = GetSanitizedHttpErrorMessage(response.StatusCode); + return CreateErrorResponse(errorMessage); + } + + return await response.Content.ReadAsStringAsync(); + } + catch (HttpRequestException ex) + { + _mcpLogger.Error(LogSource, $"Network error calling tool '{toolName}'", ex); + + // Return sanitized error to client + return CreateErrorResponse(ErrorMessages.NetworkConnectivity); + } + catch (TaskCanceledException ex) + { + _mcpLogger.Error(LogSource, $"Timeout calling tool '{toolName}'", ex); + + // Return sanitized error to client + return CreateErrorResponse(ErrorMessages.Timeout); + } + catch (Exception ex) + { + _mcpLogger.Error(LogSource, $"Unexpected error calling tool '{toolName}'", ex); + + // Return generic sanitized error to client + return CreateErrorResponse(ErrorMessages.Unexpected); + } + } + + public async Task CheckServerHealthAsync() + { + var baseUrl = await GetMcpServerUrlAsync(); + + try + { + using var httpClient = _httpClientFactory.CreateClient(needsAuthentication: false); + var response = await httpClient.GetAsync(baseUrl); + return response.IsSuccessStatusCode; + } + catch (Exception) + { + // Silently fail health check - it's optional + return false; + } + } + + public async Task> GetToolDefinitionsAsync() + { + _mcpLogger.Debug(LogSource, $"GetToolDefinitionsAsync called. Instance={GetHashCode()}"); + + var baseUrl = await GetMcpServerUrlAsync(); + var url = $"{baseUrl}/tools"; + + try + { + using var httpClient = _httpClientFactory.CreateClient(needsAuthentication: true); + var response = await httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + _mcpLogger.Error(LogSource, $"Failed to fetch tool definitions with status: {response.StatusCode}"); + + // Throw sanitized exception + var errorMessage = GetSanitizedHttpErrorMessage(response.StatusCode); + throw new CliUsageException($"Failed to fetch tool definitions: {errorMessage}"); + } + + var responseContent = await response.Content.ReadAsStringAsync(); + + // The API returns { tools: [...] } format + var result = JsonSerializer.Deserialize(responseContent, JsonSerializerOptionsWeb); + var tools = result?.Tools ?? new List(); + + // Cache tool names for validation + _validToolNames = tools.Select(t => t.Name).ToList(); + _toolDefinitionsLoaded = true; + + _mcpLogger.Debug(LogSource, $"Tool definitions loaded successfully. _toolDefinitionsLoaded={_toolDefinitionsLoaded}, Tool count={tools.Count}, Instance={GetHashCode()}"); + + return tools; + } + catch (HttpRequestException ex) + { + throw CreateHttpExceptionWithInner(ex, "Network error fetching tool definitions"); + } + catch (TaskCanceledException ex) + { + throw CreateHttpExceptionWithInner(ex, "Timeout fetching tool definitions"); + } + catch (JsonException ex) + { + throw CreateHttpExceptionWithInner(ex, "JSON parsing error"); + } + catch (CliUsageException) + { + // Already sanitized, rethrow as-is + throw; + } + catch (Exception ex) + { + throw CreateHttpExceptionWithInner(ex, "Unexpected error fetching tool definitions"); + } + } + + private async Task GetMcpServerUrlAsync() + { + return await _cachedServerUrlLazy.Value; + } + + private async Task GetMcpServerUrlInternalAsync() + { + // Check config file + if (File.Exists(CliPaths.McpConfig)) + { + try + { + var json = await FileHelper.ReadAllTextAsync(CliPaths.McpConfig); + var config = JsonSerializer.Deserialize(json, JsonSerializerOptionsWeb); + if (!string.IsNullOrWhiteSpace(config?.ServerUrl)) + { + return config.ServerUrl.TrimEnd('/'); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to read MCP config file"); + } + } + + // Return default + return CliConsts.DefaultMcpServerUrl; + } + + private string CreateErrorResponse(string errorMessage) + { + return JsonSerializer.Serialize(new + { + content = new[] + { + new + { + type = "text", + text = errorMessage + } + }, + isError = true + }, JsonSerializerOptionsWeb); + } + + private string GetSanitizedHttpErrorMessage(HttpStatusCode statusCode) + { + return statusCode switch + { + HttpStatusCode.Unauthorized => "Authentication failed. Please ensure you are logged in with a valid account.", + HttpStatusCode.Forbidden => "Access denied. You do not have permission to use this tool.", + HttpStatusCode.NotFound => "The requested tool could not be found. It may have been removed or is temporarily unavailable.", + HttpStatusCode.BadRequest => "The tool request was invalid. Please check your input parameters and try again.", + (HttpStatusCode)429 => "Rate limit exceeded. Please wait a moment before trying again.", // TooManyRequests not available in .NET Standard 2.0 + HttpStatusCode.ServiceUnavailable => "The service is temporarily unavailable. Please try again later.", + HttpStatusCode.InternalServerError => "The tool execution encountered an internal error. Please try again later.", + _ => "The tool execution failed. Please try again later." + }; + } + + private CliUsageException CreateHttpExceptionWithInner(Exception ex, string context) + { + _mcpLogger.Error(LogSource, context, ex); + + var userMessage = ex switch + { + HttpRequestException => "Network connectivity issue. Please check your internet connection and try again.", + TaskCanceledException => "Request timed out. Please try again.", + JsonException => "Invalid response format received.", + _ => "An unexpected error occurred. Please try again later." + }; + + return new CliUsageException($"Failed to fetch tool definitions: {userMessage}", ex); + } + + private static class ErrorMessages + { + public const string NetworkConnectivity = "The tool execution failed due to a network connectivity issue. Please check your internet connection and try again."; + public const string Timeout = "The tool execution timed out. The operation took too long to complete. Please try again."; + public const string Unexpected = "The tool execution failed due to an unexpected error. Please try again later."; + } + + private class McpConfig + { + public string ServerUrl { get; set; } + } + + private class McpToolsResponse + { + public List Tools { get; set; } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs new file mode 100644 index 0000000000..20d6fce469 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpLogger.cs @@ -0,0 +1,150 @@ +using System; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands.Services; + +/// +/// MCP logger implementation that writes to both file (via Serilog) and stderr. +/// - All logs at or above the configured level are written to file via ILogger +/// - Warning and Error logs are also written to stderr +/// - Log level is controlled via ABP_MCP_LOG_LEVEL environment variable +/// +public class McpLogger : IMcpLogger, ISingletonDependency +{ + private const string LogPrefix = "[MCP]"; + + private readonly ILogger _logger; + private readonly McpLogLevel _configuredLogLevel; + + public McpLogger(ILogger logger) + { + _logger = logger; + _configuredLogLevel = GetConfiguredLogLevel(); + } + + public void Debug(string source, string message) + { + Log(McpLogLevel.Debug, source, message); + } + + public void Info(string source, string message) + { + Log(McpLogLevel.Info, source, message); + } + + public void Warning(string source, string message) + { + Log(McpLogLevel.Warning, source, message); + } + + public void Error(string source, string message) + { + Log(McpLogLevel.Error, source, message); + } + + public void Error(string source, string message, Exception exception) + { +#if DEBUG + var fullMessage = $"{message} | Exception: {exception.GetType().Name}: {exception.Message}"; +#else + var fullMessage = $"{message} | Exception: {exception.GetType().Name}"; +#endif + Log(McpLogLevel.Error, source, fullMessage); + } + + private void Log(McpLogLevel level, string source, string message) + { + if (_configuredLogLevel == McpLogLevel.None || level < _configuredLogLevel) + { + return; + } + + var mcpFormattedMessage = $"{LogPrefix}[{source}] {message}"; + + // File logging via Serilog + switch (level) + { + case McpLogLevel.Debug: + _logger.LogDebug(mcpFormattedMessage); + break; + case McpLogLevel.Info: + _logger.LogInformation(mcpFormattedMessage); + break; + case McpLogLevel.Warning: + _logger.LogWarning(mcpFormattedMessage); + break; + case McpLogLevel.Error: + _logger.LogError(mcpFormattedMessage); + break; + } + + // Stderr output for MCP protocol (Warning/Error only) + if (level >= McpLogLevel.Warning) + { + WriteToStderr(level.ToString().ToUpperInvariant(), message); + } + } + + private void WriteToStderr(string level, string message) + { + try + { + // Use synchronous write to avoid async issues in MCP context + Console.Error.WriteLine($"{LogPrefix}[{level}] {message}"); + } + catch + { + // Silently ignore stderr write errors + } + } + + private static McpLogLevel GetConfiguredLogLevel() + { + var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable); + var isEmpty = string.IsNullOrWhiteSpace(envValue); + +#if DEBUG + // In development builds, allow full control via environment variable + if (isEmpty) + { + return McpLogLevel.Info; // Default level + } + + return ParseLogLevel(envValue, allowDebug: true); +#else + // In release builds, restrict to Warning or higher (ignore env variable for Debug/Info) + if (isEmpty) + { + return McpLogLevel.Info; // Default level + } + + return ParseLogLevel(envValue, allowDebug: false); +#endif + } + + private static McpLogLevel ParseLogLevel(string value, bool allowDebug) + { + return value.ToLowerInvariant() switch + { + "debug" => allowDebug ? McpLogLevel.Debug : McpLogLevel.Info, + "info" => McpLogLevel.Info, + "warning" => McpLogLevel.Warning, + "error" => McpLogLevel.Error, + "none" => McpLogLevel.None, + _ => McpLogLevel.Info + }; + } +} + +/// +/// Log levels for MCP logging. +/// +public enum McpLogLevel +{ + Debug = 0, + Info = 1, + Warning = 2, + Error = 3, + None = 4 +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs new file mode 100644 index 0000000000..01c48a1345 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpServerService.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using Volo.Abp.Cli.Commands.Models; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Cli.Commands.Services; + +public class McpServerService : ITransientDependency +{ + private const string LogSource = nameof(McpServerService); + private const int MaxLogResponseLength = 500; + + private static readonly JsonSerializerOptions JsonCamelCaseOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + private static class ToolErrorMessages + { + public const string InvalidResponseFormat = "The tool execution completed but returned an invalid response format. Please try again."; + public const string UnexpectedError = "The tool execution failed due to an unexpected error. Please try again later."; + } + + private readonly McpHttpClientService _mcpHttpClient; + private readonly McpToolsCacheService _toolsCacheService; + private readonly IMcpLogger _mcpLogger; + + public McpServerService( + McpHttpClientService mcpHttpClient, + McpToolsCacheService toolsCacheService, + IMcpLogger mcpLogger) + { + _mcpHttpClient = mcpHttpClient; + _toolsCacheService = toolsCacheService; + _mcpLogger = mcpLogger; + } + + public async Task RunAsync(CancellationToken cancellationToken = default) + { + _mcpLogger.Info(LogSource, "Starting ABP MCP Server (stdio)"); + + var options = new McpServerOptions(); + + await RegisterAllToolsAsync(options); + + // Use NullLoggerFactory to prevent ModelContextProtocol library from logging to stdout + // All our logging goes to file and stderr via IMcpLogger + var server = McpServer.Create( + new StdioServerTransport("abp-mcp-server", NullLoggerFactory.Instance), + options + ); + + await server.RunAsync(cancellationToken); + + _mcpLogger.Info(LogSource, "ABP MCP Server stopped"); + } + + private async Task RegisterAllToolsAsync(McpServerOptions options) + { + // Get tool definitions from cache (or fetch from server) + var toolDefinitions = await _toolsCacheService.GetToolDefinitionsAsync(); + + _mcpLogger.Info(LogSource, $"Registering {toolDefinitions.Count} tools"); + + // Register each tool dynamically + foreach (var toolDef in toolDefinitions) + { + RegisterToolFromDefinition(options, toolDef); + } + } + + private void RegisterToolFromDefinition(McpServerOptions options, McpToolDefinition toolDef) + { + var inputSchema = toolDef.InputSchema ?? new McpToolInputSchema(); + RegisterTool(options, toolDef.Name, toolDef.Description, inputSchema, toolDef.OutputSchema); + } + + private static CallToolResult CreateErrorResult(string errorMessage) + { + return new CallToolResult + { + Content = new List + { + new TextContentBlock + { + Text = errorMessage + } + }, + IsError = true + }; + } + + private void RegisterTool( + McpServerOptions options, + string name, + string description, + object inputSchema, + JsonElement? outputSchema) + { + if (options.ToolCollection == null) + { + options.ToolCollection = new McpServerPrimitiveCollection(); + } + + var tool = new AbpMcpServerTool( + name, + description, + JsonSerializer.SerializeToElement(inputSchema, JsonCamelCaseOptions), + outputSchema, + (context, cancellationToken) => HandleToolInvocationAsync(name, context, cancellationToken) + ); + + options.ToolCollection.Add(tool); + } + + private async ValueTask HandleToolInvocationAsync( + string toolName, + RequestContext context, + CancellationToken cancellationToken) + { + _mcpLogger.Debug(LogSource, $"Tool '{toolName}' called with arguments: {context.Params.Arguments}"); + + try + { + var argumentsJson = JsonSerializer.SerializeToElement(context.Params.Arguments); + var resultJson = await _mcpHttpClient.CallToolAsync(toolName, argumentsJson); + + var callToolResult = TryDeserializeResult(resultJson, toolName); + if (callToolResult != null) + { + LogToolResult(toolName, callToolResult); + return callToolResult; + } + + return CreateErrorResult(ToolErrorMessages.InvalidResponseFormat); + } + catch (Exception ex) + { + _mcpLogger.Error(LogSource, $"Tool '{toolName}' execution failed '{ex.Message}'", ex); + return CreateErrorResult(ToolErrorMessages.UnexpectedError); + } + } + + private CallToolResult TryDeserializeResult(string resultJson, string toolName) + { + try + { + return JsonSerializer.Deserialize(resultJson); + } + catch (Exception ex) + { + _mcpLogger.Error(LogSource, $"Failed to deserialize response as CallToolResult: {ex.Message}"); + + var logResponse = resultJson.Length <= MaxLogResponseLength + ? resultJson + : resultJson.Substring(0, MaxLogResponseLength); + _mcpLogger.Debug(LogSource, $"Response was: {logResponse}"); + + return null; + } + } + + private void LogToolResult(string toolName, CallToolResult result) + { + if (result.IsError == true) + { + _mcpLogger.Warning(LogSource, $"Tool '{toolName}' returned an error"); + } + else + { + _mcpLogger.Debug(LogSource, $"Tool '{toolName}' executed successfully"); + } + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs new file mode 100644 index 0000000000..146f5e4ba9 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/McpToolsCacheService.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.Cli.Commands.Models; +using Volo.Abp.Cli.Memory; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IO; + +namespace Volo.Abp.Cli.Commands.Services; + +public class McpToolsCacheService : ITransientDependency +{ + private const string LogSource = nameof(McpToolsCacheService); + private const int CacheValidityHours = 24; + + private readonly McpHttpClientService _mcpHttpClient; + private readonly MemoryService _memoryService; + private readonly ILogger _logger; + private readonly IMcpLogger _mcpLogger; + + public McpToolsCacheService( + McpHttpClientService mcpHttpClient, + MemoryService memoryService, + ILogger logger, + IMcpLogger mcpLogger) + { + _mcpHttpClient = mcpHttpClient; + _memoryService = memoryService; + _logger = logger; + _mcpLogger = mcpLogger; + } + + public async Task> GetToolDefinitionsAsync() + { + if (await IsCacheValidAsync()) + { + var cachedTools = await LoadFromCacheAsync(); + if (cachedTools != null) + { + _mcpLogger.Debug(LogSource, "Using cached tool definitions"); + // Initialize the HTTP client's tool names list from cache + _mcpHttpClient.InitializeToolNames(cachedTools); + return cachedTools; + } + } + + // Cache is invalid or missing, fetch from server + _mcpLogger.Info(LogSource, "Fetching tool definitions from server..."); + var tools = await _mcpHttpClient.GetToolDefinitionsAsync(); + + // Validate that we got tools + if (tools == null || tools.Count == 0) + { + throw new CliUsageException( + "Failed to fetch tool definitions from ABP.IO MCP Server. " + + "No tools available. The MCP server cannot start without tool definitions."); + } + + // Save tools to cache + await SaveToCacheAsync(tools); + await _memoryService.SetAsync(CliConsts.MemoryKeys.McpToolsLastFetchDate, DateTime.Now.ToString(CultureInfo.InvariantCulture)); + + _mcpLogger.Info(LogSource, $"Successfully fetched and cached {tools.Count} tool definitions"); + return tools; + } + + private async Task IsCacheValidAsync() + { + try + { + // Check if cache file exists + if (!File.Exists(CliPaths.McpToolsCache)) + { + return false; + } + + // Check timestamp in memory + var lastFetchTimeString = await _memoryService.GetAsync(CliConsts.MemoryKeys.McpToolsLastFetchDate); + if (string.IsNullOrEmpty(lastFetchTimeString)) + { + return false; + } + + if (DateTime.TryParse(lastFetchTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var lastFetchTime)) + { + // Check if less than configured hours old + if (DateTime.Now.Subtract(lastFetchTime).TotalHours < CacheValidityHours) + { + return true; + } + } + + return false; + } + catch (Exception ex) + { + _logger.LogWarning($"Error checking cache validity: {ex.Message}"); + return false; + } + } + + private async Task> LoadFromCacheAsync() + { + try + { + if (!File.Exists(CliPaths.McpToolsCache)) + { + return null; + } + + var json = await FileHelper.ReadAllTextAsync(CliPaths.McpToolsCache); + var tools = JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return tools; + } + catch (Exception ex) + { + _logger.LogWarning($"Error loading cached tool definitions: {ex.Message}"); + return null; + } + } + + private Task SaveToCacheAsync(List tools) + { + try + { + // Ensure directory exists + var directory = Path.GetDirectoryName(CliPaths.McpToolsCache); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var json = JsonSerializer.Serialize(tools, new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // Using synchronous File.WriteAllText is acceptable here since cache writes are not on the critical path + // and we need to support multiple target frameworks + File.WriteAllText(CliPaths.McpToolsCache, json); + + // Set restrictive file permissions (user read/write only) + SetRestrictiveFilePermissions(CliPaths.McpToolsCache); + } + catch (Exception ex) + { + _logger.LogWarning($"Error saving tool definitions to cache: {ex.Message}"); + } + + return Task.CompletedTask; + } + + private void SetRestrictiveFilePermissions(string filePath) + { + try + { + // On Unix systems, set permissions to 600 (user read/write only) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { +#if NET6_0_OR_GREATER + File.SetUnixFileMode(filePath, UnixFileMode.UserRead | UnixFileMode.UserWrite); +#endif + } + // On Windows, the file inherits permissions from the user profile directory, + // which is already restrictive to the current user + } + catch (Exception ex) + { + _logger.LogWarning($"Error setting file permissions: {ex.Message}"); + } + } +} + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs index 0e22d62480..9c0550bf3d 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromSolutionStep.cs @@ -29,8 +29,7 @@ public class RemoveProjectFromSolutionStep : ProjectBuildPipelineStep { _solutionFilePathWithoutFileExtension = solutionFilePathWithoutFileExtension.RemovePostFix(".sln"); } - - if (solutionFilePathWithoutFileExtension != null && solutionFilePathWithoutFileExtension.EndsWith(".slnx")) + else if (solutionFilePathWithoutFileExtension != null && solutionFilePathWithoutFileExtension.EndsWith(".slnx")) { _solutionFilePathWithoutFileExtension = solutionFilePathWithoutFileExtension.RemovePostFix(".slnx"); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs index 5462f3e9fb..b8f1d77bc8 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/DerivedClassFinder.cs @@ -26,10 +26,10 @@ public class DerivedClassFinder : ITransientDependency var binFile = Path.Combine(csprojFileDirectory, "bin"); var objFile = Path.Combine(csprojFileDirectory, "obj"); - var csFiles = new DirectoryInfo(csprojFileDirectory) .GetFiles("*.cs", SearchOption.AllDirectories) - .Where(f => f.DirectoryName != null && (!f.DirectoryName.StartsWith(binFile) || !f.DirectoryName.StartsWith(objFile))) + .Where(f => !f.FullName.StartsWith(binFile, StringComparison.OrdinalIgnoreCase) && + !f.FullName.StartsWith(objFile, StringComparison.OrdinalIgnoreCase)) .Select(f => f.FullName) .ToList(); @@ -53,7 +53,13 @@ public class DerivedClassFinder : ITransientDependency protected bool IsDerived(string csFile, string baseClass) { - var root = CSharpSyntaxTree.ParseText(File.ReadAllText(csFile)).GetRoot(); + var csFileText = File.ReadAllText(csFile); + if (!csFileText.Contains("class")) + { + return false; + } + + var root = CSharpSyntaxTree.ParseText(csFileText).GetRoot(); var namespaceSyntax = root.DescendantNodes().OfType().FirstOrDefault(); var classDeclaration = (namespaceSyntax?.DescendantNodes().OfType())?.FirstOrDefault(); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs index ffa6a50edf..c4f1181fa3 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs @@ -44,13 +44,20 @@ public class EfCoreMigrationManager : ITransientDependency string dbContext, string outputDirectory) { + CmdHelper.RunCmd($"dotnet build", workingDirectory: dbMigrationsProjectFolder, exitCode: out var buildExitCode); + if (buildExitCode != 0) + { + Logger.LogWarning("Dotnet build failed for project folder {ProjectFolder}. Skipping EF Core migration command.", dbMigrationsProjectFolder); + return; + } + var dbContextOption = string.IsNullOrWhiteSpace(dbContext) ? string.Empty : $"--context {dbContext}"; CmdHelper.RunCmd($"dotnet ef migrations add {migrationName}" + $" --output-dir {outputDirectory}" + - $" {dbContextOption}", + $" {dbContextOption}", workingDirectory: dbMigrationsProjectFolder); } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs index 7a815df81d..c8d9127a2c 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs @@ -18,16 +18,17 @@ public class SolutionFileModifier : ITransientDependency { _cmdHelper = cmdHelper; } - + public async Task RemoveProjectFromSolutionFileAsync(string solutionFile, string projectName) { - var list = _cmdHelper.RunCmdAndGetOutput($"dotnet sln \"{solutionFile}\" list"); + var workingDirectory = Path.GetDirectoryName(solutionFile); + var list = _cmdHelper.RunCmdAndGetOutput($"dotnet sln \"{solutionFile}\" list", workingDirectory: workingDirectory); foreach (var line in list.Split(new[] { Environment.NewLine, "\n" }, StringSplitOptions.None)) { if (Path.GetFileNameWithoutExtension(line.Trim()).Equals(projectName, StringComparison.InvariantCultureIgnoreCase)) { - _cmdHelper.RunCmd($"dotnet sln \"{solutionFile}\" remove \"{line.Trim()}\""); + _cmdHelper.RunCmd($"dotnet sln \"{solutionFile}\" remove \"{line.Trim()}\"", workingDirectory: workingDirectory); break; } } @@ -50,30 +51,27 @@ public class SolutionFileModifier : ITransientDependency private async Task AddModuleAsync(ModuleWithMastersInfo module, string solutionFile) { + var slnDir = Path.GetDirectoryName(solutionFile); var projectsUnderModule = Directory.GetFiles( - Path.Combine(Path.GetDirectoryName(solutionFile), "modules", module.Name), + Path.Combine(slnDir, "modules", module.Name), "*.csproj", SearchOption.AllDirectories); - + var projectsUnderTest = new List(); - if (Directory.Exists(Path.Combine(Path.GetDirectoryName(solutionFile), "modules", module.Name, "test"))) + if (Directory.Exists(Path.Combine(slnDir, "modules", module.Name, "test"))) { projectsUnderTest = Directory.GetFiles( - Path.Combine(Path.GetDirectoryName(solutionFile), "modules", module.Name, "test"), + Path.Combine(slnDir, "modules", module.Name, "test"), "*.csproj", SearchOption.AllDirectories).ToList(); } foreach (var projectPath in projectsUnderModule) { - var folder = projectsUnderTest.Contains(projectPath) ? "test" : "src"; - - var projectId = Path.GetFileName(projectPath).Replace(".csproj", ""); - var package = @$"modules\{module.Name}\{folder}\{projectId}\{projectId}.csproj"; - - _cmdHelper.RunCmd($"dotnet sln \"{solutionFile}\" add \"{package}\" --solution-folder {folder}"); + var solutionFolder = projectsUnderTest.Contains(projectPath) ? Path.Combine("test", module.Name) : Path.Combine("modules", module.Name); + _cmdHelper.RunCmd($"dotnet sln \"{solutionFile}\" add \"{projectPath}\" --solution-folder \"{solutionFolder}\"", workingDirectory: slnDir); } - + if (module.MasterModuleInfos != null) { foreach (var masterModule in module.MasterModuleInfos) diff --git a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs index 61f2ef9de8..54d617c143 100644 --- a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs +++ b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.Extensions.DependencyInjection; using Serilog; using Serilog.Events; @@ -15,7 +15,7 @@ public class Program Console.OutputEncoding = System.Text.Encoding.UTF8; var loggerOutputTemplate = "{Message:lj}{NewLine}{Exception}"; - Log.Logger = new LoggerConfiguration() + var config = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) @@ -26,10 +26,21 @@ public class Program #else .MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Information) #endif - .Enrich.FromLogContext() - .WriteTo.File(Path.Combine(CliPaths.Log, "abp-cli-logs.txt"), outputTemplate: loggerOutputTemplate) - .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, outputTemplate: loggerOutputTemplate) - .CreateLogger(); + .Enrich.FromLogContext(); + + if (args.Length > 0 && args[0].Equals("mcp", StringComparison.OrdinalIgnoreCase)) + { + Log.Logger = config + .WriteTo.File(Path.Combine(CliPaths.Log, "abp-cli-mcp-logs.txt"), outputTemplate: loggerOutputTemplate) + .CreateLogger(); + } + else + { + Log.Logger = config + .WriteTo.File(Path.Combine(CliPaths.Log, "abp-cli-logs.txt"), outputTemplate: loggerOutputTemplate) + .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen, outputTemplate: loggerOutputTemplate) + .CreateLogger(); + } using (var application = AbpApplicationFactory.Create( options => diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs index 64b22ef78f..103f6afd63 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Constants/ActivityNameConsts.cs @@ -1,4 +1,4 @@ -namespace Volo.Abp.Internal.Telemetry.Constants; +namespace Volo.Abp.Internal.Telemetry.Constants; public static class ActivityNameConsts { @@ -68,6 +68,7 @@ public static class ActivityNameConsts public const string AbpCliCommandsInstallModule = "AbpCli.Comands.InstallModule"; public const string AbpCliCommandsInstallLocalModule = "AbpCli.Comands.InstallLocalModule"; public const string AbpCliCommandsListModules = "AbpCli.Comands.ListModules"; + public const string AbpCliCommandsMcp = "AbpCli.Commands.Mcp"; public const string AbpCliRun = "AbpCli.Run"; public const string AbpCliExit = "AbpCli.Exit"; public const string ApplicationRun = "Application.Run"; diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj index ddf556047a..6d685652ca 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj @@ -22,8 +22,7 @@ - - + diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index ac1e8c6565..3668193c9b 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -43,7 +43,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB CorrelationIdProvider = correlationIdProvider; } - public IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class + public virtual IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class { return Subscribe(typeof(TEvent), handler); } @@ -53,7 +53,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB return PublishAsync(eventType, eventData, onUnitOfWorkComplete, useOutbox: true); } - public Task PublishAsync( + public virtual Task PublishAsync( TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) @@ -154,7 +154,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB return Task.CompletedTask; } - protected async Task AddToInboxAsync( + protected virtual async Task AddToInboxAsync( string? messageId, string eventName, Type eventType, @@ -181,6 +181,9 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (await eventInbox.ExistsByMessageIdAsync(messageId!)) { + // Message already exists in the inbox, no need to add again. + // This can happen in case of retries from the sender side. + addToInbox = true; continue; } } diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs index 6101585878..23446069a9 100644 --- a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs @@ -7,14 +7,43 @@ namespace Volo.Abp.Timing; public static class TimeZoneHelper { + /// + /// Returns timezone list ordered by display name, enriched with UTC offset, filtering out invalid ids. + /// public static List GetTimezones(List timezones) { return timezones .OrderBy(x => x.Name) - .Select(x => new NameValue( $"{x.Name} ({GetTimezoneOffset(TZConvert.GetTimeZoneInfo(x.Name))})", x.Name)) + .Select(TryCreateNameValueWithOffset) + .OfType() .ToList(); } + /// + /// Builds a with the original timezone ID in Value and a display name that includes + /// the UTC offset in the Name property; returns null if the id is not found. + /// + public static NameValue? TryCreateNameValueWithOffset(NameValue timeZone) + { + try + { + var timeZoneInfo = TZConvert.GetTimeZoneInfo(timeZone.Name); + var name = $"{timeZone.Name} ({GetTimezoneOffset(timeZoneInfo)})"; + return new NameValue(name, timeZone.Name); + } + catch (Exception) + { + // Invalid or unknown timezone IDs are expected here (e.g. from user input or + // external sources). We intentionally swallow this exception and return null + // so callers (like GetTimezones) can filter out invalid entries. + } + + return null; + } + + /// + /// Formats the base UTC offset as "+hh:mm" or "-hh:mm" for display purposes. + /// public static string GetTimezoneOffset(TimeZoneInfo timeZoneInfo) { if (timeZoneInfo.BaseUtcOffset < TimeSpan.Zero) diff --git a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs index 6cf352318a..5feaceb528 100644 --- a/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs +++ b/framework/test/Volo.Abp.AI.Tests/Volo/Abp/AI/ChatClientAccessor_Tests.cs @@ -31,14 +31,24 @@ public class ChatClientAccessor_Tests : AbpIntegratedTest } [Fact] - public void Should_Resolve_ChatClientAccessor_For_NonConfigured_Workspace() + public void Should_Resolve_Default_ChatClient_From_NonConfigured_Workspace_Accessor() { // Arrange & Act var chatClientAccessor = GetRequiredService>(); // Assert chatClientAccessor.ShouldNotBeNull(); - chatClientAccessor.ChatClient.ShouldBeNull(); + chatClientAccessor.ChatClient.ShouldNotBeNull(); + } + + [Fact] + public void Should_Resolve_Default_ChatClient_For_NonConfigured_Workspace() + { + // Arrange & Act + var chatClient = GetRequiredService>(); + + // Assert + chatClient.ShouldNotBeNull(); } public class NonConfiguredWorkspace diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs index c76f5be9ea..25cc4c9784 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Testing; @@ -751,6 +752,74 @@ public class DistributedCache_Tests : AbpIntegratedTest cacheValue[1].Value.Name.ShouldBe("jack"); } + [Fact] + public async Task GetOrAddManyAsync_Should_Return_Values_By_Key_With_Uow() + { + var key1 = "testkey"; + var key2 = "testkey2"; + var keys = new[] { key1, key2 }; + + using (GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + await personCache.SetAsync(key2, new PersonCacheItem("cached"), considerUow: true); + + var result = await personCache.GetOrAddManyAsync(keys, missingKeys => + { + missingKeys.ToArray().ShouldBe(new[] { key1 }); + return Task.FromResult(new List> + { + new(key1, new PersonCacheItem("factory")) + }); + }, considerUow: true); + + result.Length.ShouldBe(2); + result[0].Key.ShouldBe(key1); + result[0].Value.ShouldNotBeNull(); + result[0].Value.Name.ShouldBe("factory"); + result[1].Key.ShouldBe(key2); + result[1].Value.ShouldNotBeNull(); + result[1].Value.Name.ShouldBe("cached"); + } + } + + [Fact] + public async Task GetOrAddManyAsync_Should_Map_By_Key_Under_Concurrency() + { + var key1 = "testkey"; + var key2 = "testkey2"; + var keys = new[] { key1, key2 }; + + var personCache = GetRequiredService>(); + + async Task>> Factory(IEnumerable missingKeys) + { + await Task.Yield(); + + return missingKeys + .Reverse() + .Select(x => new KeyValuePair(x, new PersonCacheItem(x == key1 ? "v1" : "v2"))) + .ToList(); + } + + var task1 = personCache.GetOrAddManyAsync(keys, Factory); + var task2 = personCache.GetOrAddManyAsync(keys, Factory); + + var results = await Task.WhenAll(task1, task2); + + foreach (var result in results) + { + result.Length.ShouldBe(2); + + result[0].Key.ShouldBe(key1); + result[0].Value!.Name.ShouldBe("v1"); + + result[1].Key.ShouldBe(key2); + result[1].Value!.Name.ShouldBe("v2"); + } + } + [Fact] public async Task Cache_Should_Only_Available_In_Uow_For_GetOrAddManyAsync() { diff --git a/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs new file mode 100644 index 0000000000..54d8d37361 --- /dev/null +++ b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Shouldly; +using TimeZoneConverter; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Timing; + +public class TimeZoneHelper_Tests : AbpIntegratedTest +{ + [Fact] + public void GetTimezones_Should_Filter_Invalid_Timezones() + { + var validTimeZoneId = "UTC"; + var invalidTimeZoneId = "Invalid/Zone"; + + var timezones = new List + { + new(invalidTimeZoneId, invalidTimeZoneId), + new(validTimeZoneId, validTimeZoneId) + }; + + var result = TimeZoneHelper.GetTimezones(timezones); + + result.Count.ShouldBe(1); + + var expectedTimeZoneInfo = TZConvert.GetTimeZoneInfo(validTimeZoneId); + var expectedName = $"{validTimeZoneId} ({TimeZoneHelper.GetTimezoneOffset(expectedTimeZoneInfo)})"; + + result[0].Name.ShouldBe(expectedName); + result[0].Value.ShouldBe(validTimeZoneId); + } + + [Fact] + public void TryCreateNameValueWithOffset_Should_Return_Null_For_Invalid_Timezone() + { + TimeZoneHelper.TryCreateNameValueWithOffset(new NameValue("Invalid/Zone", "Invalid/Zone")).ShouldBeNull(); + } +} diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ar.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ar.json index 86a9ad9581..c67e0ed23f 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ar.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ar.json @@ -103,6 +103,7 @@ "Exporting": "جاري التصدير", "ExportCompleted": "تم إكمال التصدير بنجاح", "ExportFailed": "فشل التصدير", + "ThereWereNoRecordsToExport": "لم تكن هناك سجلات للتصدير.", "ExportJobQueued": "تم وضع مهمة التصدير في الطابور لـ {0} سجل. ستتلقى بريداً إلكترونياً عند اكتمال التصدير.", "ExportReady": "تم إكمال التصدير لـ {0} سجل وجاهز للتنزيل.", "FileName": "اسم الملف", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/cs.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/cs.json index a98da37703..aa7deb165e 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/cs.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/cs.json @@ -103,6 +103,7 @@ "Exporting": "Exportování", "ExportCompleted": "Export úspěšně dokončen", "ExportFailed": "Export se nezdařil", + "ThereWereNoRecordsToExport": "Nebyly žádné záznamy k exportu.", "ExportJobQueued": "Úloha exportu byla zařazena do fronty pro {0} záznamů. Obdržíte e-mail, když bude export dokončen.", "ExportReady": "Export dokončen pro {0} záznamů a připraven ke stažení.", "FileName": "Název souboru", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/de.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/de.json index cc316ff867..64f7ef591d 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/de.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/de.json @@ -103,6 +103,7 @@ "Exporting": "Exportieren", "ExportCompleted": "Export erfolgreich abgeschlossen", "ExportFailed": "Export fehlgeschlagen", + "ThereWereNoRecordsToExport": "Es gab keine Datensätze zum Exportieren.", "ExportJobQueued": "Export-Job wurde für {0} Datensätze in die Warteschlange eingereiht. Sie erhalten eine E-Mail, wenn der Export abgeschlossen ist.", "ExportReady": "Export für {0} Datensätze abgeschlossen und zum Download bereit.", "FileName": "Dateiname", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/en.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/en.json index e837e01666..9c9052e06a 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/en.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/en.json @@ -103,6 +103,7 @@ "Exporting": "Exporting", "ExportCompleted": "Export completed successfully", "ExportFailed": "Export failed", + "ThereWereNoRecordsToExport": "There were no records to export.", "ExportJobQueued": "Export job has been queued for {0} records. You will receive an email when the export is completed.", "ExportReady": "Export completed for {0} records and is ready for download.", "FileName": "File name", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/es.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/es.json index 518c29d0ea..5cd28646af 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/es.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/es.json @@ -103,6 +103,7 @@ "Exporting": "Exportando", "ExportCompleted": "Exportación completada exitosamente", "ExportFailed": "Exportación fallida", + "ThereWereNoRecordsToExport": "No había registros para exportar.", "ExportJobQueued": "El trabajo de exportación ha sido puesto en cola para {0} registros. Recibirá un correo electrónico cuando se complete la exportación.", "ExportReady": "Exportación completada para {0} registros y lista para descarga.", "FileName": "Nombre del archivo", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fi.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fi.json index a71a9cf5e8..6f28446bd3 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fi.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fi.json @@ -103,6 +103,7 @@ "Exporting": "Viedään", "ExportCompleted": "Vienti suoritettu onnistuneesti", "ExportFailed": "Vienti epäonnistui", + "ThereWereNoRecordsToExport": "Ei ollut vietäviä tietueita.", "ExportJobQueued": "Vientityö on asetettu jonoon {0} tietueelle. Saat sähköpostin, kun vienti on valmis.", "ExportReady": "Vienti valmis {0} tietueelle ja valmis ladattavaksi.", "FileName": "Tiedostonimi", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fr.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fr.json index c12e499403..92e912036b 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fr.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/fr.json @@ -103,6 +103,7 @@ "Exporting": "Exportation", "ExportCompleted": "Exportation terminée avec succès", "ExportFailed": "Échec de l'exportation", + "ThereWereNoRecordsToExport": "Il n'y avait aucun enregistrement à exporter.", "ExportJobQueued": "Le travail d'exportation a été mis en file d'attente pour {0} enregistrements. Vous recevrez un e-mail lorsque l'exportation sera terminée.", "ExportReady": "Exportation terminée pour {0} enregistrements et prête pour le téléchargement.", "FileName": "Nom du fichier", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/hu.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/hu.json index af744ac07b..706842c89e 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/hu.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/hu.json @@ -103,6 +103,7 @@ "Exporting": "Exportálás", "ExportCompleted": "Exportálás sikeresen befejezve", "ExportFailed": "Exportálás sikertelen", + "ThereWereNoRecordsToExport": "Nem voltak exportálható rekordok.", "ExportJobQueued": "Exportálási feladat sorba állítva {0} rekordhoz. E-mailt fog kapni, amikor az exportálás befejeződött.", "ExportReady": "Exportálás befejezve {0} rekordhoz és letöltésre kész.", "FileName": "Fájlnév", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/nl.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/nl.json index 9b44a3c884..34715b6c0e 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/nl.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/nl.json @@ -103,6 +103,7 @@ "Exporting": "Exporteren", "ExportCompleted": "Export succesvol voltooid", "ExportFailed": "Export mislukt", + "ThereWereNoRecordsToExport": "Er waren geen records om te exporteren.", "ExportJobQueued": "Exporttaak is in de wachtrij geplaatst voor {0} records. U ontvangt een e-mail wanneer de export is voltooid.", "ExportReady": "Export voltooid voor {0} records en klaar voor download.", "FileName": "Bestandsnaam", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pl-PL.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pl-PL.json index 17162d6af8..9fb0f61251 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pl-PL.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pl-PL.json @@ -103,6 +103,7 @@ "Exporting": "Eksportowanie", "ExportCompleted": "Eksport zakończony pomyślnie", "ExportFailed": "Eksport nieudany", + "ThereWereNoRecordsToExport": "Nie było żadnych rekordów do eksportu.", "ExportJobQueued": "Zadanie eksportu zostało umieszczone w kolejce dla {0} rekordów. Otrzymasz e-mail po zakończeniu eksportu.", "ExportReady": "Eksport zakończony dla {0} rekordów i gotowy do pobrania.", "FileName": "Nazwa pliku", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pt-BR.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pt-BR.json index 03ec81d51e..da63715a57 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pt-BR.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/pt-BR.json @@ -103,6 +103,7 @@ "Exporting": "Exportando", "ExportCompleted": "Exportação concluída com sucesso", "ExportFailed": "Exportação falhou", + "ThereWereNoRecordsToExport": "Não havia registros para exportar.", "ExportJobQueued": "O trabalho de exportação foi enfileirado para {0} registros. Você receberá um email quando a exportação for concluída.", "ExportReady": "Exportação concluída para {0} registros e pronta para download.", "FileName": "Nome do arquivo", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ru.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ru.json index f07c7baa78..9a2817f1fd 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ru.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ru.json @@ -103,6 +103,7 @@ "Exporting": "Экспорт", "ExportCompleted": "Экспорт успешно завершен", "ExportFailed": "Экспорт не удался", + "ThereWereNoRecordsToExport": "Не было записей для экспорта.", "ExportJobQueued": "Задача экспорта поставлена в очередь для {0} записей. Вы получите электронное письмо, когда экспорт будет завершен.", "ExportReady": "Экспорт завершен для {0} записей и готов к загрузке.", "FileName": "Имя файла", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sk.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sk.json index e406b3ec25..54ec9409cd 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sk.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sk.json @@ -103,6 +103,7 @@ "Exporting": "Exportovanie", "ExportCompleted": "Export úspešne dokončený", "ExportFailed": "Export sa nepodaril", + "ThereWereNoRecordsToExport": "Neboli žiadne záznamy na export.", "ExportJobQueued": "Úloha exportu bola zaradená do fronty pre {0} záznamov. Dostanete e-mail, keď bude export dokončený.", "ExportReady": "Export dokončený pre {0} záznamov a pripravený na stiahnutie.", "FileName": "Názov súboru", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sl.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sl.json index 0efc30a3d0..2ddca7cf3d 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sl.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sl.json @@ -103,6 +103,7 @@ "Exporting": "Izvažanje", "ExportCompleted": "Izvoz uspešno dokončan", "ExportFailed": "Izvoz neuspešen", + "ThereWereNoRecordsToExport": "Ni bilo zapisov za izvoz.", "ExportJobQueued": "Naloga izvoza je bila postavljena v vrsto za {0} zapisov. Prejeli boste e-pošto, ko bo izvoz dokončan.", "ExportReady": "Izvoz dokončan za {0} zapisov in pripravljen za prenos.", "FileName": "Ime datoteke", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sv.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sv.json index b3a44c27ed..f1f547aa6e 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sv.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/sv.json @@ -103,6 +103,7 @@ "Exporting": "Exporterar", "ExportCompleted": "Export slutförd framgångsrikt", "ExportFailed": "Export misslyckades", + "ThereWereNoRecordsToExport": "Det fanns inga poster att exportera.", "ExportJobQueued": "Exportjobb har köats för {0} poster. Du kommer att få ett e-postmeddelande när exporten är klar.", "ExportReady": "Export slutförd för {0} poster och redo för nedladdning.", "FileName": "Filnamn", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/tr.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/tr.json index 86105e1a08..3aa2abdf25 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/tr.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/tr.json @@ -88,6 +88,7 @@ "Exporting": "Aktarılıyor", "ExportCompleted": "Aktarma başarıyla tamamlandı", "ExportFailed": "Aktarma başarısız", + "ThereWereNoRecordsToExport": "Dışa aktarılacak kayıt bulunamadı.", "ExportJobQueued": "{0} kayıt için dışa aktarma işi sıraya alındı. İşlem tamamlandığında e-posta ile bilgilendirileceksiniz.", "ExportReady": "{0} kayıt için dışa aktarma tamamlandı ve indirme için hazır.", "FileName": "Dosya adı", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hans.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hans.json index 4b3d61f2a3..b02fc0e26d 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hans.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hans.json @@ -103,6 +103,7 @@ "Exporting": "导出中", "ExportCompleted": "导出成功完成", "ExportFailed": "导出失败", + "ThereWereNoRecordsToExport": "没有可导出的记录。", "ExportJobQueued": "导出作业已为{0}条记录排队。导出完成后您将收到电子邮件。", "ExportReady": "{0}条记录的导出已完成,可以下载。", "FileName": "文件名", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hant.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hant.json index 00e9cc4c08..46f2c9e495 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hant.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/zh-Hant.json @@ -103,6 +103,7 @@ "Exporting": "匯出中", "ExportCompleted": "匯出成功完成", "ExportFailed": "匯出失敗", + "ThereWereNoRecordsToExport": "沒有可匯出的記錄。", "ExportJobQueued": "匯出作業已為{0}條記錄排隊。匯出完成後您將收到電子郵件。", "ExportReady": "{0}條記錄的匯出已完成,可以下載。", "FileName": "檔案名稱", diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo.Abp.AuditLogging.Domain.abppkg.analyze.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo.Abp.AuditLogging.Domain.abppkg.analyze.json index 98c5a043ba..670e9fa26e 100644 --- a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo.Abp.AuditLogging.Domain.abppkg.analyze.json +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo.Abp.AuditLogging.Domain.abppkg.analyze.json @@ -254,6 +254,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs index cd74c10f82..61a034c0a0 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/DemoAppHangfireModule.cs @@ -1,17 +1,23 @@ -using Hangfire; +using System; +using System.Threading.Tasks; +using Hangfire; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Autofac; using Volo.Abp.BackgroundJobs.DemoApp.Shared; using Volo.Abp.Modularity; using Microsoft.Extensions.Configuration; using Volo.Abp.BackgroundJobs.Hangfire; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.Hangfire; namespace Volo.Abp.BackgroundJobs.DemoApp.HangFire; [DependsOn( typeof(DemoAppSharedModule), typeof(AbpAutofacModule), - typeof(AbpBackgroundJobsHangfireModule) + typeof(AbpBackgroundJobsHangfireModule), + typeof(AbpBackgroundWorkersHangfireModule) )] public class DemoAppHangfireModule : AbpModule { @@ -24,4 +30,27 @@ public class DemoAppHangfireModule : AbpModule hangfireConfiguration.UseSqlServerStorage(configuration.GetConnectionString("Default")); }); } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.ServerOptions = new BackgroundJobServerOptions + { + Queues = new []{ "default", "my-default" } + }; + }); + + Configure(options => + { + options.TimeZone = TimeZoneInfo.Local; + options.Queue = "my-default"; + }); + } + + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var backgroundWorkerManager = context.ServiceProvider.GetRequiredService(); + await backgroundWorkerManager.AddAsync(context.ServiceProvider.GetRequiredService()); + } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/TestWorker.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/TestWorker.cs new file mode 100644 index 0000000000..60d7f7d365 --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/TestWorker.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundJobs.DemoApp.HangFire; + +public class TestWorker : AsyncPeriodicBackgroundWorkerBase +{ + public TestWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory) + : base(timer, serviceScopeFactory) + { + CronExpression = Cron.Minutely(); + } + + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + { + Console.WriteLine($"[{DateTime.Now}] TestWorker executed."); + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj index 3d145c93ef..c59e3afac9 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/Volo.Abp.BackgroundJobs.DemoApp.HangFire.csproj @@ -7,8 +7,10 @@ + + diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.cs deleted file mode 100644 index aa50d3f32a..0000000000 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations; - -public partial class Initial : Migration -{ - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AbpBackgroundJobs", - columns: table => new { - Id = table.Column(nullable: false), - ExtraProperties = table.Column(nullable: true), - ConcurrencyStamp = table.Column(maxLength: 40, nullable: true), - JobName = table.Column(maxLength: 128, nullable: false), - JobArgs = table.Column(maxLength: 1048576, nullable: false), - TryCount = table.Column(nullable: false, defaultValue: (short)0), - CreationTime = table.Column(nullable: false), - NextTryTime = table.Column(nullable: false), - LastTryTime = table.Column(nullable: true), - IsAbandoned = table.Column(nullable: false, defaultValue: false), - Priority = table.Column(nullable: false, defaultValue: (byte)15) - }, - constraints: table => - { - table.PrimaryKey("PK_AbpBackgroundJobs", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_AbpBackgroundJobs_IsAbandoned_NextTryTime", - table: "AbpBackgroundJobs", - columns: new[] { "IsAbandoned", "NextTryTime" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AbpBackgroundJobs"); - } -} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.Designer.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs similarity index 73% rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.Designer.cs rename to modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs index 35129e8877..3225815926 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20201013055401_Initial.Designer.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.Designer.cs @@ -8,20 +8,24 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Volo.Abp.BackgroundJobs.DemoApp.Db; using Volo.Abp.EntityFrameworkCore; +#nullable disable + namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations { [DbContext(typeof(DemoAppDbContext))] - [Migration("20201013055401_Initial")] + [Migration("20260119064307_Initial")] partial class Initial { + /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "3.1.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => { @@ -29,19 +33,25 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .HasColumnName("ConcurrencyStamp") + .IsRequired() + .HasMaxLength(40) .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + .HasColumnName("ConcurrencyStamp"); b.Property("CreationTime") - .HasColumnName("CreationTime") - .HasColumnType("datetime2"); + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); b.Property("ExtraProperties") - .HasColumnName("ExtraProperties") - .HasColumnType("nvarchar(max)"); + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); b.Property("IsAbandoned") .ValueGeneratedOnAdd() @@ -50,13 +60,13 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.Property("JobArgs") .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(1048576); + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)"); b.Property("JobName") .IsRequired() - .HasColumnType("nvarchar(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); b.Property("LastTryTime") .HasColumnType("datetime2"); @@ -78,7 +88,7 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.HasIndex("IsAbandoned", "NextTryTime"); - b.ToTable("AbpBackgroundJobs"); + b.ToTable("AbpBackgroundJobs", (string)null); }); #pragma warning restore 612, 618 } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs new file mode 100644 index 0000000000..ab0f0d7f37 --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260119064307_Initial.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AbpBackgroundJobs", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ApplicationName = table.Column(type: "nvarchar(96)", maxLength: 96, nullable: true), + JobName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + JobArgs = table.Column(type: "nvarchar(max)", maxLength: 1048576, nullable: false), + TryCount = table.Column(type: "smallint", nullable: false, defaultValue: (short)0), + CreationTime = table.Column(type: "datetime2", nullable: false), + NextTryTime = table.Column(type: "datetime2", nullable: false), + LastTryTime = table.Column(type: "datetime2", nullable: true), + IsAbandoned = table.Column(type: "bit", nullable: false, defaultValue: false), + Priority = table.Column(type: "tinyint", nullable: false, defaultValue: (byte)15), + ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), + ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AbpBackgroundJobs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AbpBackgroundJobs_IsAbandoned_NextTryTime", + table: "AbpBackgroundJobs", + columns: new[] { "IsAbandoned", "NextTryTime" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AbpBackgroundJobs"); + } + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs index 47ee56f4bb..ab91bc354c 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs @@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Volo.Abp.BackgroundJobs.DemoApp.Db; using Volo.Abp.EntityFrameworkCore; +#nullable disable + namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations { [DbContext(typeof(DemoAppDbContext))] @@ -17,9 +19,10 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "3.1.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => { @@ -27,19 +30,25 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() - .HasColumnName("ConcurrencyStamp") + .IsRequired() + .HasMaxLength(40) .HasColumnType("nvarchar(40)") - .HasMaxLength(40); + .HasColumnName("ConcurrencyStamp"); b.Property("CreationTime") - .HasColumnName("CreationTime") - .HasColumnType("datetime2"); + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); b.Property("ExtraProperties") - .HasColumnName("ExtraProperties") - .HasColumnType("nvarchar(max)"); + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); b.Property("IsAbandoned") .ValueGeneratedOnAdd() @@ -48,13 +57,13 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.Property("JobArgs") .IsRequired() - .HasColumnType("nvarchar(max)") - .HasMaxLength(1048576); + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)"); b.Property("JobName") .IsRequired() - .HasColumnType("nvarchar(128)") - .HasMaxLength(128); + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); b.Property("LastTryTime") .HasColumnType("datetime2"); @@ -76,7 +85,7 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.HasIndex("IsAbandoned", "NextTryTime"); - b.ToTable("AbpBackgroundJobs"); + b.ToTable("AbpBackgroundJobs", (string)null); }); #pragma warning restore 612, 618 } diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.abppkg.analyze.json b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.abppkg.analyze.json index 5c294d7a38..c0c59ffb09 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.abppkg.analyze.json +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.abppkg.analyze.json @@ -83,6 +83,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", 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 63e164f4f0..e372a4405b 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.0.1", - "@abp/prismjs": "~10.0.1", - "@abp/highlight.js": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared": "~10.1.0-rc.2", + "@abp/prismjs": "~10.1.0-rc.2", + "@abp/highlight.js": "~10.1.0-rc.2" } } 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 32023c7b7c..07e9b7f188 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,203 +2,203 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.0.1.tgz#fc120857770a16c17f2d029a7920243357cda456" - integrity sha512-iMACbeAq6gSZ2/EUhwd1h/7gctRokSCMNuyE7hh7y2Rb0s4JeW5dbMx9QIc9oywPauRz4yCAJSFi7PJfObFL9w== +"@abp/clipboard@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.2.tgz#e99dbf190e3684e99c8e909bf38201c70e267502" + integrity sha512-kRS9pWc1jRgr4D4/EV9zdAy3rhhGBrcqk2as5+6Ih49npsEJY/cF5mYH7mj/ZYy8SHqtae/CR7bZsR+uCDKYrQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.0.1.tgz#abdc2033d70c8701453186cd543458154b448961" - integrity sha512-a5Jp9gvS78pK3GxMxqkpN5nBxIGUtTrrr05zLmjh3ohzo/u7p+P1QQpZlOythEEjAgF6V+1PEMl/RRaDKmqllA== +"@abp/highlight.js@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.1.0-rc.2.tgz#6ad0e1ef9e49f0f90eba9d899fd72b6c30a4f9f0" + integrity sha512-jAX4p+i3secAaI3waXoMr7yoH6G1nWvcjR5UVin168H7I4UySxbF799T89v5tK8gtfWgaTjEydFZRypSQU/dHg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/prismjs@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.0.1.tgz#dcd4c55e8fed9aa19712517c6f7a86c667269753" - integrity sha512-WdZLCL2UYFVqJnFYuSO4geAi5sdfgxITqI9BhEZdFuJzdfp89PZ4cnK7DYxnYBrgW/yh38xwzv+HVclK3xgPNA== +"@abp/prismjs@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.2.tgz#8565bab503a16fc349f4b0fa2609ad412ff838be" + integrity sha512-SmZWMyJ3cJW+qj4CWJ7y2kD6PMx2zfZMA5X5jPunsytG4Eht4AVyIR38Y4QSpO62zZgkHyZlSTFOozBfhrlv9A== dependencies: - "@abp/clipboard" "~10.0.1" - "@abp/core" "~10.0.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" 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 40dbde88a6..a3ecabd4c4 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.0.1", - "@abp/prismjs": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/prismjs": "~10.1.0-rc.2" }, "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 cd6079141e..f29a796639 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,202 +2,202 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.0.1.tgz#fc120857770a16c17f2d029a7920243357cda456" - integrity sha512-iMACbeAq6gSZ2/EUhwd1h/7gctRokSCMNuyE7hh7y2Rb0s4JeW5dbMx9QIc9oywPauRz4yCAJSFi7PJfObFL9w== +"@abp/clipboard@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.2.tgz#e99dbf190e3684e99c8e909bf38201c70e267502" + integrity sha512-kRS9pWc1jRgr4D4/EV9zdAy3rhhGBrcqk2as5+6Ih49npsEJY/cF5mYH7mj/ZYy8SHqtae/CR7bZsR+uCDKYrQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/prismjs@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.0.1.tgz#dcd4c55e8fed9aa19712517c6f7a86c667269753" - integrity sha512-WdZLCL2UYFVqJnFYuSO4geAi5sdfgxITqI9BhEZdFuJzdfp89PZ4cnK7DYxnYBrgW/yh38xwzv+HVclK3xgPNA== +"@abp/prismjs@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.2.tgz#8565bab503a16fc349f4b0fa2609ad412ff838be" + integrity sha512-SmZWMyJ3cJW+qj4CWJ7y2kD6PMx2zfZMA5X5jPunsytG4Eht4AVyIR38Y4QSpO62zZgkHyZlSTFOozBfhrlv9A== dependencies: - "@abp/clipboard" "~10.0.1" - "@abp/core" "~10.0.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo.Abp.BlobStoring.Database.Domain.abppkg.analyze.json b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo.Abp.BlobStoring.Database.Domain.abppkg.analyze.json index cd919d1727..024ac61b69 100644 --- a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo.Abp.BlobStoring.Database.Domain.abppkg.analyze.json +++ b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo.Abp.BlobStoring.Database.Domain.abppkg.analyze.json @@ -83,6 +83,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -189,6 +195,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/blogging/app/Volo.BloggingTestApp/package.json b/modules/blogging/app/Volo.BloggingTestApp/package.json index 31930b4679..0f4e62f9cf 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.0.1", - "@abp/blogging": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/blogging": "~10.1.0-rc.2" } } diff --git a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock index 251a14309b..1f2cd9805d 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock +++ b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock @@ -2,228 +2,228 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/blogging@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.0.1.tgz#5b7a821c8a2fe4c1b443070036dc8ee54588d1b9" - integrity sha512-yPHLq3i1N5ZhyELfpLFu4vNhjWem1X022PN85G24Gelr8EfMuVnnVw0znamPe1sNAKpO2vYXWYK0YfuEr14K7g== +"@abp/blogging@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.1.0-rc.2.tgz#f60d5fdfef5be11cbbb23ad7b4b246621828e5f9" + integrity sha512-GcI6JWeQKcHA0FaJZYTgx9l63jlSn1cqaWjBx6Y4KYIpy1c8vDKnve85jzsj7UOKgkMFX1c7mN2vwzH3NSr1Qg== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" - "@abp/owl.carousel" "~10.0.1" - "@abp/prismjs" "~10.0.1" - "@abp/tui-editor" "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" + "@abp/owl.carousel" "~10.1.0-rc.2" + "@abp/prismjs" "~10.1.0-rc.2" + "@abp/tui-editor" "~10.1.0-rc.2" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.0.1.tgz#fc120857770a16c17f2d029a7920243357cda456" - integrity sha512-iMACbeAq6gSZ2/EUhwd1h/7gctRokSCMNuyE7hh7y2Rb0s4JeW5dbMx9QIc9oywPauRz4yCAJSFi7PJfObFL9w== +"@abp/clipboard@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.2.tgz#e99dbf190e3684e99c8e909bf38201c70e267502" + integrity sha512-kRS9pWc1jRgr4D4/EV9zdAy3rhhGBrcqk2as5+6Ih49npsEJY/cF5mYH7mj/ZYy8SHqtae/CR7bZsR+uCDKYrQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/owl.carousel@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.0.1.tgz#04008e9c28f7958e7f1702e8238ec6431b45fb29" - integrity sha512-tIDWj/aBIglEplN1Xu4xdi55/FI9l3h709xyabtIo2t0fHmhLGSqWgIIj4sL2C3Prh9IvYy7ucRRoiCYDG7MqQ== +"@abp/owl.carousel@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.1.0-rc.2.tgz#e7697b7c8954472758547688fa6724b219a5c99c" + integrity sha512-XKciT8HNWoZvlcMGRwOz9opld4BJsAQwMKsKRu1oD4/KMCnT7LelacFPVbj3CLhsLpU+2eq0M947fEI7hQiPOQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" owl.carousel "^2.3.4" -"@abp/prismjs@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.0.1.tgz#dcd4c55e8fed9aa19712517c6f7a86c667269753" - integrity sha512-WdZLCL2UYFVqJnFYuSO4geAi5sdfgxITqI9BhEZdFuJzdfp89PZ4cnK7DYxnYBrgW/yh38xwzv+HVclK3xgPNA== +"@abp/prismjs@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.2.tgz#8565bab503a16fc349f4b0fa2609ad412ff838be" + integrity sha512-SmZWMyJ3cJW+qj4CWJ7y2kD6PMx2zfZMA5X5jPunsytG4Eht4AVyIR38Y4QSpO62zZgkHyZlSTFOozBfhrlv9A== dependencies: - "@abp/clipboard" "~10.0.1" - "@abp/core" "~10.0.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/tui-editor@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.0.1.tgz#551720cf5ce65c5c1087afae2c8f7ee3079d4ed9" - integrity sha512-D9Pe+dP9huGLiH176WM2klodENrdI9fLjbX7Pg9rD0FeeydpKREDbgcFs3iJdECrhLU2nV1xTR3gntu9pql65g== +"@abp/tui-editor@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.1.0-rc.2.tgz#ebbd5bad1ee180a0c6e6a9cfd894499614a71e96" + integrity sha512-k5V+5ZE+HZebfyXLzddRQDGri3HP7wSjDXEbSMLTgxZTem7IzksyLWLAN/woKRzWX92BJXcsmR8T1rhuMhohhA== dependencies: - "@abp/jquery" "~10.0.1" - "@abp/prismjs" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" + "@abp/prismjs" "~10.1.0-rc.2" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json b/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json index a91fd840e4..8bf3a2dbe0 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json +++ b/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json @@ -107,6 +107,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -285,6 +291,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -496,6 +508,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -745,6 +763,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -900,6 +924,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json index c73391ae10..66592df9ff 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.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2" } } diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock index bbe4b5c934..a2b6e13b29 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock @@ -2,185 +2,185 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/cms-kit/angular/package.json b/modules/cms-kit/angular/package.json index bf681bb967..d4bc7712ef 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.0.1", - "@abp/ng.identity": "~10.0.1", - "@abp/ng.setting-management": "~10.0.1", - "@abp/ng.tenant-management": "~10.0.1", - "@abp/ng.theme.basic": "~10.0.1", + "@abp/ng.account": "~10.1.0-rc.2", + "@abp/ng.identity": "~10.1.0-rc.2", + "@abp/ng.setting-management": "~10.1.0-rc.1", + "@abp/ng.tenant-management": "~10.1.0-rc.2", + "@abp/ng.theme.basic": "~10.1.0-rc.2", "@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 2a9a89e35d..aa92688cca 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.0.1", - "@abp/ng.theme.shared": ">=10.0.1" + "@abp/ng.core": ">=10.1.0-rc.2", + "@abp/ng.theme.shared": ">=10.1.0-rc.2" }, "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 587b80af76..7588fb2989 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.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock index bbe4b5c934..a2b6e13b29 100644 --- a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock @@ -2,185 +2,185 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" 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 a963ff1451..52a1fc6c21 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.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2" } } 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 bbe4b5c934..a2b6e13b29 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock @@ -2,185 +2,185 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" 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 c59f4fd76c..882508c4e3 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.0.1", - "@abp/cms-kit": "10.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/cms-kit": "10.1.0-rc.2" } } 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 7ed7ff1d59..97b79e0912 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock @@ -2,293 +2,293 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.0.1.tgz#fc120857770a16c17f2d029a7920243357cda456" - integrity sha512-iMACbeAq6gSZ2/EUhwd1h/7gctRokSCMNuyE7hh7y2Rb0s4JeW5dbMx9QIc9oywPauRz4yCAJSFi7PJfObFL9w== +"@abp/clipboard@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.2.tgz#e99dbf190e3684e99c8e909bf38201c70e267502" + integrity sha512-kRS9pWc1jRgr4D4/EV9zdAy3rhhGBrcqk2as5+6Ih49npsEJY/cF5mYH7mj/ZYy8SHqtae/CR7bZsR+uCDKYrQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/cms-kit.admin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.0.1.tgz#98ac41348454728638c3b39ddb6d40cf138cd554" - integrity sha512-klgJYE6+fvJUb5R0yJlgbalcXBjrSLXVIe1V/NSPRYhBceHX71ZUIDcYYV5EQUs0FY/xEYiuIDR/M6mYcHYLeg== +"@abp/cms-kit.admin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.1.0-rc.2.tgz#554d15856007e80cc2994d57612d08e3d6747e3e" + integrity sha512-mi9m4Nr51wyq/EN8DRMT/QlSqgIwD7WKDDSO80Tylr46r5A7wiGbgNIyy+hnHCR9IbORpTRv8VjMTtkjF7CRPA== dependencies: - "@abp/codemirror" "~10.0.1" - "@abp/jstree" "~10.0.1" - "@abp/markdown-it" "~10.0.1" - "@abp/slugify" "~10.0.1" - "@abp/tui-editor" "~10.0.1" - "@abp/uppy" "~10.0.1" + "@abp/codemirror" "~10.1.0-rc.2" + "@abp/jstree" "~10.1.0-rc.2" + "@abp/markdown-it" "~10.1.0-rc.2" + "@abp/slugify" "~10.1.0-rc.2" + "@abp/tui-editor" "~10.1.0-rc.2" + "@abp/uppy" "~10.1.0-rc.2" -"@abp/cms-kit.public@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.0.1.tgz#aab1d7ba0d34efc1733578e13730fb91cfe65c84" - integrity sha512-RwHusSvwYRZ9GIW4rvrLyPdbEwY75D/gBeABftumRYi9TYuMcrQxHfnQ0dByFiV98lq5V6sB/pvBPVyPcy6g3w== +"@abp/cms-kit.public@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.1.0-rc.2.tgz#1e25575fa7dde2661970942c868250f749ffcf7e" + integrity sha512-BbEL17D0k44+P1pBfmrRwhwukXqbggprl8DWBofBkPut1HUmo1iaD4NrMEG5NZC03L8LRDVNm3hx2zI1voF9Sg== dependencies: - "@abp/highlight.js" "~10.0.1" - "@abp/star-rating-svg" "~10.0.1" + "@abp/highlight.js" "~10.1.0-rc.2" + "@abp/star-rating-svg" "~10.1.0-rc.2" -"@abp/cms-kit@10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.0.1.tgz#9c50225de5a7b6e1a80567ab912a4b81faec3e16" - integrity sha512-LxGWXbXUeXRzrd91NuTYM0wIRlIEdkTA1tOfTpZapNDO1MwSyadjt1DrtMvlRXo0xHlzE9PRw0KI8EUOGpi49A== +"@abp/cms-kit@10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.1.0-rc.2.tgz#18d0818a07a530fccb23fa082db24e439854e94a" + integrity sha512-cwraU613er9EybLoU9wXiqfFFBjSyk/DnbZkAviavOLg+QRTFdDoJEVTPpNn6WCXc1oS5/LPuu/744MrGixYyw== dependencies: - "@abp/cms-kit.admin" "~10.0.1" - "@abp/cms-kit.public" "~10.0.1" + "@abp/cms-kit.admin" "~10.1.0-rc.2" + "@abp/cms-kit.public" "~10.1.0-rc.2" -"@abp/codemirror@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.0.1.tgz#4b1b0feab9b711809d5115eb359c5ffe77e90056" - integrity sha512-Buj11XuJuDJnEKvSE+HCD846ynqctn76xhLoYwKD6mnRzdvVKw57MpU1TQVjXfqd8hiD418oE7uEaTW/zm7RvA== +"@abp/codemirror@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.1.0-rc.2.tgz#8a55a66c8be83995045bc2724ca02c29771283a3" + integrity sha512-8jlMySyFTNnDJvQLqZgj7S0EZ0oGWoplDFDE6zEP7NBK8INzrw7D7lc93XZ8/G7lRh0xeM1OcziUHZEX/QhXNg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" codemirror "^5.65.1" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.0.1.tgz#abdc2033d70c8701453186cd543458154b448961" - integrity sha512-a5Jp9gvS78pK3GxMxqkpN5nBxIGUtTrrr05zLmjh3ohzo/u7p+P1QQpZlOythEEjAgF6V+1PEMl/RRaDKmqllA== +"@abp/highlight.js@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.1.0-rc.2.tgz#6ad0e1ef9e49f0f90eba9d899fd72b6c30a4f9f0" + integrity sha512-jAX4p+i3secAaI3waXoMr7yoH6G1nWvcjR5UVin168H7I4UySxbF799T89v5tK8gtfWgaTjEydFZRypSQU/dHg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/jstree@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.0.1.tgz#10db9cc5b9fba61424a202cf3ae15d7f26e651d6" - integrity sha512-V98/Bt++JQQjsPkcE81GAN7Sz6M5heq+t2uHn5gvqeUwkdX1OsutxKp1oSeXVrYItxeI7avWB9iTJGfsq0RhBQ== +"@abp/jstree@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.1.0-rc.2.tgz#0b145af388ca527b107464ff706c5f901344a094" + integrity sha512-RmKRKVuQBhemapF4wmNVOZfDxBSjqKoSRL/Y0Sbyqx86Qg/HYpFZD1zQgaH6NFwUOpwB4wVJRBHV5r7i4LxxyQ== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jstree "^3.3.17" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/markdown-it@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.0.1.tgz#c8266ae68fb1603dd210563f55a2d69050687b46" - integrity sha512-mDe5ZXfaSBIHe1AKV9UgebjuxhDamb63/P6GdZ43hjuUXX42UgPCDO73Y7XuxDJbKZudxVecFoXMZ5mCzeKsLg== +"@abp/markdown-it@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.1.0-rc.2.tgz#43865464fb44f470baf30296712a5f007ff9730d" + integrity sha512-R45uA/EfMEZSkWchdpE/epOPsjswuQDB+IyBdwil2kKe3Bfv3yIh8+GSU5hfhiArXmRbhcmNGdeTylXWhrInbw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" markdown-it "^14.1.0" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/prismjs@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.0.1.tgz#dcd4c55e8fed9aa19712517c6f7a86c667269753" - integrity sha512-WdZLCL2UYFVqJnFYuSO4geAi5sdfgxITqI9BhEZdFuJzdfp89PZ4cnK7DYxnYBrgW/yh38xwzv+HVclK3xgPNA== +"@abp/prismjs@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.2.tgz#8565bab503a16fc349f4b0fa2609ad412ff838be" + integrity sha512-SmZWMyJ3cJW+qj4CWJ7y2kD6PMx2zfZMA5X5jPunsytG4Eht4AVyIR38Y4QSpO62zZgkHyZlSTFOozBfhrlv9A== dependencies: - "@abp/clipboard" "~10.0.1" - "@abp/core" "~10.0.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/slugify@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.0.1.tgz#47d8ffa59e4d57b61573e22167cecaaec257d81c" - integrity sha512-fIX4w+96Qdpa/UFq5jc6alSioilNp7LwqKum9G7LRDqjJoOPHRhG0IkPayY9v/oqJeDKoqlXMkD9s4b5MGQYPw== +"@abp/slugify@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.1.0-rc.2.tgz#42389b8471f4aacd340bebe664994b778eb335ab" + integrity sha512-jhLEqxPpitTfwJgHW21urQnvP3NU+/hXZXzmnT6ZPBw7pVpbN2m1hDOm+JC+zKkLaa3WQ23rE9ewwrnJaoQ67w== dependencies: slugify "^1.6.6" -"@abp/star-rating-svg@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.0.1.tgz#3de67008012d080751cf671cf2bdede88906ca55" - integrity sha512-3vSxBayzdzMTKUeQaCrnFgJBO2V95L4GNUlYtV5txGPtziQd+VTPBL4mxVJ5F/6sizGUlmbXe4fMX33+DSfqbA== +"@abp/star-rating-svg@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.1.0-rc.2.tgz#d1754352cc3af1f42577de19e18350b289685e68" + integrity sha512-3Io7LMSnjQtc1b5CP1nZ8ucvXfq262PGMTHhwgM30xnvO9HJbtnppCmwVTbhZBQigKQFnySUKmccQbGjoqnN4A== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" star-rating-svg "^3.5.0" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/tui-editor@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.0.1.tgz#551720cf5ce65c5c1087afae2c8f7ee3079d4ed9" - integrity sha512-D9Pe+dP9huGLiH176WM2klodENrdI9fLjbX7Pg9rD0FeeydpKREDbgcFs3iJdECrhLU2nV1xTR3gntu9pql65g== +"@abp/tui-editor@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.1.0-rc.2.tgz#ebbd5bad1ee180a0c6e6a9cfd894499614a71e96" + integrity sha512-k5V+5ZE+HZebfyXLzddRQDGri3HP7wSjDXEbSMLTgxZTem7IzksyLWLAN/woKRzWX92BJXcsmR8T1rhuMhohhA== dependencies: - "@abp/jquery" "~10.0.1" - "@abp/prismjs" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" + "@abp/prismjs" "~10.1.0-rc.2" -"@abp/uppy@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.0.1.tgz#5a5df3ac9239ecaeb3ff3bf41868c25dda7e4f59" - integrity sha512-lcERKrPp3cvYplJR0qFi6BJEwWy4jaewmZjtuMV4Vy5su0T1IKadyDNAtw6WLAQae+HGu4tMIZI8kc65AQshjQ== +"@abp/uppy@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.1.0-rc.2.tgz#1b9ca53eda178e8ea5d14573da15b9c141a89fc0" + integrity sha512-QHgTiVaZKZrnNigyE1F7OR3Tdtjdq6tjm164HWAPJSJOUbcv6v498Q7DWS2dIZMU2e/226HflS2NwwNV87s3XA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" uppy "^5.1.2" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.abppkg.analyze.json b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.abppkg.analyze.json index 6850b5203c..69b15238b1 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.abppkg.analyze.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.abppkg.analyze.json @@ -117,6 +117,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -260,6 +266,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -438,6 +450,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -536,6 +554,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -646,6 +670,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -840,6 +870,23 @@ "isOptional": false } ] + }, + { + "returnType": "Void", + "namespace": "Volo.CmsKit.Pages", + "name": "SetStatus", + "summary": null, + "isAsync": false, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "PageStatus", + "name": "status", + "isOptional": false + } + ] } ], "collectionProperties": {}, @@ -891,6 +938,11 @@ "type": "System.String", "name": "LayoutName", "summary": null + }, + { + "type": "Volo.CmsKit.Pages.PageStatus", + "name": "Status", + "summary": null } ], "contentType": "aggregateRoot", @@ -911,6 +963,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1110,6 +1168,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1281,6 +1345,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1374,6 +1444,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1511,6 +1587,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1692,6 +1774,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1870,6 +1958,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -2007,6 +2101,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -2817,6 +2917,11 @@ "type": "String", "name": "layoutName", "isOptional": true + }, + { + "type": "PageStatus", + "name": "status", + "isOptional": true } ] }, @@ -2842,6 +2947,28 @@ } ] }, + { + "returnType": "Void", + "namespace": "Volo.CmsKit.Pages", + "name": "SetStatusAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Page", + "name": "page", + "isOptional": false + }, + { + "type": "PageStatus", + "name": "status", + "isOptional": false + } + ] + }, { "returnType": "Void", "namespace": "Volo.CmsKit.Pages", @@ -4381,6 +4508,11 @@ "name": "filter", "isOptional": true }, + { + "type": "Nullable", + "name": "status", + "isOptional": true + }, { "type": "CancellationToken", "name": "cancellationToken", @@ -4403,6 +4535,11 @@ "name": "filter", "isOptional": true }, + { + "type": "Nullable", + "name": "status", + "isOptional": true + }, { "type": "Int32", "name": "maxResultCount", diff --git a/modules/docs/app/VoloDocs.Web/package.json b/modules/docs/app/VoloDocs.Web/package.json index 8911a6737f..0a0a8f4726 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.0.1", - "@abp/docs": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/docs": "~10.1.0-rc.2" } } diff --git a/modules/docs/app/VoloDocs.Web/yarn.lock b/modules/docs/app/VoloDocs.Web/yarn.lock index c189a9a133..e99b91ff81 100644 --- a/modules/docs/app/VoloDocs.Web/yarn.lock +++ b/modules/docs/app/VoloDocs.Web/yarn.lock @@ -2,229 +2,229 @@ # yarn lockfile v1 -"@abp/anchor-js@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.0.1.tgz#d477fa1638e3f69cd2935ce178f571129ee1b8d0" - integrity sha512-Q9E5Xpz0U14nVgVW6Mdf9FpNjIeDe705jZzWyRJxAIcCR/nKBjEKBlDtfSnZtz0lXrWm2l19t2geEmeQ6WPm4w== +"@abp/anchor-js@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.1.0-rc.2.tgz#5ed35c420ec53f9e305e9de943ebb005677d7617" + integrity sha512-D8rH9p8gGbNc6FpW24CFFW9JZD7tUiJkM3Lx31RsygwvmlolutN4jpzV5L9h9IvbrxJCT2jSqK1s686z8SHb3w== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" anchor-js "^5.0.0" -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.0.1.tgz#fc120857770a16c17f2d029a7920243357cda456" - integrity sha512-iMACbeAq6gSZ2/EUhwd1h/7gctRokSCMNuyE7hh7y2Rb0s4JeW5dbMx9QIc9oywPauRz4yCAJSFi7PJfObFL9w== +"@abp/clipboard@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.2.tgz#e99dbf190e3684e99c8e909bf38201c70e267502" + integrity sha512-kRS9pWc1jRgr4D4/EV9zdAy3rhhGBrcqk2as5+6Ih49npsEJY/cF5mYH7mj/ZYy8SHqtae/CR7bZsR+uCDKYrQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/docs@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.0.1.tgz#54cfc033fec6240127c112486b8345579fc03766" - integrity sha512-aABetYEJdf/tq2cpBKEWlu3D0zvI4qJq8O+bkGzrtI+6brmT8T9fTK6xe87YPp/SZc+RW9A4QDwrfGaUs2slxQ== +"@abp/docs@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.1.0-rc.2.tgz#ac8c84cdc91c3ac6511ff39f9d8729f0673a1d30" + integrity sha512-oTJX12kZb8CIXQRdjsZx8VYSOgKTA6ei+5CPU4dPVZZiaYBzQ3MNsMlRrrRywwuWCTJedtThy9XOcUwB7LTqCg== dependencies: - "@abp/anchor-js" "~10.0.1" - "@abp/clipboard" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/popper.js" "~10.0.1" - "@abp/prismjs" "~10.0.1" + "@abp/anchor-js" "~10.1.0-rc.2" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/popper.js" "~10.1.0-rc.2" + "@abp/prismjs" "~10.1.0-rc.2" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.0.1.tgz#22026192cbba216c39a4635b834f764e10360cfa" - integrity sha512-9wfni2mLv+tiFfjN5gqPiiLPXS+ibHt2z1rrnZkOMWbG798BHAYLX20R2ioWObyarA9OVDoE2UAb7PwuQcINzQ== +"@abp/popper.js@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.1.0-rc.2.tgz#09a23add65422b2a34a70dc074bdd8e0ec57d2a2" + integrity sha512-z+YqO0KBr8Nf376sV031lti/bPr2SzuxWgDlNmxrebrF544hgsXC+BXsWhzXSDznEZucN1sIkeoNWEiuAjxctA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.0.1.tgz#dcd4c55e8fed9aa19712517c6f7a86c667269753" - integrity sha512-WdZLCL2UYFVqJnFYuSO4geAi5sdfgxITqI9BhEZdFuJzdfp89PZ4cnK7DYxnYBrgW/yh38xwzv+HVclK3xgPNA== +"@abp/prismjs@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.2.tgz#8565bab503a16fc349f4b0fa2609ad412ff838be" + integrity sha512-SmZWMyJ3cJW+qj4CWJ7y2kD6PMx2zfZMA5X5jPunsytG4Eht4AVyIR38Y4QSpO62zZgkHyZlSTFOozBfhrlv9A== dependencies: - "@abp/clipboard" "~10.0.1" - "@abp/core" "~10.0.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json index 8dba6531a8..adf00ecead 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json @@ -166,6 +166,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -459,6 +465,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.abppkg.analyze.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.abppkg.analyze.json index 3247a44ab8..09b738524b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.abppkg.analyze.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.abppkg.analyze.json @@ -83,6 +83,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -229,6 +235,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -335,6 +347,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -708,6 +726,42 @@ "contentType": "eventHandler", "name": "FeatureValueCacheItemInvalidator", "summary": null + }, + { + "eventHandlerType": "Local", + "namespace": "Volo.Abp.FeatureManagement", + "subscribedEvents": [ + { + "underlyingType": null, + "name": "StaticFeatureDefinitionChangedEvent", + "namespace": "Volo.Abp.Features", + "declaringAssemblyName": "Volo.Abp.Features", + "fullName": "Volo.Abp.Features.StaticFeatureDefinitionChangedEvent" + } + ], + "implementingInterfaces": [ + { + "name": "ILocalEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.ILocalEventHandler" + }, + { + "name": "IEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.IEventHandler" + }, + { + "name": "ITransientDependency", + "namespace": "Volo.Abp.DependencyInjection", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.DependencyInjection.ITransientDependency" + } + ], + "contentType": "eventHandler", + "name": "StaticFeatureDefinitionChangedEventHandler", + "summary": null } ] } \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs index 32ed4cb646..d96b7b3800 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,33 +18,19 @@ public class FeatureDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicFeatureDefinitionStore DynamicFeatureDefinitionStore { get; } - protected IStaticFeatureSaver StaticFeatureSaver { get; } - - public FeatureDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicFeatureDefinitionStore dynamicFeatureDefinitionStore, - IStaticFeatureSaver staticFeatureSaver) + + public FeatureDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - ApplicationLifetime = ServiceProvider.GetService(); - CancellationTokenProvider = cancellationTokenProvider; - DynamicFeatureDefinitionStore = dynamicFeatureDefinitionStore; - StaticFeatureSaver = staticFeatureSaver; } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider + .GetRequiredService>() + .Value; if (!options.SaveStaticFeaturesToDatabase && !options.IsDynamicFeatureStoreEnabled) { @@ -54,11 +39,12 @@ public class FeatureDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -68,20 +54,23 @@ public class FeatureDynamicInitializer : ITransientDependency return ExecuteInitializationAsync(options, cancellationToken); } - protected virtual async Task ExecuteInitializationAsync(FeatureManagementOptions options, CancellationToken cancellationToken) + protected virtual async Task ExecuteInitializationAsync( + FeatureManagementOptions options, + CancellationToken cancellationToken) { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticFeaturesToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -104,6 +93,8 @@ public class FeatureDynamicInitializer : ITransientDependency return; } + var staticFeatureSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -118,7 +109,7 @@ public class FeatureDynamicInitializer : ITransientDependency { try { - await StaticFeatureSaver.SaveAsync(); + await staticFeatureSaver.SaveAsync(); } catch (Exception ex) { @@ -128,17 +119,20 @@ public class FeatureDynamicInitializer : ITransientDependency }, cancellationToken); } - protected virtual async Task PreCacheDynamicFeaturesAsync(FeatureManagementOptions options) + protected virtual async Task PreCacheDynamicFeaturesAsync( + FeatureManagementOptions options) { if (!options.IsDynamicFeatureStoreEnabled) { return; } + var dynamicFeatureDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache features, so first request doesn't wait - await DynamicFeatureDefinitionStore.GetGroupsAsync(); + await dynamicFeatureDefinitionStore.GetGroupsAsync(); } catch (Exception ex) { diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.abppkg.analyze.json b/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.abppkg.analyze.json index 1dc468a086..9f2ecb409d 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.abppkg.analyze.json +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.abppkg.analyze.json @@ -844,6 +844,23 @@ } ] }, + { + "returnType": "ListResultDto", + "namespace": "Volo.Abp.Identity.Integration", + "name": "SearchByIdsAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Guid[]", + "name": "ids", + "isOptional": false + } + ] + }, { "returnType": "Int64", "namespace": "Volo.Abp.Identity.Integration", @@ -860,6 +877,57 @@ "isOptional": false } ] + }, + { + "returnType": "ListResultDto", + "namespace": "Volo.Abp.Identity.Integration", + "name": "SearchRoleAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "RoleLookupSearchInputDto", + "name": "input", + "isOptional": false + } + ] + }, + { + "returnType": "ListResultDto", + "namespace": "Volo.Abp.Identity.Integration", + "name": "SearchRoleByNamesAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String[]", + "name": "names", + "isOptional": false + } + ] + }, + { + "returnType": "Int64", + "namespace": "Volo.Abp.Identity.Integration", + "name": "GetRoleCountAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "RoleLookupCountInputDto", + "name": "input", + "isOptional": false + } + ] } ], "contentType": "applicationService", diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs index e851cbc6cc..0c1c13c08c 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Microsoft/AspNetCore/Identity/AbpIdentityResultExtensions.cs @@ -1,58 +1,10 @@ using System; -using System.Linq; -using System.Collections.Generic; -using System.Globalization; -using System.Resources; -using Microsoft.Extensions.Localization; -using Volo.Abp; using Volo.Abp.Identity; -using Volo.Abp.Text.Formatting; namespace Microsoft.AspNetCore.Identity; public static class AbpIdentityResultExtensions { - private static readonly Dictionary IdentityStrings = new Dictionary(); - - static AbpIdentityResultExtensions() - { - var identityResourceManager = new ResourceManager("Microsoft.Extensions.Identity.Core.Resources", typeof(UserManager<>).Assembly); - var resourceSet = identityResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, false); - if (resourceSet == null) - { - throw new AbpException("Can't get the ResourceSet of Identity."); - } - - var iterator = resourceSet.GetEnumerator(); - while (true) - { - if (!iterator.MoveNext()) - { - break; - } - - var key = iterator.Key?.ToString(); - var value = iterator.Value?.ToString(); - if (key != null && value != null) - { - IdentityStrings.Add(key, value); - } - } - - if (IdentityStrings.ContainsKey("InvalidUserName")) - { - // The default text of `InvalidUserName` is `Username '{0}' is invalid, can only contain letters or digits.` - IdentityStrings["InvalidUserName"] = "Username '{0}' is invalid."; - } - - IdentityStrings["PasswordInHistory"] = "Passwords must not match your last {0} passwords."; - - if (!IdentityStrings.Any()) - { - throw new AbpException("ResourceSet values of Identity is empty."); - } - } - public static void CheckErrors(this IdentityResult identityResult) { if (identityResult.Succeeded) @@ -68,71 +20,6 @@ public static class AbpIdentityResultExtensions throw new AbpIdentityResultException(identityResult); } - public static string[] GetValuesFromErrorMessage(this IdentityResult identityResult, IStringLocalizer localizer) - { - if (identityResult.Succeeded) - { - throw new ArgumentException( - "identityResult.Succeeded should be false in order to get values from error."); - } - - if (identityResult.Errors == null) - { - throw new ArgumentException("identityResult.Errors should not be null."); - } - - var error = identityResult.Errors.First(); - var englishString = IdentityStrings.GetOrDefault(error.Code); - - if (englishString == null) - { - return Array.Empty(); - } - - if (FormattedStringValueExtracter.IsMatch(error.Description, englishString, out var values)) - { - return values; - } - - return Array.Empty(); - } - - public static string LocalizeErrors(this IdentityResult identityResult, IStringLocalizer localizer) - { - if (identityResult.Succeeded) - { - throw new ArgumentException("identityResult.Succeeded should be false in order to localize errors."); - } - - if (identityResult.Errors == null) - { - throw new ArgumentException("identityResult.Errors should not be null."); - } - - return identityResult.Errors.Select(err => LocalizeErrorMessage(err, localizer)).JoinAsString(", "); - } - - public static string LocalizeErrorMessage(this IdentityError error, IStringLocalizer localizer) - { - var key = $"Volo.Abp.Identity:{error.Code}"; - - var localizedString = localizer[key]; - - if (!localizedString.ResourceNotFound) - { - var englishString = IdentityStrings.GetOrDefault(error.Code); - if (englishString != null) - { - if (FormattedStringValueExtracter.IsMatch(error.Description, englishString, out var values)) - { - return string.Format(localizedString.Value, values.Cast().ToArray()); - } - } - } - - return localizer["Volo.Abp.Identity:DefaultError"]; - } - public static string GetResultAsString(this SignInResult signInResult) { if (signInResult.Succeeded) 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 7b76778454..ce916cf284 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 @@ -160,6 +160,64 @@ "name": "IdentityUserOrganizationUnit", "summary": "Represents membership of a User to an OU." }, + { + "namespace": "Volo.Abp.Identity", + "primaryKeyType": null, + "properties": [ + { + "type": "System.Nullable`1[System.Guid]", + "name": "TenantId", + "summary": null + }, + { + "type": "System.Guid", + "name": "UserId", + "summary": "Gets or sets the primary key of the user that owns this passkey." + }, + { + "type": "System.Byte[]", + "name": "CredentialId", + "summary": "Gets or sets the credential ID for this passkey." + }, + { + "type": "Volo.Abp.Identity.IdentityPasskeyData", + "name": "Data", + "summary": "Gets or sets additional data associated with this passkey." + } + ], + "contentType": "entity", + "name": "IdentityUserPasskey", + "summary": "Represents a passkey credential for a user in the identity system." + }, + { + "namespace": "Volo.Abp.Identity", + "primaryKeyType": null, + "properties": [ + { + "type": "System.Nullable`1[System.Guid]", + "name": "TenantId", + "summary": null + }, + { + "type": "System.Guid", + "name": "UserId", + "summary": "Gets or sets the primary key of the user associated with this password history entry." + }, + { + "type": "System.String", + "name": "Password", + "summary": "Gets or sets the password." + }, + { + "type": "System.DateTimeOffset", + "name": "CreatedAt", + "summary": null + } + ], + "contentType": "entity", + "name": "IdentityUserPasswordHistory", + "summary": "Represents a password history entry for a user." + }, { "namespace": "Volo.Abp.Identity", "primaryKeyType": null, @@ -256,6 +314,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -382,6 +446,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -452,6 +522,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -691,6 +767,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -819,6 +901,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1011,6 +1099,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1456,6 +1550,23 @@ } ] }, + { + "returnType": "Void", + "namespace": "Volo.Abp.Identity", + "name": "AddPasswordHistory", + "summary": null, + "isAsync": false, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "password", + "isOptional": false + } + ] + }, { "returnType": "Void", "namespace": "Volo.Abp.Identity", @@ -1563,6 +1674,79 @@ } ] }, + { + "returnType": "Void", + "namespace": "Volo.Abp.Identity", + "name": "SetLastSignInTime", + "summary": null, + "isAsync": false, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Nullable", + "name": "lastSignInTime", + "isOptional": false + } + ] + }, + { + "returnType": "IdentityUserPasskey", + "namespace": "Volo.Abp.Identity", + "name": "FindPasskey", + "summary": null, + "isAsync": false, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Byte[]", + "name": "credentialId", + "isOptional": false + } + ] + }, + { + "returnType": "Void", + "namespace": "Volo.Abp.Identity", + "name": "AddPasskey", + "summary": null, + "isAsync": false, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Byte[]", + "name": "credentialId", + "isOptional": false + }, + { + "type": "IdentityPasskeyData", + "name": "passkeyData", + "isOptional": false + } + ] + }, + { + "returnType": "Void", + "namespace": "Volo.Abp.Identity", + "name": "RemovePasskey", + "summary": null, + "isAsync": false, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Byte[]", + "name": "credentialId", + "isOptional": false + } + ] + }, { "returnType": "String", "namespace": "Volo.Abp.Identity", @@ -1605,6 +1789,18 @@ "namespace": "Volo.Abp.Identity", "declaringAssemblyName": "Volo.Abp.Identity.Domain", "fullName": "Volo.Abp.Identity.IdentityUserOrganizationUnit" + }, + "passwordHistories": { + "name": "IdentityUserPasswordHistory", + "namespace": "Volo.Abp.Identity", + "declaringAssemblyName": "Volo.Abp.Identity.Domain", + "fullName": "Volo.Abp.Identity.IdentityUserPasswordHistory" + }, + "passkeys": { + "name": "IdentityUserPasskey", + "namespace": "Volo.Abp.Identity", + "declaringAssemblyName": "Volo.Abp.Identity.Domain", + "fullName": "Volo.Abp.Identity.IdentityUserPasskey" } }, "navigationProperties": {}, @@ -1716,6 +1912,11 @@ "name": "LastPasswordChangeTime", "summary": "Gets or sets the last password change time for the user." }, + { + "type": "System.Nullable`1[System.DateTimeOffset]", + "name": "LastSignInTime", + "summary": "Gets or sets the last sign-in time for the user." + }, { "type": "System.Collections.Generic.ICollection`1[Volo.Abp.Identity.IdentityUserRole]", "name": "Roles", @@ -1740,6 +1941,16 @@ "type": "System.Collections.Generic.ICollection`1[Volo.Abp.Identity.IdentityUserOrganizationUnit]", "name": "OrganizationUnits", "summary": "Navigation property for this organization units." + }, + { + "type": "System.Collections.Generic.ICollection`1[Volo.Abp.Identity.IdentityUserPasswordHistory]", + "name": "PasswordHistories", + "summary": "Navigation property for this users password history." + }, + { + "type": "System.Collections.Generic.ICollection`1[Volo.Abp.Identity.IdentityUserPasskey]", + "name": "Passkeys", + "summary": "Navigation property for this users passkeys." } ], "contentType": "aggregateRoot", @@ -1760,6 +1971,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -1841,6 +2058,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -4049,6 +4272,28 @@ } ] }, + { + "returnType": "List", + "namespace": "Volo.Abp.Identity", + "name": "GetListAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "IEnumerable", + "name": "names", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + }, { "returnType": "List", "namespace": "Volo.Abp.Identity", @@ -5528,6 +5773,33 @@ "isOptional": true } ] + }, + { + "returnType": "IdentityUser", + "namespace": "Volo.Abp.Identity", + "name": "FindByPasskeyIdAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "Byte[]", + "name": "credentialId", + "isOptional": false + }, + { + "type": "Boolean", + "name": "includeDetails", + "isOptional": true + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] } ], "contentType": "repositoryInterface", @@ -6342,6 +6614,28 @@ "name": "Abp.Identity.Password.PasswordChangePeriodDays", "summary": null }, + { + "defaultValue": "False", + "displayName": "Enable prevent password reuse", + "description": "Whether to prevent users from reusing their previous passwords.", + "isVisibleToClient": true, + "isInherited": true, + "isEncrypted": false, + "contentType": "setting", + "name": "Abp.Identity.Password.EnablePreventPasswordReuse", + "summary": null + }, + { + "defaultValue": "6", + "displayName": "Prevent password reuse count", + "description": "The number of previous passwords that cannot be reused.", + "isVisibleToClient": true, + "isInherited": true, + "isEncrypted": false, + "contentType": "setting", + "name": "Abp.Identity.Password.PreventPasswordReuseCount", + "summary": null + }, { "defaultValue": "True", "displayName": "Enabled for new users", @@ -6401,7 +6695,7 @@ "defaultValue": "False", "displayName": "Enforce email verification to register", "description": "User accounts will not be created unless they verify their email addresses.", - "isVisibleToClient": false, + "isVisibleToClient": true, "isInherited": true, "isEncrypted": false, "contentType": "setting", diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs index 3dea11faea..64ee9e58d7 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityErrorDescriber.cs @@ -1,10 +1,8 @@ -using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity.Localization; -using Volo.Abp.Localization; namespace Volo.Abp.Identity; @@ -19,15 +17,202 @@ public class AbpIdentityErrorDescriber : IdentityErrorDescriber Localizer = localizer; } - public override IdentityError InvalidUserName([CanBeNull] string userName) + public override IdentityError DefaultError() { - using (CultureHelper.Use("en")) + return new IdentityError { - return new IdentityError - { - Code = nameof(InvalidUserName), - Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName ?? ""] - }; - } + Code = nameof(DefaultError), + Description = Localizer["Volo.Abp.Identity:DefaultError"] + }; + } + + public override IdentityError ConcurrencyFailure() + { + return new IdentityError + { + Code = nameof(ConcurrencyFailure), + Description = Localizer["Volo.Abp.Identity:ConcurrencyFailure"] + }; + } + + public override IdentityError PasswordMismatch() + { + return new IdentityError + { + Code = nameof(PasswordMismatch), + Description = Localizer["Volo.Abp.Identity:PasswordMismatch"] + }; + } + + public override IdentityError InvalidToken() + { + return new IdentityError + { + Code = nameof(InvalidToken), + Description = Localizer["Volo.Abp.Identity:InvalidToken"] + }; + } + + public override IdentityError RecoveryCodeRedemptionFailed() + { + return new IdentityError + { + Code = nameof(RecoveryCodeRedemptionFailed), + Description = Localizer["Volo.Abp.Identity:RecoveryCodeRedemptionFailed"] + }; + } + + public override IdentityError LoginAlreadyAssociated() + { + return new IdentityError + { + Code = nameof(LoginAlreadyAssociated), + Description = Localizer["Volo.Abp.Identity:LoginAlreadyAssociated"] + }; + } + + public override IdentityError InvalidUserName(string? userName) + { + return new IdentityError + { + Code = nameof(InvalidUserName), + Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName ?? ""] + }; + } + + public override IdentityError InvalidEmail(string? email) + { + return new IdentityError + { + Code = nameof(InvalidEmail), + Description = Localizer["Volo.Abp.Identity:InvalidEmail", email ?? ""] + }; + } + + public override IdentityError DuplicateUserName(string userName) + { + return new IdentityError + { + Code = nameof(DuplicateUserName), + Description = Localizer["Volo.Abp.Identity:DuplicateUserName", userName] + }; + } + + public override IdentityError DuplicateEmail(string email) + { + return new IdentityError + { + Code = nameof(DuplicateEmail), + Description = Localizer["Volo.Abp.Identity:DuplicateEmail", email] + }; + } + + public override IdentityError InvalidRoleName(string? role) + { + return new IdentityError + { + Code = nameof(InvalidRoleName), + Description = Localizer["Volo.Abp.Identity:InvalidRoleName", role ?? ""] + }; + } + + public override IdentityError DuplicateRoleName(string role) + { + return new IdentityError + { + Code = nameof(DuplicateRoleName), + Description = Localizer["Volo.Abp.Identity:DuplicateRoleName", role] + }; + } + + public override IdentityError UserAlreadyHasPassword() + { + return new IdentityError + { + Code = nameof(UserAlreadyHasPassword), + Description = Localizer["Volo.Abp.Identity:UserAlreadyHasPassword"] + }; + } + + public override IdentityError UserLockoutNotEnabled() + { + return new IdentityError + { + Code = nameof(UserLockoutNotEnabled), + Description = Localizer["Volo.Abp.Identity:UserLockoutNotEnabled"] + }; + } + + public override IdentityError UserAlreadyInRole(string role) + { + return new IdentityError + { + Code = nameof(UserAlreadyInRole), + Description = Localizer["Volo.Abp.Identity:UserAlreadyInRole", role] + }; + } + + public override IdentityError UserNotInRole(string role) + { + return new IdentityError + { + Code = nameof(UserNotInRole), + Description = Localizer["Volo.Abp.Identity:UserNotInRole", role] + }; + } + + public override IdentityError PasswordTooShort(int length) + { + return new IdentityError + { + Code = nameof(PasswordTooShort), + Description = Localizer["Volo.Abp.Identity:PasswordTooShort", length] + }; + } + + public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) + { + return new IdentityError + { + Code = nameof(PasswordRequiresUniqueChars), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresUniqueChars", uniqueChars] + }; + } + + public override IdentityError PasswordRequiresNonAlphanumeric() + { + return new IdentityError + { + Code = nameof(PasswordRequiresNonAlphanumeric), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresNonAlphanumeric"] + }; + } + + + public override IdentityError PasswordRequiresDigit() + { + return new IdentityError + { + Code = nameof(PasswordRequiresDigit), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresDigit"] + }; + } + + public override IdentityError PasswordRequiresLower() + { + return new IdentityError + { + Code = nameof(PasswordRequiresLower), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresLower"] + }; + } + + public override IdentityError PasswordRequiresUpper() + { + return new IdentityError + { + Code = nameof(PasswordRequiresUpper), + Description = Localizer["Volo.Abp.Identity:PasswordRequiresUpper"] + }; } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs index 03a8c6f881..1ee25bcd64 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityResultException.cs @@ -2,9 +2,7 @@ using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Localization; using Volo.Abp.ExceptionHandling; -using Volo.Abp.Identity.Localization; using Volo.Abp.Localization; namespace Volo.Abp.Identity; @@ -14,29 +12,13 @@ public class AbpIdentityResultException : BusinessException, ILocalizeErrorMessa public IdentityResult IdentityResult { get; } public AbpIdentityResultException([NotNull] IdentityResult identityResult) - : base( - code: $"Volo.Abp.Identity:{identityResult.Errors.First().Code}", - message: identityResult.Errors.Select(err => err.Description).JoinAsString(", ")) + : base(message: identityResult.Errors.Select(err => err.Description).JoinAsString(", ")) { IdentityResult = Check.NotNull(identityResult, nameof(identityResult)); } - public virtual string LocalizeMessage(LocalizationContext context) + public string LocalizeMessage(LocalizationContext context) { - var localizer = context.LocalizerFactory.Create(); - - SetData(localizer); - - return IdentityResult.LocalizeErrors(localizer); - } - - protected virtual void SetData(IStringLocalizer localizer) - { - var values = IdentityResult.GetValuesFromErrorMessage(localizer); - - for (var index = 0; index < values.Length; index++) - { - Data[index.ToString()] = values[index]; - } + return Message; } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs index 84d1a09c25..05c733c0c0 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityUserValidator.cs @@ -17,8 +17,6 @@ namespace Volo.Abp.Identity public virtual async Task ValidateAsync(UserManager manager, IdentityUser user) { - var describer = new IdentityErrorDescriber(); - Check.NotNull(manager, nameof(manager)); Check.NotNull(user, nameof(user)); @@ -27,36 +25,28 @@ namespace Volo.Abp.Identity var userName = await manager.GetUserNameAsync(user); if (userName == null) { - errors.Add(describer.InvalidUserName(null)); + errors.Add(manager.ErrorDescriber.InvalidUserName(null)); } else { var owner = await manager.FindByEmailAsync(userName); if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { - errors.Add(new IdentityError - { - Code = "InvalidUserName", - Description = Localizer["Volo.Abp.Identity:InvalidUserName", userName] - }); + errors.Add(manager.ErrorDescriber.InvalidUserName(userName)); } } var email = await manager.GetEmailAsync(user); if (email == null) { - errors.Add(describer.InvalidEmail(null)); + errors.Add(manager.ErrorDescriber.InvalidEmail(null)); } else { var owner = await manager.FindByNameAsync(email); if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { - errors.Add(new IdentityError - { - Code = "InvalidEmail", - Description = Localizer["Volo.Abp.Identity:InvalidEmail", email] - }); + errors.Add(manager.ErrorDescriber.InvalidEmail(email)); } } 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 a4fc897bf4..41da1e5c22 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 @@ -553,6 +553,6 @@ public class IdentityUserManager : UserManager, IDomainService } Logger.LogError($"Could not get a valid user name for the given email address: {email}, allowed characters: {Options.User.AllowedUserNameCharacters}, tried {maxTryCount} times."); - throw new AbpIdentityResultException(IdentityResult.Failed(new IdentityErrorDescriber().InvalidUserName(userName))); + throw new AbpIdentityResultException(IdentityResult.Failed(ErrorDescriber.InvalidUserName(userName))); } } 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 3272929c3e..acd50b7947 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 @@ -162,7 +162,7 @@ public class IdentityUserAppService_Tests : AbpIdentityApplicationTestBase (await Assert.ThrowsAsync(async () => { await _userAppService.UpdateAsync(johnNash.Id, input); - })).Message.ShouldContain("Optimistic concurrency failure"); + })).Message.ShouldContain("Optimistic concurrency check has been failed. The entity you're working on has modified by another user. Please discard your changes and try again."); } [Fact] diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityErrorDescriber_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityErrorDescriber_Tests.cs new file mode 100644 index 0000000000..a7f68ec0b2 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityErrorDescriber_Tests.cs @@ -0,0 +1,468 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp; +using Volo.Abp.Localization; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity; + +public class AbpIdentityErrorDescriber_Tests : AbpIdentityDomainTestBase +{ + [Fact] + public void Should_Localize_Error_Messages() + { + var describer = GetRequiredService(); + + using (CultureHelper.Use("en")) + { + describer.DefaultError().Description.ShouldBe("An unknown failure has occurred."); + describer.ConcurrencyFailure().Description.ShouldBe("Optimistic concurrency check has been failed. The entity you're working on has modified by another user. Please discard your changes and try again."); + describer.PasswordMismatch().Description.ShouldBe("Incorrect password."); + describer.InvalidToken().Description.ShouldBe("Invalid token."); + describer.RecoveryCodeRedemptionFailed().Description.ShouldBe("Recovery code redemption failed."); + describer.LoginAlreadyAssociated().Description.ShouldBe("A user with this login already exists."); + describer.InvalidUserName("john").Description.ShouldBe("Username 'john' is invalid."); + describer.InvalidEmail("john@abp.io").Description.ShouldBe("Email 'john@abp.io' is invalid."); + describer.DuplicateUserName("john").Description.ShouldBe("Username 'john' is already taken."); + describer.DuplicateEmail("john@abp.io").Description.ShouldBe("Email 'john@abp.io' is already taken."); + describer.InvalidRoleName("admin").Description.ShouldBe("Role name 'admin' is invalid."); + describer.DuplicateRoleName("admin").Description.ShouldBe("Role name 'admin' is already taken."); + describer.UserAlreadyHasPassword().Description.ShouldBe("User already has a password set."); + describer.UserLockoutNotEnabled().Description.ShouldBe("Lockout is not enabled for this user."); + describer.UserAlreadyInRole("admin").Description.ShouldBe("User already in role 'admin'."); + describer.UserNotInRole("admin").Description.ShouldBe("User is not in role 'admin'."); + describer.PasswordTooShort(6).Description.ShouldBe("Password length must be greater than 6 characters."); + describer.PasswordRequiresUniqueChars(3).Description.ShouldBe("Passwords must use at least 3 different characters."); + describer.PasswordRequiresNonAlphanumeric().Description.ShouldBe("Password must contain at least one non-alphanumeric character."); + describer.PasswordRequiresDigit().Description.ShouldBe("Passwords must have at least one digit ('0'-'9')."); + describer.PasswordRequiresLower().Description.ShouldBe("Passwords must have at least one lowercase ('a'-'z')."); + describer.PasswordRequiresUpper().Description.ShouldBe("Passwords must have at least one uppercase ('A'-'Z')."); + } + + using (CultureHelper.Use("tr")) + { + describer.DefaultError().Description.ShouldBe("Bilinmeyen bir hata oluştu."); + describer.ConcurrencyFailure().Description.ShouldBe("İyimser eşzamanlılık denetimi başarısız oldu. Üzerinde çalıştığınız varlık başka bir kullanıcı tarafından değiştirildi. Lütfen değişikliklerinizi geri alın ve tekrar deneyin."); + describer.PasswordMismatch().Description.ShouldBe("Hatalı şifre."); + describer.InvalidToken().Description.ShouldBe("Geçersiz token."); + describer.RecoveryCodeRedemptionFailed().Description.ShouldBe("Kurtarma kodu kullanılamadı."); + describer.LoginAlreadyAssociated().Description.ShouldBe("Bu giriş bilgilerine sahip bir kullanıcı zaten var."); + describer.InvalidUserName("john").Description.ShouldBe("'john' kullanıcı adı geçersiz."); + describer.InvalidEmail("john@abp.io").Description.ShouldBe("'john@abp.io' email adresi hatalı."); + describer.DuplicateUserName("john").Description.ShouldBe("'john' kullanıcı adı zaten alınmış."); + describer.DuplicateEmail("john@abp.io").Description.ShouldBe("'john@abp.io' email adresi zaten alınmış."); + describer.InvalidRoleName("admin").Description.ShouldBe("'admin' rol ismi geçersizdir."); + describer.DuplicateRoleName("admin").Description.ShouldBe("'admin' rol ismi zaten alınmış."); + describer.UserAlreadyHasPassword().Description.ShouldBe("Kullanıcının zaten bir şifresi var."); + describer.UserLockoutNotEnabled().Description.ShouldBe("Bu kullanıcı için hesap kilitleme etkin değil."); + describer.UserAlreadyInRole("admin").Description.ShouldBe("Kullanıcı zaten 'admin' rolünde."); + describer.UserNotInRole("admin").Description.ShouldBe("Kullanıcı 'admin' rolünde değil."); + describer.PasswordTooShort(6).Description.ShouldBe("Şifre uzunluğu 6 karakterden uzun olmalıdır."); + describer.PasswordRequiresUniqueChars(3).Description.ShouldBe("Şifre en az 3 farklı karakter içermeli."); + describer.PasswordRequiresNonAlphanumeric().Description.ShouldBe("Parola en az bir alfasayısal olmayan karakter içermelidir."); + describer.PasswordRequiresDigit().Description.ShouldBe("Şifre en az bir sayı içermeli ('0'-'9')."); + describer.PasswordRequiresLower().Description.ShouldBe("Şifre en az bir küçük harf içermeli ('a'-'z')."); + describer.PasswordRequiresUpper().Description.ShouldBe("Şifre en az bir büyük harf içermeli ('A'-'Z')."); + } + + using (CultureHelper.Use("zh-Hans")) + { + describer.DefaultError().Description.ShouldBe("发生了一个未知错误。"); + describer.ConcurrencyFailure().Description.ShouldBe("乐观并发检查失败. 你正在处理的对象已被其他用户修改. 请放弃你的更改, 然后重试。"); + describer.PasswordMismatch().Description.ShouldBe("密码错误。"); + describer.InvalidToken().Description.ShouldBe("token无效。"); + describer.RecoveryCodeRedemptionFailed().Description.ShouldBe("恢复代码兑换失败。"); + describer.LoginAlreadyAssociated().Description.ShouldBe("此登录名的用户已存在。"); + describer.InvalidUserName("john").Description.ShouldBe("用户名 'john' 无效。"); + describer.InvalidEmail("john@abp.io").Description.ShouldBe("邮箱 'john@abp.io' 无效。"); + describer.DuplicateUserName("john").Description.ShouldBe("用户名 'john' 已存在。"); + describer.DuplicateEmail("john@abp.io").Description.ShouldBe("邮箱 'john@abp.io' 已存在。"); + describer.InvalidRoleName("admin").Description.ShouldBe("角色名 'admin' 无效。"); + describer.DuplicateRoleName("admin").Description.ShouldBe("角色名 'admin' 已存在。"); + describer.UserAlreadyHasPassword().Description.ShouldBe("用户已设置密码。"); + describer.UserLockoutNotEnabled().Description.ShouldBe("该用户未启用锁定。"); + describer.UserAlreadyInRole("admin").Description.ShouldBe("用户已具有角色 'admin'。"); + describer.UserNotInRole("admin").Description.ShouldBe("用户不具有 'admin' 角色。"); + describer.PasswordTooShort(6).Description.ShouldBe("密码长度必须大于 6 字符。"); + describer.PasswordRequiresUniqueChars(3).Description.ShouldBe("密码至少包含3个唯一字符。"); + describer.PasswordRequiresNonAlphanumeric().Description.ShouldBe("密码必须至少包含一个非字母数字字符。"); + describer.PasswordRequiresDigit().Description.ShouldBe("密码至少包含一位数字 ('0'-'9')。"); + describer.PasswordRequiresLower().Description.ShouldBe("密码至少包含一位小写字母 ('a'-'z')。"); + describer.PasswordRequiresUpper().Description.ShouldBe("密码至少包含一位大写字母 ('A'-'Z')。"); + } + } + + [Fact] + public async Task Should_Localize_UserManager_Errors() + { + using (GetRequiredService().Begin()) + { + var user = new IdentityUser(Guid.NewGuid(), "um_localize_user", "um_localize_user@abp.io"); + + using (CultureHelper.Use("en")) + { + var userManager = GetRequiredService(); + var result = await userManager.CreateAsync(user, "abc", validatePassword: true); + + result.Succeeded.ShouldBeFalse(); + var message = string.Join("; ", result.Errors.Select(e => e.Description)); + message.ShouldContain("Password length must be greater than 6 characters."); + message.ShouldContain("Password must contain at least one non-alphanumeric character."); + message.ShouldContain("Passwords must have at least one digit ('0'-'9')."); + message.ShouldContain("Passwords must have at least one uppercase ('A'-'Z')."); + + var invalidUser = new IdentityUser(Guid.NewGuid(), "invalid user?", "not-an-email"); + var invalidResult = await userManager.CreateAsync(invalidUser, "Abp123!"); + + invalidResult.Succeeded.ShouldBeFalse(); + var invalidMessage = string.Join("; ", invalidResult.Errors.Select(e => e.Description)); + invalidMessage.ShouldContain("Username 'invalid user?' is invalid."); + invalidMessage.ShouldContain("Email 'not-an-email' is invalid."); + + var firstUser = new IdentityUser(Guid.NewGuid(), "dup_user_en", "dup_user_en@abp.io"); + (await userManager.CreateAsync(firstUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var duplicateUser = new IdentityUser(Guid.NewGuid(), "dup_user_en", "dup_user_en@abp.io"); + var duplicateResult = await userManager.CreateAsync(duplicateUser, "Abp123!"); + + duplicateResult.Succeeded.ShouldBeFalse(); + var duplicateMessage = string.Join("; ", duplicateResult.Errors.Select(e => e.Description)); + duplicateMessage.ShouldContain("Username 'dup_user_en' is already taken."); + duplicateMessage.ShouldContain("Email 'dup_user_en@abp.io' is already taken."); + + var persistedUser = await userManager.FindByIdAsync(firstUser.Id.ToString()); + var addPasswordResult = await userManager.AddPasswordAsync(persistedUser, "Another123!"); + + addPasswordResult.Succeeded.ShouldBeFalse(); + addPasswordResult.Errors.ShouldContain(e => e.Description == "User already has a password set."); + + var roleManager = GetRequiredService(); + var roleName = "localized_role_en"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), roleName))).Succeeded.ShouldBeTrue(); + + var roleUser = new IdentityUser(Guid.NewGuid(), "role_user_en", "role_user_en@abp.io"); + (await userManager.CreateAsync(roleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + (await userManager.AddToRoleAsync(roleUser, roleName)).Succeeded.ShouldBeTrue(); + var alreadyInRoleResult = await userManager.AddToRoleAsync(roleUser, roleName); + alreadyInRoleResult.Succeeded.ShouldBeFalse(); + alreadyInRoleResult.Errors.ShouldContain(e => e.Description == $"User already in role '{roleName}'."); + + var notInRoleUser = new IdentityUser(Guid.NewGuid(), "notinrole_user_en", "notinrole_user_en@abp.io"); + (await userManager.CreateAsync(notInRoleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var notInRoleResult = await userManager.RemoveFromRoleAsync(notInRoleUser, roleName); + notInRoleResult.Succeeded.ShouldBeFalse(); + notInRoleResult.Errors.ShouldContain(e => e.Description == $"User is not in role '{roleName}'."); + + var loginUser1 = new IdentityUser(Guid.NewGuid(), "login_user1_en", "login_user1_en@abp.io"); + (await userManager.CreateAsync(loginUser1, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginInfo = new UserLoginInfo("Google", "assoc_key_en", "Google"); + (await userManager.AddLoginAsync(loginUser1, loginInfo)).Succeeded.ShouldBeTrue(); + + var loginUser2 = new IdentityUser(Guid.NewGuid(), "login_user2_en", "login_user2_en@abp.io"); + (await userManager.CreateAsync(loginUser2, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginConflict = await userManager.AddLoginAsync(loginUser2, loginInfo); + loginConflict.Succeeded.ShouldBeFalse(); + loginConflict.Errors.ShouldContain(e => e.Description == "A user with this login already exists."); + + var noLowerUser = new IdentityUser(Guid.NewGuid(), "nolower_user_en", "nolower_user_en@abp.io"); + var noLowerResult = await userManager.CreateAsync(noLowerUser, "ABC123!"); + + noLowerResult.Succeeded.ShouldBeFalse(); + var noLowerMessage = string.Join("; ", noLowerResult.Errors.Select(e => e.Description)); + noLowerMessage.ShouldContain("Passwords must have at least one lowercase ('a'-'z')."); + + var options = GetRequiredService>().Value; + var originalUniqueChars = options.Password.RequiredUniqueChars; + options.Password.RequiredUniqueChars = 3; + + var uniqueUser = new IdentityUser(Guid.NewGuid(), "unique_user_en", "unique_user_en@abp.io"); + var uniqueResult = await userManager.CreateAsync(uniqueUser, "aaaaaa!"); + + uniqueResult.Succeeded.ShouldBeFalse(); + var uniqueMessage = string.Join("; ", uniqueResult.Errors.Select(e => e.Description)); + uniqueMessage.ShouldContain("Passwords must use at least 3 different characters."); + + options.Password.RequiredUniqueChars = originalUniqueChars; + + var mismatchUser = new IdentityUser(Guid.NewGuid(), "mismatch_user_en", "mismatch_user_en@abp.io"); + (await userManager.CreateAsync(mismatchUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var mismatchResult = await userManager.ChangePasswordAsync(mismatchUser, "WrongOld123!", "NewAbp123!"); + mismatchResult.Succeeded.ShouldBeFalse(); + mismatchResult.Errors.ShouldContain(e => e.Description == "Incorrect password."); + + var recoveryUser = new IdentityUser(Guid.NewGuid(), "recovery_user_en", "recovery_user_en@abp.io"); + ObjectHelper.TrySetProperty(recoveryUser, x => x.TwoFactorEnabled, () => true); + + (await userManager.CreateAsync(recoveryUser, "Abp123!")).Succeeded.ShouldBeTrue(); + var recoveryResult = await userManager.RedeemTwoFactorRecoveryCodeAsync(recoveryUser, "invalid-code"); + recoveryResult.Succeeded.ShouldBeFalse(); + recoveryResult.Errors.ShouldContain(e => e.Description == "Recovery code redemption failed."); + + var invalidRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), "")); + invalidRoleResult.Succeeded.ShouldBeFalse(); + invalidRoleResult.Errors.ShouldContain(e => e.Description == "Role name '' is invalid."); + + var duplicateRoleName = "duplicate_role_en"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName))).Succeeded.ShouldBeTrue(); + var duplicateRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName)); + duplicateRoleResult.Succeeded.ShouldBeFalse(); + duplicateRoleResult.Errors.ShouldContain(e => e.Description == $"Role name '{duplicateRoleName}' is already taken."); + } + + // Recreate a fresh user per culture to avoid state issues + user = new IdentityUser(Guid.NewGuid(), "um_localize_user_tr", "um_localize_user_tr@abp.io"); + using (CultureHelper.Use("tr")) + { + var userManager = GetRequiredService(); + var result = await userManager.CreateAsync(user, "abc", validatePassword: true); + + result.Succeeded.ShouldBeFalse(); + var message = string.Join("; ", result.Errors.Select(e => e.Description)); + message.ShouldContain("Şifre uzunluğu 6 karakterden uzun olmalıdır."); + message.ShouldContain("Parola en az bir alfasayısal olmayan karakter içermelidir."); + message.ShouldContain("Şifre en az bir sayı içermeli ('0'-'9')."); + message.ShouldContain("Şifre en az bir büyük harf içermeli ('A'-'Z')."); + + var invalidUser = new IdentityUser(Guid.NewGuid(), "invalid user?", "not-an-email"); + var invalidResult = await userManager.CreateAsync(invalidUser, "Abp123!"); + + invalidResult.Succeeded.ShouldBeFalse(); + var invalidMessage = string.Join("; ", invalidResult.Errors.Select(e => e.Description)); + invalidMessage.ShouldContain("'invalid user?' kullanıcı adı geçersiz."); + invalidMessage.ShouldContain("'not-an-email' email adresi hatalı."); + + var firstUser = new IdentityUser(Guid.NewGuid(), "dup_user_tr", "dup_user_tr@abp.io"); + (await userManager.CreateAsync(firstUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var duplicateUser = new IdentityUser(Guid.NewGuid(), "dup_user_tr", "dup_user_tr@abp.io"); + var duplicateResult = await userManager.CreateAsync(duplicateUser, "Abp123!"); + + duplicateResult.Succeeded.ShouldBeFalse(); + var duplicateMessage = string.Join("; ", duplicateResult.Errors.Select(e => e.Description)); + duplicateMessage.ShouldContain("'dup_user_tr' kullanıcı adı zaten alınmış."); + duplicateMessage.ShouldContain("'dup_user_tr@abp.io' email adresi zaten alınmış."); + + var persistedUser = await userManager.FindByIdAsync(firstUser.Id.ToString()); + var addPasswordResult = await userManager.AddPasswordAsync(persistedUser, "Another123!"); + + addPasswordResult.Succeeded.ShouldBeFalse(); + addPasswordResult.Errors.ShouldContain(e => e.Description == "Kullanıcının zaten bir şifresi var."); + + var roleManager = GetRequiredService(); + var roleName = "localized_role_tr"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), roleName))).Succeeded.ShouldBeTrue(); + + var roleUser = new IdentityUser(Guid.NewGuid(), "role_user_tr", "role_user_tr@abp.io"); + (await userManager.CreateAsync(roleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + (await userManager.AddToRoleAsync(roleUser, roleName)).Succeeded.ShouldBeTrue(); + var alreadyInRoleResult = await userManager.AddToRoleAsync(roleUser, roleName); + alreadyInRoleResult.Succeeded.ShouldBeFalse(); + alreadyInRoleResult.Errors.ShouldContain(e => e.Description == $"Kullanıcı zaten '{roleName}' rolünde."); + + var notInRoleUser = new IdentityUser(Guid.NewGuid(), "notinrole_user_tr", "notinrole_user_tr@abp.io"); + (await userManager.CreateAsync(notInRoleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var notInRoleResult = await userManager.RemoveFromRoleAsync(notInRoleUser, roleName); + notInRoleResult.Succeeded.ShouldBeFalse(); + notInRoleResult.Errors.ShouldContain(e => e.Description == $"Kullanıcı '{roleName}' rolünde değil."); + + var loginUser1 = new IdentityUser(Guid.NewGuid(), "login_user1_tr", "login_user1_tr@abp.io"); + (await userManager.CreateAsync(loginUser1, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginInfo = new UserLoginInfo("Google", "assoc_key_tr", "Google"); + (await userManager.AddLoginAsync(loginUser1, loginInfo)).Succeeded.ShouldBeTrue(); + + var loginUser2 = new IdentityUser(Guid.NewGuid(), "login_user2_tr", "login_user2_tr@abp.io"); + (await userManager.CreateAsync(loginUser2, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginConflict = await userManager.AddLoginAsync(loginUser2, loginInfo); + loginConflict.Succeeded.ShouldBeFalse(); + loginConflict.Errors.ShouldContain(e => e.Description == "Bu giriş bilgilerine sahip bir kullanıcı zaten var."); + + var noLowerUser = new IdentityUser(Guid.NewGuid(), "nolower_user_tr", "nolower_user_tr@abp.io"); + var noLowerResult = await userManager.CreateAsync(noLowerUser, "ABC123!"); + + noLowerResult.Succeeded.ShouldBeFalse(); + var noLowerMessage = string.Join("; ", noLowerResult.Errors.Select(e => e.Description)); + noLowerMessage.ShouldContain("Şifre en az bir küçük harf içermeli ('a'-'z')."); + + var options = GetRequiredService>().Value; + var originalUniqueChars = options.Password.RequiredUniqueChars; + options.Password.RequiredUniqueChars = 3; + + var uniqueUser = new IdentityUser(Guid.NewGuid(), "unique_user_tr", "unique_user_tr@abp.io"); + var uniqueResult = await userManager.CreateAsync(uniqueUser, "aaaaaa!"); + + uniqueResult.Succeeded.ShouldBeFalse(); + var uniqueMessage = string.Join("; ", uniqueResult.Errors.Select(e => e.Description)); + uniqueMessage.ShouldContain("Şifre en az 3 farklı karakter içermeli."); + + options.Password.RequiredUniqueChars = originalUniqueChars; + + var mismatchUser = new IdentityUser(Guid.NewGuid(), "mismatch_user_tr", "mismatch_user_tr@abp.io"); + (await userManager.CreateAsync(mismatchUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var mismatchResult = await userManager.ChangePasswordAsync(mismatchUser, "WrongOld123!", "NewAbp123!"); + mismatchResult.Succeeded.ShouldBeFalse(); + mismatchResult.Errors.ShouldContain(e => e.Description == "Hatalı şifre."); + + var recoveryUser = new IdentityUser(Guid.NewGuid(), "recovery_user_tr", "recovery_user_tr@abp.io"); + ObjectHelper.TrySetProperty(recoveryUser, x => x.TwoFactorEnabled, () => true); + + (await userManager.CreateAsync(recoveryUser, "Abp123!")).Succeeded.ShouldBeTrue(); + var recoveryResult = await userManager.RedeemTwoFactorRecoveryCodeAsync(recoveryUser, "invalid-code"); + recoveryResult.Succeeded.ShouldBeFalse(); + recoveryResult.Errors.ShouldContain(e => e.Description == "Kurtarma kodu kullanılamadı."); + + var invalidRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), "")); + invalidRoleResult.Succeeded.ShouldBeFalse(); + invalidRoleResult.Errors.ShouldContain(e => e.Description == "'' rol ismi geçersizdir."); + + var duplicateRoleName = "duplicate_role_tr"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName))).Succeeded.ShouldBeTrue(); + var duplicateRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName)); + duplicateRoleResult.Succeeded.ShouldBeFalse(); + duplicateRoleResult.Errors.ShouldContain(e => e.Description == $"'{duplicateRoleName}' rol ismi zaten alınmış."); + } + + user = new IdentityUser(Guid.NewGuid(), "um_localize_user_zh", "um_localize_user_zh@abp.io"); + using (CultureHelper.Use("zh-Hans")) + { + var userManager = GetRequiredService(); + var result = await userManager.CreateAsync(user, "abc", validatePassword: true); + + result.Succeeded.ShouldBeFalse(); + var message = string.Join("; ", result.Errors.Select(e => e.Description)); + message.ShouldContain("密码长度必须大于 6 字符。"); + message.ShouldContain("密码必须至少包含一个非字母数字字符。"); + message.ShouldContain("密码至少包含一位数字 ('0'-'9')。"); + message.ShouldContain("密码至少包含一位大写字母 ('A'-'Z')。"); + + var invalidUser = new IdentityUser(Guid.NewGuid(), "invalid user?", "not-an-email"); + var invalidResult = await userManager.CreateAsync(invalidUser, "Abp123!"); + + invalidResult.Succeeded.ShouldBeFalse(); + var invalidMessage = string.Join("; ", invalidResult.Errors.Select(e => e.Description)); + invalidMessage.ShouldContain("用户名 'invalid user?' 无效。"); + invalidMessage.ShouldContain("邮箱 'not-an-email' 无效。"); + + var firstUser = new IdentityUser(Guid.NewGuid(), "dup_user_zh", "dup_user_zh@abp.io"); + (await userManager.CreateAsync(firstUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var duplicateUser = new IdentityUser(Guid.NewGuid(), "dup_user_zh", "dup_user_zh@abp.io"); + var duplicateResult = await userManager.CreateAsync(duplicateUser, "Abp123!"); + + duplicateResult.Succeeded.ShouldBeFalse(); + var duplicateMessage = string.Join("; ", duplicateResult.Errors.Select(e => e.Description)); + duplicateMessage.ShouldContain("用户名 'dup_user_zh' 已存在。"); + duplicateMessage.ShouldContain("邮箱 'dup_user_zh@abp.io' 已存在。"); + + var persistedUser = await userManager.FindByIdAsync(firstUser.Id.ToString()); + var addPasswordResult = await userManager.AddPasswordAsync(persistedUser, "Another123!"); + + addPasswordResult.Succeeded.ShouldBeFalse(); + addPasswordResult.Errors.ShouldContain(e => e.Description == "用户已设置密码。"); + + var roleManager = GetRequiredService(); + var roleName = "localized_role_zh"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), roleName))).Succeeded.ShouldBeTrue(); + + var roleUser = new IdentityUser(Guid.NewGuid(), "role_user_zh", "role_user_zh@abp.io"); + (await userManager.CreateAsync(roleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + (await userManager.AddToRoleAsync(roleUser, roleName)).Succeeded.ShouldBeTrue(); + var alreadyInRoleResult = await userManager.AddToRoleAsync(roleUser, roleName); + alreadyInRoleResult.Succeeded.ShouldBeFalse(); + alreadyInRoleResult.Errors.ShouldContain(e => e.Description == $"用户已具有角色 '{roleName}'。"); + + var notInRoleUser = new IdentityUser(Guid.NewGuid(), "notinrole_user_zh", "notinrole_user_zh@abp.io"); + (await userManager.CreateAsync(notInRoleUser, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var notInRoleResult = await userManager.RemoveFromRoleAsync(notInRoleUser, roleName); + notInRoleResult.Succeeded.ShouldBeFalse(); + notInRoleResult.Errors.ShouldContain(e => e.Description == $"用户不具有 '{roleName}' 角色。"); + + var loginUser1 = new IdentityUser(Guid.NewGuid(), "login_user1_zh", "login_user1_zh@abp.io"); + (await userManager.CreateAsync(loginUser1, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginInfo = new UserLoginInfo("Google", "assoc_key_zh", "Google"); + (await userManager.AddLoginAsync(loginUser1, loginInfo)).Succeeded.ShouldBeTrue(); + + var loginUser2 = new IdentityUser(Guid.NewGuid(), "login_user2_zh", "login_user2_zh@abp.io"); + (await userManager.CreateAsync(loginUser2, "Abp123!")) + .Succeeded.ShouldBeTrue(); + + var loginConflict = await userManager.AddLoginAsync(loginUser2, loginInfo); + loginConflict.Succeeded.ShouldBeFalse(); + loginConflict.Errors.ShouldContain(e => e.Description == "此登录名的用户已存在。"); + + var noLowerUser = new IdentityUser(Guid.NewGuid(), "nolower_user_zh", "nolower_user_zh@abp.io"); + var noLowerResult = await userManager.CreateAsync(noLowerUser, "ABC123!"); + + noLowerResult.Succeeded.ShouldBeFalse(); + var noLowerMessage = string.Join("; ", noLowerResult.Errors.Select(e => e.Description)); + noLowerMessage.ShouldContain("密码至少包含一位小写字母 ('a'-'z')。"); + + var options = GetRequiredService>().Value; + var originalUniqueChars = options.Password.RequiredUniqueChars; + options.Password.RequiredUniqueChars = 3; + + var uniqueUser = new IdentityUser(Guid.NewGuid(), "unique_user_zh", "unique_user_zh@abp.io"); + var uniqueResult = await userManager.CreateAsync(uniqueUser, "aaaaaa!"); + + uniqueResult.Succeeded.ShouldBeFalse(); + var uniqueMessage = string.Join("; ", uniqueResult.Errors.Select(e => e.Description)); + uniqueMessage.ShouldContain("密码至少包含3个唯一字符。"); + + options.Password.RequiredUniqueChars = originalUniqueChars; + + var mismatchUser = new IdentityUser(Guid.NewGuid(), "mismatch_user_zh", "mismatch_user_zh@abp.io"); + (await userManager.CreateAsync(mismatchUser, "Abp123!")).Succeeded.ShouldBeTrue(); + + var mismatchResult = await userManager.ChangePasswordAsync(mismatchUser, "WrongOld123!", "NewAbp123!"); + mismatchResult.Succeeded.ShouldBeFalse(); + mismatchResult.Errors.ShouldContain(e => e.Description == "密码错误。"); + + var recoveryUser = new IdentityUser(Guid.NewGuid(), "recovery_user_zh", "recovery_user_zh@abp.io"); + ObjectHelper.TrySetProperty(recoveryUser, x => x.TwoFactorEnabled, () => true); + + (await userManager.CreateAsync(recoveryUser, "Abp123!")).Succeeded.ShouldBeTrue(); + var recoveryResult = await userManager.RedeemTwoFactorRecoveryCodeAsync(recoveryUser, "invalid-code"); + recoveryResult.Succeeded.ShouldBeFalse(); + recoveryResult.Errors.ShouldContain(e => e.Description == "恢复代码兑换失败。"); + + var invalidRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), "")); + invalidRoleResult.Succeeded.ShouldBeFalse(); + invalidRoleResult.Errors.ShouldContain(e => e.Description == "角色名 '' 无效。"); + + var duplicateRoleName = "duplicate_role_zh"; + (await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName))).Succeeded.ShouldBeTrue(); + var duplicateRoleResult = await roleManager.CreateAsync(new IdentityRole(Guid.NewGuid(), duplicateRoleName)); + duplicateRoleResult.Succeeded.ShouldBeFalse(); + duplicateRoleResult.Errors.ShouldContain(e => e.Description == $"角色名 '{duplicateRoleName}' 已存在。"); + } + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs index 4a7860563e..fb45c5190e 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs @@ -1,5 +1,6 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; using Shouldly; +using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.Localization; using Xunit; @@ -10,42 +11,61 @@ public class AbpIdentityResultException_Tests : AbpIdentityDomainTestBase [Fact] public void Should_Localize_Messages() { - var exception = new AbpIdentityResultException( - IdentityResult.Failed( - new IdentityError - { - Code = "PasswordTooShort", - Description = "Passwords must be at least 6 characters." - }, - new IdentityError - { - Code = "PasswordRequiresNonAlphanumeric", - Description = "Passwords must have at least one non alphanumeric character." - }, - new IdentityError - { - Code = "UnknownError", - Description = "Unknown error" - } - ) - ); + var describer = GetRequiredService(); + var exceptionToErrorInfoConverter = GetRequiredService(); + + using (CultureHelper.Use("en")) + { + var exception = new AbpIdentityResultException( + IdentityResult.Failed( + describer.PasswordTooShort(6), + describer.PasswordRequiresNonAlphanumeric(), + describer.DefaultError()) + ); + + var message = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); + exceptionToErrorInfoConverter.Convert(exception).Message.ShouldBe(message); + + message.ShouldNotBeNull(); + message.ShouldContain("Password length must be greater than 6 characters."); + message.ShouldContain("Password must contain at least one non-alphanumeric character."); + message.ShouldContain("An unknown failure has occurred."); + } using (CultureHelper.Use("tr")) { - var localizeMessage = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); + var exception = new AbpIdentityResultException( + IdentityResult.Failed( + describer.PasswordTooShort(6), + describer.PasswordRequiresNonAlphanumeric(), + describer.DefaultError()) + ); + + var message = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); + exceptionToErrorInfoConverter.Convert(exception).Message.ShouldBe(message); - localizeMessage.ShouldContain("Şifre uzunluğu 6 karakterden uzun olmalıdır."); - localizeMessage.ShouldContain("Parola en az bir alfasayısal olmayan karakter içermeli"); - localizeMessage.ShouldContain("Bilinmeyen bir hata oluştu."); + message.ShouldNotBeNull(); + message.ShouldContain("Şifre uzunluğu 6 karakterden uzun olmalıdır."); + message.ShouldContain("Parola en az bir alfasayısal olmayan karakter içermeli"); + message.ShouldContain("Bilinmeyen bir hata oluştu."); } - using (CultureHelper.Use("en")) + using (CultureHelper.Use("zh-Hans")) { - var localizeMessage = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); + var exception = new AbpIdentityResultException( + IdentityResult.Failed( + describer.PasswordTooShort(6), + describer.PasswordRequiresNonAlphanumeric(), + describer.DefaultError()) + ); + + var message = exception.LocalizeMessage(new LocalizationContext(ServiceProvider)); + exceptionToErrorInfoConverter.Convert(exception).Message.ShouldBe(message); - localizeMessage.ShouldContain("Password length must be greater than 6 characters."); - localizeMessage.ShouldContain("Password must contain at least one non-alphanumeric character."); - localizeMessage.ShouldContain("An unknown failure has occurred."); + message.ShouldNotBeNull(); + message.ShouldContain("密码长度必须大于 6 字符。"); + message.ShouldContain("密码必须至少包含一个非字母数字字符。"); + message.ShouldContain("发生了一个未知错误。"); } } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json index c1f39e1aee..a0be1f6ff2 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json @@ -426,6 +426,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -736,6 +742,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -848,6 +860,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -968,6 +986,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -2012,6 +2036,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -2322,6 +2352,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/package.json b/modules/openiddict/app/OpenIddict.Demo.Server/package.json index a963ff1451..52a1fc6c21 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.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2" } } diff --git a/modules/openiddict/app/angular/package.json b/modules/openiddict/app/angular/package.json index 095cc672e2..0eccde558f 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.0.1", - "@abp/ng.components": "~10.0.1", - "@abp/ng.core": "~10.0.1", - "@abp/ng.oauth": "~10.0.1", - "@abp/ng.identity": "~10.0.1", - "@abp/ng.setting-management": "~10.0.1", - "@abp/ng.tenant-management": "~10.0.1", - "@abp/ng.theme.shared": "~10.0.1", - "@abp/ng.theme.lepton-x": "~5.0.1", + "@abp/ng.account": "~10.1.0-rc.2", + "@abp/ng.components": "~10.1.0-rc.2", + "@abp/ng.core": "~10.1.0-rc.2", + "@abp/ng.oauth": "~10.1.0-rc.2", + "@abp/ng.identity": "~10.1.0-rc.2", + "@abp/ng.setting-management": "~10.1.0-rc.2", + "@abp/ng.tenant-management": "~10.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", + "@abp/ng.theme.lepton-x": "~5.1.0-rc.2", "@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.0.1", + "@abp/ng.schematics": "~10.1.0-rc.2", "@angular-devkit/build-angular": "^15.0.1", "@angular-eslint/builder": "~15.1.0", "@angular-eslint/eslint-plugin": "~15.1.0", diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo.Abp.OpenIddict.Domain.abppkg.analyze.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo.Abp.OpenIddict.Domain.abppkg.analyze.json index 08195db459..c8145fba6b 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo.Abp.OpenIddict.Domain.abppkg.analyze.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo.Abp.OpenIddict.Domain.abppkg.analyze.json @@ -98,6 +98,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -215,6 +221,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -372,6 +384,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -469,6 +487,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo.Abp.PermissionManagement.Application.abppkg.analyze.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo.Abp.PermissionManagement.Application.abppkg.analyze.json index efb60c833c..8981631b7d 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo.Abp.PermissionManagement.Application.abppkg.analyze.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo.Abp.PermissionManagement.Application.abppkg.analyze.json @@ -209,6 +209,185 @@ "isOptional": false } ] + }, + { + "returnType": "GetResourceProviderListResultDto", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetResourceProviderKeyLookupServicesAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + } + ] + }, + { + "returnType": "SearchProviderKeyListResultDto", + "namespace": "Volo.Abp.PermissionManagement", + "name": "SearchResourceProviderKeyAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "serviceName", + "isOptional": false + }, + { + "type": "String", + "name": "filter", + "isOptional": false + }, + { + "type": "Int32", + "name": "page", + "isOptional": false + } + ] + }, + { + "returnType": "GetResourcePermissionDefinitionListResultDto", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetResourceDefinitionsAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + } + ] + }, + { + "returnType": "GetResourcePermissionListResultDto", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetResourceAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + } + ] + }, + { + "returnType": "GetResourcePermissionWithProviderListResultDto", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetResourceByProviderAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "String", + "name": "providerName", + "isOptional": false + }, + { + "type": "String", + "name": "providerKey", + "isOptional": false + } + ] + }, + { + "returnType": "Void", + "namespace": "Volo.Abp.PermissionManagement", + "name": "UpdateResourceAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "UpdateResourcePermissionsDto", + "name": "input", + "isOptional": false + } + ] + }, + { + "returnType": "Void", + "namespace": "Volo.Abp.PermissionManagement", + "name": "DeleteResourceAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "String", + "name": "providerName", + "isOptional": false + }, + { + "type": "String", + "name": "providerKey", + "isOptional": false + } + ] } ], "contentType": "applicationService", diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.abppkg.analyze.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.abppkg.analyze.json index 7614193615..00cc59193e 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.abppkg.analyze.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.abppkg.analyze.json @@ -69,6 +69,12 @@ "contentType": "webPage", "name": "PermissionManagementModal", "summary": null + }, + { + "namespace": "Volo.Abp.PermissionManagement.Blazor.Components", + "contentType": "webPage", + "name": "ResourcePermissionManagementModal", + "summary": null } ] } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo.Abp.PermissionManagement.Domain.abppkg.analyze.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo.Abp.PermissionManagement.Domain.abppkg.analyze.json index f65a774ef5..ac5ac400eb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo.Abp.PermissionManagement.Domain.abppkg.analyze.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo.Abp.PermissionManagement.Domain.abppkg.analyze.json @@ -108,6 +108,45 @@ "name": "PermissionGrant", "summary": null }, + { + "namespace": "Volo.Abp.PermissionManagement", + "primaryKeyType": "Guid", + "properties": [ + { + "type": "System.Nullable`1[System.Guid]", + "name": "TenantId", + "summary": null + }, + { + "type": "System.String", + "name": "Name", + "summary": null + }, + { + "type": "System.String", + "name": "ProviderName", + "summary": null + }, + { + "type": "System.String", + "name": "ProviderKey", + "summary": null + }, + { + "type": "System.String", + "name": "ResourceName", + "summary": null + }, + { + "type": "System.String", + "name": "ResourceKey", + "summary": null + } + ], + "contentType": "entity", + "name": "ResourcePermissionGrant", + "summary": null + }, { "baseClass": { "name": "BasicAggregateRoot", @@ -122,6 +161,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -204,6 +249,16 @@ "name": "Name", "summary": null }, + { + "type": "System.String", + "name": "ResourceName", + "summary": null + }, + { + "type": "System.String", + "name": "ManagementPermissionName", + "summary": null + }, { "type": "System.String", "name": "ParentName", @@ -258,6 +313,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -606,6 +667,256 @@ "name": "IPermissionGroupDefinitionRecordRepository", "summary": null }, + { + "namespace": "Volo.Abp.PermissionManagement", + "entityAnalyzeModel": { + "namespace": "Volo.Abp.PermissionManagement", + "primaryKeyType": "Guid", + "properties": [], + "contentType": "entity", + "name": "ResourcePermissionGrant", + "summary": null + }, + "implementingInterfaces": [ + { + "name": "IBasicRepository", + "namespace": "Volo.Abp.Domain.Repositories", + "declaringAssemblyName": "Volo.Abp.Ddd.Domain", + "fullName": "Volo.Abp.Domain.Repositories.IBasicRepository" + }, + { + "name": "IBasicRepository", + "namespace": "Volo.Abp.Domain.Repositories", + "declaringAssemblyName": "Volo.Abp.Ddd.Domain", + "fullName": "Volo.Abp.Domain.Repositories.IBasicRepository" + }, + { + "name": "IReadOnlyBasicRepository", + "namespace": "Volo.Abp.Domain.Repositories", + "declaringAssemblyName": "Volo.Abp.Ddd.Domain", + "fullName": "Volo.Abp.Domain.Repositories.IReadOnlyBasicRepository" + }, + { + "name": "IRepository", + "namespace": "Volo.Abp.Domain.Repositories", + "declaringAssemblyName": "Volo.Abp.Ddd.Domain", + "fullName": "Volo.Abp.Domain.Repositories.IRepository" + }, + { + "name": "IReadOnlyBasicRepository", + "namespace": "Volo.Abp.Domain.Repositories", + "declaringAssemblyName": "Volo.Abp.Ddd.Domain", + "fullName": "Volo.Abp.Domain.Repositories.IReadOnlyBasicRepository" + } + ], + "methods": [ + { + "returnType": "ResourcePermissionGrant", + "namespace": "Volo.Abp.PermissionManagement", + "name": "FindAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "name", + "isOptional": false + }, + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "String", + "name": "providerName", + "isOptional": false + }, + { + "type": "String", + "name": "providerKey", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + }, + { + "returnType": "List", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetListAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "String", + "name": "providerName", + "isOptional": false + }, + { + "type": "String", + "name": "providerKey", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + }, + { + "returnType": "List", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetListAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String[]", + "name": "names", + "isOptional": false + }, + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "String", + "name": "providerName", + "isOptional": false + }, + { + "type": "String", + "name": "providerKey", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + }, + { + "returnType": "List", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetListAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "providerName", + "isOptional": false + }, + { + "type": "String", + "name": "providerKey", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + }, + { + "returnType": "List", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetPermissionsAsync", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "resourceKey", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + }, + { + "returnType": "List", + "namespace": "Volo.Abp.PermissionManagement", + "name": "GetResourceKeys", + "summary": null, + "isAsync": true, + "isPublic": true, + "isPrivate": false, + "isStatic": false, + "parameters": [ + { + "type": "String", + "name": "resourceName", + "isOptional": false + }, + { + "type": "String", + "name": "name", + "isOptional": false + }, + { + "type": "CancellationToken", + "name": "cancellationToken", + "isOptional": true + } + ] + } + ], + "contentType": "repositoryInterface", + "name": "IResourcePermissionGrantRepository", + "summary": null + }, { "eventHandlerType": "Local", "namespace": "Volo.Abp.PermissionManagement", @@ -646,6 +957,83 @@ "contentType": "eventHandler", "name": "PermissionGrantCacheItemInvalidator", "summary": null + }, + { + "eventHandlerType": "Local", + "namespace": "Volo.Abp.PermissionManagement", + "subscribedEvents": [ + { + "underlyingType": { + "name": "ResourcePermissionGrant", + "namespace": "Volo.Abp.PermissionManagement", + "declaringAssemblyName": "Volo.Abp.PermissionManagement.Domain", + "fullName": "Volo.Abp.PermissionManagement.ResourcePermissionGrant" + }, + "name": "EntityChangedEventData`1", + "namespace": "Volo.Abp.Domain.Entities.Events", + "declaringAssemblyName": "Volo.Abp.Ddd.Domain", + "fullName": "Volo.Abp.Domain.Entities.Events.EntityChangedEventData`1" + } + ], + "implementingInterfaces": [ + { + "name": "ILocalEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.ILocalEventHandler" + }, + { + "name": "IEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.IEventHandler" + }, + { + "name": "ITransientDependency", + "namespace": "Volo.Abp.DependencyInjection", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.DependencyInjection.ITransientDependency" + } + ], + "contentType": "eventHandler", + "name": "ResourcePermissionGrantCacheItemInvalidator", + "summary": null + }, + { + "eventHandlerType": "Local", + "namespace": "Volo.Abp.PermissionManagement", + "subscribedEvents": [ + { + "underlyingType": null, + "name": "StaticPermissionDefinitionChangedEvent", + "namespace": "Volo.Abp.Authorization.Permissions", + "declaringAssemblyName": "Volo.Abp.Authorization", + "fullName": "Volo.Abp.Authorization.Permissions.StaticPermissionDefinitionChangedEvent" + } + ], + "implementingInterfaces": [ + { + "name": "ILocalEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.ILocalEventHandler" + }, + { + "name": "IEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.IEventHandler" + }, + { + "name": "ITransientDependency", + "namespace": "Volo.Abp.DependencyInjection", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.DependencyInjection.ITransientDependency" + } + ], + "contentType": "eventHandler", + "name": "StaticPermissionDefinitionChangedEventHandler", + "summary": null } ] } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs index 74bdf1821b..ef3cf355bb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,33 +18,19 @@ public class PermissionDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicPermissionDefinitionStore DynamicPermissionDefinitionStore { get; } - protected IStaticPermissionSaver StaticPermissionSaver { get; } - - public PermissionDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicPermissionDefinitionStore dynamicPermissionDefinitionStore, - IStaticPermissionSaver staticPermissionSaver) + + public PermissionDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - ApplicationLifetime = ServiceProvider.GetService(); - CancellationTokenProvider = cancellationTokenProvider; - DynamicPermissionDefinitionStore = dynamicPermissionDefinitionStore; - StaticPermissionSaver = staticPermissionSaver; } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider + .GetRequiredService>() + .Value; if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled) { @@ -54,11 +39,12 @@ public class PermissionDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -68,20 +54,23 @@ public class PermissionDynamicInitializer : ITransientDependency return ExecuteInitializationAsync(options, cancellationToken); } - protected virtual async Task ExecuteInitializationAsync(PermissionManagementOptions options, CancellationToken cancellationToken) + protected virtual async Task ExecuteInitializationAsync( + PermissionManagementOptions options, + CancellationToken cancellationToken) { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticPermissionsToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -104,6 +93,8 @@ public class PermissionDynamicInitializer : ITransientDependency return; } + var staticPermissionSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -118,7 +109,7 @@ public class PermissionDynamicInitializer : ITransientDependency { try { - await StaticPermissionSaver.SaveAsync(); + await staticPermissionSaver.SaveAsync(); } catch (Exception ex) { @@ -129,17 +120,20 @@ public class PermissionDynamicInitializer : ITransientDependency }, cancellationToken); } - protected virtual async Task PreCacheDynamicPermissionsAsync(PermissionManagementOptions options) + protected virtual async Task PreCacheDynamicPermissionsAsync( + PermissionManagementOptions options) { if (!options.IsDynamicPermissionStoreEnabled) { return; } + var dynamicPermissionDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache permissions, so first request doesn't wait - await DynamicPermissionDefinitionStore.GetGroupsAsync(); + await dynamicPermissionDefinitionStore.GetGroupsAsync(); } catch (Exception ex) { diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo.Abp.PermissionManagement.EntityFrameworkCore.abppkg.analyze.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo.Abp.PermissionManagement.EntityFrameworkCore.abppkg.analyze.json index 769b7bf6c4..dc4dea375a 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo.Abp.PermissionManagement.EntityFrameworkCore.abppkg.analyze.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.EntityFrameworkCore/Volo.Abp.PermissionManagement.EntityFrameworkCore.abppkg.analyze.json @@ -85,6 +85,12 @@ "contentType": "databaseTable", "name": "PermissionGrants", "summary": null + }, + { + "entityFullName": "Volo.Abp.PermissionManagement.ResourcePermissionGrant", + "contentType": "databaseTable", + "name": "ResourcePermissionGrants", + "summary": null } ], "replacedDbContexts": [], diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.MongoDB/Volo.Abp.PermissionManagement.MongoDB.abppkg.analyze.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.MongoDB/Volo.Abp.PermissionManagement.MongoDB.abppkg.analyze.json index ab836e4108..a91528d4f3 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.MongoDB/Volo.Abp.PermissionManagement.MongoDB.abppkg.analyze.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.MongoDB/Volo.Abp.PermissionManagement.MongoDB.abppkg.analyze.json @@ -85,6 +85,12 @@ "contentType": "databaseCollection", "name": "PermissionGrants", "summary": null + }, + { + "entityFullName": "Volo.Abp.PermissionManagement.ResourcePermissionGrant", + "contentType": "databaseCollection", + "name": "ResourcePermissionGrants", + "summary": null } ], "implementingInterfaces": [ diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js index 7a6c100c07..971290b941 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/resource-permission-management-modal.js @@ -34,7 +34,7 @@ var abp = abp || {}; }, action: function (data) { volo.abp.permissionManagement.permissions.deleteResource($("#ResourceName").val(), $("#ResourceKey").val(), data.record.providerName, data.record.providerKey).then(function () { - abp.notify.info(l('DeletedSuccessfully')); + abp.notify.success(l('DeletedSuccessfully')); _dataTable.ajax.reloadEx(function (json) { _dataTable.columns.adjust(); }); diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.abppkg.analyze.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.abppkg.analyze.json index 44b864a0e0..524b3a5489 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.abppkg.analyze.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.abppkg.analyze.json @@ -69,11 +69,29 @@ "name": "AbpPermissionManagementWebModule", "summary": null }, + { + "namespace": "Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement", + "contentType": "webPage", + "name": "AddResourcePermissionManagementModal", + "summary": null + }, { "namespace": "Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement", "contentType": "webPage", "name": "PermissionManagementModal", "summary": null + }, + { + "namespace": "Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement", + "contentType": "webPage", + "name": "ResourcePermissionManagementModal", + "summary": null + }, + { + "namespace": "Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement", + "contentType": "webPage", + "name": "UpdateResourcePermissionManagementModal", + "summary": null } ] } \ No newline at end of file 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 04b385fff7..3a8321fed9 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.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2" } } 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 bbe4b5c934..a2b6e13b29 100644 --- a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock +++ b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock @@ -2,185 +2,185 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.0.1.tgz#3db3ac9291915c0b129fe90fd9a72ef1703c4b4d" - integrity sha512-D0Nv7VjNk03xF2Ii7pFEKSGzrggS5Y7NVApgOeFXbLhU/XZSfbR1A2wocLy6K+cKInH6+xhEyMdwTjwlUMx/Vw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.2.tgz#599f5c47a417d1230fc17c0446a0229f920f7246" + integrity sha512-8F4nEK+VtgRRf8n+66HMbtCEaOMCW/OdbSEWRl9ahMNoj860oPIJ8P8Qn/2+LjtkPMdDAfCdEzyDzCd3igaFaA== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.0.1.tgz#3c525bbc0da2b4e603b609289af623a030953f6d" - integrity sha512-euCjtPG2AjZ9AFbRQNh9649f40rQRfq58ZLvjUCfCvotbe7Fl+FaZgyIukpxXqKgd14NtCd2xPbvRM6/3Wj6IQ== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.2.tgz#e5056e4e159f5815e3cffecab5c46f3d7d4f79d7" + integrity sha512-bo56XzQZPYL/3ckWTTTSSUsSFSFJobvfE29cz13NIrZ/tBtWyQCAJn92wYHuY+6IezYUWb4ga3PkFeHRzR142A== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.0.1" - "@abp/bootstrap" "~10.0.1" - "@abp/bootstrap-datepicker" "~10.0.1" - "@abp/bootstrap-daterangepicker" "~10.0.1" - "@abp/datatables.net-bs5" "~10.0.1" - "@abp/font-awesome" "~10.0.1" - "@abp/jquery-form" "~10.0.1" - "@abp/jquery-validation-unobtrusive" "~10.0.1" - "@abp/lodash" "~10.0.1" - "@abp/luxon" "~10.0.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.0.1" - "@abp/moment" "~10.0.1" - "@abp/select2" "~10.0.1" - "@abp/sweetalert2" "~10.0.1" - "@abp/timeago" "~10.0.1" - -"@abp/aspnetcore.mvc.ui@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.0.1.tgz#b004dc6313b9320b05f465eeb9e74877766ea0f0" - integrity sha512-IEiLfdpDwtrGek/z7iBlgKlZdCvgaL2q9/GGLySrLknnVtv/qONzYburveZsKw8LT7PbZWRQRBh2n7v6TT7M9w== + "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.2" + "@abp/bootstrap" "~10.1.0-rc.2" + "@abp/bootstrap-datepicker" "~10.1.0-rc.2" + "@abp/bootstrap-daterangepicker" "~10.1.0-rc.2" + "@abp/datatables.net-bs5" "~10.1.0-rc.2" + "@abp/font-awesome" "~10.1.0-rc.2" + "@abp/jquery-form" "~10.1.0-rc.2" + "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.2" + "@abp/lodash" "~10.1.0-rc.2" + "@abp/luxon" "~10.1.0-rc.2" + "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.2" + "@abp/moment" "~10.1.0-rc.2" + "@abp/select2" "~10.1.0-rc.2" + "@abp/sweetalert2" "~10.1.0-rc.2" + "@abp/timeago" "~10.1.0-rc.2" + +"@abp/aspnetcore.mvc.ui@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.2.tgz#e25d3575d40bfcb3f809bd2d355671181ee5ff40" + integrity sha512-MOF86bVbi7N/nIla+361nsBrN4tiSka8xzpWcgqlLcCAl9ILG4rugbtafBAjN81taPma2peZM7egaOR4SDkTMw== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.0.1.tgz#5d58e5039e39d84b179a0c343d616ab0fc7c38d4" - integrity sha512-hwpSDUTM/A/Rn+3Hjjt3xG7QdhmFruM7fGEEU8kd7Qowimx2XVNIM5Ua5obt5bGTyBmsaAx4HnurjJJn1oh4ng== +"@abp/bootstrap-datepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.2.tgz#be80c6104ba53e18935fbf62ca2c1890f4b2fde4" + integrity sha512-BNcDYUSbZaLah4SfXm0efoqFTsOViVm6370k9L7vix/OGpIWwklJsr8y78lvdM5ANgNCfl0LPSq+seLJFc/OLA== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.0.1.tgz#8ff88aed898e46c36a3e9601a791cb0fa1134f28" - integrity sha512-a1hhaSk+SffutUI0CxUgAG6Zmx/Y4L7i1LEsQff4OEq0j8ipaHT+5UHMXf2DbCMo7yoZh2yUXQAATO0A/O2V+A== +"@abp/bootstrap-daterangepicker@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.2.tgz#f189f7d070ebd97d9cfdcb99571cab2d6a198ab5" + integrity sha512-bV8J0MuiAFVLkr48JsB6aZU6aPoqw+Gyhq1szQ74bEwNQlRBPuF92WVA5FACaUBj8dMUzR9HDDAYQuxUzpKYKA== dependencies: bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.0.1.tgz#cb857f21814097522fbd7e5c1a36cb22182e9f3e" - integrity sha512-AK/8ykw4SYjLgFgJE1zb2Mevn6ypqXqETbndN887JSny1QRrLUBVOKy6g+pnPUqI45/4wPfas7H9WgjFINiK2g== +"@abp/bootstrap@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.2.tgz#2300800a29ea09b91f5ed2e6177e5921fe7d2a0f" + integrity sha512-K+tDI9vz/Y9B/yu0i3AVpm4v3Odi44Q/yH5hAprL7f4pGxEOiqAFB/qzHAxG+7Oa7wjv5tPLv+Cz4DavBQjd8Q== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.0.1.tgz#c96563310d28137b0ac24efff74a969fa13aa7bb" - integrity sha512-mc/Wve/fl/B3cLqQ18IXO0lw1QCi3kCbi8PxRoLowD8NZEguezNglFjNqdvHNvBaWpZpMgJc2U+B15giB0946w== +"@abp/core@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.2.tgz#403687aff5a30788f7b7ca660abdfd85d89438aa" + integrity sha512-euuG2Hna/DT6/R1dGOjgp3vcehYtF+CcOkRj31oquYKaM5YWk4OaZ314DSpnjgs/xo8DuVc4eKFQwIxD9RK41w== dependencies: - "@abp/utils" "~10.0.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.0.1.tgz#1b33c14e9dabd0b29cc4d6f05120607a14313d55" - integrity sha512-0ww7HZ9m/OZWRQ2/9gNNgd59FpfvWSdw21onCBgJ7eaLd6KQeeFqbXm4eYjHoLgyRwk8c6u/F9ciir3EeTivhw== +"@abp/datatables.net-bs5@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.2.tgz#a60650d1802b40751d30f8f6c56beb23fd66481b" + integrity sha512-IWwexNqbMpET54Fvm9LoPTJYf+4CoBbjFOvz3sL6CgO2feV5R5fKigjVU8zXKNh2W+RG8L6zEarfVxrr114TsA== dependencies: - "@abp/datatables.net" "~10.0.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.0.1.tgz#ed45edcb4ee6832f38d4f1a56b5c0e1b126e8b82" - integrity sha512-DR53PGhHbW0ZdzeT7PWvBSfZrSyF2eWo1zAzCXsG+MsVRIiNzUNjipeq1igmd0PtXi2FHb76xS2utgAfgZEZ1A== +"@abp/datatables.net@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.2.tgz#9147f68bc6dbc4eb40a9ddf65c7859e788cdcac2" + integrity sha512-a9DJpwg14S4nVOiC4ipw0CQwEYWB602e2gCJiH7W1mxopbQb135RxwhtdTnW//eIONcxC9IrEuvcBEAUVt2B7Q== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.0.1.tgz#8c75feda6e394143f7e1cbe1ac8fc12b275bafed" - integrity sha512-2DDjc+EJcHDPUm/LHzbVjVMmChIaiEqMasKQ7qhxDq6yL102wMibPz0JR6Q9EYmYWro+Blf3Q0/0ECYLa8BgnQ== +"@abp/font-awesome@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.2.tgz#364466cfe67e41e0c4d16b57d3923d10f66369f1" + integrity sha512-F1Jy8xoFV2aA+VN+NH1gtrG96/j9w7Picc+KLoCoIyNnJr/xJur11XkJyu5ln8KF4V7p/DY7QaQodWV/btOs8g== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.0.1.tgz#e19c89863e2175a5e5db638ab95f648d2ee18ab3" - integrity sha512-nVZwEv0VeIP+xQZk7bz8S2RbKhkOUTKSf6mkdkLlna+8TNW0Ry8tBns79n/I0wYUh5007hxQbqb3L5TG4kq4+Q== +"@abp/jquery-form@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.2.tgz#3857717d07569c22d4bbbe459238abeb816d606a" + integrity sha512-2D5WHVnfK9bhRces1tgPwOEoc7KCYKYiKHBOcqct+LTA7zoRjJv/PM8/JhFVl+grVIw1aSwO4tU3YfZ22Vxipg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.0.1.tgz#35d43938c05ed3f6aab67bfd574c82f5e83a277f" - integrity sha512-jch+haMxPqMcN7CrFoEnULXHSdP43E+CdwDkCYJnTjEydISMyr2CwW4cIA/ab4kjNXs1DloW+r8+unRnOzClWQ== +"@abp/jquery-validation-unobtrusive@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.2.tgz#efd7b69a078a20c0bf405408dbdf52a7bf770b3b" + integrity sha512-tZ0MWgzBqp+SNfMxM0z2cGB21NiTHuVJyyQaXKE/ptuD5pc0uRkcqw/J2kWfiqsoVgChz27IB6h8/jqDafS4qg== dependencies: - "@abp/jquery-validation" "~10.0.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.0.1.tgz#20c4c313d9ec73b4dc242729ab3250f747b76b66" - integrity sha512-cUGUCOuwKc1TR1R8GHpjN9HokWK6p6ElM4sN/J3yY/Nef9wKn4zY98Q3hmFLsCDeV+9Sjex1xcqNjqU3ZOiZSg== +"@abp/jquery-validation@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.2.tgz#d39537a7356c51f9db2e66f6740cf6df86bd0442" + integrity sha512-LOkS0NKk4pLtLjPU0CCbwROyUg6EtJN8Z/it7QuKK1CIRfYYcAStgNnNm5geZP7CqECIkoiFfgWjI+L5Z9/Tfg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.0.1.tgz#fc6fb5fed08e6ab113e0096cd6c00ad10461fef4" - integrity sha512-hIQkMc9ouQz4QKaEJSVzZqQMyWdF7tmzZ8WlVN6EeWEDUKPLyuibhwTvTEO6u+17ZP7GhlldONHsRwTdc0zlJA== +"@abp/jquery@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.2.tgz#101a55f70d510978c8c05f5857d0e9d4965263f7" + integrity sha512-bQV1uFWGtwRYjNOsqJ8FM2004idX2Jj7YVL19YF1/PjyPUSMX+s8/IvJizBjyY5hPAiWBBhmV9g+IFWzxlDQoQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.0.1.tgz#41df6cc6d375ea1e6a5a27745b68b0964bcbc295" - integrity sha512-15uv5kNtXBb+3hm7Qorh95mLhSIJkIbGa2bp3Tyw4jEdXTFPsb1v5FCC2m7LEaEUNuNgzXFJGT818Xi58AI0Rw== +"@abp/lodash@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.2.tgz#d08c03f8d3d0fbaa3e71e603cbe5fb7f176933ef" + integrity sha512-KCnD1p2y52ZI+2ifpiFIUAiDPsKehnOD8HV5qKeObO6UCP97okif8IP+sQDmNQb8O33y/NKTyx/HcpwBbe/NYQ== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.0.1.tgz#97c920867775def2bb1628b47507e45388179e40" - integrity sha512-LL/J4oyA+o9res57cq/+qsilTvo7ikxtCdpxGSIEjkvNTmYzcChv1ixmDMvqqMvJFELJ9R+1V7NeZhXBAiR6Lw== +"@abp/luxon@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.2.tgz#ef8d2b323bac054fc9610e241e1b1763d229e065" + integrity sha512-qYFl6XO3g9mZiu0dtIczI7LRuYWwc+RkpbDzSmruXcRks3KA+ZZco2vhHNnlwtXcINl/TXtbW7Wc0MX+8IB1Kw== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.0.1.tgz#53dc824537110f155d00c46b1ffe7c6eff060040" - integrity sha512-S4zKvlTMvhkFCBhakql1bLB/lHlRjPy8An0KB2pBKvxfIvzUQn8YmiBSK51Hq/Hf6ZnnSJkNr35cb9TcHUwkNA== +"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.2.tgz#dfaf666442c7c122f7da72c83b9adf194d5b6ec8" + integrity sha512-PudMHmNQgZ6JZeaVt1ZoXLqO0UZXJzUYiBah2LDkC4EMLjnMJFINHBoEVVa4ooXH0yjFv+zsbN0vWZYJ8TBJIA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.0.1.tgz#dce2c26602ac9c77ea0209e6a3727a7e9f04f4c4" - integrity sha512-fRrMLQhYzOSATSM4hWdr7Y5ggbMd23ffivpDB4O2BDYUXTcfiWyVVKDd+5uLZi+znkWz29bNN4WswileuHvaGw== +"@abp/moment@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.2.tgz#610a1592d13984aea51abbd13df8c5995a089149" + integrity sha512-ep8PnAXARw0t/wtGOVp/oiNhF3B0Bh6y2vRzKrcSoyXAQREGGm4fJdZVYZLGTfI4lFLTjebEgf4O7T9feUwJAw== dependencies: moment "^2.30.1" -"@abp/select2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.0.1.tgz#df92daf67f46aa2fc884ef84f9da68b1fb8e63d7" - integrity sha512-VQxYH0Uqa7EN+F6XBkDMz3yy8yTM6xcZ6593WtZupl9UbiHToGElsem3ibnZueoyMHcd3ByYw968uvJX7zudNw== +"@abp/select2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.2.tgz#40c5418d007fc36817eecbe6388d767e4e7ca887" + integrity sha512-Pq0wlpL01sWRLUg5um3JtBXIqi3mmbwPwvgxP8hFbQngAt9JXAK8geNRiTMrIZgtW/ycXtM1v6I4zuWOLOeAGg== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.0.1.tgz#a26874fd51ddffeba60f4506eb1a5c914ae3efae" - integrity sha512-4USaGSA5+7O6D+5a4YhluYPKUyOAassUUuKJATP8IqLRtpkh16P4tJ/7+QWvnFDIgJLURpbjmOiN2xhVzhpxvw== +"@abp/sweetalert2@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.2.tgz#d35858c69e10c6726b02cdfcea88dfc32385963f" + integrity sha512-s9VPRToohN45uzHcKCF5Mcj8FVjsXcXUb0U3tuaT/Y+u4adHB3fBxYiXJFM0sVsCJ81dFktxwka40Wm8Taz/zA== dependencies: - "@abp/core" "~10.0.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.0.1.tgz#6bc36ab7dd3deea114ff1d68dbda908202837cf6" - integrity sha512-Wd2KY8B95ycsRDn5ouY3l3U+niBMEd+XCgZs6CoaMtiQ1AxkT7/iPqNCMJMKjeIjBQ/A1CSmKL7MI+BGw1bxBA== +"@abp/timeago@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.2.tgz#98d630cc3843eee64dbcc34fb8ca5afbab034718" + integrity sha512-vJmk+otyXXJE2s2J8iYpLVaFuNAYnIUSOitmi7umYnL+k/UE2KQhBXU7FR0/OBY9mAZYd+shaiGIU1LMSaJ+Xg== dependencies: - "@abp/jquery" "~10.0.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.0.1.tgz#0db8713481cb781c3c4d07a150b7f45a65466b50" - integrity sha512-YGXgco/qYSxGaQfNTHDIMU1MyEuVDe3FayIZWPW5+p+elwp4DPFD4rvD+6ZLM0Jr30k5UdKT4IFAsw7wduQWrw== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo.Abp.SettingManagement.Domain.abppkg.analyze.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo.Abp.SettingManagement.Domain.abppkg.analyze.json index 33a15e01ee..9ea10c8d16 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo.Abp.SettingManagement.Domain.abppkg.analyze.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo.Abp.SettingManagement.Domain.abppkg.analyze.json @@ -88,6 +88,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -164,6 +170,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", @@ -535,6 +547,42 @@ "contentType": "eventHandler", "name": "SettingCacheItemInvalidator", "summary": null + }, + { + "eventHandlerType": "Local", + "namespace": "Volo.Abp.SettingManagement", + "subscribedEvents": [ + { + "underlyingType": null, + "name": "StaticSettingDefinitionChangedEvent", + "namespace": "Volo.Abp.Settings", + "declaringAssemblyName": "Volo.Abp.Settings", + "fullName": "Volo.Abp.Settings.StaticSettingDefinitionChangedEvent" + } + ], + "implementingInterfaces": [ + { + "name": "ILocalEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.ILocalEventHandler" + }, + { + "name": "IEventHandler", + "namespace": "Volo.Abp.EventBus", + "declaringAssemblyName": "Volo.Abp.EventBus.Abstractions", + "fullName": "Volo.Abp.EventBus.IEventHandler" + }, + { + "name": "ITransientDependency", + "namespace": "Volo.Abp.DependencyInjection", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.DependencyInjection.ITransientDependency" + } + ], + "contentType": "eventHandler", + "name": "StaticSettingDefinitionChangedEventHandler", + "summary": null } ] } \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs index 41db669d9b..af9858353c 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,33 +18,19 @@ public class SettingDynamicInitializer : ITransientDependency public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } - protected IOptions Options { get; } - [CanBeNull] - protected IHostApplicationLifetime ApplicationLifetime { get; } - protected ICancellationTokenProvider CancellationTokenProvider { get; } - protected IDynamicSettingDefinitionStore DynamicSettingDefinitionStore { get; } - protected IStaticSettingSaver StaticSettingSaver { get; } - - public SettingDynamicInitializer( - IServiceProvider serviceProvider, - IOptions options, - ICancellationTokenProvider cancellationTokenProvider, - IDynamicSettingDefinitionStore dynamicSettingDefinitionStore, - IStaticSettingSaver staticSettingSaver) + + public SettingDynamicInitializer(IServiceProvider serviceProvider) { Logger = NullLogger.Instance; ServiceProvider = serviceProvider; - Options = options; - ApplicationLifetime = ServiceProvider.GetService(); - CancellationTokenProvider = cancellationTokenProvider; - DynamicSettingDefinitionStore = dynamicSettingDefinitionStore; - StaticSettingSaver = staticSettingSaver; } public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) { - var options = Options.Value; + var options = ServiceProvider + .GetRequiredService>() + .Value; if (!options.SaveStaticSettingsToDatabase && !options.IsDynamicSettingStoreEnabled) { @@ -54,11 +39,12 @@ public class SettingDynamicInitializer : ITransientDependency if (runInBackground) { + var applicationLifetime = ServiceProvider.GetService(); Task.Run(async () => { - if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) + if (cancellationToken == default && applicationLifetime?.ApplicationStopping != null) { - cancellationToken = ApplicationLifetime.ApplicationStopping; + cancellationToken = applicationLifetime.ApplicationStopping; } await ExecuteInitializationAsync(options, cancellationToken); }, cancellationToken); @@ -69,20 +55,23 @@ public class SettingDynamicInitializer : ITransientDependency return ExecuteInitializationAsync(options, cancellationToken); } - protected virtual async Task ExecuteInitializationAsync(SettingManagementOptions options, CancellationToken cancellationToken) + protected virtual async Task ExecuteInitializationAsync( + SettingManagementOptions options, + CancellationToken cancellationToken) { try { - using (CancellationTokenProvider.Use(cancellationToken)) + var cancellationTokenProvider = ServiceProvider.GetRequiredService(); + using (cancellationTokenProvider.Use(cancellationToken)) { - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } await SaveStaticSettingsToDatabaseAsync(options, cancellationToken); - if (CancellationTokenProvider.Token.IsCancellationRequested) + if (cancellationTokenProvider.Token.IsCancellationRequested) { return; } @@ -105,6 +94,8 @@ public class SettingDynamicInitializer : ITransientDependency return; } + var staticSettingSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -119,7 +110,7 @@ public class SettingDynamicInitializer : ITransientDependency { try { - await StaticSettingSaver.SaveAsync(); + await staticSettingSaver.SaveAsync(); } catch (Exception ex) { @@ -129,17 +120,20 @@ public class SettingDynamicInitializer : ITransientDependency }, cancellationToken); } - protected virtual async Task PreCacheDynamicSettingsAsync(SettingManagementOptions options) + protected virtual async Task PreCacheDynamicSettingsAsync( + SettingManagementOptions options) { if (!options.IsDynamicSettingStoreEnabled) { return; } + var dynamicSettingDefinitionStore = ServiceProvider.GetRequiredService(); + try { // Pre-cache settings, so first request doesn't wait - await DynamicSettingDefinitionStore.GetAllAsync(); + await dynamicSettingDefinitionStore.GetAllAsync(); } catch (Exception ex) { diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.abppkg.analyze.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.abppkg.analyze.json index eb40b9d33f..51b4a91d36 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.abppkg.analyze.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.abppkg.analyze.json @@ -122,6 +122,12 @@ "declaringAssemblyName": "Volo.Abp.Ddd.Domain", "fullName": "Volo.Abp.Domain.Entities.IEntity" }, + { + "name": "IKeyedObject", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IKeyedObject" + }, { "name": "IEntity", "namespace": "Volo.Abp.Domain.Entities", diff --git a/modules/virtual-file-explorer/app/package.json b/modules/virtual-file-explorer/app/package.json index ca6bf2d608..50866814bc 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.0.1", - "@abp/virtual-file-explorer": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/virtual-file-explorer": "~10.1.0-rc.2" } } diff --git a/npm/lerna.json b/npm/lerna.json index a015f56501..ee02c6794e 100644 --- a/npm/lerna.json +++ b/npm/lerna.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "packages": [ "packs/*" ], diff --git a/npm/ng-packs/.gitignore b/npm/ng-packs/.gitignore index 9d756a8b67..84b48a798c 100644 --- a/npm/ng-packs/.gitignore +++ b/npm/ng-packs/.gitignore @@ -58,3 +58,5 @@ Thumbs.db .angular .nx/ + +vitest.config.*.timestamp* \ No newline at end of file diff --git a/npm/ng-packs/apps/dev-app/project.json b/npm/ng-packs/apps/dev-app/project.json index bea3bf6612..c397d8d449 100644 --- a/npm/ng-packs/apps/dev-app/project.json +++ b/npm/ng-packs/apps/dev-app/project.json @@ -175,10 +175,12 @@ "executor": "@nx/eslint:lint" }, "test": { - "executor": "@nx/jest:jest", + "executor": "@nx/vitest:test", "outputs": ["{workspaceRoot}/coverage/apps/dev-app"], "options": { - "jestConfig": "apps/dev-app/jest.config.ts" + "passWithNoTests": true, + "reportsDirectory": "../../coverage/apps/dev-app", + "silent": false } }, "serve-static": { diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html index b938978534..c675dca707 100644 --- a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html +++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html @@ -1,6 +1,111 @@
-

Dynamic Form Page

- @if (formFields.length) { - - } +
+
+

Dynamic Form Showcase

+

+ Comprehensive example demonstrating all available field types with validation and conditional logic. +

+
+
+
+ +
+
+
+
+

+ + User Registration Form +

+ All 16 field types + Nested Forms (Group & Array) with full accessibility support +
+
+ @if (formFields.length) { + + } @else { +
+
+ Loading... +
+

Loading form configuration...

+
+ } +
+
+ + +
+
+
+ + Available Field Types +
+
+
+
+
+
Text Inputs
+
    +
  • Text
  • +
  • Email
  • +
  • Password
  • +
  • Tel
  • +
  • URL
  • +
  • Textarea
  • +
+
+
+
Special Inputs
+
    +
  • Number
  • +
  • Date
  • +
  • DateTime-Local
  • +
  • Time
  • +
  • Range
  • +
  • Color
  • +
+
+
+
Selection
+
    +
  • Select (Dropdown)
  • +
  • Radio
  • +
  • Checkbox
  • +
+
+
+
Files
+
    +
  • File (Single/Multiple)
  • +
+
+
+
Nested Forms
+
    +
  • Group (Nested Fields)
  • +
  • Array (Dynamic List)
  • +
+
+
+
+
+ + Features: Full validation support, conditional logic, nested forms (groups & arrays), + grid-based layout, ARIA accessibility, keyboard navigation, and screen reader support. +
+
+
+ + New: Try the nested forms below! Add/remove phone numbers dynamically, + or fill in your work experience. The Address section shows a grouped form. +
+
+
+
+
\ No newline at end of file diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts index 1b9f1bb764..0955a448e5 100644 --- a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts +++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts @@ -5,7 +5,6 @@ import { FormConfigService } from './form-config.service'; @Component({ selector: 'app-dynamic-form-page', templateUrl: './dynamic-form-page.component.html', - standalone: true, imports: [DynamicFormComponent], }) export class DynamicFormPageComponent implements OnInit { @@ -20,8 +19,20 @@ export class DynamicFormPageComponent implements OnInit { }); } - submit(val) { - console.log('submit', val); + submit(formData: any) { + console.log('✅ Form Submitted Successfully!', formData); + console.table(formData); + + // Show success message + alert('✅ Form submitted successfully! Check the console for details.'); + + // Reset form after submission + this.dynamicFormComponent.resetForm(); + } + + cancel() { + console.log('❌ Form Cancelled'); + alert('Form cancelled'); this.dynamicFormComponent.resetForm(); } } diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts index 0c5c86c841..b88423f945 100644 --- a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts +++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts @@ -1,15 +1,381 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; import { FormFieldConfig } from '@abp/ng.components/dynamic-form'; +import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class FormConfigService { - protected readonly http = inject(HttpClient); - getFormConfig(): Observable { - return this.http.get('/assets/form-config.json'); + const formConfig: FormFieldConfig[] = [ + // Section 1: Basic Text Inputs + { + key: 'firstName', + label: 'First Name', + type: 'text', + placeholder: 'Enter your first name', + required: true, + gridSize: 6, + order: 1, + validators: [{ type: 'required', message: 'First name is required' }], + }, + { + key: 'lastName', + label: 'Last Name', + type: 'text', + placeholder: 'Enter your last name', + required: true, + gridSize: 6, + order: 2, + validators: [{ type: 'required', message: 'Last name is required' }], + }, + + // Section 2: Email & Password + { + key: 'email', + label: 'Email Address', + type: 'email', + placeholder: 'example@domain.com', + required: true, + gridSize: 6, + order: 3, + validators: [ + { type: 'required', message: 'Email is required' }, + { type: 'email', message: 'Invalid email address' }, + ], + }, + { + key: 'password', + label: 'Password', + type: 'password', + placeholder: 'Enter a strong password', + required: true, + gridSize: 6, + order: 4, + minLength: 8, + maxLength: 50, + validators: [ + { type: 'required', message: 'Password is required' }, + { type: 'minLength', value: 8, message: 'Password must be at least 8 characters' }, + ], + }, + + // Section 3: Contact Information + { + key: 'phone', + label: 'Phone Number', + type: 'tel', + placeholder: '555-123-4567', + gridSize: 6, + order: 5, + pattern: '[0-9]{3}-[0-9]{3}-[0-9]{4}', + }, + { + key: 'website', + label: 'Website', + type: 'url', + placeholder: 'https://example.com', + gridSize: 6, + order: 6, + }, + + // Section 4: Numbers & Dates + { + key: 'age', + label: 'Age', + type: 'number', + placeholder: 'Enter your age', + required: true, + gridSize: 4, + order: 7, + min: 18, + max: 100, + validators: [ + { type: 'required', message: 'Age is required' }, + { type: 'min', value: 18, message: 'You must be at least 18 years old' }, + ], + }, + { + key: 'birthdate', + label: 'Birth Date', + type: 'date', + required: true, + gridSize: 4, + order: 8, + max: new Date().toISOString().split('T')[0], + validators: [{ type: 'required', message: 'Birth date is required' }], + }, + { + key: 'appointmentTime', + label: 'Appointment Date & Time', + type: 'datetime-local', + gridSize: 4, + order: 9, + min: new Date().toISOString().slice(0, 16), + }, + + // Section 5: Select & Radio + { + key: 'country', + label: 'Country', + type: 'select', + required: true, + gridSize: 6, + order: 10, + options: { + defaultValues: [ + { key: 'usa', value: 'United States' }, + { key: 'uk', value: 'United Kingdom' }, + { key: 'canada', value: 'Canada' }, + { key: 'germany', value: 'Germany' }, + { key: 'france', value: 'France' }, + ], + }, + validators: [{ type: 'required', message: 'Country is required' }], + }, + { + key: 'gender', + label: 'Gender', + type: 'radio', + required: true, + gridSize: 6, + order: 11, + options: { + defaultValues: [ + { key: 'male', value: 'Male' }, + { key: 'female', value: 'Female' }, + { key: 'other', value: 'Other' }, + { key: 'prefer-not-to-say', value: 'Prefer not to say' }, + ], + }, + validators: [{ type: 'required', message: 'Gender is required' }], + }, + + // Section 6: Conditional Field (shown when country is USA) + { + key: 'state', + label: 'State (USA Only)', + type: 'select', + gridSize: 6, + order: 12, + options: { + defaultValues: [ + { key: 'ca', value: 'California' }, + { key: 'ny', value: 'New York' }, + { key: 'tx', value: 'Texas' }, + { key: 'fl', value: 'Florida' }, + ], + }, + conditionalLogic: [ + { dependsOn: 'country', condition: 'equals', value: 'usa', action: 'show' }, + ], + }, + + // Section 7: Time & Range + { + key: 'preferredTime', + label: 'Preferred Contact Time', + type: 'time', + gridSize: 6, + order: 13, + step: '900', // 15 minutes + }, + { + key: 'experienceLevel', + label: 'Experience Level (0-10)', + type: 'range', + gridSize: 6, + order: 14, + min: 0, + max: 10, + step: 1, + value: 5, + }, + + // Section 8: Color & File + { + key: 'favoriteColor', + label: 'Favorite Color', + type: 'color', + gridSize: 6, + order: 15, + value: '#007bff', + }, + { + key: 'profilePicture', + label: 'Profile Picture', + type: 'file', + gridSize: 6, + order: 16, + accept: 'image/*', + multiple: false, + }, + + // Section 9: Textarea & Checkbox + { + key: 'bio', + label: 'Biography', + type: 'textarea', + placeholder: 'Tell us about yourself...', + gridSize: 12, + order: 17, + maxLength: 500, + }, + { + key: 'newsletter', + label: 'Subscribe to newsletter', + type: 'checkbox', + gridSize: 6, + order: 18, + }, + { + key: 'terms', + label: 'I agree to the terms and conditions', + type: 'checkbox', + required: true, + gridSize: 6, + order: 19, + validators: [{ type: 'requiredTrue', message: 'You must agree to the terms' }], + }, + + // Section 10: NESTED FORM - Phone Numbers (Array) + { + key: 'phoneNumbers', + type: 'array', + label: 'Phone Numbers', + gridSize: 12, + order: 20, + minItems: 1, + maxItems: 5, + children: [ + { + key: 'type', + type: 'select', + label: 'Type', + gridSize: 4, + required: true, + options: { + defaultValues: [ + { key: 'mobile', value: 'Mobile' }, + { key: 'home', value: 'Home' }, + { key: 'work', value: 'Work' }, + { key: 'other', value: 'Other' } + ] + }, + validators: [{ type: 'required', message: 'Phone type is required' }], + }, + { + key: 'number', + type: 'tel', + label: 'Number', + gridSize: 8, + required: true, + placeholder: '555-123-4567', + validators: [{ type: 'required', message: 'Phone number is required' }], + }, + ] + }, + + // Section 11: NESTED FORM - Work Experience (Array with nested group) + { + key: 'workExperience', + type: 'array', + label: 'Work Experience', + gridSize: 12, + order: 21, + minItems: 0, + maxItems: 10, + children: [ + { + key: 'company', + type: 'text', + label: 'Company Name', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'Company name is required' }], + }, + { + key: 'position', + type: 'text', + label: 'Position', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'Position is required' }], + }, + { + key: 'startDate', + type: 'date', + label: 'Start Date', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'Start date is required' }], + }, + { + key: 'endDate', + type: 'date', + label: 'End Date', + gridSize: 6, + }, + { + key: 'currentJob', + type: 'checkbox', + label: 'Currently working here', + gridSize: 12, + }, + { + key: 'description', + type: 'textarea', + label: 'Job Description', + placeholder: 'Describe your responsibilities...', + gridSize: 12, + maxLength: 500, + }, + ] + }, + + // Section 12: NESTED FORM - Address Group (Group type) + { + key: 'address', + type: 'group', + label: 'Address Information', + gridSize: 12, + order: 22, + children: [ + { + key: 'street', + type: 'text', + label: 'Street Address', + gridSize: 8, + placeholder: '123 Main St', + }, + { + key: 'apartment', + type: 'text', + label: 'Apt/Suite', + gridSize: 4, + placeholder: 'Apt 4B', + }, + { + key: 'city', + type: 'text', + label: 'City', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'City is required' }], + }, + { + key: 'zipCode', + type: 'text', + label: 'ZIP Code', + gridSize: 6, + required: true, + pattern: '[0-9]{5}', + validators: [{ type: 'required', message: 'ZIP code is required' }], + }, + ] + }, + ]; + + return of(formConfig); } } diff --git a/npm/ng-packs/apps/dev-app/src/app/home/custom-input.ts b/npm/ng-packs/apps/dev-app/src/app/home/custom-input.ts deleted file mode 100644 index 9d1aeeab26..0000000000 --- a/npm/ng-packs/apps/dev-app/src/app/home/custom-input.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - DestroyRef, - forwardRef, - inject, - Injector, - input, - OnInit, -} from '@angular/core'; -import { - ControlValueAccessor, - FormBuilder, - FormControl, - FormControlName, - FormGroupDirective, - NG_VALUE_ACCESSOR, - NgControl, - FormGroup, - ReactiveFormsModule, -} from '@angular/forms'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormFieldConfig } from '@abp/ng.components/dynamic-form'; - - -const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => AppCustomInputComponent), - multi: true, -}; - -@Component({ - selector: 'abp-dynamic-form-field', - template: ` -
- -
- `, - providers: [ - CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR - ], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule], -}) -export class AppCustomInputComponent implements OnInit, ControlValueAccessor { - field = input.required(); - visible = input(true); - control!: FormControl; - fieldFormGroup: FormGroup; - readonly changeDetectorRef = inject(ChangeDetectorRef); - readonly destroyRef = inject(DestroyRef); - private injector = inject(Injector); - private formBuilder = inject(FormBuilder); - - constructor() { - this.fieldFormGroup = this.formBuilder.group({ - value: [{ value: '' }] - }); - } - - ngOnInit() { - const ngControl = this.injector.get(NgControl, null); - if (ngControl) { - this.control = this.injector.get(FormGroupDirective).getControl(ngControl as FormControlName); - } - this.value.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => { - this.onChange(value); - }); - } - - writeValue(value: any[]): void { - this.value.setValue(value || ''); - this.changeDetectorRef.markForCheck(); - } - - registerOnChange(fn: any): void { - this.onChange = fn; - } - - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - - setDisabledState(isDisabled: boolean): void { - if (isDisabled) { - this.value.disable(); - } else { - this.value.enable(); - } - this.changeDetectorRef.markForCheck(); - } - - get isInvalid(): boolean { - if (this.control) { - return (this.control.invalid && (this.control.dirty || this.control.touched)); - } - return false; - } - - get errors(): string[] { - if (this.control && this.control.errors) { - const errorKeys = Object.keys(this.control.errors); - return errorKeys.map(key => { - const validator = this.field().validators.find(v => v.type.toLowerCase() === key.toLowerCase()); - console.log(this.field().validators, key); - if (validator && validator.message) { - return validator.message; - } - // Fallback error messages - if (key === 'required') return `${this.field().label} is required`; - if (key === 'email') return 'Please enter a valid email address'; - if (key === 'minlength') return `Minimum length is ${this.control.errors[key].requiredLength}`; - if (key === 'maxlength') return `Maximum length is ${this.control.errors[key].requiredLength}`; - return `${this.field().label} is invalid due to ${key} validation.`; - }); - } - } - get value() { - return this.fieldFormGroup.get('value'); - } - private onChange: (value: any) => void = () => {}; - private onTouched: () => void = () => {}; -} diff --git a/npm/ng-packs/jest.config.ts b/npm/ng-packs/jest.config.ts index 6b3f2d6e24..2aa4dd9d8a 100644 --- a/npm/ng-packs/jest.config.ts +++ b/npm/ng-packs/jest.config.ts @@ -1,5 +1,8 @@ import { getJestProjectsAsync } from '@nx/jest'; - +/** + * @deprecated use vitest instead of jest + * @see https://vitest.dev/guide/migration.html#jest + */ export default async () => ({ projects: await getJestProjectsAsync(), }); diff --git a/npm/ng-packs/jest.preset.js b/npm/ng-packs/jest.preset.js index c1c3c4cdcd..861f91ccc4 100644 --- a/npm/ng-packs/jest.preset.js +++ b/npm/ng-packs/jest.preset.js @@ -1,3 +1,7 @@ +/** + * @deprecated use vitest instead of jest + * @see https://vitest.dev/guide/migration.html#jest + */ const nxPreset = require('@nx/jest/preset').default; module.exports = { diff --git a/npm/ng-packs/nx.json b/npm/ng-packs/nx.json index 500c939ad2..a573c5c59a 100644 --- a/npm/ng-packs/nx.json +++ b/npm/ng-packs/nx.json @@ -118,6 +118,10 @@ "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] + }, + "@nx/vitest:test": { + "cache": true, + "inputs": ["default", "^production"] } }, "namedInputs": { diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 80b1ea7f33..9690a32d7e 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -10,6 +10,8 @@ "build:all": "nx run-many --target=build --all --exclude=dev-app,schematics --prod && npm run build:schematics", "test": "ng test --detect-open-handles=true --run-in-band=true --watch-all=true", "test:all": "nx run-many --target=test --all", + "test:vitest": "vitest", + "test:vitest:project": "sh -c 'vitest --project \"${1:-core}\"' _", "lint-staged": "lint-staged", "lint": "nx workspace-lint && ng lint", "lint:all": "nx run-many --target=lint --all", @@ -46,8 +48,8 @@ }, "private": true, "devDependencies": { - "@abp/ng.theme.lepton-x": "~5.0.1", - "@abp/utils": "~10.0.1", + "@abp/ng.theme.lepton-x": "~5.1.0-rc.2", + "@abp/utils": "~10.1.0-rc.2", "@angular-devkit/build-angular": "~21.0.0", "@angular-devkit/core": "~21.0.0", "@angular-devkit/schematics": "~21.0.0", @@ -80,23 +82,26 @@ "@nx/eslint": "~22.2.0", "@nx/eslint-plugin": "~22.2.0", "@nx/jest": "~22.2.0", - "@nx/js": "~22.2.0", + "@nx/js": "22.2.7", "@nx/plugin": "~22.2.0", + "@nx/vite": "22.2.7", + "@nx/vitest": "22.2.7", "@nx/web": "~22.2.0", "@nx/workspace": "~22.2.0", "@popperjs/core": "~2.11.0", "@schematics/angular": "~21.0.0", "@swc-node/register": "1.9.2", "@swc/cli": "0.6.0", - "@swc/core": "~1.5.0", - "@swc/helpers": "~0.5.0", + "@swc/core": "~1.5.7", + "@swc/helpers": "~0.5.11", "@swimlane/ngx-datatable": "~22.0.0", "@types/express": "~5.0.0", "@types/jest": "29.5.14", - "@types/node": "~20.11.0", + "@types/node": "20.19.9", "@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/parser": "7.16.0", "@typescript-eslint/utils": "^7.16.0", + "@vitest/coverage-v8": "^4.0.0", "angular-oauth2-oidc": "~20.0.0", "autoprefixer": "^10.4.21", "bootstrap": "~5.0.0", @@ -114,6 +119,7 @@ "jest-canvas-mock": "^2.0.0", "jest-environment-jsdom": "^29.0.0", "jest-preset-angular": "14.6.0", + "jsdom": "~22.1.0", "jsonc-eslint-parser": "^2.0.0", "jsonc-parser": "^2.0.0", "just-clone": "^6.0.0", @@ -137,6 +143,8 @@ "tslib": "^2.3.0", "tslint": "~6.1.0", "typescript": "~5.9.0", + "vite": "^7.0.0", + "vitest": "^4.0.0", "zone.js": "~0.15.0" }, "lint-staged": { diff --git a/npm/ng-packs/packages/account-core/.eslintrc.json b/npm/ng-packs/packages/account-core/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/account-core/.eslintrc.json +++ b/npm/ng-packs/packages/account-core/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/account-core/jest.config.ts b/npm/ng-packs/packages/account-core/jest.config.ts index 7ef24776c3..f6aa97e8c1 100644 --- a/npm/ng-packs/packages/account-core/jest.config.ts +++ b/npm/ng-packs/packages/account-core/jest.config.ts @@ -1,4 +1,8 @@ /* eslint-disable */ +/** + * @deprecated use vitest instead of jest + * @see https://vitest.dev/guide/migration.html#jest + */ export default { displayName: 'account-core', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/account-core/package.json b/npm/ng-packs/packages/account-core/package.json index 03cb819f9f..48749859c2 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.0.1", + "version": "10.1.0-rc.2", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.core": "~10.0.1", - "@abp/ng.theme.shared": "~10.0.1", + "@abp/ng.core": "~10.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/account-core/project.json b/npm/ng-packs/packages/account-core/project.json index 312bdd30e9..8a201b8006 100644 --- a/npm/ng-packs/packages/account-core/project.json +++ b/npm/ng-packs/packages/account-core/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/account-core"], - "options": { - "jestConfig": "packages/account-core/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/account-core" + } } } } diff --git a/npm/ng-packs/packages/account-core/tsconfig.lib.json b/npm/ng-packs/packages/account-core/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/account-core/tsconfig.lib.json +++ b/npm/ng-packs/packages/account-core/tsconfig.lib.json @@ -10,6 +10,23 @@ "lib": ["dom", "es2020"], "useDefineForClassFields": false }, - "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], + "exclude": [ + "src/test-setup.ts", + "**/*.spec.ts", + "jest.config.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/test-setup.ts" + ], "include": ["**/*.ts"] } diff --git a/npm/ng-packs/packages/account-core/tsconfig.spec.json b/npm/ng-packs/packages/account-core/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/account-core/tsconfig.spec.json +++ b/npm/ng-packs/packages/account-core/tsconfig.spec.json @@ -2,10 +2,22 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"], - "esModuleInterop": true + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"] }, - "files": ["src/test-setup.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "files": ["src/test-setup.ts"] } diff --git a/npm/ng-packs/packages/account-core/vitest.config.mts b/npm/ng-packs/packages/account-core/vitest.config.mts new file mode 100644 index 0000000000..0ef8748fc2 --- /dev/null +++ b/npm/ng-packs/packages/account-core/vitest.config.mts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/account-core', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'account-core', + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/account-core', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/account/.eslintrc.json b/npm/ng-packs/packages/account/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/account/.eslintrc.json +++ b/npm/ng-packs/packages/account/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/account/jest.config.ts b/npm/ng-packs/packages/account/jest.config.ts index 7630a18eb3..207a21558c 100644 --- a/npm/ng-packs/packages/account/jest.config.ts +++ b/npm/ng-packs/packages/account/jest.config.ts @@ -1,4 +1,8 @@ /* eslint-disable */ +/** + * @deprecated use vitest instead of jest + * @see https://vitest.dev/guide/migration.html#jest + */ export default { displayName: 'account', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/account/package.json b/npm/ng-packs/packages/account/package.json index a89b53cebb..8e667e60ee 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.0.1", + "version": "10.1.0-rc.2", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/ng.account.core": "~10.0.1", - "@abp/ng.theme.shared": "~10.0.1", + "@abp/ng.account.core": "~10.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/account/project.json b/npm/ng-packs/packages/account/project.json index 41c6597e2a..5d02bebd76 100644 --- a/npm/ng-packs/packages/account/project.json +++ b/npm/ng-packs/packages/account/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/account"], - "options": { - "jestConfig": "packages/account/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/account" + } } } } diff --git a/npm/ng-packs/packages/account/src/lib/account.routes.ts b/npm/ng-packs/packages/account/src/lib/account.routes.ts index 5fb4236ee1..3c674162aa 100644 --- a/npm/ng-packs/packages/account/src/lib/account.routes.ts +++ b/npm/ng-packs/packages/account/src/lib/account.routes.ts @@ -1,4 +1,5 @@ import { Routes } from '@angular/router'; +import { Provider } from '@angular/core'; import { ForgotPasswordComponent, LoginComponent, @@ -23,7 +24,7 @@ import { RouterOutletComponent, } from '@abp/ng.core'; -export function provideAccount(options: AccountConfigOptions = {}) { +export function provideAccount(options: AccountConfigOptions = {}): Provider[] { return [ { provide: ACCOUNT_CONFIG_OPTIONS, useValue: options }, { diff --git a/npm/ng-packs/packages/account/tsconfig.lib.json b/npm/ng-packs/packages/account/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/account/tsconfig.lib.json +++ b/npm/ng-packs/packages/account/tsconfig.lib.json @@ -10,6 +10,23 @@ "lib": ["dom", "es2020"], "useDefineForClassFields": false }, - "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], + "exclude": [ + "src/test-setup.ts", + "**/*.spec.ts", + "jest.config.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/test-setup.ts" + ], "include": ["**/*.ts"] } diff --git a/npm/ng-packs/packages/account/tsconfig.spec.json b/npm/ng-packs/packages/account/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/account/tsconfig.spec.json +++ b/npm/ng-packs/packages/account/tsconfig.spec.json @@ -2,10 +2,22 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"], - "esModuleInterop": true + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"] }, - "files": ["src/test-setup.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "files": ["src/test-setup.ts"] } diff --git a/npm/ng-packs/packages/account/vitest.config.mts b/npm/ng-packs/packages/account/vitest.config.mts new file mode 100644 index 0000000000..ed2d3c8b2c --- /dev/null +++ b/npm/ng-packs/packages/account/vitest.config.mts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/account', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'account', + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/account', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/components/.eslintrc.json b/npm/ng-packs/packages/components/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/components/.eslintrc.json +++ b/npm/ng-packs/packages/components/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/components/dynamic-form/NESTED-FORMS.md b/npm/ng-packs/packages/components/dynamic-form/NESTED-FORMS.md new file mode 100644 index 0000000000..1e628035bb --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/NESTED-FORMS.md @@ -0,0 +1,338 @@ +# Nested Forms Guide + +## Overview + +Dynamic Form now supports **nested forms** with two new field types: +- **`group`** - Group related fields together +- **`array`** - Dynamic lists with add/remove functionality + +## Quick Start + +### 1. Group Type (Nested Fields) + +Group related fields together with visual hierarchy: + +```typescript +{ + key: 'address', + type: 'group', + label: 'Address Information', + gridSize: 12, + children: [ + { + key: 'street', + type: 'text', + label: 'Street', + gridSize: 8 + }, + { + key: 'city', + type: 'text', + label: 'City', + gridSize: 4 + }, + { + key: 'zipCode', + type: 'text', + label: 'ZIP Code', + gridSize: 6 + } + ] +} +``` + +**Output:** +```json +{ + "address": { + "street": "123 Main St", + "city": "New York", + "zipCode": "10001" + } +} +``` + +### 2. Array Type (Dynamic Lists) + +Create dynamic lists with add/remove buttons: + +```typescript +{ + key: 'phoneNumbers', + type: 'array', + label: 'Phone Numbers', + minItems: 1, + maxItems: 5, + gridSize: 12, + children: [ + { + key: 'type', + type: 'select', + label: 'Type', + gridSize: 4, + options: { + defaultValues: [ + { key: 'mobile', value: 'Mobile' }, + { key: 'home', value: 'Home' }, + { key: 'work', value: 'Work' } + ] + } + }, + { + key: 'number', + type: 'tel', + label: 'Number', + gridSize: 8 + } + ] +} +``` + +**Output:** +```json +{ + "phoneNumbers": [ + { "type": "mobile", "number": "555-1234" }, + { "type": "work", "number": "555-5678" } + ] +} +``` + +## Features + +### Array Features +- ✅ **Add Button** - Adds new item (respects maxItems) +- ✅ **Remove Button** - Removes item (respects minItems) +- ✅ **Item Counter** - Shows current count and limits +- ✅ **Item Labels** - "Phone Number #1", "Phone Number #2" +- ✅ **Min/Max Validation** - Buttons automatically disabled +- ✅ **Empty State** - Shows info message when no items + +### Group Features +- ✅ **Visual Hierarchy** - Border and background styling +- ✅ **Legend Label** - Fieldset with legend for accessibility +- ✅ **Grid Support** - All children support gridSize +- ✅ **Nested Groups** - Groups inside groups supported + +### Recursive Support +- ✅ **Array in Array** - Phone numbers can have sub-arrays +- ✅ **Group in Array** - Work experience can have grouped fields +- ✅ **Array in Group** - Address can have multiple phone numbers +- ✅ **Unlimited Nesting** - No depth limit + +## Advanced Examples + +### Complex Nested Structure + +```typescript +{ + key: 'workExperience', + type: 'array', + label: 'Work Experience', + minItems: 0, + maxItems: 10, + children: [ + { + key: 'company', + type: 'text', + label: 'Company Name', + gridSize: 6, + required: true + }, + { + key: 'position', + type: 'text', + label: 'Position', + gridSize: 6, + required: true + }, + { + key: 'dates', + type: 'group', // Nested group inside array + label: 'Employment Dates', + gridSize: 12, + children: [ + { + key: 'startDate', + type: 'date', + label: 'Start Date', + gridSize: 6 + }, + { + key: 'endDate', + type: 'date', + label: 'End Date', + gridSize: 6 + } + ] + }, + { + key: 'description', + type: 'textarea', + label: 'Description', + gridSize: 12 + } + ] +} +``` + +## API Reference + +### FormFieldConfig (Extended) + +```typescript +interface FormFieldConfig { + // ... existing properties + + // NEW: Nested form properties + children?: FormFieldConfig[]; // Child fields for group/array types + minItems?: number; // Minimum items for array (default: 0) + maxItems?: number; // Maximum items for array (default: unlimited) +} +``` + +### New Components + +#### DynamicFormGroupComponent +```typescript +@Input() groupConfig: FormFieldConfig; +@Input() formGroup: FormGroup; +@Input() visible: boolean = true; +``` + +#### DynamicFormArrayComponent +```typescript +@Input() arrayConfig: FormFieldConfig; +@Input() formGroup: FormGroup; +@Input() visible: boolean = true; + +addItem(): void; // Add new item to array +removeItem(index): void; // Remove item from array +``` + +## Styling + +### Group Styling + +```scss +.form-group-container { + border-left: 3px solid var(--bs-primary); + padding: 1rem; + background-color: var(--bs-light); +} +``` + +### Array Styling + +```scss +.array-item { + border: 1px solid var(--bs-border-color); + padding: 1rem; + margin-bottom: 1rem; + background: white; + + &:hover { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + } +} +``` + +## Accessibility + +All nested forms include: +- ✅ **ARIA roles** (`role="group"`, `role="list"`, `role="listitem"`) +- ✅ **ARIA labels** (`aria-label`, `aria-labelledby`) +- ✅ **Live regions** (`aria-live="polite"` for item count) +- ✅ **Semantic HTML** (`
`, ``) +- ✅ **Keyboard navigation** (Tab, Enter, Space) +- ✅ **Screen reader announcements** + +## Migration Guide + +### From Simple to Nested + +**Before:** +```typescript +{ + key: 'street', + type: 'text', + label: 'Street' +}, +{ + key: 'city', + type: 'text', + label: 'City' +} +``` + +**After:** +```typescript +{ + key: 'address', + type: 'group', + label: 'Address', + children: [ + { key: 'street', type: 'text', label: 'Street' }, + { key: 'city', type: 'text', label: 'City' } + ] +} +``` + +### Data Structure Change + +**Before:** +```json +{ + "street": "123 Main St", + "city": "New York" +} +``` + +**After:** +```json +{ + "address": { + "street": "123 Main St", + "city": "New York" + } +} +``` + +## Best Practices + +1. **Use Groups** for logical field grouping (address, contact info) +2. **Use Arrays** for dynamic lists (phone numbers, work history) +3. **Set minItems/maxItems** to prevent empty or excessive arrays +4. **Use gridSize** for responsive layouts within nested forms +5. **Keep nesting shallow** (max 2-3 levels for UX) +6. **Add validation** to required nested fields +7. **Use meaningful labels** for array items + +## Examples + +See `apps/dev-app/src/app/dynamic-form-page` for complete examples: +- Phone Numbers (simple array) +- Work Experience (complex array) +- Address (group) + +## Troubleshooting + +### Array items not showing +- Check `minItems` - may need to be > 0 +- Verify `children` array is not empty + +### Can't add items +- Check `maxItems` limit +- Verify button is not disabled + +### Form data not nested +- Confirm `type: 'group'` or `type: 'array'` +- Check FormGroup structure in component + +## Performance + +- ✅ **OnPush** change detection +- ✅ **TrackBy** functions for arrays +- ✅ **Lazy rendering** for conditional fields +- ✅ **Minimal re-renders** on add/remove + diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.html new file mode 100644 index 0000000000..2608180d15 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.html @@ -0,0 +1,93 @@ +@if (visible()) { +
+ + +
+ + +
+ + +
+ @for (item of formArray.controls; track trackByIndex($index)) { +
+ + +
+ + {{ arrayConfig().label | abpLocalization }} #{{ $index + 1 }} + + +
+ + +
+ @for (field of sortedChildren; track field.key) { +
+ + + @if (field.type === 'group') { + + } + + + @else if (field.type === 'array') { + + } + + + @else { + + } + +
+ } +
+
+ } @empty { +
+ + {{ '::NoItemsAdded' | abpLocalization }} +
+ } +
+ + + +
+} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.scss new file mode 100644 index 0000000000..71707783e8 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.scss @@ -0,0 +1,75 @@ +.form-array-container { + margin-bottom: 1.5rem; +} + +.array-header { + border-bottom: 2px solid var(--bs-primary, #007bff); + padding-bottom: 0.5rem; +} + +.form-array-label { + font-size: 1.1rem; + font-weight: 600; + color: var(--bs-dark, #212529); + margin-bottom: 0; +} + +.array-items { + margin-top: 1rem; +} + +.array-item { + background-color: var(--bs-white, #fff); + transition: all 0.2s ease; + position: relative; + + &:hover { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + transform: translateY(-1px); + } + + // Nested arrays get lighter background + .array-item { + background-color: var(--bs-light, #f8f9fa); + } +} + +.item-header { + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + padding-bottom: 0.75rem; +} + +.item-title { + color: var(--bs-primary, #007bff); + font-size: 0.95rem; +} + +.array-footer { + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--bs-border-color, #dee2e6); +} + +// Accessibility: Focus styles for buttons +button { + &:focus-visible { + outline: 2px solid var(--bs-primary, #007bff); + outline-offset: 2px; + } +} + +// Animation for add/remove +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.array-item { + animation: slideIn 0.3s ease; +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.ts new file mode 100644 index 0000000000..eb86a9da7f --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/dynamic-form-array.component.ts @@ -0,0 +1,88 @@ +import { + ChangeDetectionStrategy, + Component, + input, + inject, + ChangeDetectorRef, + forwardRef, +} from '@angular/core'; +import { FormGroup, FormArray, FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { FormFieldConfig } from '../dynamic-form.models'; +import { DynamicFormService } from '../dynamic-form.service'; +import { LocalizationPipe } from '@abp/ng.core'; +import { DynamicFormFieldComponent } from '../dynamic-form-field'; +import { DynamicFormGroupComponent } from '../dynamic-form-group'; + +@Component({ + selector: 'abp-dynamic-form-array', + templateUrl: './dynamic-form-array.component.html', + styleUrls: ['./dynamic-form-array.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + ReactiveFormsModule, + LocalizationPipe, + DynamicFormFieldComponent, + DynamicFormGroupComponent, + forwardRef(() => DynamicFormArrayComponent), // Self reference for recursion + ], +}) +export class DynamicFormArrayComponent { + arrayConfig = input.required(); + formGroup = input.required(); + visible = input(true); + + private fb = inject(FormBuilder); + private dynamicFormService = inject(DynamicFormService); + private cdr = inject(ChangeDetectorRef); + + get formArray(): FormArray { + return this.formGroup().get(this.arrayConfig().key) as FormArray; + } + + get sortedChildren(): FormFieldConfig[] { + const children = this.arrayConfig().children || []; + return children.sort((a, b) => (a.order || 0) - (b.order || 0)); + } + + get canAddItem(): boolean { + const maxItems = this.arrayConfig().maxItems; + return maxItems ? this.formArray.length < maxItems : true; + } + + get canRemoveItem(): boolean { + const minItems = this.arrayConfig().minItems || 0; + return this.formArray.length > minItems; + } + + addItem() { + if (!this.canAddItem) return; + + const itemGroup = this.dynamicFormService.createFormGroup( + this.arrayConfig().children || [] + ); + + this.formArray.push(itemGroup); + this.cdr.markForCheck(); + } + + removeItem(index: number) { + if (!this.canRemoveItem) return; + + this.formArray.removeAt(index); + this.cdr.markForCheck(); + } + + getItemFormGroup(index: number): FormGroup { + return this.formArray.at(index) as FormGroup; + } + + getNestedFormGroup(index: number, key: string): FormGroup { + return this.getItemFormGroup(index).get(key) as FormGroup; + } + + trackByIndex(index: number): number { + return index; + } +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/index.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/index.ts new file mode 100644 index 0000000000..2ea9bc1460 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-array/index.ts @@ -0,0 +1 @@ +export * from './dynamic-form-array.component'; diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts index 54600b4e7a..56f32f6e0d 100644 --- a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts @@ -5,12 +5,10 @@ import { ChangeDetectionStrategy, forwardRef, Type, - Injector, effect, DestroyRef, inject, input, - ChangeDetectorRef, } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html index c07e215de5..ed33dca227 100644 --- a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html @@ -1,11 +1,23 @@ @if (visible()) { -
+
+ + + + @if (field().type === 'text') {
- + @if (isInvalid) { } @@ -14,7 +26,15 @@
- @for (option of options$ | async; track option.key) {
} @else if (field().type === 'checkbox') { -
- +
+ @if (isInvalid) { } @@ -38,8 +64,18 @@
- + @if (isInvalid) { } @@ -48,23 +84,286 @@
- @if (isInvalid) { }
+ } @else if (field().type === 'number') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'date') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'datetime-local') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'time') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'password') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'tel') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'url') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'radio') { + +
+ +
+ @for (option of options$ | async; track option.key) { +
+ + +
+ } +
+ @if (isInvalid) { + + } +
+ } @else if (field().type === 'file') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'range') { + +
+ +
+ + {{ value.value }} +
+ @if (isInvalid) { + + } +
+ } @else if (field().type === 'color') { + +
+ +
+ + {{ value.value || '#000000' }} +
+ @if (isInvalid) { + + } +
}
} - + -
+