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/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index f916789293..559bdb637e 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -1,13 +1,13 @@ -name: Merge branch dev with rel-10.0 +name: Merge branch dev with rel-10.1 on: push: branches: - - rel-10.0 + - rel-10.1 permissions: contents: read jobs: - merge-dev-with-rel-10-0: + merge-dev-with-rel-10-1: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR @@ -18,15 +18,14 @@ jobs: ref: dev - name: Reset promotion branch run: | - git fetch origin rel-10.0:rel-10.0 - git reset --hard rel-10.0 + git fetch origin rel-10.1:rel-10.1 + git reset --hard rel-10.1 - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: - branch: auto-merge/rel-10-0/${{github.run_number}} - title: Merge branch dev with rel-10.0 - body: This PR generated automatically to merge dev with rel-10.0. Please review the changed files before merging to prevent any errors that may occur. - reviewers: maliming + branch: auto-merge/rel-10-1/${{github.run_number}} + title: Merge branch dev with rel-10.1 + body: This PR generated automatically to merge dev with rel-10.1. Please review the changed files before merging to prevent any errors that may occur. draft: true token: ${{ github.token }} - name: Merge Pull Request @@ -34,5 +33,5 @@ jobs: GH_TOKEN: ${{ secrets.BOT_SECRET }} run: | gh pr ready - gh pr review auto-merge/rel-10-0/${{github.run_number}} --approve - gh pr merge auto-merge/rel-10-0/${{github.run_number}} --merge --auto --delete-branch + gh pr review auto-merge/rel-10-1/${{github.run_number}} --approve + gh pr merge auto-merge/rel-10-1/${{github.run_number}} --merge --auto --delete-branch 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 820f8a2b33..bd5a55694f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ - @@ -31,8 +30,9 @@ + - + @@ -57,63 +57,63 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -141,7 +141,7 @@ - + @@ -169,20 +169,20 @@ - + - - + + - - - + + + - + @@ -194,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..8c5050d4bc --- /dev/null +++ b/ai-rules/common/application-layer.mdc @@ -0,0 +1,236 @@ +--- +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..300cda28ee --- /dev/null +++ b/ai-rules/common/authorization.mdc @@ -0,0 +1,186 @@ +--- +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..4968e2ce9e --- /dev/null +++ b/ai-rules/common/cli-commands.mdc @@ -0,0 +1,92 @@ +--- +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..0e6220a4b6 --- /dev/null +++ b/ai-rules/common/ddd-patterns.mdc @@ -0,0 +1,244 @@ +--- +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..32b95d10d4 --- /dev/null +++ b/ai-rules/common/dependency-rules.mdc @@ -0,0 +1,153 @@ +--- +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..692d0e72a6 --- /dev/null +++ b/ai-rules/common/development-flow.mdc @@ -0,0 +1,299 @@ +--- +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..81d3cb7a20 --- /dev/null +++ b/ai-rules/common/infrastructure.mdc @@ -0,0 +1,249 @@ +--- +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..2bf1e5fd32 --- /dev/null +++ b/ai-rules/common/multi-tenancy.mdc @@ -0,0 +1,165 @@ +--- +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..84d71596f6 --- /dev/null +++ b/ai-rules/data/ef-core.mdc @@ -0,0 +1,257 @@ +--- +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..10526a41ba --- /dev/null +++ b/ai-rules/data/mongodb.mdc @@ -0,0 +1,206 @@ +--- +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..5bcc3d39cf --- /dev/null +++ b/ai-rules/template-specific/app-nolayers.mdc @@ -0,0 +1,83 @@ +--- +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..a1c49a320a --- /dev/null +++ b/ai-rules/testing/patterns.mdc @@ -0,0 +1,274 @@ +--- +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..eabbfce512 --- /dev/null +++ b/ai-rules/ui/angular.mdc @@ -0,0 +1,224 @@ +--- +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..d339744d65 --- /dev/null +++ b/ai-rules/ui/blazor.mdc @@ -0,0 +1,210 @@ +--- +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..80525fab17 --- /dev/null +++ b/ai-rules/ui/mvc.mdc @@ -0,0 +1,262 @@ +--- +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/deployment/configuring-production.md b/docs/en/deployment/configuring-production.md index 557b671614..8b3430046d 100644 --- a/docs/en/deployment/configuring-production.md +++ b/docs/en/deployment/configuring-production.md @@ -113,6 +113,6 @@ ABP uses .NET's standard [Logging services](../framework/fundamentals/logging.md ABP's startup solution templates come with [Swagger UI](https://swagger.io/) pre-installed. Swagger is a pretty standard and useful tool to discover and test your HTTP APIs on a built-in UI that is embedded into your application or service. It is typically used in development environment, but you may want to enable it on staging or production environments too. -While you will always secure your HTTP APIs with other techniques (like the [Authorization](../framework/fundamentals/authorization.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment. +While you will always secure your HTTP APIs with other techniques (like the [Authorization](../framework/fundamentals/authorization/index.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment. > You may also want to see the [ABP Swagger integration](../framework/api-development/swagger.md) document. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 1cce28fa3f..fb94db85b2 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -333,7 +333,7 @@ "path": "studio/solution-explorer.md" }, { - "text": "Running Applications", + "text": "Solution Runner", "path": "studio/running-applications.md" }, { @@ -347,6 +347,10 @@ { "text": "Working with ABP Suite", "path": "studio/working-with-suite.md" + }, + { + "text": "Custom Commands", + "path": "studio/custom-commands.md" } ] }, @@ -458,12 +462,16 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/authorization.md", + "path": "framework/fundamentals/authorization/index.md", "isIndex": true }, { "text": "Dynamic Claims", "path": "framework/fundamentals/dynamic-claims.md" + }, + { + "text": "Resource Based Authorization", + "path": "framework/fundamentals/authorization/resource-based-authorization.md" } ] }, @@ -1284,7 +1292,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/api-development/standard-apis/configuration.md b/docs/en/framework/api-development/standard-apis/configuration.md index 3fcd22f6d9..53a666546c 100644 --- a/docs/en/framework/api-development/standard-apis/configuration.md +++ b/docs/en/framework/api-development/standard-apis/configuration.md @@ -9,7 +9,7 @@ ABP provides a pre-built and standard endpoint that contains some useful information about the application/service. Here, is the list of some fundamental information at this endpoint: -* Granted [policies](../../fundamentals/authorization.md) (permissions) for the current user. +* Granted [policies](../../fundamentals/authorization/index.md) (permissions) for the current user. * [Setting](../../infrastructure/settings.md) values for the current user. * Info about the [current user](../../infrastructure/current-user.md) (like id and user name). * Info about the current [tenant](../../architecture/multi-tenancy) (like id and name). diff --git a/docs/en/framework/architecture/domain-driven-design/application-services.md b/docs/en/framework/architecture/domain-driven-design/application-services.md index a0d0c2d5fc..8683241534 100644 --- a/docs/en/framework/architecture/domain-driven-design/application-services.md +++ b/docs/en/framework/architecture/domain-driven-design/application-services.md @@ -218,7 +218,7 @@ See the [validation document](../../fundamentals/validation.md) for more. It's possible to use declarative and imperative authorization for application service methods. -See the [authorization document](../../fundamentals/authorization.md) for more. +See the [authorization document](../../fundamentals/authorization/index.md) for more. ## CRUD Application Services diff --git a/docs/en/framework/architecture/domain-driven-design/entities.md b/docs/en/framework/architecture/domain-driven-design/entities.md index df727a61ba..c0b0869461 100644 --- a/docs/en/framework/architecture/domain-driven-design/entities.md +++ b/docs/en/framework/architecture/domain-driven-design/entities.md @@ -135,6 +135,29 @@ if (book1.EntityEquals(book2)) //Check equality } ``` +### `IKeyedObject` Interface + +ABP entities implement the `IKeyedObject` interface, which provides a way to get the entity's primary key as a string: + +```csharp +public interface IKeyedObject +{ + string? GetObjectKey(); +} +``` + +The `GetObjectKey()` method returns a string representation of the entity's primary key. For entities with a single key (like `Entity` or `Entity`), it returns the `Id` property converted to a string. For entities with composite keys, it returns the keys combined with a comma separator. + +This interface is particularly useful for scenarios where you need to identify an entity by its key in a type-agnostic way, such as: + +* **Resource-based authorization**: When checking or granting permissions for specific entity instances +* **Caching**: When creating cache keys based on entity identifiers +* **Logging and auditing**: When recording entity identifiers in a consistent format + +Since all ABP entities implement this interface through the `IEntity` interface, you can use `GetObjectKey()` on any entity without additional implementation. + +> See the [Resource-Based Authorization](../../fundamentals/authorization/resource-based-authorization.md) documentation for a practical example of using `IKeyedObject` with the permission system. + ## AggregateRoot Class "*Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it's useful to treat the order (together with its line items) as a single aggregate.*" (see the [full description](http://martinfowler.com/bliki/DDD_Aggregate.html)) 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/architecture/modularity/extending/customizing-application-modules-guide.md b/docs/en/framework/architecture/modularity/extending/customizing-application-modules-guide.md index 1d6c5ffe99..77f4a70d90 100644 --- a/docs/en/framework/architecture/modularity/extending/customizing-application-modules-guide.md +++ b/docs/en/framework/architecture/modularity/extending/customizing-application-modules-guide.md @@ -112,4 +112,4 @@ Also, see the following documents: * See [the localization document](../../../fundamentals/localization.md) to learn how to extend existing localization resources. * See [the settings document](../../../infrastructure/settings.md) to learn how to change setting definitions of a depended module. -* See [the authorization document](../../../fundamentals/authorization.md) to learn how to change permission definitions of a depended module. +* See [the authorization document](../../../fundamentals/authorization/index.md) to learn how to change permission definitions of a depended module. diff --git a/docs/en/framework/fundamentals/authorization.md b/docs/en/framework/fundamentals/authorization/index.md similarity index 74% rename from docs/en/framework/fundamentals/authorization.md rename to docs/en/framework/fundamentals/authorization/index.md index 1a104ef420..6cbd1a7a6a 100644 --- a/docs/en/framework/fundamentals/authorization.md +++ b/docs/en/framework/fundamentals/authorization/index.md @@ -9,13 +9,15 @@ Authorization is used to check if a user is allowed to perform some specific operations in the application. -ABP extends [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing authorization system to be usable in the **[application services](../architecture/domain-driven-design/application-services.md)** too. +ABP extends [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing authorization system to be usable in the **[application services](../../architecture/domain-driven-design/application-services.md)** too. So, all the ASP.NET Core authorization features and the documentation are valid in an ABP based application. This document focuses on the features that are added on top of ASP.NET Core authorization features. +ABP supports two types of permissions: **Standard permissions** apply globally (e.g., "can create documents"), while **resource-based permissions** target specific instances (e.g., "can edit Document #123"). This document covers standard permissions; see [Resource-Based Authorization](./resource-based-authorization.md) for fine-grained, per-resource access control. + ## Authorize Attribute -ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/simple) attribute that can be used for an action, a controller or a page. ABP allows you to use the same attribute for an [application service](../architecture/domain-driven-design/application-services.md) too. +ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/simple) attribute that can be used for an action, a controller or a page. ABP allows you to use the same attribute for an [application service](../../architecture/domain-driven-design/application-services.md) too. Example: @@ -87,9 +89,11 @@ namespace Acme.BookStore.Permissions > ABP automatically discovers this class. No additional configuration required! -> You typically define this class inside the `Application.Contracts` project of your [application](../../solution-templates/layered-web-application). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with. +> You typically define this class inside the `Application.Contracts` project of your [application](../../../solution-templates/layered-web-application/index.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with. + +In the `Define` method, you first need to add a **permission group** (or get an existing group), then add **permissions** to this group using the `AddPermission` method. -In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group. +> For resource-specific fine-grained permissions, use the `AddResourcePermission` method instead. See [Resource-Based Authorization](./resource-based-authorization.md) for details. When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role: @@ -100,6 +104,8 @@ When you define a permission, it becomes usable in the ASP.NET Core authorizatio When you save the dialog, it is saved to the database and used in the authorization system. +> **Note:** Only standard (global) permissions are shown in this dialog. Resource-based permissions are managed through the [Resource Permission Management Dialog](../../../modules/permission-management.md#resource-permission-management-dialog) on individual resource instances. + > The screen above is available when you have installed the identity module, which is basically used for user and role management. Startup templates come with the identity module pre-installed. #### Localizing the Permission Name @@ -125,15 +131,15 @@ Then you can define texts for "BookStore" and "Permission:BookStore_Author_Creat "Permission:BookStore_Author_Create": "Creating a new author" ``` -> For more information, see the [localization document](./localization.md) on the localization system. +> For more information, see the [localization document](../localization.md) on the localization system. The localized UI will be as seen below: -![authorization-new-permission-ui-localized](../../images/authorization-new-permission-ui-localized.png) +![authorization-new-permission-ui-localized](../../../images/authorization-new-permission-ui-localized.png) #### Multi-Tenancy -ABP supports [multi-tenancy](../architecture/multi-tenancy) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below: +ABP supports [multi-tenancy](../../architecture/multi-tenancy/index.md) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below: - **Host**: The permission is available only for the host side. - **Tenant**: The permission is available only for the tenant side. @@ -180,7 +186,7 @@ authorManagement.AddChild("Author_Management_Delete_Books"); The result on the UI is shown below (you probably want to localize permissions for your application): -![authorization-new-permission-ui-hierarcy](../../images/authorization-new-permission-ui-hierarcy.png) +![authorization-new-permission-ui-hierarcy](../../../images/authorization-new-permission-ui-hierarcy.png) For the example code, it is assumed that a role/user with "Author_Management" permission granted may have additional permissions. Then a typical application service that checks permissions can be defined as shown below: @@ -229,7 +235,7 @@ See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/se ### Changing Permission Definitions of a Depended Module -A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](../architecture/modularity/basics.md)) and change their definitions. +A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](../../architecture/modularity/basics.md)) and change their definitions. Example: @@ -247,12 +253,12 @@ When you write this code inside your permission definition provider, it finds th You may want to disable a permission based on a condition. Disabled permissions are not visible on the UI and always returns `prohibited` when you check them. There are two built-in conditional dependencies for a permission definition; -* A permission can be automatically disabled if a [Feature](../infrastructure/features.md) was disabled. -* A permission can be automatically disabled if a [Global Feature](../infrastructure/global-features.md) was disabled. +* A permission can be automatically disabled if a [Feature](../../infrastructure/features.md) was disabled. +* A permission can be automatically disabled if a [Global Feature](../../infrastructure/global-features.md) was disabled. In addition, you can create your custom extensions. -#### Depending on a Features +#### Depending on Features Use the `RequireFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled: @@ -261,7 +267,7 @@ myGroup.AddPermission("Book_Creation") .RequireFeatures("BookManagement"); ```` -#### Depending on a Global Feature +#### Depending on Global Features Use the `RequireGlobalFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled: @@ -272,13 +278,13 @@ myGroup.AddPermission("Book_Creation") #### Creating a Custom Permission Dependency -`PermissionDefinition` supports state check, Please refer to [Simple State Checker's documentation](../infrastructure/simple-state-checker.md) +`PermissionDefinition` supports state check, please refer to [Simple State Checker's documentation](../../infrastructure/simple-state-checker.md) ## IAuthorizationService -ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization. +ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject it, you can use it in your code to conditionally control the authorization. -Example: +**Example:** ```csharp public async Task CreateAsync(CreateAuthorDto input) @@ -295,7 +301,7 @@ public async Task CreateAsync(CreateAuthorDto input) } ``` -> `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](./dependency-injection.md) it into your class. +> `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](../dependency-injection.md) it into your class. Since this is a typical code block, ABP provides extension methods to simplify it. @@ -320,15 +326,15 @@ public async Task CreateAsync(CreateAuthorDto input) See the following documents to learn how to re-use the authorization system on the client side: -* [ASP.NET Core MVC / Razor Pages UI: Authorization](../ui/mvc-razor-pages/javascript-api/auth.md) -* [Angular UI Authorization](../ui/angular/authorization.md) -* [Blazor UI Authorization](../ui/blazor/authorization.md) +* [ASP.NET Core MVC / Razor Pages UI: Authorization](../../ui/mvc-razor-pages/javascript-api/auth.md) +* [Angular UI Authorization](../../ui/angular/authorization.md) +* [Blazor UI Authorization](../../ui/blazor/authorization.md) ## Permission Management Permission management is normally done by an admin user using the permission management modal: -![authorization-new-permission-ui-localized](../../images/authorization-new-permission-ui-localized.png) +![authorization-new-permission-ui-localized](../../../images/authorization-new-permission-ui-localized.png) If you need to manage permissions by code, inject the `IPermissionManager` and use as shown below: @@ -356,13 +362,13 @@ public class MyService : ITransientDependency `SetForUserAsync` sets the value (true/false) for a permission of a user. There are more extension methods like `SetForRoleAsync` and `SetForClientAsync`. -`IPermissionManager` is defined by the permission management module. See the [permission management module documentation](../../modules/permission-management.md) for more information. +`IPermissionManager` is defined by the Permission Management module. For resource-based permissions, use `IResourcePermissionManager` instead. See the [Permission Management Module documentation](../../../modules/permission-management.md) for more information. ## Advanced Topics ### Permission Value Providers -Permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers: +The permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers: - `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property. - `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property. @@ -412,15 +418,35 @@ Configure(options => }); ``` +### Resource Permission Value Providers + +Similar to standard permission value providers, you can extend the resource permission checking system by creating custom **resource permission value providers**. ABP provides two built-in resource permission value providers: + +* `UserResourcePermissionValueProvider`: Checks permissions granted directly to users for a specific resource. +* `RoleResourcePermissionValueProvider`: Checks permissions granted to roles for a specific resource. + +You can create custom providers by implementing `IResourcePermissionValueProvider` or inheriting from `ResourcePermissionValueProvider`. Register them using: + +```csharp +Configure(options => +{ + options.ResourceValueProviders.Add(); +}); +``` + +> See the [Permission Management Module](../../../modules/permission-management.md#resource-permission-value-providers) documentation for detailed examples. + ### Permission Store -`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](../../modules/permission-management.md) for more information +`IPermissionStore` is the interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and is pre-installed in the application startup template. See the [Permission Management Module documentation](../../../modules/permission-management.md) for more information. + +For resource-based permissions, `IResourcePermissionStore` serves the same purpose, storing and retrieving permissions for specific resource instances. ### AlwaysAllowAuthorizationService `AlwaysAllowAuthorizationService` is a class that is used to bypass the authorization service. It is generally used in integration tests where you may want to disable the authorization system. -Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](./dependency-injection.md) system: +Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](../../dependency-injection.md) system: ```csharp public override void ConfigureServices(ServiceConfigurationContext context) @@ -466,11 +492,24 @@ public static class CurrentUserExtensions } ``` -> If you use OpenIddict please see [Updating Claims in Access Token and ID Token](../../modules/openiddict#updating-claims-in-access_token-and-id_token). +> If you use OpenIddict please see [Updating Claims in Access Token and ID Token](../../../modules/openiddict#updating-claims-in-access_token-and-id_token). + +## Resource-Based Authorization + +While this document covers standard (global) permissions, ABP also supports **resource-based authorization** for fine-grained access control on specific resource instances. Resource-based authorization allows you to grant permissions for a specific document, project, or any other entity rather than granting a permission for all resources of that type. + +**Example scenarios:** + +* Allow users to edit **only their own** blog posts or documents +* Grant access to **specific projects** based on team membership +* Implement document sharing where **different users have different access levels** to the same document + +> See the [Resource-Based Authorization](./resource-based-authorization.md) document for implementation details. ## See Also -* [Permission Management Module](../../modules/permission-management.md) -* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](../ui/mvc-razor-pages/javascript-api/auth.md) -* [Permission Management in Angular UI](../ui/angular/Permission-Management.md) +* [Resource-Based Authorization](./resource-based-authorization.md) +* [Permission Management Module](../../../modules/permission-management.md) +* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](../../ui/mvc-razor-pages/javascript-api/auth.md) +* [Permission Management in Angular UI](../../ui/angular/Permission-Management.md) * [Video tutorial](https://abp.io/video-courses/essentials/authorization) \ No newline at end of file diff --git a/docs/en/framework/fundamentals/authorization/resource-based-authorization.md b/docs/en/framework/fundamentals/authorization/resource-based-authorization.md new file mode 100644 index 0000000000..310f2d1b65 --- /dev/null +++ b/docs/en/framework/fundamentals/authorization/resource-based-authorization.md @@ -0,0 +1,241 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to implement resource-based authorization in ABP Framework for fine-grained access control on specific resource instances like documents, projects, or any entity." +} +``` + +# Resource-Based Authorization + +**Resource-Based Authorization** is a powerful feature that enables fine-grained access control based on specific resource instances. While the standard [authorization system](./index.md) grants permissions at a general level (e.g., "can edit documents"), resource-based authorization allows you to grant permissions for a **specific** document, project, or any other entity rather than granting a permission for all of them. + +## When to Use Resource-Based Authorization? + +Consider resource-based authorization when you need to: + +* Allow users to edit **only their own blog posts or documents** +* Grant access to **specific projects** based on team membership +* Implement document sharing **where different users have different access levels to the same document** +* Control access to resources based on ownership or custom sharing rules + +**Example Scenarios:** + +Imagine a document management system where: + +- User A can view and edit Document 1 +- User B can only view Document 1 +- User A has no access to Document 2 +- User C can manage permissions for Document 2 + +This level of granular control is what resource-based authorization provides. + +## Usage + +Implementing resource-based authorization involves three main steps: + +1. **Define** resource permissions in your `PermissionDefinitionProvider` +2. **Check** permissions using `IResourcePermissionChecker` +3. **Manage** permissions via UI or using `IResourcePermissionManager` for programmatic usages + +### Defining Resource Permissions + +Define resource permissions in your `PermissionDefinitionProvider` class using the `AddResourcePermission` method: + +```csharp +namespace Acme.BookStore.Permissions; + +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string ManagePermissions = Default + ".ManagePermissions"; + + public static class Resources + { + public const string Name = "Acme.BookStore.Books.Book"; + public const string View = Name + ".View"; + public const string Edit = Name + ".Edit"; + public const string Delete = Name + ".Delete"; + } + } +} +``` + +```csharp +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace Acme.BookStore.Permissions +{ + public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider + { + public override void Define(IPermissionDefinitionContext context) + { + var myGroup = context.AddGroup("BookStore"); + + // Standard permissions + myGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); + + // Permission to manage resource permissions (required) + myGroup.AddPermission(BookStorePermissions.Books.ManagePermissions, L("Permission:Books:ManagePermissions")); + + // Resource-based permissions + context.AddResourcePermission( + name: BookStorePermissions.Books.Resources.View, + resourceName: BookStorePermissions.Books.Resources.Name, + managementPermissionName: BookStorePermissions.Books.ManagePermissions, + displayName: L("Permission:Books:View") + ); + + context.AddResourcePermission( + name: BookStorePermissions.Books.Resources.Edit, + resourceName: BookStorePermissions.Books.Resources.Name, + managementPermissionName: BookStorePermissions.Books.ManagePermissions, + displayName: L("Permission:Books:Edit") + ); + + context.AddResourcePermission( + name: BookStorePermissions.Books.Resources.Delete, + resourceName: BookStorePermissions.Books.Resources.Name, + managementPermissionName: BookStorePermissions.Books.ManagePermissions, + displayName: L("Permission:Books:Delete"), + multiTenancySide: MultiTenancySides.Host + ); + } + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} +``` + +The `AddResourcePermission` method requires the following parameters: + +* `name`: A unique name for the resource permission. +* `resourceName`: An identifier for the resource type. This is typically the full name of the entity class (e.g., `Acme.BookStore.Books.Book`). +* `managementPermissionName`: A standard permission that controls who can manage resource permissions. Users with this permission can grant/revoke resource permissions for specific resources. +* `displayName`: (Optional) A localized display name shown in the UI. +* `multiTenancySide`: (Optional) Specifies on which side of a multi-tenant application this permission can be used. Accepts `MultiTenancySides.Host` (only for the host side), `MultiTenancySides.Tenant` (only for tenants), or `MultiTenancySides.Both` (default, available on both sides). + +### Checking Resource Permissions + +Use the `IAuthorizationService` service to check if a user/role/client has a specific permission for a resource: + +```csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Authorization.Permissions.Resources; + +namespace Acme.BookStore.Books +{ + public class BookAppService : ApplicationService, IBookAppService + { + private readonly IBookRepository _bookRepository; + + public BookAppService(IBookRepository bookRepository) + { + _bookRepository = bookRepository; + } + + public virtual async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + + // Check if the current user can view this specific book + var isGranted = await AuthorizationService.IsGrantedAsync(book, BookStorePermissions.Books.Resources.View); // AuthorizationService is a property of the ApplicationService class and will be automatically injected. + if (!isGranted) + { + throw new AbpAuthorizationException("You don't have permission to view this book."); + } + + return ObjectMapper.Map(book); + } + + public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await _bookRepository.GetAsync(id); + + // Check if the current user can edit this specific book + var isGranted = await AuthorizationService.IsGrantedAsync(book, BookStorePermissions.Books.Resources.Edit); // AuthorizationService is a property of the ApplicationService class and will be automatically injected. + if (!isGranted) + { + throw new AbpAuthorizationException("You don't have permission to edit this book."); + } + + book.Title = input.Title; + book.Content = input.Content; + await _bookRepository.UpdateAsync(book); + } + } +} +``` + +In this example, the `BookAppService` uses `IAuthorizationService` to check if the current user has the required permission for a specific book before performing the operation. The method takes the `Book` entity object and resource permission name as parameters. + +#### IKeyedObject + +The `IAuthorizationService` internally uses `IResourcePermissionChecker` to check resource permissions, and gets the resource key by calling the `GetObjectKey()` method of the `IKeyedObject` interface. All ABP entities implement the `IKeyedObject` interface, so you can directly pass entity objects to the `IsGrantedAsync` method. + +> See the [Entities documentation](../../architecture/domain-driven-design/entities.md) for more information about the `IKeyedObject` interface. + +#### IResourcePermissionChecker + +You can also directly use the `IResourcePermissionChecker` service to check resource permissions which provides more advanced features, such as checking multiple permissions at once: + +> You have to pass the resource key (obtained via `GetObjectKey()`) explicitly when using `IResourcePermissionChecker`. + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IBookRepository _bookRepository; + private readonly IResourcePermissionChecker _resourcePermissionChecker; + + public BookAppService(IBookRepository bookRepository, IResourcePermissionChecker resourcePermissionChecker) + { + _bookRepository = bookRepository; + _resourcePermissionChecker = resourcePermissionChecker; + } + + public async Task GetPermissionsAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + + var result = await _resourcePermissionChecker.IsGrantedAsync(new[] + { + BookStorePermissions.Books.Resources.View, + BookStorePermissions.Books.Resources.Edit, + BookStorePermissions.Books.Resources.Delete + }, + BookStorePermissions.Books.Resources.Name, + book.GetObjectKey()!); + + return new BookPermissionsDto + { + CanView = result.Result[BookStorePermissions.Books.Resources.View] == PermissionGrantResult.Granted, + CanEdit = result.Result[BookStorePermissions.Books.Resources.Edit] == PermissionGrantResult.Granted, + CanDelete = result.Result[BookStorePermissions.Books.Resources.Delete] == PermissionGrantResult.Granted + }; + } +} +``` + +### Managing Resource Permissions + +Once you have defined resource permissions, you need a way to grant or revoke them for specific users, roles, or clients. The [Permission Management Module](../../../modules/permission-management.md) provides the infrastructure for managing resource permissions: + +- **UI Components**: Built-in modal dialogs for managing resource permissions on all supported UI frameworks (MVC/Razor Pages, Blazor, and Angular). These components allow administrators to grant or revoke permissions for users and roles on specific resource instances through a user-friendly interface. +- **`IResourcePermissionManager` Service**: A service for programmatically granting, revoking, and querying resource permissions at runtime. This is useful for scenarios like automatically granting permissions when a resource is created, implementing sharing functionality, or integrating with external systems. + +> See the [Permission Management Module](../../../modules/permission-management.md#resource-permission-management-dialog) documentation for detailed information on using the UI components and the `IResourcePermissionManager` service. + +## See Also + +* [Authorization](./index.md) +* [Permission Management Module](../../../modules/permission-management.md) +* [Entities](../../architecture/domain-driven-design/entities.md) diff --git a/docs/en/framework/fundamentals/dynamic-claims.md b/docs/en/framework/fundamentals/dynamic-claims.md index 03ddc701cd..24d95755c5 100644 --- a/docs/en/framework/fundamentals/dynamic-claims.md +++ b/docs/en/framework/fundamentals/dynamic-claims.md @@ -94,6 +94,6 @@ If you want to add your own dynamic claims contributor, you can create a class t ## See Also -* [Authorization](./authorization.md) +* [Authorization](./authorization/index.md) * [Claims-based authorization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims) * [Mapping, customizing, and transforming claims in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims) diff --git a/docs/en/framework/fundamentals/exception-handling.md b/docs/en/framework/fundamentals/exception-handling.md index 85b1c3b9c4..5ccfcf124a 100644 --- a/docs/en/framework/fundamentals/exception-handling.md +++ b/docs/en/framework/fundamentals/exception-handling.md @@ -322,7 +322,7 @@ The `context` object contains necessary information about the exception occurred Some exception types are automatically thrown by the framework: -- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](./authorization.md) for more. +- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](./authorization/index.md) for more. - `AbpValidationException` is thrown if the input of the current request is not valid. See [validation](./validation.md) for more. - `EntityNotFoundException` is thrown if the requested entity is not available. This is mostly thrown by [repositories](../architecture/domain-driven-design/repositories.md). diff --git a/docs/en/framework/fundamentals/index.md b/docs/en/framework/fundamentals/index.md index 6aef484c41..7ffa99fd89 100644 --- a/docs/en/framework/fundamentals/index.md +++ b/docs/en/framework/fundamentals/index.md @@ -10,7 +10,7 @@ The following documents explains the fundamental building blocks to create ABP solutions: * [Application Startup](./application-startup.md) -* [Authorization](./authorization.md) +* [Authorization](./authorization/index.md) * [Caching](./caching.md) * [Configuration](./configuration.md) * [Connection Strings](./connection-strings.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/infrastructure/background-jobs/hangfire.md b/docs/en/framework/infrastructure/background-jobs/hangfire.md index 61408f303d..05cd214016 100644 --- a/docs/en/framework/infrastructure/background-jobs/hangfire.md +++ b/docs/en/framework/infrastructure/background-jobs/hangfire.md @@ -149,7 +149,7 @@ namespace MyProject Hangfire Dashboard provides information about your background jobs, including method names and serialized arguments as well as gives you an opportunity to manage them by performing different actions – retry, delete, trigger, etc. So it is important to restrict access to the Dashboard. To make it secure by default, only local requests are allowed, however you can change this by following the [official documentation](http://docs.hangfire.io/en/latest/configuration/using-dashboard.html) of Hangfire. -You can integrate the Hangfire dashboard to [ABP authorization system](../../fundamentals/authorization.md) using the **AbpHangfireAuthorizationFilter** +You can integrate the Hangfire dashboard to [ABP authorization system](../../fundamentals/authorization/index.md) using the **AbpHangfireAuthorizationFilter** class. This class is defined in the `Volo.Abp.Hangfire` package. The following example, checks if the current user is logged in to the application: ```csharp diff --git a/docs/en/framework/infrastructure/interceptors.md b/docs/en/framework/infrastructure/interceptors.md index d50de3ad84..9005c3d612 100644 --- a/docs/en/framework/infrastructure/interceptors.md +++ b/docs/en/framework/infrastructure/interceptors.md @@ -42,7 +42,7 @@ Automatically begins and commits/rolls back a database transaction when entering Input DTOs are automatically validated against data annotation attributes and custom validation rules before executing the service logic, providing consistent validation behavior across all services. -### [Authorization](../fundamentals/authorization.md) +### [Authorization](../fundamentals/authorization/index.md) Checks user permissions before allowing the execution of application service methods, ensuring security policies are enforced consistently. diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 4a54500814..39218e7c86 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -313,6 +313,34 @@ It is suggested to use the `MapExtraPropertiesAttribute` attribute if both class Mapperly requires that properties of both source and destination objects have `setter` methods. Otherwise, the property will be ignored. You can use `protected set` or `private set` to control the visibility of the `setter` method, but each property must have a `setter` method. +### Nullable Reference Types + +Mapperly respects C# nullable reference types (NRT). If your project enables NRT via `enable` in the project file, Mapperly will treat reference type properties as **non-nullable by default**. + +That means: + +- If a property can be `null`, declare it as nullable so Mapperly (and the compiler) understands it can be missing. +- If you declare a property as non-nullable, Mapperly assumes it is not `null`. + +Otherwise, the generated mapping code may throw runtime exceptions (e.g., `NullReferenceException`) if a value is actually `null` during the mapping process. + +Example: + +````xml + + + enable + +```` + +````csharp +public class PersonDto +{ + public Country? Country { get; set; } // Nullable (can be null) + public City City { get; set; } = default!; // Non-nullable (cannot be null) +} +```` + ### Deep Cloning By default, Mapperly does not create deep copies of objects to improve performance. If an object can be directly assigned to the target, it will do so (e.g., if the source and target type are both `List`, the list and its entries will not be cloned). To create deep copies, set the `UseDeepCloning` property on the `MapperAttribute` to `true`. @@ -505,6 +533,7 @@ Each solution has its own advantages: Choose the approach that best aligns with your application's architecture and maintainability requirements. + ### More Mapperly Features Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. 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/permission-management.md b/docs/en/framework/ui/angular/permission-management.md index abba8843de..8d30d925bb 100644 --- a/docs/en/framework/ui/angular/permission-management.md +++ b/docs/en/framework/ui/angular/permission-management.md @@ -7,7 +7,7 @@ # Permission Management -A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../fundamentals/authorization.md) document. +A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../fundamentals/authorization/index.md) document. You can get permission of authenticated user using `getGrantedPolicy` or `getGrantedPolicy$` method of `PermissionService`. 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/blazor/authorization.md b/docs/en/framework/ui/blazor/authorization.md index fc006136b4..453f37963b 100644 --- a/docs/en/framework/ui/blazor/authorization.md +++ b/docs/en/framework/ui/blazor/authorization.md @@ -9,7 +9,7 @@ Blazor applications can use the same authorization system and permissions defined in the server side. -> This document is only for authorizing on the Blazor UI. See the [Server Side Authorization](../../fundamentals/authorization.md) to learn how to define permissions and control the authorization system. +> This document is only for authorizing on the Blazor UI. See the [Server Side Authorization](../../fundamentals/authorization/index.md) to learn how to define permissions and control the authorization system. ## Basic Usage @@ -76,7 +76,7 @@ There are some useful extension methods for the `IAuthorizationService`: ## See Also -* [Authorization](../../fundamentals/authorization.md) (server side) +* [Authorization](../../fundamentals/authorization/index.md) (server side) * [Blazor Security](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) (Microsoft documentation) * [ICurrentUser Service](../../infrastructure/current-user.md) * [Video tutorial](https://abp.io/video-courses/essentials/authorization) diff --git a/docs/en/framework/ui/blazor/page-toolbar-extensions.md b/docs/en/framework/ui/blazor/page-toolbar-extensions.md index 7e341c50fe..9c96bf90f7 100644 --- a/docs/en/framework/ui/blazor/page-toolbar-extensions.md +++ b/docs/en/framework/ui/blazor/page-toolbar-extensions.md @@ -102,7 +102,7 @@ protected override async ValueTask SetToolbarItemsAsync() #### Permissions -If your button/component should be available based on a [permission/policy](../../fundamentals/authorization.md), you can pass the permission/policy name as the `RequiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. +If your button/component should be available based on a [permission/policy](../../fundamentals/authorization/index.md), you can pass the permission/policy name as the `RequiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. ### Add a Page Toolbar Contributor diff --git a/docs/en/framework/ui/mvc-razor-pages/auto-complete-select.md b/docs/en/framework/ui/mvc-razor-pages/auto-complete-select.md index 69f92be3f2..7157c3ab95 100644 --- a/docs/en/framework/ui/mvc-razor-pages/auto-complete-select.md +++ b/docs/en/framework/ui/mvc-razor-pages/auto-complete-select.md @@ -78,4 +78,4 @@ It'll be automatically bound to a collection of defined value type. ## Notices If the authenticated user doesn't have permission on the given URL, the user will get an authorization error. Be careful while designing this kind of UIs. -You can create a specific, [unauthorized](../../fundamentals/authorization.md) endpoint/method to get the list of items, so the page can retrieve lookup data of dependent entity without giving the entire read permission to users. \ No newline at end of file +You can create a specific, [unauthorized](../../fundamentals/authorization/index.md) endpoint/method to get the list of items, so the page can retrieve lookup data of dependent entity without giving the entire read permission to users. \ No newline at end of file diff --git a/docs/en/framework/ui/mvc-razor-pages/javascript-api/ajax.md b/docs/en/framework/ui/mvc-razor-pages/javascript-api/ajax.md index 3ee58ca458..c710cb4c45 100644 --- a/docs/en/framework/ui/mvc-razor-pages/javascript-api/ajax.md +++ b/docs/en/framework/ui/mvc-razor-pages/javascript-api/ajax.md @@ -32,7 +32,7 @@ abp.ajax({ }); ```` -This command logs the list of users to the console, if you've **logged in** to the application and have [permission](../../../fundamentals/authorization.md) for the user management page of the [Identity Module](../../../../modules/identity.md). +This command logs the list of users to the console, if you've **logged in** to the application and have [permission](../../../fundamentals/authorization/index.md) for the user management page of the [Identity Module](../../../../modules/identity.md). ## Error Handling diff --git a/docs/en/framework/ui/mvc-razor-pages/javascript-api/auth.md b/docs/en/framework/ui/mvc-razor-pages/javascript-api/auth.md index 5f31c184ec..83677eec57 100644 --- a/docs/en/framework/ui/mvc-razor-pages/javascript-api/auth.md +++ b/docs/en/framework/ui/mvc-razor-pages/javascript-api/auth.md @@ -9,7 +9,7 @@ Auth API allows you to check permissions (policies) for the current user in the client side. In this way, you can conditionally show/hide UI parts or perform your client side logic based on the current permissions. -> This document only explains the JavaScript API. See the [authorization document](../../../fundamentals/authorization.md) to understand the ABP authorization & permission system. +> This document only explains the JavaScript API. See the [authorization document](../../../fundamentals/authorization/index.md) to understand the ABP authorization & permission system. ## Basic Usage 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/navigation-menu.md b/docs/en/framework/ui/mvc-razor-pages/navigation-menu.md index 41cc31f77f..f26d18ebbd 100644 --- a/docs/en/framework/ui/mvc-razor-pages/navigation-menu.md +++ b/docs/en/framework/ui/mvc-razor-pages/navigation-menu.md @@ -117,7 +117,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. -One use case is the [authorization](../../fundamentals/authorization.md). You typically want to add menu items by checking a permission. +One use case is the [authorization](../../fundamentals/authorization/index.md). You typically want to add menu items by checking a permission. **Example: Check if the current user has a permission** diff --git a/docs/en/framework/ui/mvc-razor-pages/page-toolbar-extensions.md b/docs/en/framework/ui/mvc-razor-pages/page-toolbar-extensions.md index 54ad4e182c..00ac30a49d 100644 --- a/docs/en/framework/ui/mvc-razor-pages/page-toolbar-extensions.md +++ b/docs/en/framework/ui/mvc-razor-pages/page-toolbar-extensions.md @@ -134,7 +134,7 @@ Configure(options => #### Permissions -If your button/component should be available based on a [permission/policy](../../fundamentals/authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. +If your button/component should be available based on a [permission/policy](../../fundamentals/authorization/index.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. ### Add a Page Toolbar Contributor 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/toolbars.md b/docs/en/framework/ui/mvc-razor-pages/toolbars.md index fd7160417d..2876242ff0 100644 --- a/docs/en/framework/ui/mvc-razor-pages/toolbars.md +++ b/docs/en/framework/ui/mvc-razor-pages/toolbars.md @@ -79,7 +79,7 @@ public class MyToolbarContributor : IToolbarContributor } ```` -You can use the [authorization](../../fundamentals/authorization.md) to decide whether to add a `ToolbarItem`. +You can use the [authorization](../../fundamentals/authorization/index.md) to decide whether to add a `ToolbarItem`. ````csharp if (await context.IsGrantedAsync("MyPermissionName")) diff --git a/docs/en/framework/ui/mvc-razor-pages/widgets.md b/docs/en/framework/ui/mvc-razor-pages/widgets.md index ac9901733b..0e7d708551 100644 --- a/docs/en/framework/ui/mvc-razor-pages/widgets.md +++ b/docs/en/framework/ui/mvc-razor-pages/widgets.md @@ -12,7 +12,7 @@ ABP provides a model and infrastructure to create **reusable widgets**. Widget s * Have **scripts & styles** dependencies for your widget. * Create **dashboards** with widgets used inside. * Define widgets in reusable **[modules](../../architecture/modularity/basics.md)**. -* Co-operate widgets with **[authorization](../../fundamentals/authorization.md)** and **[bundling](bundling-minification.md)** systems. +* Co-operate widgets with **[authorization](../../fundamentals/authorization/index.md)** and **[bundling](bundling-minification.md)** systems. ## Basic Widget Definition @@ -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**: @@ -482,7 +482,7 @@ Used to refresh the widget when needed. It has a filter argument that can be use Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: * `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). -* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../../fundamentals/authorization.md) for more info about policies. +* `RequiredPolicies` (`List`): A list of policy names to authorize the user. See [the authorization document](../../fundamentals/authorization/index.md) for more info about policies. Example: 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/images/resource-based-permission.gif b/docs/en/images/resource-based-permission.gif new file mode 100644 index 0000000000..fbf86ad7a6 Binary files /dev/null and b/docs/en/images/resource-based-permission.gif differ diff --git a/docs/en/index.md b/docs/en/index.md index 46b9923b57..86b7104b06 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -68,7 +68,7 @@ There are a lot of features provided by ABP to achieve real world scenarios easi #### Cross Cutting Concerns -ABP also simplifies (and even automates wherever possible) cross cutting concerns and common non-functional requirements like [Exception Handling](./framework/fundamentals/exception-handling.md), [Validation](./framework/fundamentals/validation.md), [Authorization](./framework/fundamentals/authorization.md), [Localization](./framework/fundamentals/localization.md), [Caching](./framework/fundamentals/caching.md), [Dependency Injection](./framework/fundamentals/dependency-injection.md), [Setting Management](./framework/infrastructure/settings.md), etc. +ABP also simplifies (and even automates wherever possible) cross cutting concerns and common non-functional requirements like [Exception Handling](./framework/fundamentals/exception-handling.md), [Validation](./framework/fundamentals/validation.md), [Authorization](./framework/fundamentals/authorization/index.md), [Localization](./framework/fundamentals/localization.md), [Caching](./framework/fundamentals/caching.md), [Dependency Injection](./framework/fundamentals/dependency-injection.md), [Setting Management](./framework/infrastructure/settings.md), etc. ### Tooling diff --git a/docs/en/modules/identity-pro.md b/docs/en/modules/identity-pro.md index 0d26154932..7e66e5514c 100644 --- a/docs/en/modules/identity-pro.md +++ b/docs/en/modules/identity-pro.md @@ -75,7 +75,7 @@ You can manage permissions of a role: * A permission is an **action of the application** granted to roles and users. * A user with a role will **inherit** all the permissions granted for the role. -* Any module can **[define permissions](../framework/fundamentals/authorization.md#permission-system)**. Once you define a new permission, it will be available in this page. +* Any module can **[define permissions](../framework/fundamentals/authorization/index.md#permission-system)**. Once you define a new permission, it will be available in this page. * Left side is the **list of modules**. Once you click to a module name, you can check/uncheck permissions related to that module. ##### Role claims diff --git a/docs/en/modules/identity.md b/docs/en/modules/identity.md index 8d800fe131..b302f74428 100644 --- a/docs/en/modules/identity.md +++ b/docs/en/modules/identity.md @@ -31,7 +31,7 @@ The menu items and the related pages are authorized. That means the current user ![identity-module-permissions](../images/identity-module-permissions.png) -See the [Authorization document](../framework/fundamentals/authorization.md) to understand the permission system. +See the [Authorization document](../framework/fundamentals/authorization/index.md) to understand the permission system. ### Pages diff --git a/docs/en/modules/openiddict-pro.md b/docs/en/modules/openiddict-pro.md index f531f65531..94d8deb631 100644 --- a/docs/en/modules/openiddict-pro.md +++ b/docs/en/modules/openiddict-pro.md @@ -384,7 +384,7 @@ PreConfigure(options => #### Updating Claims In Access_token and Id_token -[Claims Principal Factory](../framework/fundamentals/authorization.md#claims-principal-factory) can be used to add/remove claims to the `ClaimsPrincipal`. +[Claims Principal Factory](../framework/fundamentals/authorization/index.md#claims-principal-factory) can be used to add/remove claims to the `ClaimsPrincipal`. The `AbpDefaultOpenIddictClaimDestinationsProvider` service will add `Name`, `Email,` and `Role` types of Claims to `access_token` and `id_token`, other claims are only added to `access_token` by default, and remove the `SecurityStampClaimType` secret claim of `Identity`. diff --git a/docs/en/modules/openiddict.md b/docs/en/modules/openiddict.md index c17b8200d2..c1ed40ff9f 100644 --- a/docs/en/modules/openiddict.md +++ b/docs/en/modules/openiddict.md @@ -332,7 +332,7 @@ Configure(options => #### Updating Claims In Access_token and Id_token -[Claims Principal Factory](../framework/fundamentals/authorization.md#claims-principal-factory) can be used to add/remove claims to the `ClaimsPrincipal`. +[Claims Principal Factory](../framework/fundamentals/authorization/index.md#claims-principal-factory) can be used to add/remove claims to the `ClaimsPrincipal`. The `AbpDefaultOpenIddictClaimsPrincipalHandler` service will add `Name`, `Email,` and `Role` types of Claims to `access_token` and `id_token`, other claims are only added to `access_token` by default, and remove the `SecurityStampClaimType` secret claim of `Identity`. 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/modules/permission-management.md b/docs/en/modules/permission-management.md index 85cefffe7d..2e5f997423 100644 --- a/docs/en/modules/permission-management.md +++ b/docs/en/modules/permission-management.md @@ -7,9 +7,9 @@ # Permission Management Module -This module implements the `IPermissionStore` to store and manage permissions values in a database. +This module implements the `IPermissionStore` and `IResourcePermissionStore` to store and manage permission values in a database. -> This document covers only the permission management module which persists permission values to a database. See the [Authorization document](../framework/fundamentals/authorization.md) to understand the authorization and permission systems. +> This document covers only the permission management module which persists permission values to a database. See the [Authorization document](../framework/fundamentals/authorization/index.md) to understand the authorization and permission systems. ## How to Install @@ -33,11 +33,87 @@ When you click *Actions* -> *Permissions* for a role, the permission management In this dialog, you can grant permissions for the selected role. The tabs in the left side represents main permission groups and the right side contains the permissions defined in the selected group. +### Resource Permission Management Dialog + +In addition to standard permissions, this module provides a reusable dialog for managing **resource-based permissions** on specific resource instances. This allows administrators to grant or revoke permissions for users, roles and clients on individual resources (e.g., a specific document, project, or any entity). + +![resource-permissions-module-dialog](../images/resource-based-permission.gif) + +The dialog displays all resource permissions defined for the resource type and allows granting them to specific users, roles or clients for the selected resource instance. + +You can integrate this dialog into your own application to manage permissions for your custom entities and resources. See the following sections to learn how to use the component in each UI framework. + +#### MVC / Razor Pages + +Use the `abp.ModalManager` to open the resource permission management dialog: + +````javascript +var _resourcePermissionsModal = new abp.ModalManager( + abp.appPath + 'AbpPermissionManagement/ResourcePermissionManagementModal' +); + +// Open the modal for a specific resource +_resourcePermissionsModal.open({ + resourceName: 'MyApp.Document', + resourceKey: documentId, + resourceDisplayName: documentTitle +}); +```` + +#### Blazor + +Use the `ResourcePermissionManagementModal` component's `OpenAsync` method to open the dialog: + +````razor +@using Volo.Abp.PermissionManagement.Blazor.Components + + + +@code { + private ResourcePermissionManagementModal ResourcePermissionModal { get; set; } + + private async Task OpenPermissionsModal() + { + await ResourcePermissionModal.OpenAsync( + resourceName: "MyApp.Document", + resourceKey: Document.Id.ToString(), + resourceDisplayName: Document.Title + ); + } +} +```` + +#### Angular + +Use the `ResourcePermissionManagementComponent`: + +````typescript +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ResourcePermissionManagementComponent } from '@abp/ng.permission-management'; + +@Component({ + // ... +}) +export class DocumentListComponent { + constructor(private modalService: NgbModal) {} + + openPermissionsModal(document: DocumentDto) { + const modalRef = this.modalService.open( + ResourcePermissionManagementComponent, + { size: 'lg' } + ); + modalRef.componentInstance.resourceName = 'MyApp.Document'; + modalRef.componentInstance.resourceKey = document.id; + modalRef.componentInstance.resourceDisplayName = document.title; + } +} +```` + ## IPermissionManager -`IPermissionManager` is the main service provided by this module. It is used to read and change the permission values. `IPermissionManager` is typically used by the *Permission Management Dialog*. However, you can inject it if you need to set a permission value. +`IPermissionManager` is the main service provided by this module. It is used to read and change the global permission values. `IPermissionManager` is typically used by the *Permission Management Dialog*. However, you can inject it if you need to set a permission value. -> If you just want to read/check permission values for the current user, use the `IAuthorizationService` or the `[Authorize]` attribute as explained in the [Authorization document](../framework/fundamentals/authorization.md). +> If you just want to read/check permission values for the current user, use the `IAuthorizationService` or the `[Authorize]` attribute as explained in the [Authorization document](../framework/fundamentals/authorization/index.md). **Example: Grant permissions to roles and users using the `IPermissionManager` service** @@ -67,9 +143,125 @@ public class MyService : ITransientDependency } ```` +## IResourcePermissionManager + +`IResourcePermissionManager` is the service for programmatically managing resource-based permissions. It is typically used by the *Resource Permission Management Dialog*. However, you can inject it when you need to grant, revoke, or query permissions for specific resource instances. + +> If you just want to check resource permission values for the current user, use the `IResourcePermissionChecker` service as explained in the [Resource-Based Authorization](../framework/fundamentals/authorization/resource-based-authorization.md) document. + +**Example: Grant and revoke resource permissions programmatically** + +````csharp +public class DocumentPermissionService : ITransientDependency +{ + private readonly IResourcePermissionManager _resourcePermissionManager; + + public DocumentPermissionService( + IResourcePermissionManager resourcePermissionManager) + { + _resourcePermissionManager = resourcePermissionManager; + } + + public async Task GrantViewPermissionToUserAsync( + Guid documentId, + Guid userId) + { + await _resourcePermissionManager.SetAsync( + permissionName: "MyApp_Document_View", + resourceName: "MyApp.Document", + resourceKey: documentId.ToString(), + providerName: "U", // User + providerKey: userId.ToString(), + isGranted: true + ); + } + + public async Task GrantEditPermissionToRoleAsync( + Guid documentId, + string roleName) + { + await _resourcePermissionManager.SetAsync( + permissionName: "MyApp_Document_Edit", + resourceName: "MyApp.Document", + resourceKey: documentId.ToString(), + providerName: "R", // Role + providerKey: roleName, + isGranted: true + ); + } + + public async Task RevokeUserPermissionsAsync( + Guid documentId, + Guid userId) + { + await _resourcePermissionManager.DeleteAsync( + resourceName: "MyApp.Document", + resourceKey: documentId.ToString(), + providerName: "U", + providerKey: userId.ToString() + ); + } +} +```` + +## IResourcePermissionStore + +The `IResourcePermissionStore` interface is responsible for retrieving resource permission values from the database. This module provides the default implementation that stores permissions in the database. + +You can query the store directly if needed: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IResourcePermissionStore _resourcePermissionStore; + + public MyService(IResourcePermissionStore resourcePermissionStore) + { + _resourcePermissionStore = resourcePermissionStore; + } + + public async Task GetGrantedResourceKeysAsync(string permissionName) + { + // Get all resource keys where the permission is granted + return await _resourcePermissionStore.GetGrantedResourceKeysAsync( + "MyApp.Document", + permissionName); + } +} +```` + +## Cleaning Up Resource Permissions + +When a resource is deleted, you should clean up its associated permissions to avoid orphaned permission records in the database. You can do this directly in your delete logic or handle it asynchronously through event handlers: + +````csharp +public async Task DeleteDocumentAsync(Guid id) +{ + // Delete the document + await _documentRepository.DeleteAsync(id); + + // Clean up all permissions for this resource + await _resourcePermissionManager.DeleteAsync( + resourceName: "MyApp.Document", + resourceKey: id.ToString(), + providerName: "U", + providerKey: null // Deletes for all users + ); + + await _resourcePermissionManager.DeleteAsync( + resourceName: "MyApp.Document", + resourceKey: id.ToString(), + providerName: "R", + providerKey: null // Deletes for all roles + ); +} +```` + +> ABP modules automatically handle permission cleanup for their own entities. For your custom entities, you are responsible for cleaning up resource permissions when resources are deleted. + ## Permission Management Providers -Permission Management Module is extensible, just like the [permission system](../framework/fundamentals/authorization.md). You can extend it by defining permission management providers. +Permission Management Module is extensible, just like the [permission system](../framework/fundamentals/authorization/index.md). You can extend it by defining permission management providers. [Identity Module](identity.md) defines the following permission management providers: @@ -111,6 +303,109 @@ Configure(options => The order of the providers are important. Providers are executed in the reverse order. That means the `CustomPermissionManagementProvider` is executed first for this example. You can insert your provider in any order in the `Providers` list. +### Resource Permission Management Providers + +Similar to standard permission management providers, you can create custom providers for resource permissions by implementing `IResourcePermissionManagementProvider` or inheriting from `ResourcePermissionManagementProvider`: + +````csharp +public class CustomResourcePermissionManagementProvider + : ResourcePermissionManagementProvider +{ + public override string Name => "Custom"; + + public CustomResourcePermissionManagementProvider( + IResourcePermissionGrantRepository resourcePermissionGrantRepository, + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) + : base( + resourcePermissionGrantRepository, + guidGenerator, + currentTenant) + { + } +} +```` + +After creating the custom provider, you need to register your provider using the `PermissionManagementOptions` in your module class: + +````csharp +Configure(options => +{ + options.ResourceManagementProviders.Add(); +}); +```` + +## Permission Value Providers + +Permission value providers are used to determine if a permission is granted. They are different from management providers: **value providers** are used when *checking* permissions, while **management providers** are used when *setting* permissions. + +> For standard permissions, see the [Authorization document](../framework/fundamentals/authorization/index.md) for details on permission value providers. + +### Resource Permission Value Providers + +Similar to the standard permission system, you can create custom value providers for resource permissions. ABP comes with two built-in resource permission value providers: + +* `UserResourcePermissionValueProvider` (`U`): Checks permissions granted directly to users +* `RoleResourcePermissionValueProvider` (`R`): Checks permissions granted to roles + +You can create your own custom value provider by implementing the `IResourcePermissionValueProvider` interface or inheriting from the `ResourcePermissionValueProvider` base class: + +````csharp +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; + +public class OwnerResourcePermissionValueProvider : ResourcePermissionValueProvider +{ + public override string Name => "Owner"; + + public OwnerResourcePermissionValueProvider( + IResourcePermissionStore permissionStore) + : base(permissionStore) + { + } + + public override async Task CheckAsync( + ResourcePermissionValueCheckContext context) + { + // Check if the current user is the owner of the resource + var currentUserId = context.Principal?.FindUserId(); + if (currentUserId == null) + { + return PermissionGrantResult.Undefined; + } + + // Your logic to determine if user is the owner + var isOwner = await CheckIfUserIsOwnerAsync( + currentUserId.Value, + context.ResourceName, + context.ResourceKey); + + return isOwner + ? PermissionGrantResult.Granted + : PermissionGrantResult.Undefined; + } + + private Task CheckIfUserIsOwnerAsync( + Guid userId, + string resourceName, + string resourceKey) + { + // Implement your ownership check logic + throw new NotImplementedException(); + } +} +```` + +Register your custom provider in your module's `ConfigureServices` method: + +````csharp +Configure(options => +{ + options.ResourceValueProviders.Add(); +}); +```` + ## See Also -* [Authorization](../framework/fundamentals/authorization.md) \ No newline at end of file +* [Authorization](../framework/fundamentals/authorization/index.md) +* [Resource-Based Authorization](../framework/fundamentals/authorization/resource-based-authorization.md) 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/solution-templates/layered-web-application/overview.md b/docs/en/solution-templates/layered-web-application/overview.md index 4c9678fcb2..1a091649cc 100644 --- a/docs/en/solution-templates/layered-web-application/overview.md +++ b/docs/en/solution-templates/layered-web-application/overview.md @@ -41,7 +41,7 @@ The following **libraries and services** come **pre-installed** and **configured The following features are built and pre-configured for you in the solution. * **Authentication** is fully configured based on best practices. -* **[Permission](../../framework/fundamentals/authorization.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. +* **[Permission](../../framework/fundamentals/authorization/index.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. * **[Background job system](../../framework/infrastructure/background-jobs/index.md)**. * **[BLOB storge](../../framework/infrastructure/blob-storing/index.md)** system is installed with the [database provider](../../framework/infrastructure/blob-storing/database.md). * **On-the-fly database migration** system (services automatically migrated their database schema when you deploy a new version). **\*** diff --git a/docs/en/solution-templates/microservice/overview.md b/docs/en/solution-templates/microservice/overview.md index 6f0e2e9399..fc322565c3 100644 --- a/docs/en/solution-templates/microservice/overview.md +++ b/docs/en/solution-templates/microservice/overview.md @@ -49,7 +49,7 @@ The following features are built and pre-configured for you in the solution. * **OpenId Connect Authentication**, if you have selected the MVC UI. * **Authorization code flow** is implemented, if you have selected a SPA UI (Angular or Blazor WASM). * Other flows (resource owner password, client credentials...) are easy to use when you need them. -* **[Permission](../../framework/fundamentals/authorization.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. +* **[Permission](../../framework/fundamentals/authorization/index.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. * **[Background job system](../../framework/infrastructure/background-jobs/index.md)** with [RabbitMQ integrated](../../framework/infrastructure/background-jobs/rabbitmq.md). * **[BLOB storge](../../framework/infrastructure/blob-storing/index.md)** system is installed with the [database provider](../../framework/infrastructure/blob-storing/database.md) and a separate database. * **On-the-fly database migration** system (services automatically migrated their database schema when you deploy a new version) diff --git a/docs/en/solution-templates/microservice/permission-management.md b/docs/en/solution-templates/microservice/permission-management.md index e4e721a2cd..87aa0f8757 100644 --- a/docs/en/solution-templates/microservice/permission-management.md +++ b/docs/en/solution-templates/microservice/permission-management.md @@ -27,7 +27,7 @@ Since [Permission Management](../../modules/permission-management.md) is a funda ## Permission Management -The *Administration* microservice provides a set of APIs to manage permissions. Every microservice [defines](../../framework/fundamentals/authorization.md) its own permissions. When a microservice starts, it registers its permissions to the related permission definition tables if `SaveStaticPermissionsToDatabase` option is true for `PermissionManagementOptions`. Since the default value is true, this behavior is ensured. After that, you can see the permissions from the [Permission Management Dialog](../../modules/permission-management.md#permission-management-dialog) for related provider such as *User*, *Role* or *Client (OpenIddict Applications)*. +The *Administration* microservice provides a set of APIs to manage permissions. Every microservice [defines](../../framework/fundamentals/authorization/index.md) its own permissions. When a microservice starts, it registers its permissions to the related permission definition tables if `SaveStaticPermissionsToDatabase` option is true for `PermissionManagementOptions`. Since the default value is true, this behavior is ensured. After that, you can see the permissions from the [Permission Management Dialog](../../modules/permission-management.md#permission-management-dialog) for related provider such as *User*, *Role* or *Client (OpenIddict Applications)*. ![user-permissions](images/user-permissions.png) diff --git a/docs/en/solution-templates/single-layer-web-application/overview.md b/docs/en/solution-templates/single-layer-web-application/overview.md index a0e1e50093..c7e16090a7 100644 --- a/docs/en/solution-templates/single-layer-web-application/overview.md +++ b/docs/en/solution-templates/single-layer-web-application/overview.md @@ -39,7 +39,7 @@ The following **libraries and services** come **pre-installed** and **configured The solution comes with the following built-in and pre-configured features: * **Authentication** is fully configured based on best practices. -* **[Permission](../../framework/fundamentals/authorization.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. +* **[Permission](../../framework/fundamentals/authorization/index.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. * **[Background job system](../../framework/infrastructure/background-jobs/index.md)**. * **[BLOB storge](../../framework/infrastructure/blob-storing/index.md)** system is installed with the [database provider](../../framework/infrastructure/blob-storing/database.md). * **On-the-fly database migration** system (services automatically migrated their database schema when you deploy a new version). **\*** diff --git a/docs/en/solution-templates/single-layer-web-application/solution-structure.md b/docs/en/solution-templates/single-layer-web-application/solution-structure.md index ffb085855f..dd24b91f09 100644 --- a/docs/en/solution-templates/single-layer-web-application/solution-structure.md +++ b/docs/en/solution-templates/single-layer-web-application/solution-structure.md @@ -56,7 +56,7 @@ This template uses a single-project structure, with concerns separated into fold * **Migrations**: Contains the database migration files. It is created automatically by EF Core. * **ObjectMapping**: Define your [object-to-object mapping](../../framework/infrastructure/object-to-object-mapping.md) classes in this folder. * **Pages**: Define your UI pages (Razor Pages) in this folder (create `Controllers` and `Views` folders yourself if you prefer the MVC pattern). -* **Permissions**: Define your [permissions](../../framework/fundamentals/authorization.md) in this folder. +* **Permissions**: Define your [permissions](../../framework/fundamentals/authorization/index.md) in this folder. * **Services**: Define your [application services](../../framework/architecture/domain-driven-design/application-services.md) in this folder. ### How to Run? 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/images/overview/kubernetes-integration-helm.png b/docs/en/studio/images/overview/kubernetes-integration-helm.png index dfd68bd0df..6931739948 100644 Binary files a/docs/en/studio/images/overview/kubernetes-integration-helm.png and b/docs/en/studio/images/overview/kubernetes-integration-helm.png differ diff --git a/docs/en/studio/images/overview/solution-runner.png b/docs/en/studio/images/overview/solution-runner.png index 956a669ea3..bfe88f607c 100644 Binary files a/docs/en/studio/images/overview/solution-runner.png and b/docs/en/studio/images/overview/solution-runner.png differ diff --git a/docs/en/studio/images/solution-explorer/solution-explorer.png b/docs/en/studio/images/solution-explorer/solution-explorer.png index 377cc6a4f3..6b5cc0ad71 100644 Binary files a/docs/en/studio/images/solution-explorer/solution-explorer.png and b/docs/en/studio/images/solution-explorer/solution-explorer.png differ diff --git a/docs/en/studio/images/solution-runner/add-task-window.png b/docs/en/studio/images/solution-runner/add-task-window.png new file mode 100644 index 0000000000..2b96f85118 Binary files /dev/null and b/docs/en/studio/images/solution-runner/add-task-window.png differ diff --git a/docs/en/studio/images/solution-runner/builtin-tasks.png b/docs/en/studio/images/solution-runner/builtin-tasks.png new file mode 100644 index 0000000000..c988687c1c Binary files /dev/null and b/docs/en/studio/images/solution-runner/builtin-tasks.png differ diff --git a/docs/en/studio/images/solution-runner/csharp-application-context-menu-run-connection.png b/docs/en/studio/images/solution-runner/csharp-application-context-menu-run-connection.png index b85a6df117..e2727570eb 100644 Binary files a/docs/en/studio/images/solution-runner/csharp-application-context-menu-run-connection.png and b/docs/en/studio/images/solution-runner/csharp-application-context-menu-run-connection.png differ diff --git a/docs/en/studio/images/solution-runner/csharp-application-context-menu.png b/docs/en/studio/images/solution-runner/csharp-application-context-menu.png index 08b79c0787..3559a17e49 100644 Binary files a/docs/en/studio/images/solution-runner/csharp-application-context-menu.png and b/docs/en/studio/images/solution-runner/csharp-application-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/folder-context-menu-add.png b/docs/en/studio/images/solution-runner/folder-context-menu-add.png index c83a54f5e0..e50e8010c8 100644 Binary files a/docs/en/studio/images/solution-runner/folder-context-menu-add.png and b/docs/en/studio/images/solution-runner/folder-context-menu-add.png differ diff --git a/docs/en/studio/images/solution-runner/folder-context-menu.png b/docs/en/studio/images/solution-runner/folder-context-menu.png index 47f56995f0..cdb9de734d 100644 Binary files a/docs/en/studio/images/solution-runner/folder-context-menu.png and b/docs/en/studio/images/solution-runner/folder-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/initial-task-properties-behavior.png b/docs/en/studio/images/solution-runner/initial-task-properties-behavior.png new file mode 100644 index 0000000000..7adb487ffc Binary files /dev/null and b/docs/en/studio/images/solution-runner/initial-task-properties-behavior.png differ diff --git a/docs/en/studio/images/solution-runner/profile-root-context-menu-add.png b/docs/en/studio/images/solution-runner/profile-root-context-menu-add.png index ec26ebcaf0..823bc622a2 100644 Binary files a/docs/en/studio/images/solution-runner/profile-root-context-menu-add.png and b/docs/en/studio/images/solution-runner/profile-root-context-menu-add.png differ diff --git a/docs/en/studio/images/solution-runner/profile-root-context-menu.png b/docs/en/studio/images/solution-runner/profile-root-context-menu.png index 355869511c..8616ffca35 100644 Binary files a/docs/en/studio/images/solution-runner/profile-root-context-menu.png and b/docs/en/studio/images/solution-runner/profile-root-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/solution-runner-edit.png b/docs/en/studio/images/solution-runner/solution-runner-edit.png index 99d0bce1d5..9449456d95 100644 Binary files a/docs/en/studio/images/solution-runner/solution-runner-edit.png and b/docs/en/studio/images/solution-runner/solution-runner-edit.png differ diff --git a/docs/en/studio/images/solution-runner/solution-runner-properties.png b/docs/en/studio/images/solution-runner/solution-runner-properties.png new file mode 100644 index 0000000000..a6641f892f Binary files /dev/null and b/docs/en/studio/images/solution-runner/solution-runner-properties.png differ diff --git a/docs/en/studio/images/solution-runner/solution-runner.png b/docs/en/studio/images/solution-runner/solution-runner.png index 5a8b123d6d..460d210e0c 100644 Binary files a/docs/en/studio/images/solution-runner/solution-runner.png and b/docs/en/studio/images/solution-runner/solution-runner.png differ diff --git a/docs/en/studio/images/solution-runner/solutioın-runner-properties.png b/docs/en/studio/images/solution-runner/solutioın-runner-properties.png deleted file mode 100644 index 9a6505a7d1..0000000000 Binary files a/docs/en/studio/images/solution-runner/solutioın-runner-properties.png and /dev/null differ diff --git a/docs/en/studio/images/solution-runner/task-context-menu-add.png b/docs/en/studio/images/solution-runner/task-context-menu-add.png new file mode 100644 index 0000000000..6ae43bf1de Binary files /dev/null and b/docs/en/studio/images/solution-runner/task-context-menu-add.png differ diff --git a/docs/en/studio/images/solution-runner/task-context-menu.png b/docs/en/studio/images/solution-runner/task-context-menu.png new file mode 100644 index 0000000000..7f4f41b2f7 Binary files /dev/null and b/docs/en/studio/images/solution-runner/task-context-menu.png differ diff --git a/docs/en/studio/images/solution-runner/task-logs-window.png b/docs/en/studio/images/solution-runner/task-logs-window.png new file mode 100644 index 0000000000..f68b59aa26 Binary files /dev/null and b/docs/en/studio/images/solution-runner/task-logs-window.png differ diff --git a/docs/en/studio/images/solution-runner/task-panel.png b/docs/en/studio/images/solution-runner/task-panel.png new file mode 100644 index 0000000000..c988687c1c Binary files /dev/null and b/docs/en/studio/images/solution-runner/task-panel.png differ diff --git a/docs/en/studio/images/solution-runner/task-properties.png b/docs/en/studio/images/solution-runner/task-properties.png new file mode 100644 index 0000000000..e1246c0c31 Binary files /dev/null and b/docs/en/studio/images/solution-runner/task-properties.png differ diff --git a/docs/en/studio/installation.md b/docs/en/studio/installation.md index 6f12953f99..2e1d061920 100644 --- a/docs/en/studio/installation.md +++ b/docs/en/studio/installation.md @@ -65,3 +65,7 @@ When you see the "New Version Available" window, follow these steps to upgrade A 2. A progress indicator will display the download status. 3. Once the download is complete, a new modal will appear with the "Install and Relaunch" buttons. 4. Click on the "Install and Relaunch" button to complete the installation process. + +## Installing a Specific Version + +There is no official support for installing an older version of ABP Studio yet. But, if you want to install an older version of ABP Studio, you can use approach explanined here [https://github.com/enisn/AbpDevTools?tab=readme-ov-file#switch-abp-studio-version](https://github.com/enisn/AbpDevTools?tab=readme-ov-file#switch-abp-studio-version) \ No newline at end of file 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..04dac9ef13 100644 --- a/docs/en/studio/monitoring-applications.md +++ b/docs/en/studio/monitoring-applications.md @@ -27,7 +27,7 @@ If you want to open any of these tabs in separate window, just drag it from the ## Collecting Telemetry Information -There are two application [types](./running-applications.md#abp-studio-running-applications): C# and CLI. Only C# applications can establish a connection with ABP Studio and transmit telemetry information via the `Volo.Abp.Studio.Client.AspNetCore` package. However, we can view the *Logs* and *Browse* (if there is a *Launch URL*) for both CLI and C# application types. Upon starting C# applications, they attempt to establish a connection with ABP Studio. When connection successful, you should see a chain icon next to the application name in [Solution Runner](./running-applications.md#run-1). Applications can connect the ABP Studio with *Solution Runner* -> *C# Application* -> *Run* -> *Start* or from an outside environment such as debugging with Visual Studio. Additionally, they can establish a connection from a Kubernetes Cluster through the ABP Studio [Kubernetes Integration: Connecting to the Cluster](../get-started/microservice.md#kubernetes-integration-connecting-to-the-cluster). +There are two application [types](./running-applications.md#applications): C# and CLI. Only C# applications can establish a connection with ABP Studio and transmit telemetry information via the `Volo.Abp.Studio.Client.AspNetCore` package. However, we can view the *Logs* and *Browse* (if there is a *Launch URL*) for both CLI and C# application types. Upon starting C# applications, they attempt to establish a connection with ABP Studio. When connection successful, you should see a chain icon next to the application name in [Solution Runner](./running-applications.md#start--stop--restart). Applications can connect the ABP Studio with *Solution Runner* -> *C# Application* -> *Run* -> *Start* or from an outside environment such as debugging with Visual Studio. Additionally, they can establish a connection from a Kubernetes Cluster through the ABP Studio [Kubernetes Integration: Connecting to the Cluster](../get-started/microservice.md#kubernetes-integration-connecting-to-the-cluster). You can [configure](../framework/fundamentals/options.md) the `AbpStudioClientOptions` to disable send telemetry information. The package automatically gets the [configuration](../framework/fundamentals/configuration.md) from the `IConfiguration`. So, you can set your configuration inside the `appsettings.json`: @@ -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/overview.md b/docs/en/studio/overview.md index 3f5835c998..9ad50ad95a 100644 --- a/docs/en/studio/overview.md +++ b/docs/en/studio/overview.md @@ -91,13 +91,13 @@ Kubernetes integration in ABP Studio enables users to deploy solutions directly This pane is dedicated to managing [Helm](https://helm.sh/) charts, which are packages used in Kubernetes deployments. It simplifies the process of building images and installing charts. -![kubernetes-integration-helm-pane](./images/overview/kubernetes-integration-helm.png) +![kubernetes-integration-helm-panel](./images/overview/kubernetes-integration-helm.png) #### Kubernetes This pane is dedicated to managing Kubernetes services. It simplifies the process of redeploying and intercepting application service. -![kubernetes-integration-kubernetes-pane](./images/overview/kubernetes-integration-kubernetes.png) +![kubernetes-integration-kubernetes-panel](./images/overview/kubernetes-integration-kubernetes.png) ## Application Monitoring Area 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..a46e137db4 100644 --- a/docs/en/studio/running-applications.md +++ b/docs/en/studio/running-applications.md @@ -1,11 +1,11 @@ ```json //[doc-seo] { - "Description": "Learn how to use the ABP Studio's Solution Runner to efficiently run applications and organize projects with customizable profiles." + "Description": "Learn how to use the ABP Studio's Solution Runner to run applications, manage tasks, and organize projects with customizable profiles." } ``` -# ABP Studio: Running Applications +# ABP Studio: Solution Runner ````json //[doc-nav] @@ -17,19 +17,16 @@ } ```` -Use the *Solution Runner* to easily run your application(s) and set up infrastructure. You can create different profiles to organize projects based on your needs and teams. Simply navigate to the *Solution Runner* panel in the left menu. +Use the *Solution Runner* to easily run your application(s), execute tasks, and set up infrastructure. You can create different profiles to organize projects based on your needs and teams. Simply navigate to the *Solution Runner* panel in the left menu. ![solution-runner](images/solution-runner/solution-runner.png) -> The project structure might be different based on your selection. For example MVC microservice project, looks like the following. You can edit the tree structure as you wish. +The Solution Runner contains two tabs: -The solution runner contains 4 different types to define tree structure. +- **Applications**: For managing and running your applications. +- **Tasks**: For managing tasks that can be executed on demand or automatically when the solution is opened. -- **Profile**: We can create different profiles to manage the tree as our needs. For example we can create 2 different profile for `team-1` and `team-2`. `team-1` want to see the only *Administration* and *Identity* service, `team-2` see the *Saas* and *AuditLogging* services. With that way each team see the only services they need to run. In this example `Default` profile *Acme.BookStore (Default)* comes out of the box when we create the project. -- **Folder**: We can organize the applications with *Folder* type. In this example, we keep services in `services` folder for our microservice projects. We can also use nested folder if we want `apps`, `gateways`` and `services` is the folders in current(`Default`) profile. -- **C# Application**: We can add any C# application from our [Solution Explorer](./solution-explorer.md). If the application is not in our solution, we can add it externally by providing the *.csproj* file path. The .NET icon indicates that the application is a C# project. For example, `Acme.BookStore.AuthServer`, `Acme.BookStore.Web`, `Acme.BookStore.WebGateway`, etc., are C# applications. -- **CLI Application**: We can add [powershell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core) commands to prepare some environments or run other application types than C# such as angular. -- **Docker Container**: We can add Docker container files to control them on UI, start/stop containers individually. +> The project structure might be different based on your selection. For example MVC microservice project, looks like the following. You can edit the tree structure as you wish. ## Profile @@ -47,13 +44,23 @@ When you click *Add New Profile*, it opens the *Create New Profile* window. You > When a profile is edited or deleted while running some applications, those applications will be stopped. However, applications running under a different profile will continue to run unaffected. Lastly, if we add a new profile, all applications running under existing profiles will be stopped. -## Using the Profile +## Applications + +The **Applications tab** allows you to manage and run your applications. The solution runner contains 4 different types to define tree structure: + +- **Profile**: We can create different profiles to manage the tree as our needs. For example we can create 2 different profile for `team-1` and `team-2`. `team-1` want to see the only *Administration* and *Identity* service, `team-2` see the *Saas* and *AuditLogging* services. With that way each team see the only services they need to run. In this example `Default` profile *Acme.BookStore (Default)* comes out of the box when we create the project. +- **Folder**: We can organize the applications with *Folder* type. In this example, we keep services in `services` folder for our microservice projects. We can also use nested folder if we want `apps`, `gateways` and `services` are the folders in current(`Default`) profile. +- **C# Application**: We can add any C# application from our [Solution Explorer](./solution-explorer.md). If the application is not in our solution, we can add it externally by providing the *.csproj* file path. The .NET icon indicates that the application is a C# project. For example, `Acme.BookStore.AuthServer`, `Acme.BookStore.Web`, `Acme.BookStore.WebGateway`, etc., are C# applications. +- **CLI Application**: We can add [powershell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core) commands to prepare some environments or run other application types than C# such as angular. +- **Docker Container**: We can add Docker container files to control them on UI, start/stop containers individually. + +### Using the Profile After selecting the current profile, which is the *Default* profile that comes pre-configured, we can utilize the tree items. This allows us to execute collective commands and create various tree structures based on our specific needs. You can navigate through the root of the tree and right-click to view the context menu, which includes the following options: `Start All`, `Stop All`, `Build`, `Add`, and `Manage Start Actions`. ![profile-root-context-menu](images/solution-runner/profile-root-context-menu.png) -### Start/Stop All +#### Start/Stop All We can start/stop the applications with these options. Go to the root of the tree and right-click to view the context menu: @@ -62,7 +69,7 @@ We can start/stop the applications with these options. Go to the root of the tre > You can change the current profile while applications are running in the previous profile. The applications continue to run under the previous profile. For example, if we start the `Acme.BookStore.AdministrationService`, `Acme.BookStore.IdentityService` applications when the current profile is *team-1* and after changing the current profile to *team-2* the applications continue to run under *team-1*. -### Build +#### Build We can use common [dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools) commands in this option. Go to the root of the tree and right-click to view the context menu, in this example *Acme.Bookstore(Default)* -> *Build*, there are 4 options available: @@ -75,13 +82,15 @@ We can use common [dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools) > Since *Solution Runner* may contain numerous C# projects, the *Build* options uses the [Background Tasks](./overview#background-tasks), ensuring a seamless experience while using ABP Studio. -### Add +#### Add + +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`. -We can add 3 different item type to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application` 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) -#### C# Application +##### C# Application When we go to the root of the tree and right-click, in this example *Acme.BookStore(Default)* -> *Add* -> *C# Application* it opens the *Add Application* window. There are two methods to add applications: *This solution* and *External*. To add via the *This solution* tab, follow these steps: @@ -98,7 +107,6 @@ The C# project doesn't have to be within the current [Solution Explorer](./solut ![profile-root-add-external-csharp-application](images/solution-runner/profile-root-add-external-csharp-application.png) - - `Path`: Provide the path to the .csproj file you wish to add. The path will be [normalized](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#path-normalization), allowing the project location to be flexible, as long as it's accessible from the current [ABP Solution](./concepts.md#solution). - `Name`: Give an arbitrary name to see in solution runner. This name should be unique for each profile. - `Launch url`: This is the url when we want to browse. But if the added project doesn't have launch url we can leave it empty. @@ -106,7 +114,7 @@ The C# project doesn't have to be within the current [Solution Explorer](./solut You can click the `OK` button to add the C# application to the profile. -#### CLI Application +##### CLI Application We can add any [powershell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core) file to execute from the solution runner. With this flexibility we can prepare our infrastructure environment such as `Docker-Dependencies` or run different application types like `Angular`. You can add CLI applications with root of the tree and right-click, in this example *Acme.BookStore(Default)* -> *Add* -> *CLI Application*. @@ -122,7 +130,7 @@ We can add any [powershell](https://learn.microsoft.com/en-us/powershell/module/ You can click the `OK` button to add the CLI application to the profile. -#### Folder +##### Folder When adding applications directly to the root of the tree, it can become disorganized, especially with numerous projects. Utilizing a folder structure allows us to organize applications more efficiently. This method enables executing collective commands within a specified folder. When we go to root of the tree and right-click, in this example *Acme.BookStore(Default)* -> *Add* -> *Folder* it opens *New folder* window. @@ -132,7 +140,7 @@ When adding applications directly to the root of the tree, it can become disorga You can click the `OK` button to add the folder to the profile. -### Miscellaneous +#### Miscellaneous - You can drag and drop folder and application into folder for organization purposes. Click and hold an item, then drag it into the desired folder. - We can start all applications by clicking the *Play* icon on the left side, similar way we can stop all applications by clicking the *Stop* icon on the left side. @@ -140,7 +148,7 @@ You can click the `OK` button to add the folder to the profile. - To remove a folder from the tree, open the context menu by right-clicking the folder and selecting *Delete*. - When starting applications, they continue to restart until the application starts gracefully. To stop the restarting process when attempting to restart the application, click the icon on the left. Additionally, you can review the *Logs* to understand why the application isn't starting gracefully. -### Manage Start Actions +#### Manage Start Actions This command will open a dialog where you can set start actions and start orders of sub-applications and sub-folders. @@ -152,83 +160,107 @@ You can order the applications by dragging the icon in the first column. In the - **Build**: This option allows to disable/enable build before starting the application. If you are working on a single application, you can exclude the other applications from build to save time. This option can also be set by performing `right click > properties` on applications. - **Watch**: When enabled, changes in your code are watched and dotnet hot-reloads the application or restarts it if needed. This option also can be set by performing `right click > properties` on applications. -## Folder +### Folder Context Menu -We already now why we need folder in the [previous](./running-applications.md#folder) section, we can use collective commands within this folder items. To do that go to folder and open the context menu by right-clicking, which includes 5 options `Start`, `Stop`, `Build`, `Add`, `Manage Start Actions`, `Rename` and `Delete`. +We already now why we need folder in the [previous](#folder) section, we can use collective commands within this folder items. To do that go to folder and open the context menu by right-clicking, which includes 5 options `Start`, `Stop`, `Build`, `Add`, `Manage Start Actions`, `Rename` and `Delete`. ![folder-context-menu](images/solution-runner/folder-context-menu.png) -### Start/Stop +#### Start/Stop You can see the context menu by right-clicking *Folder*. It will start/stop all the applications under the folder. -### Build +#### Build *Folder* -> *Build* context menu, it's the [similar](#build) options like *Acme.BookStore(Default)* -> *Builds* options there are 4 options available. The only difference between them it's gonna be execute in selected folder. ![folder-context-menu-build](images/solution-runner/folder-context-menu-build.png) -### Add +#### Add *Folder* -> *Add* context menu, it's the [same](#add) options like *Acme.BookStore(Default)* -> *Add* there are 3 options avaiable. The only difference, it's gonna add item to the selected folder. ![folder-context-menu-add](images/solution-runner/folder-context-menu-add.png) -### Miscellaneous +#### Miscellaneous - You can rename a folder with *Folder* -> *Rename*. - You can delete a folder with *Folder* -> *Delete*. -## C# Application +### 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. -### Build +#### Build It's the [similar](#build) options like root of the tree options. The only difference between them it's gonna be execute the selected application. ![csharp-application-context-menu-build](images/solution-runner/csharp-application-context-menu-build.png) -### Monitoring +#### 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 +#### Properties We can open the *Application Properties* window to change *Launch url*, *Health check endpoints*, *Kubernetes service* and *run* information. To access the *Application Properties* window, navigate to a C# application, right-click to view the context menu, and select the Properties option. -![solutioın-runner-properties](images/solution-runner/solutioın-runner-properties.png) +![solution-runner-properties](images/solution-runner/solution-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*. -## CLI Application +### CLI Application CLI applications uses the [powershell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core) commands. With this way we can start and stop anything we want. After we add the CLI applications to root of the tree or folder, we can go to any CLI application and right-click to view the context menu. @@ -242,7 +274,7 @@ CLI applications uses the [powershell](https://learn.microsoft.com/en-us/powersh > When CLI applications start chain icon won't be visible, because only C# applications can connect the ABP Studio. -## Docker Containers +### Docker Containers Each Docker container represents a `.yml` file. Each file can be run on UI individually. A file may contain one or more services. To start/stop each service individually, we recommend to keep services in separate files. @@ -294,9 +326,7 @@ If the `yml` file contains multiple services, they will be represented as a sing > > ![docker-container-warning](images/solution-runner/docker-container-warning.png) - - -### Properties +#### Properties ![docker-container-properties](images/solution-runner/docker-container-properties.png) @@ -304,10 +334,160 @@ In properties dialog, you can set the name of docker compose stack name of the c ![docker-container-stack](images/solution-runner/docker-container-stack.png) -## Docker Compose +### Docker Compose You can manually run applications using [Docker Compose](https://docs.docker.com/compose/). This allows for easy setup and management of multi-container Docker applications. To get started, ensure you have Docker and Docker Compose installed on your machine. Refer to the [Deployment with Docker Compose](../solution-templates/layered-web-application/deployment/deployment-docker-compose.md) documentation for detailed instructions on how to configure and run your applications using `docker-compose`. -> Note: The **Docker Compose** is not available in the ABP Studio interface. \ No newline at end of file +> Note: The **Docker Compose** is not available in the ABP Studio interface. + +## Tasks + +The **Tasks tab** in the Solution Runner allows you to define and execute tasks for your solution. You can run initialization tasks after solution creation, create reusable tasks that can be executed on demand, and optionally configure tasks to run automatically when a solution is opened. Navigate to the *Solution Runner* panel in the left menu and select the *Tasks* tab. + +![task-panel](images/solution-runner/task-panel.png) + +> The task structure might vary based on your solution template. ABP Studio solution templates come with pre-configured tasks like *Initialize Solution*. + +### Tasks Panel Overview + +The Tasks panel has a tree structure similar to the Applications panel: + +- **Root**: The root item displays your solution name, similar to the Solution Explorer. +- **Tasks**: Individual tasks that can be started, stopped, and configured. + +Tasks are associated with the current *Profile*, just like applications. When you switch profiles, the task configuration may differ based on what's defined in each profile. + +### Built-in Tasks + +When you create a new solution using ABP Studio templates, the following tasks are automatically configured in the *Default* profile: + +![builtin-tasks](images/solution-runner/builtin-tasks.png) + +#### Initialize Solution + +The *Initialize Solution* task performs the initial setup required after creating a new solution or cloning an existing one from source control. This task is configured with the *Run on solution initialization (1 time per computer)* behaviour, meaning it runs automatically only once per computer when the solution is initialized. + +![task-behavior](images/solution-runner/initial-task-properties-behavior.png) + +The initialization typically includes: + +- Installing NPM packages (`abp install-libs`) +- Creating development certificates (for OpenIddict) +- and more... (depends on solution type) + +> This task is designed to be idempotent, meaning it can be run multiple times without causing issues. + +#### Migrate Database + +The *Migrate Database* task runs the database migration for your solution. This is useful when you need to apply new migrations after pulling changes from source control or when you've added new migrations yourself. + +> Unlike the *Initialize Solution* task, the *Migrate Database* task is not configured to run automatically on solution open by default. + +### Adding a Task + +To add a new task, right-click on the root item (solution name) in the Tasks panel and select *AddTask* item. + +![task-context-menu-add](images/solution-runner/task-context-menu-add.png) + +This opens the *Add Task* window where you can configure the task: + +![add-task-window](images/solution-runner/add-task-window.png) + +- **Name**: Provide a unique name for the task. This name will be displayed in the Tasks panel tree. +- **Working Directory**: Specify the directory where the task will be executed. You can use the folder picker to browse and select the directory. The path will be [normalized](https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#path-normalization), allowing flexibility in the folder location. +- **Start Command**: Provide the script or command to execute when the task starts. Use the local path prefix `./` if the script is in the working directory (e.g., `./scripts/my-task.ps1`). You can also pass arguments like `./my-script.ps1 -param value`. +- **Task Behaviour**: Select how the task should run: *Manual*, *Run on solution open*, or *Run on solution initialization (1 per computer)*. +- **Short Description**: Short description for the task. + +Click *OK* to add the task to the profile. + +### Managing Tasks + +Once tasks are added to the panel, you can manage them using the context menu. Right-click on a task to see the available options: + +![task-context-menu](images/solution-runner/task-context-menu.png) + +#### Start / Stop + +- **Start**: Executes the task's start command. The task status will change to indicate it's running. +- **Stop**: Stops a running task. If a stop command is configured, it will be executed; otherwise, the process will be terminated. + +> Unlike applications, tasks do not automatically restart if they fail or stop. A task can complete its execution and stop itself (like running a database migration), or it can continue running until manually stopped. + +#### Logs + +Click *Logs* to view the task's output in a dedicated window. This shows the console output from the task's execution, which is helpful for debugging or monitoring progress. + +![task-logs-window](images/solution-runner/task-logs-window.png) + +#### Properties + +Click *Properties* to open the task configuration window where you can modify the task settings: + +![task-properties](images/solution-runner/task-properties.png) + +- **Name**: The display name of the task. +- **Working Directory**: The execution directory for the task. +- **Start Command**: The command to run when starting the task. +- **Task Behaviour**: Select how the task should run: *Manual*, *Run on solution open*, or *Run on solution initialization (1 per computer)*. +- **Short Description**: Short description for the task. + +#### Remove + +Select *Remove* to delete the task from the profile. This action only removes the task configuration; it does not delete any script files. + +### Run On Solution Open + +Tasks configured with *Run on solution open* behaviour are automatically executed every time you open the solution in ABP Studio. This is useful for: + +- **Environment checks**: Verifying that required services or dependencies are available. +- **Recurring setup**: Tasks that need to run on each session, such as starting background services or refreshing configurations. + +When the solution is opened: + +1. ABP Studio loads the solution and profiles +2. Tasks marked with *Run on solution open* are queued for execution +3. Tasks execute in the background, and you can monitor their progress in the [Background Tasks](./overview.md#background-tasks) panel + +> Tasks configured with this behaviour should be idempotent, meaning running them multiple times should not cause errors or duplicate operations. + +### Run On Solution Initialization (1 Per Computer) + +Tasks configured with *Run on solution initialization (1 per computer)* behaviour are executed only once per computer when the solution is first opened. After the initial execution, the task will not run automatically on subsequent solution opens. This is ideal for: + +- **One-time setup**: Tasks like *Initialize Solution* that install dependencies, run database migrations, or create development certificates. +- **First-time configuration**: Setting up environment-specific configurations that only need to happen once. + +This behaviour is particularly useful for: + +1. **New team members**: When a developer clones the repository and opens the solution for the first time, all initialization tasks run automatically. +2. **Fresh clones**: Ensures the solution is properly configured without requiring manual intervention. +3. **Avoiding redundant operations**: Prevents running time-consuming setup tasks on every solution open. + +> The *Initialize Solution* task that comes with ABP Studio templates uses this behaviour by default. It checks if NPM packages are already installed before running `abp install-libs` and performs other initialization steps only when necessary. + +### Task Configuration in Profiles + +Tasks are stored in the run profile JSON files (`*.abprun.json`). The task configuration looks like this: + +```json +{ + "tasks": { + "Initialize Solution": { + "behaviour": 2, + "startCommand": "./initialize-solution.ps1", + "workingDirectory": "../../scripts", + "shortDescription": "Installs required UI libraries and creates required certificates." + } + } +} +``` + +You can manually edit these files if needed, but it's recommended to use the ABP Studio UI for managing tasks to ensure proper formatting and validation. + +## See Also + +- [Solution Explorer](./solution-explorer.md) +- [Monitoring Applications](./monitoring-applications.md) diff --git a/docs/en/studio/solution-explorer.md b/docs/en/studio/solution-explorer.md index 21dde505e5..05a7f69403 100644 --- a/docs/en/studio/solution-explorer.md +++ b/docs/en/studio/solution-explorer.md @@ -11,7 +11,7 @@ //[doc-nav] { "Next": { - "Name": "Running Applications", + "Name": "Solution Runner", "Path": "studio/running-applications" } } @@ -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/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png index adebe653b5..94dfb63a56 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png and b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png index a0ecbd469d..e3032dfbde 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png and b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png index 687dca3593..5c4a377b4a 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png index 5535659b45..e690e9b0e9 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png index 1bf3253fe7..961e7565a7 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png index 54860a5e9f..7a2850afc2 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png b/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png index 47a37bbd9b..bf1394a0a2 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png and b/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png b/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png index dbb1890c14..b1e4a7d06e 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png and b/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-02.md b/docs/en/tutorials/book-store-with-abp-suite/part-02.md index 11725201cb..f9462b9130 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-02.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-02.md @@ -96,7 +96,7 @@ Here is the all details for the `Book` entity: * `Name` is **required**, it's a **string** property and maximum length is **128**. * `Type` is an **enum** and the enum file path is *\Acme.BookStore.Domain.Shared\Books\BookType.cs*. * `PublishDate` is a **DateTime** property and **not nullable**. -* `Price` is a **float** property and **required**. +* `Price` is a **float** property. You can leave the other configurations as default. diff --git a/docs/en/tutorials/book-store/part-05.md b/docs/en/tutorials/book-store/part-05.md index 3db00ea423..7449b4b65d 100644 --- a/docs/en/tutorials/book-store/part-05.md +++ b/docs/en/tutorials/book-store/part-05.md @@ -30,7 +30,7 @@ ## Permissions -ABP provides an [authorization system](../../framework/fundamentals/authorization.md) based on the ASP.NET Core's [authorization infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). One major feature added on top of the standard authorization infrastructure is the **permission system** which allows to define permissions and enable/disable per role, user or client. +ABP provides an [authorization system](../../framework/fundamentals/authorization/index.md) based on the ASP.NET Core's [authorization infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). One major feature added on top of the standard authorization infrastructure is the **permission system** which allows to define permissions and enable/disable per role, user or client. ### Permission Names diff --git a/docs/en/tutorials/book-store/part-08.md b/docs/en/tutorials/book-store/part-08.md index 68796b99eb..02a0182fe5 100644 --- a/docs/en/tutorials/book-store/part-08.md +++ b/docs/en/tutorials/book-store/part-08.md @@ -183,7 +183,7 @@ public class AuthorAppService : BookStoreAppService, IAuthorAppService } ```` -* `[Authorize(BookStorePermissions.Authors.Default)]` is a declarative way to check a permission (policy) to authorize the current user. See the [authorization document](../../framework/fundamentals/authorization.md) for more. `BookStorePermissions` class will be updated below, don't worry for the compile error for now. +* `[Authorize(BookStorePermissions.Authors.Default)]` is a declarative way to check a permission (policy) to authorize the current user. See the [authorization document](../../framework/fundamentals/authorization/index.md) for more. `BookStorePermissions` class will be updated below, don't worry for the compile error for now. * Derived from the `BookStoreAppService`, which is a simple base class comes with the startup template. It is derived from the standard `ApplicationService` class. * Implemented the `IAuthorAppService` which was defined above. * Injected the `IAuthorRepository` and `AuthorManager` to use in the service methods. diff --git a/docs/en/tutorials/microservice/part-07.md b/docs/en/tutorials/microservice/part-07.md index 5dc638dd04..a12b000415 100644 --- a/docs/en/tutorials/microservice/part-07.md +++ b/docs/en/tutorials/microservice/part-07.md @@ -216,7 +216,7 @@ Implementing `ITransientDependency` registers the `OrderEventHandler` class to t ### Testing the Order Creation -To keep this tutorial simple, we will not implement a user interface for creating orders. Instead, we will use the Swagger UI to create an order. Open the *Solution Runner* panel in ABP Studio and use the *Start* action to launch the `CloudCrm.OrderingService` and `CloudCrm.CatalogService` applications. Then, use the *Start All* action to start the remaining applications listed in the [Solution Runner root item](../../studio/running-applications.md#run). +To keep this tutorial simple, we will not implement a user interface for creating orders. Instead, we will use the Swagger UI to create an order. Open the *Solution Runner* panel in ABP Studio and use the *Start* action to launch the `CloudCrm.OrderingService` and `CloudCrm.CatalogService` applications. Then, use the *Start All* action to start the remaining applications listed in the [Solution Runner root item](../../studio/running-applications.md#startall). Once the application is running and ready, [Browse](../../studio/running-applications.md#c-application) the `CloudCrm.OrderingService` application. Use the `POST /api/ordering/order` endpoint to create a new order: 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.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs index ad2f6ba983..26e51dbd38 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Volo.Abp.AspNetCore.Components.Web.Configuration; using Volo.Abp.DependencyInjection; @@ -16,7 +17,7 @@ public class MauiCurrentApplicationConfigurationCacheResetService : _mauiBlazorCachedApplicationConfigurationClient = mauiBlazorCachedApplicationConfigurationClient; } - public async Task ResetAsync() + public async Task ResetAsync(Guid? userId = null) { await _mauiBlazorCachedApplicationConfigurationClient.InitializeAsync(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs index 02de9d8bf7..144d975cb5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Volo.Abp.AspNetCore.Components.Web.Configuration; using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; using Volo.Abp.DependencyInjection; @@ -19,10 +20,8 @@ public class BlazorServerCurrentApplicationConfigurationCacheResetService : _localEventBus = localEventBus; } - public async Task ResetAsync() + public async Task ResetAsync(Guid? userId = null) { - await _localEventBus.PublishAsync( - new CurrentApplicationConfigurationCacheResetEventData() - ); + await _localEventBus.PublishAsync(new CurrentApplicationConfigurationCacheResetEventData(userId)); } } 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.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs index c3e33a9e41..2d44399f9d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs @@ -1,8 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Volo.Abp.AspNetCore.Components.Web.Configuration; public interface ICurrentApplicationConfigurationCacheResetService { - Task ResetAsync(); + Task ResetAsync(Guid? userId = null); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs index bb91d70775..1cfaee3315 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; @@ -5,7 +6,7 @@ namespace Volo.Abp.AspNetCore.Components.Web.Configuration; public class NullCurrentApplicationConfigurationCacheResetService : ICurrentApplicationConfigurationCacheResetService, ISingletonDependency { - public Task ResetAsync() + public Task ResetAsync(Guid? userId = null) { return Task.CompletedTask; } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs index 40ac508030..359678daf4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Volo.Abp.AspNetCore.Components.Web.Configuration; using Volo.Abp.DependencyInjection; @@ -16,7 +17,7 @@ public class BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService : _webAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient; } - public async Task ResetAsync() + public async Task ResetAsync(Guid? userId = null) { await _webAssemblyCachedApplicationConfigurationClient.InitializeAsync(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs index cc1180fd20..ea0d778fad 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs @@ -1,13 +1,26 @@ -using System.Globalization; -using Volo.Abp.Users; +using System; +using System.Globalization; +using System.Threading.Tasks; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; namespace Volo.Abp.AspNetCore.Mvc.Client; -public static class MvcCachedApplicationConfigurationClientHelper +public class MvcCachedApplicationConfigurationClientHelper : ITransientDependency { - public static string CreateCacheKey(ICurrentUser currentUser) + protected IDistributedCache ApplicationVersionCache { get; } + + public MvcCachedApplicationConfigurationClientHelper(IDistributedCache applicationVersionCache) + { + ApplicationVersionCache = applicationVersionCache; + } + + public virtual async Task CreateCacheKeyAsync(Guid? userId) { - var userKey = currentUser.Id?.ToString("N") ?? "Anonymous"; - return $"ApplicationConfiguration_{userKey}_{CultureInfo.CurrentUICulture.Name}"; + var appVersion = await ApplicationVersionCache.GetOrAddAsync(MvcCachedApplicationVersionCacheItem.CacheKey, + () => Task.FromResult(new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N")))) ?? + new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N")); + var userKey = userId?.ToString("N") ?? "Anonymous"; + return $"ApplicationConfiguration_{appVersion.Version}_{userKey}_{CultureInfo.CurrentUICulture.Name}"; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs new file mode 100644 index 0000000000..1cd9990a44 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs @@ -0,0 +1,13 @@ +namespace Volo.Abp.AspNetCore.Mvc.Client; + +public class MvcCachedApplicationVersionCacheItem +{ + public const string CacheKey = "Mvc_Application_Version"; + + public string Version { get; set; } + + public MvcCachedApplicationVersionCacheItem(string version) + { + Version = version; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs index 8d787bec65..ba42b55d18 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs @@ -20,6 +20,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP protected IHttpClientFactory HttpClientFactory { get; } protected IRemoteServiceHttpClientAuthenticator HttpClientAuthenticator { get; } protected IDistributedCache ApplicationConfigurationDtoCache { get; } + protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; } protected ICurrentUser CurrentUser { get; } public RemoteDynamicClaimsPrincipalContributorCache( @@ -28,7 +29,8 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP IOptions abpClaimsPrincipalFactoryOptions, IRemoteServiceHttpClientAuthenticator httpClientAuthenticator, IDistributedCache applicationConfigurationDtoCache, - ICurrentUser currentUser) + ICurrentUser currentUser, + MvcCachedApplicationConfigurationClientHelper cacheHelper) : base(abpClaimsPrincipalFactoryOptions) { Cache = cache; @@ -36,6 +38,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP HttpClientAuthenticator = httpClientAuthenticator; ApplicationConfigurationDtoCache = applicationConfigurationDtoCache; CurrentUser = currentUser; + CacheHelper = cacheHelper; } protected async override Task GetCacheAsync(Guid userId, Guid? tenantId = null) @@ -56,7 +59,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP catch (Exception e) { Logger.LogWarning(e, $"Failed to refresh remote claims for user: {userId}"); - await ApplicationConfigurationDtoCache.RemoveAsync(MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser)); + await ApplicationConfigurationDtoCache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id)); throw; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs index 0bff9d09b1..e3d3c12370 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; @@ -13,14 +14,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Client; public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigurationClient, ITransientDependency { + private const string ApplicationConfigurationDtoCacheKey = "ApplicationConfigurationDto_CacheKey"; + protected IHttpContextAccessor HttpContextAccessor { get; } protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; } protected AbpApplicationLocalizationClientProxy ApplicationLocalizationClientProxy { get; } protected ICurrentUser CurrentUser { get; } + protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; } protected IDistributedCache Cache { get; } protected AbpAspNetCoreMvcClientCacheOptions Options { get; } public MvcCachedApplicationConfigurationClient( + MvcCachedApplicationConfigurationClientHelper cacheHelper, IDistributedCache cache, AbpApplicationConfigurationClientProxy applicationConfigurationAppService, ICurrentUser currentUser, @@ -33,13 +38,27 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu HttpContextAccessor = httpContextAccessor; ApplicationLocalizationClientProxy = applicationLocalizationClientProxy; Options = options.Value; + CacheHelper = cacheHelper; Cache = cache; } - public async Task GetAsync() + public virtual async Task GetAsync() { - var cacheKey = CreateCacheKey(); + string? cacheKey = null; var httpContext = HttpContextAccessor?.HttpContext; + if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key) + { + cacheKey = key; + } + + if (cacheKey.IsNullOrWhiteSpace()) + { + cacheKey = await CreateCacheKeyAsync(); + if (httpContext != null) + { + httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey; + } + } if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration) { @@ -86,8 +105,21 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu public ApplicationConfigurationDto Get() { - var cacheKey = CreateCacheKey(); + string? cacheKey = null; var httpContext = HttpContextAccessor?.HttpContext; + if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key) + { + cacheKey = key; + } + + if (cacheKey.IsNullOrWhiteSpace()) + { + cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync); + if (httpContext != null) + { + httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey; + } + } if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration) { @@ -97,8 +129,8 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu return AsyncHelper.RunSync(GetAsync); } - protected virtual string CreateCacheKey() + protected virtual async Task CreateCacheKeyAsync() { - return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser); + return await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs index 8bd3971779..c32b63249c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs @@ -3,7 +3,6 @@ using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus; -using Volo.Abp.Users; namespace Volo.Abp.AspNetCore.Mvc.Client; @@ -11,23 +10,29 @@ public class MvcCurrentApplicationConfigurationCacheResetEventHandler : ILocalEventHandler, ITransientDependency { - protected ICurrentUser CurrentUser { get; } protected IDistributedCache Cache { get; } + protected IDistributedCache ApplicationVersionCache { get; } + protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; } - public MvcCurrentApplicationConfigurationCacheResetEventHandler(ICurrentUser currentUser, - IDistributedCache cache) + public MvcCurrentApplicationConfigurationCacheResetEventHandler( + IDistributedCache cache, + IDistributedCache applicationVersionCache, + MvcCachedApplicationConfigurationClientHelper cacheHelper) { - CurrentUser = currentUser; Cache = cache; + ApplicationVersionCache = applicationVersionCache; + CacheHelper = cacheHelper; } public virtual async Task HandleEventAsync(CurrentApplicationConfigurationCacheResetEventData eventData) { - await Cache.RemoveAsync(CreateCacheKey()); - } - - protected virtual string CreateCacheKey() - { - return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser); + if (eventData.UserId.HasValue) + { + await Cache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(eventData.UserId)); + } + else + { + await ApplicationVersionCache.RemoveAsync(MvcCachedApplicationVersionCacheItem.CacheKey); + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs index a50cb7b136..fccc295429 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs @@ -1,9 +1,21 @@ -namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using System; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; /// /// This event is used to invalidate current user's cached configuration. /// public class CurrentApplicationConfigurationCacheResetEventData { + public Guid? UserId { get; set; } + + public CurrentApplicationConfigurationCacheResetEventData() + { + + } + public CurrentApplicationConfigurationCacheResetEventData(Guid? userId) + { + UserId = userId; + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs index 9935b90903..f2fbb2e4bf 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc.Rendering; +using System; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.TagHelpers; using System.Collections.Generic; using System.Text.Encodings.Web; @@ -41,12 +42,12 @@ public class AbpBreadcrumbItemTagHelperService : AbpTagHelperService : AbpTagHelperSe } var span = new TagBuilder("span"); - span.InnerHtml.AppendHtml(TagHelper.Text!); + span.InnerHtml.Append(TagHelper.Text!); output.Content.AppendHtml(span); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs index b8462f4b5b..dea75d44d7 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs @@ -22,7 +22,7 @@ public class AbpCardBodyTagHelperService : AbpTagHelperService var label = new TagBuilder("label"); label.Attributes.Add("for", GetIdAttributeValue(inputTag)); - label.InnerHtml.AppendHtml(_encoder.Encode(TagHelper.Label)); + label.InnerHtml.Append(TagHelper.Label); label.AddCssClass(isCheckbox ? "form-check-label" : "form-label"); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs index ad2b7abe15..0dedb9c755 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs @@ -107,7 +107,7 @@ public class AbpRadioInputTagHelperService : AbpTagHelperService var label = new TagBuilder("label"); label.AddCssClass("form-label"); label.Attributes.Add("for", GetIdAttributeValue(selectTag)); - label.InnerHtml.AppendHtml(_encoder.Encode(TagHelper.Label)); + label.InnerHtml.Append(TagHelper.Label); label.InnerHtml.AppendHtml(GetRequiredSymbol(context, output)); return label.ToHtmlString(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs index 5088c08293..dfd9ab60e4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs @@ -556,7 +556,7 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp var label = new TagBuilder("label"); label.Attributes.Add("for", GetIdAttributeValue(inputTag)); - label.InnerHtml.AppendHtml(Encoder.Encode(TagHelper.Label)); + label.InnerHtml.Append(TagHelper.Label); label.AddCssClass("form-label"); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs index 52ced17912..a6e0b76683 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs @@ -27,7 +27,7 @@ public class AbpModalHeaderTagHelperService : AbpTagHelperService anchor.Attributes.Add(attr.Name, attr.Value.ToString()); } - anchor.InnerHtml.AppendHtml(title); + anchor.InnerHtml.Append(title); return anchor.ToHtmlString(); } @@ -73,7 +73,7 @@ public class AbpTabTagHelperService : AbpTagHelperService anchor.Attributes.Add(attr.Name, attr.Value.ToString()); } - anchor.InnerHtml.AppendHtml(title); + anchor.InnerHtml.Append(title); var listItem = new TagBuilder("li"); listItem.AddCssClass("nav-item"); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs index 3026860ed6..c27be9a8c6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs @@ -225,6 +225,6 @@ public class AbpTabsTagHelperService : AbpTagHelperService protected virtual string SetTabItemNameIfNotProvided(string content, int index) { - return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name) + "_" + index); + return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name ?? string.Empty) + "_" + index); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs index c4701aa7d5..0e87d88370 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs @@ -19,9 +19,9 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService IBundleManager bundleManager, IOptions options, IWebHostEnvironment hostingEnvironment) : base( - bundleManager, - options, - hostingEnvironment) + bundleManager, + options, + hostingEnvironment) { } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs index 1ed3a76fe1..a61d000d6b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs @@ -22,9 +22,9 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService IOptions options, IWebHostEnvironment hostingEnvironment, IOptions securityHeadersOptions) : base( - bundleManager, - options, - hostingEnvironment) + bundleManager, + options, + hostingEnvironment) { SecurityHeadersOptions = securityHeadersOptions.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.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs index 2e16c79809..bb2f080e71 100644 --- a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs +++ b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -33,6 +34,15 @@ public abstract class AbpWebApplicationFactoryIntegratedTest : WebAppl return base.CreateHost(builder); } + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((hostingContext, config) => + { + hostingContext.HostingEnvironment.EnvironmentName = "Production"; + }); + base.ConfigureWebHost(builder); + } + protected virtual T? GetService() { return Services.GetService(); 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/LIbs/InstallLibsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs index 0141195cb5..af007c0901 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs @@ -6,11 +6,21 @@ using System.Threading.Tasks; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Cli.LIbs; +public enum JavaScriptFrameworkType +{ + None, + ReactNative, + React, + Vue, + NextJs +} + public class InstallLibsService : IInstallLibsService, ITransientDependency { private readonly static List ExcludeDirectory = new List() @@ -78,36 +88,155 @@ public class InstallLibsService : IInstallLibsService, ITransientDependency await CleanAndCopyResources(projectDirectory); } + + // JavaScript frameworks (React Native, React, Vue, Next.js) + if (projectPath.EndsWith("package.json")) + { + var frameworkType = DetectFrameworkTypeFromPackageJson(projectPath); + + if (frameworkType != JavaScriptFrameworkType.None) + { + var frameworkName = frameworkType switch + { + JavaScriptFrameworkType.ReactNative => "React Native", + JavaScriptFrameworkType.React => "React", + JavaScriptFrameworkType.Vue => "Vue.js", + JavaScriptFrameworkType.NextJs => "Next.js", + _ => "JavaScript" + }; + + Logger.LogInformation($"Installing dependencies for {frameworkName} project: {projectDirectory}"); + NpmHelper.RunYarn(projectDirectory); + } + } + } + } + + private JavaScriptFrameworkType DetectFrameworkTypeFromPackageJson(string packageJsonFilePath) + { + if (!File.Exists(packageJsonFilePath)) + { + return JavaScriptFrameworkType.None; + } + + try + { + var packageJsonContent = File.ReadAllText(packageJsonFilePath); + var packageJson = JObject.Parse(packageJsonContent); + + var dependencies = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Check dependencies + if (packageJson["dependencies"] is JObject deps) + { + foreach (var prop in deps.Properties()) + { + dependencies.Add(prop.Name); + } + } + + // Check devDependencies + if (packageJson["devDependencies"] is JObject devDeps) + { + foreach (var prop in devDeps.Properties()) + { + dependencies.Add(prop.Name); + } + } + + // Check for React Native first (has priority over React) + if (dependencies.Contains("react-native")) + { + return JavaScriptFrameworkType.ReactNative; + } + + // Check for other frameworks + if (dependencies.Contains("next")) + { + return JavaScriptFrameworkType.NextJs; + } + + if (dependencies.Contains("vue")) + { + return JavaScriptFrameworkType.Vue; + } + + if (dependencies.Contains("react")) + { + return JavaScriptFrameworkType.React; + } + + return JavaScriptFrameworkType.None; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to parse package.json at {PackageJsonFilePath}", packageJsonFilePath); + return JavaScriptFrameworkType.None; } } private List FindAllProjects(string directory) { - return Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories) - .Union(Directory.GetFiles(directory, "angular.json", SearchOption.AllDirectories)) + var projects = new List(); + + // Find .csproj files (existing logic) + var csprojFiles = Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories) .Where(file => ExcludeDirectory.All(x => file.IndexOf(x + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) == -1)) .Where(file => { - if (file.EndsWith(".csproj")) + var packageJsonFilePath = Path.Combine(Path.GetDirectoryName(file), "package.json"); + if (!File.Exists(packageJsonFilePath)) { - var packageJsonFilePath = Path.Combine(Path.GetDirectoryName(file), "package.json"); - if (!File.Exists(packageJsonFilePath)) - { - return false; - } + return false; + } - using (var reader = File.OpenText(file)) - { - var fileTexts = reader.ReadToEnd(); - return fileTexts.Contains("Microsoft.NET.Sdk.Web") || - fileTexts.Contains("Microsoft.NET.Sdk.Razor") || - fileTexts.Contains("Microsoft.NET.Sdk.BlazorWebAssembly"); - } + using (var reader = File.OpenText(file)) + { + var fileTexts = reader.ReadToEnd(); + return fileTexts.Contains("Microsoft.NET.Sdk.Web") || + fileTexts.Contains("Microsoft.NET.Sdk.Razor") || + fileTexts.Contains("Microsoft.NET.Sdk.BlazorWebAssembly"); } - return true; - }) - .OrderBy(x => x) - .ToList(); + }); + + projects.AddRange(csprojFiles); + + // Find angular.json files (existing logic) + var angularFiles = Directory.GetFiles(directory, "angular.json", SearchOption.AllDirectories) + .Where(file => ExcludeDirectory.All(x => file.IndexOf(x + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) == -1)); + + projects.AddRange(angularFiles); + + // Find package.json files for JavaScript frameworks + var packageJsonFiles = Directory.GetFiles(directory, "package.json", SearchOption.AllDirectories) + .Where(file => ExcludeDirectory.All(x => file.IndexOf(x + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) == -1)) + .Where(packageJsonFile => + { + var packageJsonDirectory = Path.GetDirectoryName(packageJsonFile); + if (packageJsonDirectory == null) + { + return false; + } + + // Skip if already handled by Angular or .NET detection + if (File.Exists(Path.Combine(packageJsonDirectory, "angular.json"))) + { + return false; + } + + if (Directory.GetFiles(packageJsonDirectory, "*.csproj", SearchOption.TopDirectoryOnly).Any()) + { + return false; + } + + // Check if it's a JavaScript framework project + var frameworkType = DetectFrameworkTypeFromPackageJson(packageJsonFile); + return frameworkType != JavaScriptFrameworkType.None; + }); + + projects.AddRange(packageJsonFiles); + + return projects.OrderBy(x => x).ToList(); } private async Task CleanAndCopyResources(string fileDirectory) 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.Core/Volo/Abp/Threading/KeyedLock.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs new file mode 100644 index 0000000000..7cab60c8d7 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Threading; + +/// +/// Per-key asynchronous lock for coordinating concurrent flows. +/// +/// +/// Based on the pattern described in https://stackoverflow.com/a/31194647. +/// Use within a using scope to ensure the lock is released via IDisposable.Dispose(). +/// +public static class KeyedLock +{ + private static readonly Dictionary> SemaphoreSlims = new(); + + /// + /// Acquires an exclusive asynchronous lock for the specified . + /// This method waits until the lock becomes available. + /// + /// A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock. + /// An handle that must be disposed to release the lock. + /// Thrown when is . + /// + /// + /// var key = "my-critical-section"; + /// using (await KeyedLock.LockAsync(key)) + /// { + /// // protected work + /// } + /// + /// + public static async Task LockAsync(object key) + { + Check.NotNull(key, nameof(key)); + return await LockAsync(key, CancellationToken.None); + } + + /// + /// Acquires an exclusive asynchronous lock for the specified , observing a . + /// + /// A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock. + /// A token to cancel the wait for the lock. + /// An handle that must be disposed to release the lock. + /// Thrown when is . + /// Thrown if the wait is canceled via . + /// + /// + /// var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + /// using (await KeyedLock.LockAsync("db-update", cts.Token)) + /// { + /// // protected work + /// } + /// + /// + public static async Task LockAsync(object key, CancellationToken cancellationToken) + { + Check.NotNull(key, nameof(key)); + var semaphore = GetOrCreate(key); + try + { + await semaphore.WaitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + var toDispose = DecrementRefAndMaybeRemove(key); + toDispose?.Dispose(); + throw; + } + return new Releaser(key); + } + + /// + /// Attempts to acquire an exclusive lock for the specified without waiting. + /// + /// A non-null object that identifies the lock. + /// + /// An handle if the lock was immediately acquired; otherwise . + /// + /// Thrown when is . + /// + /// + /// var handle = await KeyedLock.TryLockAsync("cache-key"); + /// if (handle != null) + /// { + /// using (handle) + /// { + /// // protected work + /// } + /// } + /// + /// + public static async Task TryLockAsync(object key) + { + Check.NotNull(key, nameof(key)); + return await TryLockAsync(key, default, CancellationToken.None); + } + + /// + /// Attempts to acquire an exclusive lock for the specified , waiting up to . + /// + /// A non-null object that identifies the lock. + /// Maximum time to wait for the lock. If set to , the method performs an immediate, non-blocking attempt. + /// A token to cancel the wait. + /// + /// An handle if the lock was acquired within the timeout; otherwise . + /// + /// Thrown when is . + /// Thrown if the wait is canceled via . + /// + /// + /// var handle = await KeyedLock.TryLockAsync("send-mail", TimeSpan.FromSeconds(1)); + /// if (handle != null) + /// { + /// using (handle) + /// { + /// // protected work + /// } + /// } + /// else + /// { + /// // lock not acquired within timeout + /// } + /// + /// + public static async Task TryLockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken = default) + { + Check.NotNull(key, nameof(key)); + var semaphore = GetOrCreate(key); + bool acquired; + try + { + if (timeout == default) + { + acquired = await semaphore.WaitAsync(0, cancellationToken); + } + else + { + acquired = await semaphore.WaitAsync(timeout, cancellationToken); + } + } + catch (OperationCanceledException) + { + var toDispose = DecrementRefAndMaybeRemove(key); + toDispose?.Dispose(); + throw; + } + + if (acquired) + { + return new Releaser(key); + } + + var toDisposeOnFail = DecrementRefAndMaybeRemove(key); + toDisposeOnFail?.Dispose(); + + return null; + } + + private static SemaphoreSlim GetOrCreate(object key) + { + RefCounted item; + lock (SemaphoreSlims) + { + if (SemaphoreSlims.TryGetValue(key, out item!)) + { + ++item.RefCount; + } + else + { + item = new RefCounted(new SemaphoreSlim(1, 1)); + SemaphoreSlims[key] = item; + } + } + return item.Value; + } + + private sealed class RefCounted(T value) + { + public int RefCount { get; set; } = 1; + + public T Value { get; } = value; + } + + private sealed class Releaser(object key) : IDisposable + { + private int _disposed; + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + { + return; + } + + RefCounted item; + var shouldDispose = false; + lock (SemaphoreSlims) + { + if (!SemaphoreSlims.TryGetValue(key, out item!)) + { + return; + } + --item.RefCount; + if (item.RefCount == 0) + { + SemaphoreSlims.Remove(key); + shouldDispose = true; + } + } + + if (shouldDispose) + { + item.Value.Dispose(); + } + else + { + item.Value.Release(); + } + } + } + + private static SemaphoreSlim? DecrementRefAndMaybeRemove(object key) + { + RefCounted? itemToDispose = null; + lock (SemaphoreSlims) + { + if (SemaphoreSlims.TryGetValue(key, out var item)) + { + --item.RefCount; + if (item.RefCount == 0) + { + SemaphoreSlims.Remove(key); + itemToDispose = item; + } + } + } + return itemToDispose?.Value; + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs index e028b55844..5680f9e857 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs @@ -10,36 +10,38 @@ public abstract class BasicAggregateRoot : Entity, IAggregateRoot, IGeneratesDomainEvents { - private readonly ICollection _distributedEvents = new Collection(); - private readonly ICollection _localEvents = new Collection(); + private ICollection? _distributedEvents; + private ICollection? _localEvents; public virtual IEnumerable GetLocalEvents() { - return _localEvents; + return _localEvents ?? Array.Empty(); } public virtual IEnumerable GetDistributedEvents() { - return _distributedEvents; + return _distributedEvents ?? Array.Empty(); } public virtual void ClearLocalEvents() { - _localEvents.Clear(); + _localEvents?.Clear(); } public virtual void ClearDistributedEvents() { - _distributedEvents.Clear(); + _distributedEvents?.Clear(); } protected virtual void AddLocalEvent(object eventData) { + _localEvents ??= new Collection(); _localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); } protected virtual void AddDistributedEvent(object eventData) { + _distributedEvents ??= new Collection(); _distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); } } @@ -49,8 +51,8 @@ public abstract class BasicAggregateRoot : Entity, IAggregateRoot, IGeneratesDomainEvents { - private readonly ICollection _distributedEvents = new Collection(); - private readonly ICollection _localEvents = new Collection(); + private ICollection? _distributedEvents; + private ICollection? _localEvents; protected BasicAggregateRoot() { @@ -65,31 +67,33 @@ public abstract class BasicAggregateRoot : Entity, public virtual IEnumerable GetLocalEvents() { - return _localEvents; + return _localEvents ?? Array.Empty(); } public virtual IEnumerable GetDistributedEvents() { - return _distributedEvents; + return _distributedEvents ?? Array.Empty(); } public virtual void ClearLocalEvents() { - _localEvents.Clear(); + _localEvents?.Clear(); } public virtual void ClearDistributedEvents() { - _distributedEvents.Clear(); + _distributedEvents?.Clear(); } protected virtual void AddLocalEvent(object eventData) { + _localEvents ??= new Collection(); _localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); } protected virtual void AddDistributedEvent(object eventData) { + _distributedEvents ??= new Collection(); _distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext())); } } diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj index 83f8f0076b..773c954051 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj @@ -18,7 +18,6 @@ - diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs index 15956b159e..5d12d0deeb 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs @@ -1,19 +1,13 @@ using System; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using AsyncKeyedLock; using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; namespace Volo.Abp.DistributedLocking; public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency { - private readonly AsyncKeyedLocker _localSyncObjects = new(o => - { - o.PoolSize = 20; - o.PoolInitialFill = 1; - }); protected IDistributedLockKeyNormalizer DistributedLockKeyNormalizer { get; } public LocalAbpDistributedLock(IDistributedLockKeyNormalizer distributedLockKeyNormalizer) @@ -21,7 +15,6 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency DistributedLockKeyNormalizer = distributedLockKeyNormalizer; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public async Task TryAcquireAsync( string name, TimeSpan timeout = default, @@ -29,12 +22,11 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency { Check.NotNullOrWhiteSpace(name, nameof(name)); var key = DistributedLockKeyNormalizer.NormalizeKey(name); - - var timeoutReleaser = await _localSyncObjects.LockOrNullAsync(key, timeout, cancellationToken); - if (timeoutReleaser is not null) + var disposable = await KeyedLock.TryLockAsync(key, timeout, cancellationToken); + if (disposable == null) { - return new LocalAbpDistributedLockHandle(timeoutReleaser); + return null; } - return null; + return new LocalAbpDistributedLockHandle(disposable); } } diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs new file mode 100644 index 0000000000..165aebc64a --- /dev/null +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.DistributedLocking; + +/// +/// This implementation of does not provide any distributed locking functionality. +/// Useful in scenarios where distributed locking is not required or during testing. +/// +public class NullAbpDistributedLock : IAbpDistributedLock +{ + public Task TryAcquireAsync(string name, TimeSpan timeout = default, CancellationToken cancellationToken = default) + { + return Task.FromResult(new LocalAbpDistributedLockHandle(NullDisposable.Instance)); + } +} 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.Features/Volo/Abp/Features/FeatureChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs index 04eef96e6c..39f1cf5be8 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs @@ -28,7 +28,12 @@ public class FeatureChecker : FeatureCheckerBase public override async Task GetOrNullAsync(string name) { - var featureDefinition = await FeatureDefinitionManager.GetAsync(name); + var featureDefinition = await FeatureDefinitionManager.GetOrNullAsync(name); + if (featureDefinition == null) + { + return null; + } + var providers = FeatureValueProviderManager.ValueProviders .Reverse(); diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs index 2b0bdfcf9f..90c0527067 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs @@ -23,7 +23,12 @@ public class SettingProvider : ISettingProvider, ITransientDependency public virtual async Task GetOrNullAsync(string name) { - var setting = await SettingDefinitionManager.GetAsync(name); + var setting = await SettingDefinitionManager.GetOrNullAsync(name); + if (setting == null) + { + return null; + } + var providers = Enumerable .Reverse(SettingValueProviderManager.Providers); 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.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs new file mode 100644 index 0000000000..1477bd7dd5 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs @@ -0,0 +1,179 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Threading; + +public class KeyedLock_Tests +{ + [Fact] + public async Task TryLock_Should_Acquire_Immediately_When_Free() + { + var key = "key-try-1"; + var handle = await KeyedLock.TryLockAsync(key); + handle.ShouldNotBeNull(); + handle!.Dispose(); + + var handle2 = await KeyedLock.TryLockAsync(key); + handle2.ShouldNotBeNull(); + handle2!.Dispose(); + } + + [Fact] + public async Task TryLock_Should_Return_Null_When_Already_Locked() + { + var key = "key-try-2"; + using (await KeyedLock.LockAsync(key)) + { + var handle2 = await KeyedLock.TryLockAsync(key); + handle2.ShouldBeNull(); + } + + var handle3 = await KeyedLock.TryLockAsync(key); + handle3.ShouldNotBeNull(); + handle3!.Dispose(); + } + + [Fact] + public async Task LockAsync_Should_Block_Until_Released() + { + var key = "key-block-1"; + var sw = Stopwatch.StartNew(); + + Task inner; + using (await KeyedLock.LockAsync(key)) + { + inner = Task.Run(async () => + { + using (await KeyedLock.LockAsync(key)) + { + // Acquired only after outer lock is released + } + }); + + // While holding the outer lock, inner waiter should not complete + await Task.Delay(200); + inner.IsCompleted.ShouldBeFalse(); + } + + // After releasing, inner should complete; elapsed >= hold time + await inner; + sw.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(180); + } + + [Fact] + public async Task TryLock_With_Timeout_Should_Return_Null_When_Busy() + { + var key = "key-timeout-1"; + using (await KeyedLock.LockAsync(key)) + { + var handle = await KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(50)); + handle.ShouldBeNull(); + } + } + + [Fact] + public async Task TryLock_With_Timeout_Should_Succeed_If_Released_In_Time() + { + var key = "key-timeout-2"; + // Hold the lock manually + var outer = await KeyedLock.LockAsync(key); + var tryTask = KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(200)); + await Task.Delay(50); + // Release within the timeout window + outer.Dispose(); + var handle2 = await tryTask; + handle2.ShouldNotBeNull(); + handle2!.Dispose(); + } + + [Fact] + public async Task LockAsync_With_Cancellation_Should_Rollback_RefCount() + { + var key = "key-cancel-1"; + var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + await Should.ThrowAsync(async () => + { + await KeyedLock.LockAsync(key, cts.Token); + }); + + // After cancellation, we should still be able to acquire the key + var handle = await KeyedLock.TryLockAsync(key); + handle.ShouldNotBeNull(); + handle!.Dispose(); + } + + [Fact] + public async Task TryLock_With_Cancellation_Should_Rollback() + { + var key = "key-cancel-2"; + // Ensure it's initially free + var h0 = await KeyedLock.TryLockAsync(key); + h0?.Dispose(); + + var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + await Should.ThrowAsync(async () => + { + await KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(200), cts.Token); + }); + + // After cancellation, the key should be acquirable + var handle = await KeyedLock.TryLockAsync(key); + handle.ShouldNotBeNull(); + handle!.Dispose(); + } + + [Fact] + public async Task Serializes_Access_For_Same_Key() + { + var key = "key-serial-1"; + int counter = 0; + var tasks = Enumerable.Range(0, 10).Select(async _ => + { + using (await KeyedLock.LockAsync(key)) + { + var current = counter; + await Task.Delay(10); + counter = current + 1; + } + }); + + await Task.WhenAll(tasks); + counter.ShouldBe(10); + } + + [Fact] + public async Task Multiple_Keys_Should_Not_Block_Each_Other() + { + var key1 = "key-multi-1"; + var key2 = "key-multi-2"; + + using (await KeyedLock.LockAsync(key1)) + { + var handle2 = await KeyedLock.TryLockAsync(key2); + handle2.ShouldNotBeNull(); + handle2!.Dispose(); + } + } + + [Fact] + public async Task TryLock_Default_Overload_Delegates_To_Full_Overload() + { + var key = "key-default-1"; + using (await KeyedLock.LockAsync(key)) + { + var h1 = await KeyedLock.TryLockAsync(key); + h1.ShouldBeNull(); + } + + var h2 = await KeyedLock.TryLockAsync(key); + h2.ShouldNotBeNull(); + h2!.Dispose(); + } +} 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/AbpFeatureManagementDomainModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs index 95d892692e..1c465817f2 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs @@ -25,7 +25,6 @@ namespace Volo.Abp.FeatureManagement; public class AbpFeatureManagementDomainModule : AbpModule { private readonly CancellationTokenSource _cancellationTokenSource = new(); - private Task _initializeDynamicFeaturesTask; public override void ConfigureServices(ServiceConfigurationContext context) { @@ -65,7 +64,6 @@ public class AbpFeatureManagementDomainModule : AbpModule var rootServiceProvider = context.ServiceProvider.GetRequiredService(); var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(true, _cancellationTokenSource.Token); - _initializeDynamicFeaturesTask = initializer.GetInitializationTask(); } public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) @@ -73,9 +71,4 @@ public class AbpFeatureManagementDomainModule : AbpModule _cancellationTokenSource.Cancel(); return Task.CompletedTask; } - - public Task GetInitializeDynamicFeaturesTask() - { - return _initializeDynamicFeaturesTask ?? Task.CompletedTask; - } } 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 9f6820274c..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; @@ -16,38 +15,22 @@ namespace Volo.Abp.FeatureManagement; public class FeatureDynamicInitializer : ITransientDependency { - private Task _initializeDynamicFeaturesTask; - 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) { @@ -56,40 +39,38 @@ public class FeatureDynamicInitializer : ITransientDependency if (runInBackground) { - _initializeDynamicFeaturesTask = Task.Run(async () => + 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); return Task.CompletedTask; } - _initializeDynamicFeaturesTask = ExecuteInitializationAsync(options, cancellationToken); - return _initializeDynamicFeaturesTask; - } - - public virtual Task GetInitializationTask() - { - return _initializeDynamicFeaturesTask ?? Task.CompletedTask; + 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; } @@ -112,6 +93,8 @@ public class FeatureDynamicInitializer : ITransientDependency return; } + var staticFeatureSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -126,7 +109,7 @@ public class FeatureDynamicInitializer : ITransientDependency { try { - await StaticFeatureSaver.SaveAsync(); + await staticFeatureSaver.SaveAsync(); } catch (Exception ex) { @@ -136,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/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs index c370e5f558..71254e009f 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.Sqlite; using Volo.Abp.Modularity; @@ -53,15 +54,8 @@ public class AbpFeatureManagementEntityFrameworkCoreTestModule : AbpModule public override void OnApplicationInitialization(ApplicationInitializationContext context) { - var task = context.ServiceProvider.GetRequiredService().GetInitializeDynamicFeaturesTask(); - if (!task.IsCompleted) - { - AsyncHelper.RunSync(() => Awaited(task)); - } - } - - private async static Task Awaited(Task task) - { - await task; + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var initializer = rootServiceProvider.GetRequiredService(); + AsyncHelper.RunSync(() => initializer.InitializeAsync(false)); } } 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/Components/PermissionManagementModal.razor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs index 2a618b9d4b..b3c917177b 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs @@ -6,6 +6,7 @@ using Blazorise; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Components.Web.Configuration; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Localization; using Volo.Abp.PermissionManagement.Localization; @@ -153,7 +154,13 @@ public partial class PermissionManagementModal await PermissionAppService.UpdateAsync(_providerName, _providerKey, updateDto); - await CurrentApplicationConfigurationCacheResetService.ResetAsync(); + Guid? userId = null; + if (_providerName == UserPermissionValueProvider.ProviderName && Guid.TryParse(_providerKey, out var parsedUserId)) + { + userId = parsedUserId; + } + + await CurrentApplicationConfigurationCacheResetService.ResetAsync(userId); await InvokeAsync(_modal.Hide); await Notify.Success(L["SavedSuccessfully"]); 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/AbpPermissionManagementDomainModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs index e3825ff397..3029625ffb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,6 +12,7 @@ using Volo.Abp.Authorization.Permissions; using Volo.Abp.Caching; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; using Volo.Abp.Domain; using Volo.Abp.Json; using Volo.Abp.Modularity; @@ -26,10 +28,11 @@ namespace Volo.Abp.PermissionManagement; public class AbpPermissionManagementDomainModule : AbpModule { private readonly CancellationTokenSource _cancellationTokenSource = new(); - private Task _initializeDynamicPermissionsTask; public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.Replace(ServiceDescriptor.Singleton()); + if (context.Services.IsDataMigrationEnvironment()) { Configure(options => @@ -50,7 +53,6 @@ public class AbpPermissionManagementDomainModule : AbpModule var rootServiceProvider = context.ServiceProvider.GetRequiredService(); var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(true, _cancellationTokenSource.Token); - _initializeDynamicPermissionsTask = initializer.GetInitializationTask(); } public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) @@ -58,9 +60,4 @@ public class AbpPermissionManagementDomainModule : AbpModule _cancellationTokenSource.Cancel(); return Task.CompletedTask; } - - public Task GetInitializeDynamicPermissionsTask() - { - return _initializeDynamicPermissionsTask ?? Task.CompletedTask; - } } 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 e78885c07f..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; @@ -16,38 +15,22 @@ namespace Volo.Abp.PermissionManagement; public class PermissionDynamicInitializer : ITransientDependency { - private Task _initializeDynamicPermissionsTask; - 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) { @@ -56,40 +39,38 @@ public class PermissionDynamicInitializer : ITransientDependency if (runInBackground) { - _initializeDynamicPermissionsTask = Task.Run(async () => + 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); return Task.CompletedTask; } - _initializeDynamicPermissionsTask = ExecuteInitializationAsync(options, cancellationToken); - return _initializeDynamicPermissionsTask; - } - - public virtual Task GetInitializationTask() - { - return _initializeDynamicPermissionsTask ?? Task.CompletedTask; + 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; } @@ -112,6 +93,8 @@ public class PermissionDynamicInitializer : ITransientDependency return; } + var staticPermissionSaver = ServiceProvider.GetRequiredService(); + await Policy .Handle() .WaitAndRetryAsync( @@ -126,7 +109,7 @@ public class PermissionDynamicInitializer : ITransientDependency { try { - await StaticPermissionSaver.SaveAsync(); + await staticPermissionSaver.SaveAsync(); } catch (Exception ex) { @@ -137,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.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs index fc06819b47..c4b898e761 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs @@ -270,7 +270,7 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD } var currentGrantInfo = await GetInternalAsync(permission, resourceName, resourceKey, providerName, providerKey); - if (currentGrantInfo.IsGranted == isGranted) + if (currentGrantInfo.IsGranted == isGranted && currentGrantInfo.Providers.Any(x => x.Name == providerName && x.Key == providerKey)) { return; } 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/PermissionManagementModal.cshtml.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs index 2c9d2dcc1c..6f6d6113e9 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -6,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.EventBus.Local; using Volo.Abp.Localization; using Volo.Abp.PermissionManagement.Web.Utils; @@ -105,9 +107,13 @@ public class PermissionManagementModal : AbpPageModel } ); - await LocalEventBus.PublishAsync( - new CurrentApplicationConfigurationCacheResetEventData() - ); + Guid? userId = null; + if (ProviderName == UserPermissionValueProvider.ProviderName && Guid.TryParse(ProviderKey, out var parsedUserId)) + { + userId = parsedUserId; + } + + await LocalEventBus.PublishAsync(new CurrentApplicationConfigurationCacheResetEventData(userId)); return NoContent(); } @@ -130,7 +136,7 @@ public class PermissionManagementModal : AbpPageModel public bool IsDisabled(string currentProviderName) { var grantedProviders = Permissions.SelectMany(x => x.GrantedProviders); - + return Permissions.All(x => x.IsGranted) && grantedProviders.All(p => p.ProviderName != currentProviderName); } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml index 5717448c70..15a5c0bff4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml @@ -34,7 +34,7 @@ else { - + @@ -318,9 +279,8 @@
- Details + Details
@@ -340,4 +300,4 @@ border-left: 0 !important; } } - + \ No newline at end of file diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts index 44bf9f269e..aa25b046d7 100644 --- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts +++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts @@ -2,16 +2,24 @@ import { AuthService, LocalizationPipe } from '@abp/ng.core'; import { Component, inject } from '@angular/core'; import { NgTemplateOutlet } from '@angular/common'; import { ButtonComponent, CardBodyComponent, CardComponent } from '@abp/ng.theme.shared'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'app-home', templateUrl: './home.component.html', - imports: [NgTemplateOutlet, LocalizationPipe, CardComponent, CardBodyComponent, ButtonComponent], + imports: [ + NgTemplateOutlet, + LocalizationPipe, + CardComponent, + CardBodyComponent, + ButtonComponent, + RouterLink + ], }) export class HomeComponent { protected readonly authService = inject(AuthService); - loading = false; + get hasLoggedIn(): boolean { return this.authService.isAuthenticated; } diff --git a/npm/ng-packs/apps/dev-app/src/assets/form-config.json b/npm/ng-packs/apps/dev-app/src/assets/form-config.json new file mode 100644 index 0000000000..d6876b9e96 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/assets/form-config.json @@ -0,0 +1,73 @@ +[ + { + "key": "firstName", + "type": "text", + "label": "First Name", + "placeholder": "Enter first name", + "value": "erdemc", + "required": true, + "validators": [ + { "type": "required", "message": "First name is required" }, + { "type": "minLength", "value": 2, "message": "Minimum 2 characters required" } + ], + "gridSize": 6, + "order": 1 + }, + { + "key": "lastName", + "type": "text", + "label": "Last Name", + "placeholder": "Enter last name", + "required": true, + "validators": [{ "type": "required", "message": "Last name is required" }], + "gridSize": 12, + "order": 3 + }, + { + "key": "email", + "type": "email", + "label": "AbpAccount::EmailAddress", + "placeholder": "Enter email", + "required": true, + "validators": [ + { "type": "required", "message": "Email is required" }, + { "type": "email", "message": "Please enter a valid email" } + ], + "gridSize": 6, + "order": 2 + }, + { + "key": "userType", + "type": "select", + "label": "User Type", + "required": true, + "options": [ + { "key": "admin", "value": "Administrator" }, + { "key": "user", "value": "Regular User" }, + { "key": "guest", "value": "Guest User" } + ], + "validators": [{ "type": "required", "message": "Please select user type" }], + "order": 4 + }, + { + "key": "adminNotes", + "type": "textarea", + "label": "Admin Notes", + "placeholder": "Enter admin-specific notes", + "conditionalLogic": [ + { + "dependsOn": "userType", + "condition": "equals", + "value": "admin", + "action": "show" + } + ], + "order": 5 + }, + { + "key": "isSelected", + "type": "checkbox", + "label": "Is Selected", + "order": 6 + } +] diff --git a/npm/ng-packs/apps/dev-app/src/server.ts b/npm/ng-packs/apps/dev-app/src/server.ts index a8d7558341..fb45c1dec0 100644 --- a/npm/ng-packs/apps/dev-app/src/server.ts +++ b/npm/ng-packs/apps/dev-app/src/server.ts @@ -11,9 +11,7 @@ import {environment} from './environments/environment'; import * as oidc from 'openid-client'; import { ServerCookieParser } from '@abp/ng.core'; -if (environment.production === false) { - process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; -} +process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); 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 444df9187f..c95dd0026c 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", @@ -115,6 +120,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", @@ -139,6 +145,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/src/lib/components/personal-settings/personal-settings.component.ts b/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts index 53ebf25a39..731c589ba0 100644 --- a/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts @@ -87,7 +87,7 @@ export class PersonalSettingsComponent .subscribe(profile => { this.manageProfileState.setProfile(profile); this.configState.refreshAppState(); - this.toasterService.success('AbpAccount::PersonalSettingsSaved', 'Success', { life: 5000 }); + this.toasterService.success('AbpAccount::PersonalSettingsSaved', '', { life: 5000 }); if (isRefreshTokenExists) { return this.authService.refreshToken(); 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/ng-package.json b/npm/ng-packs/packages/components/dynamic-form/ng-package.json new file mode 100644 index 0000000000..e09fb3fd03 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "src/public-api.ts" + } +} 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 new file mode 100644 index 0000000000..56f32f6e0d --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts @@ -0,0 +1,135 @@ +import { + Component, + ViewChild, + ViewContainerRef, + ChangeDetectionStrategy, + forwardRef, + Type, + effect, + DestroyRef, + inject, + input, +} from '@angular/core'; +import { + ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule +} from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +type controlValueAccessorLike = Partial & { setDisabledState?(d: boolean): void }; +type acceptsFormControl = { formControl?: FormControl }; + +@Component({ + selector: 'abp-dynamic-form-field-host', + imports: [CommonModule, ReactiveFormsModule], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFieldHostComponent), + multi: true + }] +}) +export class DynamicFieldHostComponent implements ControlValueAccessor { + component = input>(); + inputs = input>({}); + + @ViewChild('vcRef', { read: ViewContainerRef, static: true }) viewContainerRef!: ViewContainerRef; + private componentRef?: any; + + private value: any; + private disabled = false; + + // if child has not implemented ControlValueAccessor. Create form control + private innerControl = new FormControl(null); + readonly destroyRef = inject(DestroyRef); + + constructor() { + effect(() => { + if (this.component()) { + this.createChild(); + } else if (this.componentRef && this.inputs()) { + this.applyInputs(); + } + }); + } + + private createChild() { + this.viewContainerRef.clear(); + if (!this.component()) return; + + this.componentRef = this.viewContainerRef.createComponent(this.component()); + this.applyInputs(); + + const instance: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl; + + if (this.isCVA(instance)) { + // Child CVA ise wrapper -> child delege + instance.registerOnChange?.((v: any) => this.onChange(v)); + instance.registerOnTouched?.(() => this.onTouched()); + if (this.disabled && instance.setDisabledState) { + instance.setDisabledState(true); + } + // set initial value + if (this.value !== undefined) { + instance.writeValue?.(this.value); + } + } else { + // No CVA -> use form control + if ('formControl' in instance) { + instance.formControl = this.innerControl; + // apply initial value/disabled state + if (this.value !== undefined) { + this.innerControl.setValue(this.value, { emitEvent: false }); + } + this.innerControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(v => this.onChange(v)); + this.innerControl.disabled ? null : (this.disabled && this.innerControl.disable({ emitEvent: false })); + } + } + } + + private applyInputs() { + if (!this.componentRef) return; + const inst = this.componentRef.instance; + for (const [k, v] of Object.entries(this.inputs ?? {})) { + inst[k] = v; + } + this.componentRef.changeDetectorRef?.markForCheck?.(); + } + + private isCVA(obj: any): obj is controlValueAccessorLike { + return obj && typeof obj.writeValue === 'function' && typeof obj.registerOnChange === 'function'; + } + + writeValue(obj: any): void { + this.value = obj; + if (!this.componentRef) return; + + const inst: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl; + + if (this.isCVA(inst)) { + inst.writeValue?.(obj); + } else if ('formControl' in inst && inst.formControl instanceof FormControl) { + inst.formControl.setValue(obj, { emitEvent: false }); + } + } + + private onChange: (v: any) => void = () => {}; + private onTouched: () => void = () => {}; + + registerOnChange(fn: any): void { this.onChange = fn; } + registerOnTouched(fn: any): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (!this.componentRef) return; + + const inst = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl; + + if (this.isCVA(inst) && inst.setDisabledState) { + inst.setDisabledState(isDisabled); + } else if ('formControl' in inst && inst.formControl instanceof FormControl) { + isDisabled ? inst.formControl.disable({ emitEvent: false }) : inst.formControl.enable({ emitEvent: false }); + } + } +} 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 new file mode 100644 index 0000000000..ed33dca227 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.html @@ -0,0 +1,371 @@ +@if (visible()) { +
+ + + + + @if (field().type === 'text') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'select') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'checkbox') { + +
+ + @if (isInvalid) { + + } +
+ } @else if (field().type === 'email') { + +
+ + + @if (isInvalid) { + + } +
+ } @else if (field().type === 'textarea') { + +
+ + + @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) { + + } +
+ } +
+} + + + + + + + + \ No newline at end of file diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss new file mode 100644 index 0000000000..12870a4141 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.scss @@ -0,0 +1,12 @@ +// Minimal styling - rely on Bootstrap/Lepton-X theme styles +.form-group { + display: flex; + flex-direction: column; + + // Radio group spacing (layout only) + .radio-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.ts new file mode 100644 index 0000000000..5302d5cdb9 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field.component.ts @@ -0,0 +1,180 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + inject, + InjectionToken, Injector, + input, + OnInit, +} from '@angular/core'; +import { FormFieldConfig } from '../dynamic-form.models'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormControlName, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgControl, + FormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NgTemplateOutlet, AsyncPipe } from '@angular/common'; +import { LocalizationPipe } from '@abp/ng.core'; +import { FormCheckboxComponent } from '@abp/ng.theme.shared'; +import { Observable, of } from 'rxjs'; +import { DynamicFormService } from '../dynamic-form.service'; + +export const ABP_DYNAMIC_FORM_FIELD = new InjectionToken('AbpDynamicFormField'); + +const DYNAMIC_FORM_FIELD_CONTROL_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormFieldComponent), + multi: true, +}; + +@Component({ + selector: 'abp-dynamic-form-field', + templateUrl: './dynamic-form-field.component.html', + styleUrls: ['./dynamic-form-field.component.scss'], + providers: [ + { provide: ABP_DYNAMIC_FORM_FIELD, useExisting: DynamicFormFieldComponent }, + DYNAMIC_FORM_FIELD_CONTROL_VALUE_ACCESSOR, + ], + host: { class: 'abp-dynamic-form-field' }, + exportAs: 'abpDynamicFormField', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgTemplateOutlet, LocalizationPipe, ReactiveFormsModule, FormCheckboxComponent, AsyncPipe], +}) +export class DynamicFormFieldComponent 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); + private dynamicFormService = inject(DynamicFormService); + + options$: Observable<{ key: string; value: any }[]> = of([]); + + // Accessibility: Generate unique IDs for ARIA + get fieldId(): string { + return `field-${this.field().key}`; + } + + get errorId(): string { + return `${this.fieldId}-error`; + } + + get helpTextId(): string { + return `${this.fieldId}-help`; + } + + 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); + }); + + const options = this.field().options; + + if (options?.url) { + this.options$ = this.dynamicFormService.getOptions(options.url, options.apiName); + } else if (options?.defaultValues?.length) { + this.options$ = of( + options.defaultValues.map(item => { + return { + key: item[options.valueProp || 'key'] || item, + value: item[options.labelProp || 'value'] || item + }; + }) + ); + } else { + this.options$ = of([]); + } + } + + 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?.errors) return []; + if (this.control && this.control.errors) { + const errorKeys = Object.keys(this.control.errors); + const validators = this.field().validators || []; + return errorKeys.map(key => { + const validator = validators.find( + v => v.type.toLowerCase() === key.toLowerCase(), + ); + 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.`; + }); + } + return []; + } + get value() { + return this.fieldFormGroup.get('value'); + } + + onFileChange(event: Event) { + const input = event.target as HTMLInputElement; + if (input.files) { + const files = Array.from(input.files); + const value = this.field().multiple ? files : files[0]; + this.value.setValue(value); + this.onChange(value); + } + } + + private onChange: (value: any) => void = () => { }; + private onTouched: () => void = () => { }; +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts new file mode 100644 index 0000000000..826f7c70d2 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/index.ts @@ -0,0 +1,2 @@ +export * from './dynamic-form-field.component'; +export * from './dynamic-form-field-host.component'; diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.html new file mode 100644 index 0000000000..1222faa58a --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.html @@ -0,0 +1,36 @@ +@if (visible()) { +
+ + {{ groupConfig().label | abpLocalization }} + + +
+ @for (field of sortedChildren; track field.key) { +
+ + + @if (field.type === 'group') { + + } + + + @else if (field.type === 'array') { + + } + + + @else { + + } + +
+ } +
+
+} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.scss new file mode 100644 index 0000000000..b98d92458c --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.scss @@ -0,0 +1,26 @@ +.form-group-container { + border-left: 3px solid var(--bs-primary, #007bff); + padding-left: 1rem; + margin-bottom: 1.5rem; + border: 1px solid var(--bs-border-color, #dee2e6); + border-radius: 0.375rem; + padding: 1rem; + background-color: var(--bs-light, #f8f9fa); + + // Nested groups get lighter styling + .form-group-container { + border-left-color: var(--bs-secondary, #6c757d); + padding-left: 0.75rem; + background-color: var(--bs-white, #fff); + } +} + +.form-group-legend { + font-size: 1.1rem; + font-weight: 600; + color: var(--bs-primary, #007bff); + margin-bottom: 1rem; + padding: 0 0.5rem; + float: none; + width: auto; +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.ts new file mode 100644 index 0000000000..ab9cb0f3ad --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/dynamic-form-group.component.ts @@ -0,0 +1,45 @@ +import { + ChangeDetectionStrategy, + Component, + input, + forwardRef, +} from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { FormFieldConfig } from '../dynamic-form.models'; +import { LocalizationPipe } from '@abp/ng.core'; +import { DynamicFormFieldComponent } from '../dynamic-form-field'; +import { DynamicFormArrayComponent } from '../dynamic-form-array'; + +@Component({ + selector: 'abp-dynamic-form-group', + templateUrl: './dynamic-form-group.component.html', + styleUrls: ['./dynamic-form-group.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + CommonModule, + ReactiveFormsModule, + LocalizationPipe, + DynamicFormFieldComponent, + forwardRef(() => DynamicFormArrayComponent), + forwardRef(() => DynamicFormGroupComponent), // Self reference for recursion + ], +}) +export class DynamicFormGroupComponent { + groupConfig = input.required(); + formGroup = input.required(); + visible = input(true); + + get sortedChildren(): FormFieldConfig[] { + const children = this.groupConfig().children || []; + return children.sort((a, b) => (a.order || 0) - (b.order || 0)); + } + + getChildFormGroup(key: string): FormGroup { + return this.formGroup().get(key) as FormGroup; + } + + getChildControl(key: string) { + return this.formGroup().get(key); + } +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/index.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/index.ts new file mode 100644 index 0000000000..899c3e295e --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-group/index.ts @@ -0,0 +1 @@ +export * from './dynamic-form-group.component'; diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.html b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.html new file mode 100644 index 0000000000..17bce17b1f --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.html @@ -0,0 +1,78 @@ +
+
+
+ @for (field of sortedFields; track field.key) { +
+ + + @if (field.component) { + + + } + + + @else if (field.type === 'group') { + + + } + + + @else if (field.type === 'array') { + + + } + + + @else { + + + } + +
+ } +
+ + + + +
+ + +
+ @if (showCancelButton()) { + + } + +
+
+
diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.scss b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.scss new file mode 100644 index 0000000000..038d8eed94 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.scss @@ -0,0 +1,15 @@ +:host(.abp-dynamic-form) { + form { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .form-wrapper { + text-align: left; + } +} +.form-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.ts new file mode 100644 index 0000000000..9e03cc0a2e --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.component.ts @@ -0,0 +1,203 @@ +import { + ChangeDetectionStrategy, + Component, + input, + output, + inject, + OnInit, + DestroyRef, + ChangeDetectorRef, +} from '@angular/core'; +import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DynamicFormService } from './dynamic-form.service'; +import { ConditionalAction, FormFieldConfig } from './dynamic-form.models'; +import { DynamicFormFieldComponent, DynamicFieldHostComponent } from './dynamic-form-field'; +import { DynamicFormGroupComponent } from './dynamic-form-group'; +import { DynamicFormArrayComponent } from './dynamic-form-array'; + +@Component({ + selector: 'abp-dynamic-form', + templateUrl: './dynamic-form.component.html', + styleUrls: ['./dynamic-form.component.scss'], + host: { class: 'abp-dynamic-form' }, + changeDetection: ChangeDetectionStrategy.OnPush, + exportAs: 'abpDynamicForm', + imports: [ + CommonModule, + DynamicFormFieldComponent, + DynamicFormGroupComponent, + DynamicFormArrayComponent, + ReactiveFormsModule, + DynamicFieldHostComponent, + ], +}) +export class DynamicFormComponent implements OnInit { + fields = input([]); + values = input>(); + submitButtonText = input('Submit'); + submitInProgress = input(false); + showCancelButton = input(false); + onSubmit = output(); + formCancel = output(); + private dynamicFormService = inject(DynamicFormService); + readonly destroyRef = inject(DestroyRef); + readonly changeDetectorRef = inject(ChangeDetectorRef); + + dynamicForm!: FormGroup; + fieldVisibility: { [key: string]: boolean } = {}; + + ngOnInit() { + this.setupFormAndLogic(); + } + + get sortedFields(): FormFieldConfig[] { + return this.fields().sort((a, b) => (a.order || 0) - (b.order || 0)); + } + + submit() { + if (this.dynamicForm.valid) { + this.onSubmit.emit(this.dynamicForm.getRawValue()); + } else { + this.markAllFieldsAsTouched(); + this.focusFirstInvalidField(); + } + } + + onCancel() { + this.formCancel.emit(); + } + + onFieldChange(event: { fieldKey: string; value: any }) { + this.evaluateConditionalLogic(event.fieldKey); + } + + isFieldVisible(field: FormFieldConfig): boolean { + return this.fieldVisibility[field.key] !== false; + } + + getChildFormGroup(key: string): FormGroup { + return this.dynamicForm.get(key) as FormGroup; + } + + resetForm() { + const initialValues: { [key: string]: any } = this.dynamicFormService.getInitialValues( + this.fields(), + ); + this.dynamicForm.reset({ ...initialValues }); + this.dynamicForm.markAsUntouched(); + this.dynamicForm.markAsPristine(); + this.changeDetectorRef.markForCheck(); + } + + private initializeFieldVisibility() { + this.fields().forEach(field => { + this.fieldVisibility = { + ...this.fieldVisibility, + [field.key]: !field.conditionalLogic?.length, + }; + }); + } + + private setupConditionalLogic() { + this.fields().forEach(field => { + if (field.conditionalLogic) { + field.conditionalLogic.forEach(rule => { + const dependentControl = this.dynamicForm.get(rule.dependsOn); + if (dependentControl) { + this.evaluateConditionalLogic(field.key); + dependentControl.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.evaluateConditionalLogic(field.key); + }); + } + }); + } + }); + } + + private evaluateConditionalLogic(fieldKey: string) { + const field = this.fields().find(f => f.key === fieldKey); + if (!field?.conditionalLogic) return; + + field.conditionalLogic.forEach(rule => { + const dependentValue = this.dynamicForm.get(rule.dependsOn)?.value; + const conditionMet = this.evaluateCondition(dependentValue, rule.condition, rule.value); + + this.applyConditionalAction(fieldKey, rule.action, conditionMet); + }); + } + + private evaluateCondition(fieldValue: any, condition: string, ruleValue: any): boolean { + switch (condition) { + case 'equals': + return fieldValue === ruleValue; + case 'notEquals': + return fieldValue !== ruleValue; + case 'contains': + return fieldValue && fieldValue.includes && fieldValue.includes(ruleValue); + case 'greaterThan': + return Number(fieldValue) > Number(ruleValue); + case 'lessThan': + return Number(fieldValue) < Number(ruleValue); + default: + return false; + } + } + + private applyConditionalAction(fieldKey: string, action: string, shouldApply: boolean) { + const control = this.dynamicForm.get(fieldKey); + + switch (action) { + case ConditionalAction.SHOW: + this.fieldVisibility = { ...this.fieldVisibility, [fieldKey]: shouldApply }; + break; + case ConditionalAction.HIDE: + this.fieldVisibility = { ...this.fieldVisibility, [fieldKey]: !shouldApply }; + break; + case ConditionalAction.ENABLE: + if (control) { + shouldApply ? control.enable() : control.disable(); + } + break; + case ConditionalAction.DISABLE: + if (control) { + shouldApply ? control.disable() : control.enable(); + } + break; + } + } + + private setupFormAndLogic() { + this.dynamicForm = this.dynamicFormService.createFormGroup(this.fields()); + this.initializeFieldVisibility(); + this.setupConditionalLogic(); + this.changeDetectorRef.markForCheck(); + } + + private markAllFieldsAsTouched() { + Object.keys(this.dynamicForm.controls).forEach(key => { + this.dynamicForm.get(key)?.markAsTouched(); + }); + } + + private focusFirstInvalidField() { + // Accessibility: Focus first invalid field for screen readers + const firstInvalidField = this.sortedFields.find(field => { + const control = this.dynamicForm.get(field.key); + return control && control.invalid && control.touched; + }); + + if (firstInvalidField) { + setTimeout(() => { + const element = document.getElementById(`field-${firstInvalidField.key}`); + if (element) { + element.focus(); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 100); + } + } +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.models.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.models.ts new file mode 100644 index 0000000000..864fc989f5 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.models.ts @@ -0,0 +1,60 @@ +import { Type } from '@angular/core'; +import { ControlValueAccessor } from '@angular/forms'; + +export interface FormFieldConfig { + key: string; + value?: any; + type: 'text' | 'email' | 'number' | 'select' | 'checkbox' | 'date' | 'textarea' | 'datetime-local' | 'time' | 'password' | 'tel' | 'url' | 'radio' | 'file' | 'range' | 'color' | 'group' | 'array'; + label: string; + placeholder?: string; + required?: boolean; + disabled?: boolean; + options?: OptionProps; + validators?: ValidatorConfig[]; + conditionalLogic?: ConditionalRule[]; + order?: number; + gridSize?: number; + component?: Type; + // Additional field attributes + min?: number | string; // For number, date, time, range + max?: number | string; // For number, date, time, range + step?: number | string; // For number, time, range + minLength?: number; // For text, password + maxLength?: number; // For text, password + pattern?: string; // For tel, text + accept?: string; // For file input (e.g., "image/*") + multiple?: boolean; // For file input + // Nested form support (for group and array types) + children?: FormFieldConfig[]; // Child fields for nested forms + minItems?: number; // For array type: minimum number of items + maxItems?: number; // For array type: maximum number of items +} + +export interface ValidatorConfig { + type: 'required' | 'email' | 'minLength' | 'maxLength' | 'pattern' | 'custom' | 'min' | 'max' | 'requiredTrue'; + value?: any; + message: string; +} + +export interface ConditionalRule { + dependsOn: string; + condition: 'equals' | 'notEquals' | 'contains' | 'greaterThan' | 'lessThan'; + value: any; + action: 'show' | 'hide' | 'enable' | 'disable'; +} + +export enum ConditionalAction { + SHOW = 'show', + HIDE = 'hide', + ENABLE = 'enable', + DISABLE = 'disable' +} + +export interface OptionProps { + defaultValues?: T[]; + url?: string; + disabled?: (option: T) => boolean; + labelProp?: string; + valueProp?: string; + apiName?: string; +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.service.ts b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.service.ts new file mode 100644 index 0000000000..0dbc302c20 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/dynamic-form.service.ts @@ -0,0 +1,119 @@ +import {Injectable, inject} from '@angular/core'; +import {FormControl, FormGroup, FormArray, ValidatorFn, Validators, FormBuilder} from '@angular/forms'; +import {FormFieldConfig, ValidatorConfig} from './dynamic-form.models'; +import { RestService } from '@abp/ng.core'; + +@Injectable({ + providedIn: 'root' +}) + +export class DynamicFormService { + + private formBuilder = inject(FormBuilder); + private restService = inject(RestService); + apiName = 'DynamicFormService'; + + createFormGroup(fields: FormFieldConfig[]): FormGroup { + const group: any = {}; + + fields.forEach(field => { + // Nested Group + if (field.type === 'group') { + group[field.key] = this.createFormGroup(field.children || []); + } + // Nested Array + else if (field.type === 'array') { + group[field.key] = this.createFormArray(field); + } + // Regular Field + else { + const validators = this.buildValidators(field.validators || []); + const initialValue = this.getInitialValue(field); + + group[field.key] = new FormControl({ + value: initialValue, + disabled: field.disabled || false + }, validators); + } + }); + + return this.formBuilder.group(group); + } + + createFormArray(arrayConfig: FormFieldConfig): FormArray { + const items: FormGroup[] = []; + const minItems = arrayConfig.minItems || 0; + + // Create minimum required items + for (let i = 0; i < minItems; i++) { + items.push(this.createFormGroup(arrayConfig.children || [])); + } + + return this.formBuilder.array(items); + } + + getInitialValues(fields: FormFieldConfig[]): any { + const initialValues: any = {}; + fields.forEach(field => { + if (field.type === 'group') { + initialValues[field.key] = this.getInitialValues(field.children || []); + } else if (field.type === 'array') { + initialValues[field.key] = []; + } else { + initialValues[field.key] = this.getInitialValue(field); + } + }); + return initialValues; + } + + getOptions(url: string, apiName?: string): any { + return this.restService.request({ + method: 'GET', + url, + }, + { apiName: apiName || this.apiName }); + } + + private buildValidators(validatorConfigs: ValidatorConfig[]): ValidatorFn[] { + return validatorConfigs.map(config => { + switch (config.type) { + case 'required': + return Validators.required; + case 'email': + return Validators.email; + case 'minLength': + return Validators.minLength(config.value); + case 'maxLength': + return Validators.maxLength(config.value); + case 'pattern': + return Validators.pattern(config.value); + case 'min': + return Validators.min(config.value); + case 'max': + return Validators.max(config.value); + case 'requiredTrue': + return Validators.requiredTrue; + default: + return Validators.nullValidator; + } + }); + } + + private getInitialValue(field: FormFieldConfig): any { + if (field.value !== undefined) { + return field.value; + } + switch (field.type) { + case 'checkbox': + return false; + case 'number': + return 0; + case 'group': + return this.getInitialValues(field.children || []); + case 'array': + return []; + default: + return ''; + } + } +} diff --git a/npm/ng-packs/packages/components/dynamic-form/src/public-api.ts b/npm/ng-packs/packages/components/dynamic-form/src/public-api.ts new file mode 100644 index 0000000000..f9dc670737 --- /dev/null +++ b/npm/ng-packs/packages/components/dynamic-form/src/public-api.ts @@ -0,0 +1,6 @@ +export * from './dynamic-form.component'; +export * from './dynamic-form-field'; +export * from './dynamic-form.models'; +export * from './dynamic-form.service'; +export * from './dynamic-form-group'; +export * from './dynamic-form-array'; diff --git a/npm/ng-packs/packages/components/jest.config.ts b/npm/ng-packs/packages/components/jest.config.ts index ff37b7e9e4..4a2a95bc72 100644 --- a/npm/ng-packs/packages/components/jest.config.ts +++ b/npm/ng-packs/packages/components/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: 'components', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html index e795025aec..5e09e264e8 100644 --- a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html +++ b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html @@ -1,45 +1,61 @@
@if (label()) { - + }
- + @if (displayValue() && !disabled()) { - + }
@if (showDropdown() && !disabled()) { -
- @if (isLoading()) { -
- - {{ 'AbpUi::Loading' | abpLocalization }} -
- } @else if (searchResults().length > 0) { - @for (item of searchResults(); track item.key) { - + } + } @else if (displayValue()) { + @if (noResultsTemplate()) { + + } @else { +
+ {{ 'AbpUi::NoDataAvailableInDatatable' | abpLocalization }} +
+ } } - - } - } @else if (displayValue()) { - @if (noResultsTemplate()) { - - } @else { -
- {{ 'AbpUi::NoRecordsFound' | abpLocalization }}
- } - } -
} -
\ No newline at end of file + diff --git a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss index 6fd1e2dc26..4b9a9ab12e 100644 --- a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss +++ b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss @@ -1,5 +1,8 @@ .abp-lookup-dropdown { - z-index: 1050; - max-height: 200px; - overflow-y: auto; + z-index: 1060; + max-height: 200px; + overflow-y: auto; + top: 100%; + margin-top: 0.25rem; + background-color: var(--lpx-content-bg); } diff --git a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts index 9633b15ab8..b78bfa19ce 100644 --- a/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts +++ b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts @@ -1,15 +1,15 @@ import { - Component, - input, - output, - model, - signal, - OnInit, - ChangeDetectionStrategy, - TemplateRef, - contentChild, - DestroyRef, - inject, + Component, + input, + output, + model, + signal, + OnInit, + ChangeDetectionStrategy, + TemplateRef, + contentChild, + DestroyRef, + inject, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; @@ -18,119 +18,123 @@ import { LocalizationPipe } from '@abp/ng.core'; import { Subject, Observable, debounceTime, distinctUntilChanged, of, finalize } from 'rxjs'; export interface LookupItem { - key: string; - displayName: string; - [key: string]: unknown; + key: string; + displayName: string; + [key: string]: unknown; } export type LookupSearchFn = (filter: string) => Observable; @Component({ - selector: 'abp-lookup-search', - templateUrl: './lookup-search.component.html', - styleUrl: './lookup-search.component.scss', - imports: [CommonModule, FormsModule, LocalizationPipe], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'abp-lookup-search', + templateUrl: './lookup-search.component.html', + styleUrl: './lookup-search.component.scss', + imports: [CommonModule, FormsModule, LocalizationPipe], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class LookupSearchComponent implements OnInit { - private readonly destroyRef = inject(DestroyRef); - - readonly label = input(); - readonly placeholder = input(''); - readonly debounceTime = input(300); - readonly minSearchLength = input(0); - readonly displayKey = input('displayName' as keyof T); - readonly valueKey = input('key' as keyof T); - readonly disabled = input(false); - - readonly searchFn = input>(() => of([])); - - readonly selectedValue = model(''); - readonly displayValue = model(''); - - readonly itemSelected = output(); - readonly searchChanged = output(); - - readonly itemTemplate = contentChild>('itemTemplate'); - readonly noResultsTemplate = contentChild>('noResultsTemplate'); - - readonly searchResults = signal([]); - readonly showDropdown = signal(false); - readonly isLoading = signal(false); - - private readonly searchSubject = new Subject(); - - ngOnInit() { - this.searchSubject.pipe( - debounceTime(this.debounceTime()), - distinctUntilChanged(), - takeUntilDestroyed(this.destroyRef) - ).subscribe(filter => { - this.performSearch(filter); - }); - } - - onSearchInput(filter: string) { - this.displayValue.set(filter); - this.showDropdown.set(true); - this.searchChanged.emit(filter); - - if (filter.length >= this.minSearchLength()) { - this.searchSubject.next(filter); - } else { - this.searchResults.set([]); - } - } - - onSearchFocus() { - this.showDropdown.set(true); - const currentFilter = this.displayValue() || ''; - if (currentFilter.length >= this.minSearchLength()) { - this.performSearch(currentFilter); - } - } - - onSearchBlur(event: FocusEvent) { - const relatedTarget = event.relatedTarget as HTMLElement; - if (!relatedTarget?.closest('.abp-lookup-dropdown')) { - this.showDropdown.set(false); - } - } - - selectItem(item: T) { - const displayKeyValue = String(item[this.displayKey()] ?? ''); - const valueKeyValue = String(item[this.valueKey()] ?? ''); - - this.displayValue.set(displayKeyValue); - this.selectedValue.set(valueKeyValue); - this.searchResults.set([]); - this.showDropdown.set(false); - this.itemSelected.emit(item); - } - - clearSelection() { - this.displayValue.set(''); - this.selectedValue.set(''); - this.searchResults.set([]); + private readonly destroyRef = inject(DestroyRef); + + readonly label = input(); + readonly placeholder = input(''); + readonly debounceTime = input(300); + readonly minSearchLength = input(0); + readonly displayKey = input('displayName' as keyof T); + readonly valueKey = input('key' as keyof T); + readonly disabled = input(false); + + readonly searchFn = input>(() => of([])); + + readonly selectedValue = model(''); + readonly displayValue = model(''); + + readonly itemSelected = output(); + readonly searchChanged = output(); + + readonly itemTemplate = contentChild>('itemTemplate'); + readonly noResultsTemplate = contentChild>('noResultsTemplate'); + + readonly searchResults = signal([]); + readonly showDropdown = signal(true); + readonly isLoading = signal(false); + + private readonly searchSubject = new Subject(); + + ngOnInit() { + this.searchSubject + .pipe( + debounceTime(this.debounceTime()), + distinctUntilChanged(), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(filter => { + this.performSearch(filter); + }); + } + + onSearchInput(filter: string) { + this.displayValue.set(filter); + this.showDropdown.set(true); + this.searchChanged.emit(filter); + + if (filter.length >= this.minSearchLength()) { + this.searchSubject.next(filter); + } else { + this.searchResults.set([]); } + } - private performSearch(filter: string) { - this.isLoading.set(true); - - this.searchFn()(filter).pipe( - takeUntilDestroyed(this.destroyRef), - finalize(() => this.isLoading.set(false)) - ).subscribe({ - next: results => { - this.searchResults.set(results); - }, - error: () => { - this.searchResults.set([]); - } - }); + onSearchFocus() { + this.showDropdown.set(true); + const currentFilter = this.displayValue() || ''; + if (currentFilter.length >= this.minSearchLength()) { + this.performSearch(currentFilter); } + } - getDisplayValue(item: T): string { - return String(item[this.displayKey()] ?? item[this.valueKey()] ?? ''); + onSearchBlur(event: FocusEvent) { + const relatedTarget = event.relatedTarget as HTMLElement; + if (!relatedTarget?.closest('.abp-lookup-dropdown')) { + this.showDropdown.set(false); } + } + + selectItem(item: T) { + const displayKeyValue = String(item[this.displayKey()] ?? ''); + const valueKeyValue = String(item[this.valueKey()] ?? ''); + + this.displayValue.set(displayKeyValue); + this.selectedValue.set(valueKeyValue); + this.searchResults.set([]); + this.showDropdown.set(false); + this.itemSelected.emit(item); + } + + clearSelection() { + this.displayValue.set(''); + this.selectedValue.set(''); + this.searchResults.set([]); + } + + private performSearch(filter: string) { + this.isLoading.set(true); + + this.searchFn()(filter) + .pipe( + takeUntilDestroyed(this.destroyRef), + finalize(() => this.isLoading.set(false)), + ) + .subscribe({ + next: results => { + this.searchResults.set(results); + }, + error: () => { + this.searchResults.set([]); + }, + }); + } + + getDisplayValue(item: T): string { + return String(item[this.displayKey()] ?? item[this.valueKey()] ?? ''); + } } diff --git a/npm/ng-packs/packages/components/package.json b/npm/ng-packs/packages/components/package.json index 1fb6de5e0c..f991cb13d4 100644 --- a/npm/ng-packs/packages/components/package.json +++ b/npm/ng-packs/packages/components/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.components", - "version": "10.0.1", + "version": "10.1.0-rc.2", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "peerDependencies": { - "@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": { "chart.js": "^3.5.1", diff --git a/npm/ng-packs/packages/components/project.json b/npm/ng-packs/packages/components/project.json index 671fabe461..d80db9b26d 100644 --- a/npm/ng-packs/packages/components/project.json +++ b/npm/ng-packs/packages/components/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/components"], - "options": { - "jestConfig": "packages/components/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/components" + } } } } diff --git a/npm/ng-packs/packages/components/tsconfig.lib.json b/npm/ng-packs/packages/components/tsconfig.lib.json index 7dde5f04bf..e4e2e20714 100644 --- a/npm/ng-packs/packages/components/tsconfig.lib.json +++ b/npm/ng-packs/packages/components/tsconfig.lib.json @@ -13,7 +13,19 @@ "exclude": [ "src/test-setup.ts", "src/**/*.spec.ts", - "jest.config.ts" + "jest.config.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/test-setup.ts" ], "include": ["src/**/*.ts"] } diff --git a/npm/ng-packs/packages/components/tsconfig.spec.json b/npm/ng-packs/packages/components/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/components/tsconfig.spec.json +++ b/npm/ng-packs/packages/components/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/components/vitest.config.mts b/npm/ng-packs/packages/components/vitest.config.mts new file mode 100644 index 0000000000..2037268556 --- /dev/null +++ b/npm/ng-packs/packages/components/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/components', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'components', + 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/components', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/core/.eslintrc.json b/npm/ng-packs/packages/core/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/core/.eslintrc.json +++ b/npm/ng-packs/packages/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/core/jest.config.ts b/npm/ng-packs/packages/core/jest.config.ts deleted file mode 100644 index 3dda0ce769..0000000000 --- a/npm/ng-packs/packages/core/jest.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'core', - preset: '../../jest.preset.js', - setupFilesAfterEnv: ['/src/test-setup.ts'], - globals: {}, - coverageDirectory: '../../coverage/packages/core', - transform: { - '^.+.(ts|mjs|js|html)$': [ - 'jest-preset-angular', - { - tsconfig: '/tsconfig.spec.json', - stringifyContentPathRegex: '\\.(html|svg)$', - }, - ], - }, - transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], -}; diff --git a/npm/ng-packs/packages/core/package.json b/npm/ng-packs/packages/core/package.json index 6b50f3f937..da1307759c 100644 --- a/npm/ng-packs/packages/core/package.json +++ b/npm/ng-packs/packages/core/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.core", - "version": "10.0.1", + "version": "10.1.0-rc.2", "homepage": "https://abp.io", "repository": { "type": "git", "url": "https://github.com/abpframework/abp.git" }, "dependencies": { - "@abp/utils": "~10.0.1", + "@abp/utils": "~10.1.0-rc.2", "just-clone": "^6.0.0", "just-compare": "^2.0.0", "ts-toolbelt": "^9.0.0", diff --git a/npm/ng-packs/packages/core/project.json b/npm/ng-packs/packages/core/project.json index a6b7789814..69bbb4f572 100644 --- a/npm/ng-packs/packages/core/project.json +++ b/npm/ng-packs/packages/core/project.json @@ -22,16 +22,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/core"], - "options": { - "jestConfig": "packages/core/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/core" + } } } } diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index 3d77d1bd8f..81f9305f68 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -3,9 +3,6 @@ import { inject, input, isDevMode, - OnInit, - Optional, - SkipSelf, Type, } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; diff --git a/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts index 7b3bd0319b..f26c12b706 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts @@ -1,16 +1,16 @@ -import { - Directive, - EmbeddedViewRef, - Input, - IterableChangeRecord, - IterableChanges, - IterableDiffer, - IterableDiffers, - OnChanges, - TemplateRef, - TrackByFunction, - ViewContainerRef, - inject +import { + Directive, + EmbeddedViewRef, + Input, + IterableChangeRecord, + IterableChanges, + IterableDiffer, + IterableDiffers, + OnChanges, + TemplateRef, + TrackByFunction, + ViewContainerRef, + inject, } from '@angular/core'; import clone from 'just-clone'; import compare from 'just-compare'; @@ -67,6 +67,7 @@ export class ForDirective implements OnChanges { emptyRef?: TemplateRef; private differ!: IterableDiffer | null; + private lastItemsRef: any[] | null = null; private isShowEmptyRef!: boolean; @@ -136,6 +137,7 @@ export class ForDirective implements OnChanges { this.vcRef.createEmbeddedView(this.emptyRef).rootNodes; this.isShowEmptyRef = true; this.differ = null; + this.lastItemsRef = null; return; } @@ -169,6 +171,14 @@ export class ForDirective implements OnChanges { } ngOnChanges() { + if (!this.items) return; + + // Recreate differ if items array reference changed + if (this.lastItemsRef !== this.items) { + this.differ = null; + this.lastItemsRef = this.items; + } + let items = clone(this.items) as any[]; if (!Array.isArray(items)) return; diff --git a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts index 50b805dba4..2c17885984 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts @@ -1,13 +1,13 @@ -import { - AfterViewInit, - ChangeDetectorRef, - Directive, - Input, - OnChanges, - OnDestroy, - TemplateRef, - ViewContainerRef, - inject +import { + AfterViewInit, + ChangeDetectorRef, + Directive, + Input, + OnChanges, + OnDestroy, + TemplateRef, + ViewContainerRef, + inject, } from '@angular/core'; import { ReplaySubject, Subscription } from 'rxjs'; import { distinctUntilChanged, take } from 'rxjs/operators'; @@ -19,7 +19,7 @@ import { QueueManager } from '../utils/queue'; selector: '[abpPermission]', }) export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit { - private templateRef = inject>(TemplateRef, { optional: true })!; + private templateRef = inject>(TemplateRef, { optional: true }); private vcRef = inject(ViewContainerRef); private permissionService = inject(PermissionService); private cdRef = inject(ChangeDetectorRef); @@ -45,7 +45,9 @@ export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit .pipe(distinctUntilChanged()) .subscribe(isGranted => { this.vcRef.clear(); - if (isGranted) this.vcRef.createEmbeddedView(this.templateRef); + if (isGranted && this.templateRef) { + this.vcRef.createEmbeddedView(this.templateRef); + } if (this.runChangeDetection) { if (!this.rendered) { this.cdrSubject.next(); diff --git a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts index 3b77913d2e..41e07f8d1b 100644 --- a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts +++ b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts @@ -8,10 +8,10 @@ import { } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { Observable, of } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { AuthService, IAbpGuard } from '../abstracts'; import { findRoute, getRoutePath } from '../utils/route-utils'; -import { RoutesService, PermissionService, HttpErrorReporterService } from '../services'; +import { RoutesService, PermissionService, HttpErrorReporterService, ConfigStateService } from '../services'; import { isPlatformServer } from '@angular/common'; /** * @deprecated Use `permissionGuard` *function* instead. @@ -25,6 +25,7 @@ export class PermissionGuard implements IAbpGuard { protected readonly authService = inject(AuthService); protected readonly permissionService = inject(PermissionService); protected readonly httpErrorReporter = inject(HttpErrorReporterService); + protected readonly configStateService = inject(ConfigStateService); canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { let { requiredPolicy } = route.data || {}; @@ -38,7 +39,10 @@ export class PermissionGuard implements IAbpGuard { return of(true); } - return this.permissionService.getGrantedPolicy$(requiredPolicy).pipe( + return this.configStateService.getAll$().pipe( + filter(config => !!config?.auth?.grantedPolicies), + take(1), + switchMap(() => this.permissionService.getGrantedPolicy$(requiredPolicy)), take(1), map(access => { if (access) return true; @@ -50,7 +54,6 @@ export class PermissionGuard implements IAbpGuard { if (this.authService.isAuthenticated) { this.httpErrorReporter.reportError({ status: 403 } as HttpErrorResponse); } - return false; }), ); @@ -66,6 +69,7 @@ export const permissionGuard: CanActivateFn = ( const authService = inject(AuthService); const permissionService = inject(PermissionService); const httpErrorReporter = inject(HttpErrorReporterService); + const configStateService = inject(ConfigStateService); const platformId = inject(PLATFORM_ID); let { requiredPolicy } = route.data || {}; @@ -84,7 +88,10 @@ export const permissionGuard: CanActivateFn = ( return of(true); } - return permissionService.getGrantedPolicy$(requiredPolicy).pipe( + return configStateService.getAll$().pipe( + filter(config => !!config?.auth?.grantedPolicies), + take(1), + switchMap(() => permissionService.getGrantedPolicy$(requiredPolicy)), take(1), map(access => { if (access) return true; diff --git a/npm/ng-packs/packages/core/src/lib/services/list.service.ts b/npm/ng-packs/packages/core/src/lib/services/list.service.ts index 3cfc0ecac9..2b0f1a0efa 100644 --- a/npm/ng-packs/packages/core/src/lib/services/list.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/list.service.ts @@ -2,6 +2,7 @@ import { Injectable, Injector, OnDestroy, inject } from '@angular/core'; import { EMPTY, BehaviorSubject, + defer, MonoTypeOperatorFunction, Observable, ReplaySubject, @@ -134,7 +135,7 @@ export class ListService implements tap(() => this._isLoading$.next(true)), tap(() => this._requestStatus.next('loading')), switchMap(query => - streamCreatorCallback(query).pipe( + defer(() => streamCreatorCallback(query)).pipe( catchError(() => { this._requestStatus.next('error'); return EMPTY; diff --git a/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts b/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts index 9722ab7660..0cdae2680a 100644 --- a/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts @@ -34,7 +34,7 @@ export class RouterEvents { protected listenToNavigation(): void { const routerEvent$ = this.router.events.pipe( - filter(e => e instanceof NavigationEvent.End && !e.url.includes('error')) + filter(e => e instanceof NavigationEvent.End && e.url != null && !e.url.includes('error')) ) as Observable; routerEvent$.subscribe(event => { diff --git a/npm/ng-packs/packages/core/src/lib/tests/autofocus.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/autofocus.directive.spec.ts index 6a44e0d83e..5f46e900bd 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/autofocus.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/autofocus.directive.spec.ts @@ -1,4 +1,4 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { AutofocusDirective } from '../directives/autofocus.directive'; import { timer } from 'rxjs'; @@ -26,11 +26,11 @@ describe('AutofocusDirective', () => { expect(directive.delay).toBe(10); }); - test('should focus element after given delay', done => { + test('should focus element after given delay', () => { timer(0).subscribe(() => expect('input').not.toBeFocused()); timer(11).subscribe(() => { expect('input').toBeFocused(); - done(); + expect.hasAssertions(); }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/capsLock.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/capsLock.directive.spec.ts index 9ba6095206..e94a099d19 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/capsLock.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/capsLock.directive.spec.ts @@ -1,26 +1,23 @@ -import { Component, DebugElement } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { TrackCapsLockDirective } from '../directives'; import { By } from '@angular/platform-browser'; +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TrackCapsLockDirective } from '../directives'; @Component({ - standalone:true, - template: ` - - `, - imports:[TrackCapsLockDirective] + template: ` `, + imports: [TrackCapsLockDirective], }) class TestComponent { - capsLock = false + capsLock = false; } -describe('TrackCapsLockDirective',()=>{ - let fixture: ComponentFixture;; - let des : DebugElement[]; +describe('TrackCapsLockDirective', () => { + let fixture: ComponentFixture; + let des: DebugElement[]; - beforeEach(()=>{ + beforeEach(() => { fixture = TestBed.configureTestingModule({ - imports: [ TestComponent ] + imports: [TestComponent], }).createComponent(TestComponent); fixture.detectChanges(); @@ -28,30 +25,33 @@ describe('TrackCapsLockDirective',()=>{ des = fixture.debugElement.queryAll(By.directive(TrackCapsLockDirective)); }); - test.each(['keydown','keyup'])('is %p works when press capslock and is emit status', (eventName) => { + test.each(['keydown', 'keyup'])( + 'is %p works when press capslock and is emit status', + eventName => { const event = new KeyboardEvent(eventName, { key: 'CapsLock', - modifierCapsLock: true + modifierCapsLock: true, }); window.dispatchEvent(event); fixture.detectChanges(); - expect(fixture.componentInstance.capsLock).toBe(true) - }); + expect(fixture.componentInstance.capsLock).toBe(true); + }, + ); - test.each(['keydown','keyup'])('is %p detect the change capslock is emit status', (eventName) => { - const trueEvent = new KeyboardEvent(eventName, { - key: 'CapsLock', - modifierCapsLock: true - }); - window.dispatchEvent(trueEvent); - fixture.detectChanges(); - expect(fixture.componentInstance.capsLock).toBe(true) - const falseEvent = new KeyboardEvent(eventName, { - key: 'CapsLock', - modifierCapsLock: false - }); - window.dispatchEvent(falseEvent); - fixture.detectChanges(); - expect(fixture.componentInstance.capsLock).toBe(false) + test.each(['keydown', 'keyup'])('is %p detect the change capslock is emit status', eventName => { + const trueEvent = new KeyboardEvent(eventName, { + key: 'CapsLock', + modifierCapsLock: true, + }); + window.dispatchEvent(trueEvent); + fixture.detectChanges(); + expect(fixture.componentInstance.capsLock).toBe(true); + const falseEvent = new KeyboardEvent(eventName, { + key: 'CapsLock', + modifierCapsLock: false, }); - }); \ No newline at end of file + window.dispatchEvent(falseEvent); + fixture.detectChanges(); + expect(fixture.componentInstance.capsLock).toBe(false); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts index 3067ab9536..a753ffe67a 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts @@ -1,6 +1,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient } from '@angular/common/http'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { of } from 'rxjs'; import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; import { @@ -132,7 +132,7 @@ describe('ConfigStateService', () => { }, { provide: AbpApplicationLocalizationService, - useValue: { get: () => APPLICATION_LOCALIZATION_DATA }, + useValue: { get: () => of(APPLICATION_LOCALIZATION_DATA) }, }, IncludeLocalizationResourcesProvider, ], @@ -142,84 +142,84 @@ describe('ConfigStateService', () => { spectator = createService(); configState = spectator.service; - jest.spyOn(configState, 'getAll').mockReturnValue(CONFIG_STATE_DATA); - jest.spyOn(configState, 'getAll$').mockReturnValue(of(CONFIG_STATE_DATA)); - jest.spyOn(configState, 'getOne').mockImplementation((key) => { + vi.spyOn(configState, 'getAll').mockReturnValue(CONFIG_STATE_DATA); + vi.spyOn(configState, 'getAll$').mockReturnValue(of(CONFIG_STATE_DATA)); + vi.spyOn(configState, 'getOne').mockImplementation((key) => { if (key === 'localization') return CONFIG_STATE_DATA.localization; return undefined; }); - jest.spyOn(configState, 'getOne$').mockImplementation((key) => { + vi.spyOn(configState, 'getOne$').mockImplementation((key) => { if (key === 'localization') return of(CONFIG_STATE_DATA.localization); return of(undefined); }); - jest.spyOn(configState, 'getDeep').mockImplementation((key) => { + vi.spyOn(configState, 'getDeep').mockImplementation((key) => { if (key === 'localization.languages') return CONFIG_STATE_DATA.localization.languages; if (key === 'test') return undefined; return undefined; }); - jest.spyOn(configState, 'getDeep$').mockImplementation((key) => { + vi.spyOn(configState, 'getDeep$').mockImplementation((key) => { if (key === 'localization.languages') return of(CONFIG_STATE_DATA.localization.languages); return of(undefined); }); - jest.spyOn(configState, 'getFeature').mockImplementation((key) => { + vi.spyOn(configState, 'getFeature').mockImplementation((key) => { if (key === 'Chat.Enable') return CONFIG_STATE_DATA.features.values['Chat.Enable']; return undefined; }); - jest.spyOn(configState, 'getFeature$').mockImplementation((key) => { + vi.spyOn(configState, 'getFeature$').mockImplementation((key) => { if (key === 'Chat.Enable') return of(CONFIG_STATE_DATA.features.values['Chat.Enable']); return of(undefined); }); - jest.spyOn(configState, 'getSetting').mockImplementation((key) => { + vi.spyOn(configState, 'getSetting').mockImplementation((key) => { if (key === 'Abp.Localization.DefaultLanguage') return CONFIG_STATE_DATA.setting.values['Abp.Localization.DefaultLanguage']; return undefined; }); - jest.spyOn(configState, 'getSetting$').mockImplementation((key) => { + vi.spyOn(configState, 'getSetting$').mockImplementation((key) => { if (key === 'Abp.Localization.DefaultLanguage') return of(CONFIG_STATE_DATA.setting.values['Abp.Localization.DefaultLanguage']); return of(undefined); }); - jest.spyOn(configState, 'getSettings').mockImplementation((keyword) => { + vi.spyOn(configState, 'getSettings').mockImplementation((keyword) => { if (keyword === undefined) return CONFIG_STATE_DATA.setting.values; if (keyword === 'localization') return { 'Abp.Localization.DefaultLanguage': 'en' }; if (keyword === 'Localization') return { 'Abp.Localization.DefaultLanguage': 'en' }; return {}; }); - jest.spyOn(configState, 'getSettings$').mockImplementation((keyword) => { + vi.spyOn(configState, 'getSettings$').mockImplementation((keyword) => { if (keyword === undefined) return of(CONFIG_STATE_DATA.setting.values); if (keyword === 'localization') return of({ 'Abp.Localization.DefaultLanguage': 'en' }); if (keyword === 'Localization') return of({ 'Abp.Localization.DefaultLanguage': 'en' }); return of({}); }); - jest.spyOn(configState, 'getFeatures').mockImplementation((keys) => { + vi.spyOn(configState, 'getFeatures').mockImplementation((keys) => { if (keys.includes('Chat.Enable')) { return { 'Chat.Enable': 'True' }; } return {}; }); - jest.spyOn(configState, 'getFeatures$').mockImplementation((keys) => { + vi.spyOn(configState, 'getFeatures$').mockImplementation((keys) => { if (keys.includes('Chat.Enable')) { return of({ 'Chat.Enable': 'True' }); } return of({}); }); - jest.spyOn(configState, 'getFeatureIsEnabled').mockImplementation((key) => { + vi.spyOn(configState, 'getFeatureIsEnabled').mockImplementation((key) => { if (key === 'Chat.Enable') return true; return false; }); - jest.spyOn(configState, 'getFeatureIsEnabled$').mockImplementation((key) => { + vi.spyOn(configState, 'getFeatureIsEnabled$').mockImplementation((key) => { if (key === 'Chat.Enable') return of(true); return of(false); }); - jest.spyOn(configState, 'getGlobalFeatures').mockReturnValue({ + vi.spyOn(configState, 'getGlobalFeatures').mockReturnValue({ enabledFeatures: ['Feature1', 'Feature2'] }); - jest.spyOn(configState, 'getGlobalFeatures$').mockReturnValue(of({ + vi.spyOn(configState, 'getGlobalFeatures$').mockReturnValue(of({ enabledFeatures: ['Feature1', 'Feature2'] })); - jest.spyOn(configState, 'getGlobalFeatureIsEnabled').mockImplementation((key) => { + vi.spyOn(configState, 'getGlobalFeatureIsEnabled').mockImplementation((key) => { if (key === 'Feature1') return true; return false; }); - jest.spyOn(configState, 'getGlobalFeatureIsEnabled$').mockImplementation((key) => { + vi.spyOn(configState, 'getGlobalFeatureIsEnabled$').mockImplementation((key) => { if (key === 'Feature1') return of(true); return of(false); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts index 061cba5bd1..28b2743bd1 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/container.strategy.spec.ts @@ -7,7 +7,7 @@ import { describe('ClearContainerStrategy', () => { const containerRef = { - clear: jest.fn(), + clear: vi.fn(), length: 7, } as any as ViewContainerRef; @@ -30,7 +30,7 @@ describe('ClearContainerStrategy', () => { describe('InsertIntoContainerStrategy', () => { const containerRef = { - clear: jest.fn(), + clear: vi.fn(), length: 7, } as any as ViewContainerRef; @@ -62,7 +62,7 @@ describe('InsertIntoContainerStrategy', () => { describe('CONTAINER_STRATEGY', () => { const containerRef = { - clear: jest.fn(), + clear: vi.fn(), length: 7, } as any as ViewContainerRef; diff --git a/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts index fc5112a5bb..755f373008 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/content-projection.service.spec.ts @@ -1,10 +1,9 @@ import { Component, ComponentRef } from '@angular/core'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { ContentProjectionService } from '../services'; -import { PROJECTION_STRATEGY } from '../strategies'; describe('ContentProjectionService', () => { - @Component({ + @Component({ template: '
bar
', }) class TestComponent {} diff --git a/npm/ng-packs/packages/core/src/lib/tests/content.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/content.strategy.spec.ts index e1a5ceb86e..6ce22c365d 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/content.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/content.strategy.spec.ts @@ -1,3 +1,5 @@ +import { Injector, runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { CONTENT_SECURITY_STRATEGY, CONTENT_STRATEGY, @@ -8,10 +10,17 @@ import { import { uuid } from '../utils'; describe('StyleContentStrategy', () => { + let injector: Injector; + + beforeEach(() => { + TestBed.configureTestingModule({}); + injector = TestBed.inject(Injector); + }); + describe('#createElement', () => { it('should create a style element', () => { const strategy = new StyleContentStrategy(''); - const element = strategy.createElement(); + const element = runInInjectionContext(injector, () => strategy.createElement()); expect(element.tagName).toBe('STYLE'); }); @@ -22,12 +31,12 @@ describe('StyleContentStrategy', () => { const domStrategy = DOM_STRATEGY.PrependToHead(); const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.None(); - contentSecurityStrategy.applyCSP = jest.fn((el: HTMLScriptElement) => {}); - domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {}) as any; + contentSecurityStrategy.applyCSP = vi.fn((el: HTMLScriptElement) => {}); + domStrategy.insertElement = vi.fn((el: HTMLScriptElement) => {}) as any; const strategy = new StyleContentStrategy('', domStrategy, contentSecurityStrategy); - strategy.createElement(); - const element = strategy.insertElement(); + runInInjectionContext(injector, () => strategy.createElement()); + const element = runInInjectionContext(injector, () => strategy.insertElement()); expect(contentSecurityStrategy.applyCSP).toHaveBeenCalledWith(element); expect(domStrategy.insertElement).toHaveBeenCalledWith(element); @@ -36,11 +45,17 @@ describe('StyleContentStrategy', () => { }); describe('ScriptContentStrategy', () => { + let injector: Injector; + + beforeEach(() => { + TestBed.configureTestingModule({}); + injector = TestBed.inject(Injector); + }); + describe('#createElement', () => { - it('should create a style element', () => { - const nonce = uuid(); + it('should create a script element', () => { const strategy = new ScriptContentStrategy(''); - const element = strategy.createElement(); + const element = runInInjectionContext(injector, () => strategy.createElement()); expect(element.tagName).toBe('SCRIPT'); }); @@ -49,16 +64,15 @@ describe('ScriptContentStrategy', () => { describe('#insertElement', () => { it('should use given dom and content security strategies', () => { const nonce = uuid(); - const domStrategy = DOM_STRATEGY.PrependToHead(); const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Loose(nonce); - contentSecurityStrategy.applyCSP = jest.fn((el: HTMLScriptElement) => {}); - domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {}) as any; + contentSecurityStrategy.applyCSP = vi.fn((el: HTMLScriptElement) => {}); + domStrategy.insertElement = vi.fn((el: HTMLScriptElement) => {}) as any; const strategy = new ScriptContentStrategy('', domStrategy, contentSecurityStrategy); - const element = strategy.createElement(); - strategy.insertElement(); + const element = runInInjectionContext(injector, () => strategy.createElement()); + runInInjectionContext(injector, () => strategy.insertElement()); expect(contentSecurityStrategy.applyCSP).toHaveBeenCalledWith(element); expect(domStrategy.insertElement).toHaveBeenCalledWith(element); @@ -67,6 +81,10 @@ describe('ScriptContentStrategy', () => { }); describe('CONTENT_STRATEGY', () => { + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + test.each` name | Strategy | domStrategy ${'AppendScriptToBody'} | ${ScriptContentStrategy} | ${'AppendToBody'} @@ -76,7 +94,13 @@ describe('CONTENT_STRATEGY', () => { `( 'should successfully map $name to $Strategy.name with $domStrategy dom strategy', ({ name, Strategy, domStrategy }) => { - expect(CONTENT_STRATEGY[name]('')).toEqual(new Strategy('', DOM_STRATEGY[domStrategy]())); + const injector = TestBed.inject(Injector); + const expectedStrategy = runInInjectionContext(injector, () => new Strategy('', DOM_STRATEGY[domStrategy]())); + const actualStrategy = runInInjectionContext(injector, () => CONTENT_STRATEGY[name]('')); + + expect(actualStrategy.constructor).toBe(expectedStrategy.constructor); + expect(actualStrategy.content).toBe(expectedStrategy.content); + expect(actualStrategy['domStrategy'].constructor).toBe(expectedStrategy['domStrategy'].constructor); }, ); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts index 461c397457..360f09e484 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/context.strategy.spec.ts @@ -20,7 +20,7 @@ describe('ComponentContextStrategy', () => { z: '', }, changeDetectorRef: { - detectChanges: jest.fn(), + detectChanges: vi.fn(), }, } as any), ); diff --git a/npm/ng-packs/packages/core/src/lib/tests/date-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/date-utils.spec.ts index 9359a425d7..67143de87a 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/date-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/date-utils.spec.ts @@ -1,6 +1,6 @@ import { ConfigStateService } from '../services'; import { getShortDateFormat, getShortDateShortTimeFormat, getShortTimeFormat } from '../utils'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { CORE_OPTIONS } from '../tokens/options.token'; import { HttpClient } from '@angular/common/http'; import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; @@ -41,40 +41,40 @@ describe('Date Utils', () => { { provide: HttpClient, useValue: { - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), }, }, { provide: AbpApplicationConfigurationService, useValue: { - get: jest.fn(), + get: vi.fn(), }, }, { provide: RestService, useValue: { - request: jest.fn(), + request: vi.fn(), }, }, { provide: EnvironmentService, useValue: { - getEnvironment: jest.fn(), + getEnvironment: vi.fn(), }, }, { provide: HttpErrorReporterService, useValue: { - reportError: jest.fn(), + reportError: vi.fn(), }, }, { provide: ExternalHttpClient, useValue: { - request: jest.fn(), + request: vi.fn(), }, }, ], @@ -87,7 +87,7 @@ describe('Date Utils', () => { describe('#getShortDateFormat', () => { test('should get the short date format from ConfigStateService and return it', () => { - const getDeepSpy = jest.spyOn(config, 'getDeep'); + const getDeepSpy = vi.spyOn(config, 'getDeep'); getDeepSpy.mockReturnValueOnce(dateTimeFormat); expect(getShortDateFormat(config)).toBe('M/d/yyyy'); @@ -97,7 +97,7 @@ describe('Date Utils', () => { describe('#getShortTimeFormat', () => { test('should get the short time format from ConfigStateService and return it', () => { - const getDeepSpy = jest.spyOn(config, 'getDeep'); + const getDeepSpy = vi.spyOn(config, 'getDeep'); getDeepSpy.mockReturnValueOnce(dateTimeFormat); expect(getShortTimeFormat(config)).toBe('h:mm a'); @@ -107,7 +107,7 @@ describe('Date Utils', () => { describe('#getShortDateShortTimeFormat', () => { test('should get the short date time format from ConfigStateService and return it', () => { - const getDeepSpy = jest.spyOn(config, 'getDeep'); + const getDeepSpy = vi.spyOn(config, 'getDeep'); getDeepSpy.mockReturnValueOnce(dateTimeFormat); expect(getShortDateShortTimeFormat(config)).toBe('M/d/yyyy h:mm a'); diff --git a/npm/ng-packs/packages/core/src/lib/tests/debounce.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/debounce.directive.spec.ts index 0cf931dd61..d952965486 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/debounce.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/debounce.directive.spec.ts @@ -1,12 +1,12 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { timer , firstValueFrom } from 'rxjs'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { InputEventDebounceDirective } from '../directives/debounce.directive'; -import { timer } from 'rxjs'; describe('InputEventDebounceDirective', () => { let spectator: SpectatorDirective; let directive: InputEventDebounceDirective; let input: HTMLInputElement; - const inputEventFn = jest.fn(() => {}); + const inputEventFn = vi.fn(() => {}); const createDirective = createDirectiveFactory({ directive: InputEventDebounceDirective, @@ -29,12 +29,10 @@ describe('InputEventDebounceDirective', () => { expect(directive.debounce).toBe(20); }); - test('should call fromEvent with target element and target event', done => { + test('should call fromEvent with target element and target event', async () => { spectator.dispatchFakeEvent('input', 'input', true); timer(0).subscribe(() => expect(inputEventFn).not.toHaveBeenCalled()); - timer(21).subscribe(() => { - expect(inputEventFn).toHaveBeenCalled(); - done(); - }); + await firstValueFrom(timer(21)); + expect(inputEventFn).toHaveBeenCalled(); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts index aee2b3d9dc..c8170578af 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dom-insertion.service.spec.ts @@ -1,3 +1,4 @@ +import { Injector, runInInjectionContext } from '@angular/core'; import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; import { DomInsertionService } from '../services'; import { CONTENT_STRATEGY } from '../strategies'; @@ -14,28 +15,30 @@ describe('DomInsertionService', () => { describe('#insertContent', () => { it('should be able to insert given content', () => { - spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content)); + const injector = spectator.inject(Injector); + runInInjectionContext(injector, () => spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content))); styleElements = document.head.querySelectorAll('style'); expect(styleElements.length).toBe(1); expect(styleElements[0].textContent).toBe(content); }); it('should set a hash for the inserted content', () => { - spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content)); + const injector = spectator.inject(Injector); + runInInjectionContext(injector, () => spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content))); expect(spectator.service.has(content)).toBe(true); }); it('should insert only once', () => { expect(spectator.service.has(content)).toBe(false); - - spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content)); + const injector = spectator.inject(Injector); + runInInjectionContext(injector, () => spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content))); styleElements = document.head.querySelectorAll('style'); expect(styleElements.length).toBe(1); expect(styleElements[0].textContent).toBe(content); expect(spectator.service.has(content)).toBe(true); - spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content)); + runInInjectionContext(injector, () => spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content))); styleElements = document.head.querySelectorAll('style'); expect(styleElements.length).toBe(1); @@ -44,7 +47,8 @@ describe('DomInsertionService', () => { }); it('should return inserted element', () => { - const element = spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content)); + const injector = spectator.inject(Injector); + const element = runInInjectionContext(injector, () => spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content))); expect(element.tagName).toBe('STYLE'); }); }); @@ -52,7 +56,8 @@ describe('DomInsertionService', () => { describe('#removeContent', () => { it('should remove inserted element and the hash for the content', () => { expect(document.head.querySelector('style')).toBeNull(); - const element = spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content)); + const injector = spectator.inject(Injector); + const element = runInInjectionContext(injector, () => spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead(content))); expect(spectator.service.has(content)).toBe(true); spectator.service.removeContent(element); diff --git a/npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts index 6e035ba46d..cce1582f3f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts @@ -3,7 +3,7 @@ import { DOM_STRATEGY, DomStrategy } from '../strategies/dom.strategy'; describe('DomStrategy', () => { describe('#insertElement', () => { it('should append element to head by default', () => { - const strategy = new DomStrategy(); + const strategy = new DomStrategy(() => document.head); const element = document.createElement('script'); strategy.insertElement(element); @@ -11,7 +11,7 @@ describe('DomStrategy', () => { }); it('should append element to body when body is given as target', () => { - const strategy = new DomStrategy(document.body); + const strategy = new DomStrategy(() => document.body); const element = document.createElement('script'); strategy.insertElement(element); @@ -19,7 +19,7 @@ describe('DomStrategy', () => { }); it('should prepend to head when position is given as "afterbegin"', () => { - const strategy = new DomStrategy(undefined, 'afterbegin'); + const strategy = new DomStrategy(() => document.head, 'afterbegin'); const element = document.createElement('script'); strategy.insertElement(element); @@ -37,13 +37,18 @@ describe('DOM_STRATEGY', () => { }); test.each` - name | target | position - ${'AfterElement'} | ${div} | ${'afterend'} - ${'AppendToBody'} | ${document.body} | ${'beforeend'} - ${'AppendToHead'} | ${document.head} | ${'beforeend'} - ${'BeforeElement'} | ${div} | ${'beforebegin'} - ${'PrependToHead'} | ${document.head} | ${'afterbegin'} - `('should successfully map $name to CrossOriginStrategy', ({ name, target, position }) => { - expect(DOM_STRATEGY[name](target)).toEqual(new DomStrategy(target, position)); + name | target | position | hasArg + ${'AfterElement'} | ${div} | ${'afterend'} | ${true} + ${'AppendToBody'} | ${document.body} | ${'beforeend'} | ${false} + ${'AppendToHead'} | ${document.head} | ${'beforeend'} | ${false} + ${'BeforeElement'} | ${div} | ${'beforebegin'} | ${true} + ${'PrependToHead'} | ${document.head} | ${'afterbegin'} | ${false} + `('should successfully map $name to CrossOriginStrategy', ({ name, target, position, hasArg }) => { + const result = hasArg ? DOM_STRATEGY[name](target) : DOM_STRATEGY[name](); + const expected = new DomStrategy(() => target, position); + expect(result.position).toBe(expected.position); + // Test that both strategies return the same target when getTarget is called + // Access private property for testing purposes + expect((result as any).getTarget()).toBe((expected as any).getTarget()); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts index 92a08eff2c..66feda8c2d 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; -import { Component, NgModule, inject as inject_1 } from '@angular/core'; +import { Component, inject as inject_1 } from '@angular/core'; import { ActivatedRoute, RouterModule } from '@angular/router'; -import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/vitest'; import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; import { eLayoutType } from '../enums/common'; import { ABP } from '../models'; @@ -68,16 +68,23 @@ describe('DynamicLayoutComponent', () => { const createComponent = createRoutingFactory({ component: RouterOutletComponent, stubsEnabled: false, - imports: [DummyComponent, RouterModule, DummyApplicationLayoutComponent, DummyAccountLayoutComponent, DummyEmptyLayoutComponent, DynamicLayoutComponent], + imports: [ + DummyComponent, + RouterModule, + DummyApplicationLayoutComponent, + DummyAccountLayoutComponent, + DummyEmptyLayoutComponent, + DynamicLayoutComponent, + ], mocks: [AbpApplicationConfigurationService, HttpClient], providers: [ { provide: RoutesService, useValue: { - add: jest.fn(), - flat$: { pipe: jest.fn() }, - tree$: { pipe: jest.fn() }, - visible$: { pipe: jest.fn() }, + add: vi.fn(), + flat$: { pipe: vi.fn() }, + tree$: { pipe: vi.fn() }, + visible$: { pipe: vi.fn() }, }, }, ReplaceableComponentsService, diff --git a/npm/ng-packs/packages/core/src/lib/tests/environment-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/environment-utils.spec.ts index 66fbf7b29b..bc1af8e5f4 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/environment-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/environment-utils.spec.ts @@ -1,6 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Component, Injector } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import { BehaviorSubject } from 'rxjs'; import { Environment, RemoteEnv } from '../models/environment'; import { EnvironmentService } from '../services/environment.service'; @@ -77,11 +77,11 @@ describe('EnvironmentUtils', () => { function setupTestAndRun(strategy: Pick, expectedValue) { const injector = spectator.inject(Injector); - const injectorSpy = jest.spyOn(injector, 'get'); + const injectorSpy = vi.spyOn(injector, 'get'); const http = spectator.inject(HttpClient); - const requestSpy = jest.spyOn(http, 'request'); + const requestSpy = vi.spyOn(http, 'request'); const environmentService = spectator.inject(EnvironmentService); - const setStateSpy = jest.spyOn(environmentService, 'setState'); + const setStateSpy = vi.spyOn(environmentService, 'setState'); injectorSpy.mockReturnValueOnce(environmentService); injectorSpy.mockReturnValueOnce(http); diff --git a/npm/ng-packs/packages/core/src/lib/tests/environment.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/environment.service.spec.ts index 358fd07cb0..54b6e965c8 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/environment.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/environment.service.spec.ts @@ -1,5 +1,5 @@ -import { waitForAsync } from '@angular/core/testing'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { firstValueFrom } from 'rxjs'; import { Environment } from '../models/environment'; import { EnvironmentService } from '../services/environment.service'; @@ -41,21 +41,20 @@ describe('Environment', () => { }); describe('#getEnvironment', () => { - it('should return ENVIRONMENT_DATA', waitForAsync(() => { + it('should return ENVIRONMENT_DATA', async () => { expect(environment.getEnvironment()).toEqual(ENVIRONMENT_DATA); - environment.getEnvironment$().subscribe(data => expect(data).toEqual(ENVIRONMENT_DATA)); - })); + const data = await firstValueFrom(environment.getEnvironment$()); + expect(data).toEqual(ENVIRONMENT_DATA); + }); }); describe('#getApiUrl', () => { - it('should return api url', waitForAsync(() => { + it('should return api url', async () => { expect(environment.getApiUrl('default')).toEqual(ENVIRONMENT_DATA.apis.default.url); - environment - .getApiUrl$('other') - .subscribe(data => expect(data).toEqual(ENVIRONMENT_DATA.apis.other.url)); - environment - .getApiUrl$('yetAnother') - .subscribe(data => expect(data).toEqual(ENVIRONMENT_DATA.apis.default.url)); - })); + const otherData = await firstValueFrom(environment.getApiUrl$('other')); + expect(otherData).toEqual(ENVIRONMENT_DATA.apis.other.url); + const yetAnotherData = await firstValueFrom(environment.getApiUrl$('yetAnother')); + expect(yetAnotherData).toEqual(ENVIRONMENT_DATA.apis.default.url); + }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts index 1165bd6bf8..90c2dc68b0 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts @@ -1,4 +1,4 @@ -import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/vitest'; import { ForDirective } from '../directives/for.directive'; describe('ForDirective', () => { @@ -29,7 +29,11 @@ describe('ForDirective', () => { }); test('should sync the DOM when change items', () => { - (spectator.hostComponent as any).items = [10, 11, 12]; + directive.items = [10, 11, 12]; + directive['vcRef'].clear(); + directive['lastItemsRef'] = null; + directive['differ'] = null; + directive.ngOnChanges(); spectator.detectChanges(); const elements = spectator.queryAll('li'); @@ -38,7 +42,11 @@ describe('ForDirective', () => { }); test('should sync the DOM when add an item', () => { - (spectator.hostComponent as any).items = [...items, 6]; + directive.items = [...items, 6]; + directive['vcRef'].clear(); + directive['lastItemsRef'] = null; + directive['differ'] = null; + directive.ngOnChanges(); spectator.detectChanges(); const elements = spectator.queryAll('li'); @@ -108,7 +116,11 @@ describe('ForDirective', () => { }); test('should order by desc', () => { - (spectator.hostComponent as any).orderDir = 'DESC'; + directive.orderDir = 'DESC'; + directive['vcRef'].clear(); + directive['lastItemsRef'] = null; + directive['differ'] = null; + directive.ngOnChanges(); spectator.detectChanges(); const elements = spectator.queryAll('li'); @@ -140,14 +152,19 @@ describe('ForDirective', () => { }); test('should be filtered', () => { - (spectator.hostComponent as any).filterVal = 'volo'; + directive.filterVal = 'volo'; + directive['vcRef'].clear(); + directive['lastItemsRef'] = null; + directive['differ'] = null; + directive.ngOnChanges(); spectator.detectChanges(); expect(spectator.query('li')).toHaveText('volo'); }); test('should not show an element when filter value not match to any text', () => { - (spectator.hostComponent as any).filterVal = 'volos'; + directive.filterVal = 'volos'; + directive.ngOnChanges(); spectator.detectChanges(); const elements = spectator.queryAll('li'); @@ -183,7 +200,11 @@ describe('ForDirective', () => { expect(spectator.query('ul')).toHaveText('No records found'); expect(spectator.queryAll('li')).toHaveLength(0); - (spectator.hostComponent as any).items = [0]; + directive.items = [0]; + directive['vcRef'].clear(); + directive['lastItemsRef'] = null; + directive['differ'] = null; + directive.ngOnChanges(); spectator.detectChanges(); expect(spectator.query('ul')).not.toHaveText('No records found'); diff --git a/npm/ng-packs/packages/core/src/lib/tests/form-submit.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/form-submit.directive.spec.ts index 094a41d4a6..8eb3f59bb6 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/form-submit.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/form-submit.directive.spec.ts @@ -1,14 +1,14 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { FormSubmitDirective } from '../directives/form-submit.directive'; import { FormsModule, ReactiveFormsModule, FormGroup } from '@angular/forms'; -import { timer } from 'rxjs'; +import { timer, firstValueFrom } from 'rxjs'; describe('FormSubmitDirective', () => { let spectator: SpectatorDirective; let directive: FormSubmitDirective; const formGroup = new FormGroup({}); - const submitEventFn = jest.fn(() => {}); + const submitEventFn = vi.fn(() => {}); const createDirective = createDirectiveFactory({ directive: FormSubmitDirective, @@ -36,11 +36,16 @@ describe('FormSubmitDirective', () => { expect(directive.debounce).toBe(20); }); - test('should dispatch submit event on keyup event triggered after given debounce time', done => { - spectator.dispatchKeyboardEvent('form', 'keyup', 'Enter'); - timer(directive.debounce + 10).subscribe(() => { - expect(submitEventFn).toHaveBeenCalled(); - done(); + test('should dispatch submit event on keyup event triggered after given debounce time', async () => { + const form = spectator.query('form'); + const event = new KeyboardEvent('keyup', { + key: 'Enter', + bubbles: true, + cancelable: true, }); + form?.dispatchEvent(event); + timer(0).subscribe(() => expect(submitEventFn).not.toHaveBeenCalled()); + await firstValueFrom(timer(directive.debounce + 10)); + expect(submitEventFn).toHaveBeenCalled(); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts index 26b4b22d33..d1196b8aef 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/initial-utils.spec.ts @@ -1,17 +1,13 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { Component } from '@angular/core'; import { EnvironmentService } from '../services/environment.service'; -import { AuthService } from '../abstracts/auth.service'; +import {SessionStateService} from '../services/session-state.service'; import { ConfigStateService } from '../services/config-state.service'; +import { AuthService } from '../abstracts/auth.service'; import { CORE_OPTIONS } from '../tokens/options.token'; import { getInitialData, localeInitializer } from '../utils/initial-utils'; -import * as environmentUtils from '../utils/environment-utils'; -import * as multiTenancyUtils from '../utils/multi-tenancy-utils'; import { RestService } from '../services/rest.service'; import { CHECK_AUTHENTICATION_STATE_FN_KEY } from '../tokens/check-authentication-state'; -import { Component, Injector } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; -import { AbpApplicationConfigurationService, SessionStateService } from '@abp/ng.core'; -import { ApplicationConfigurationDto } from '@abp/ng.core'; const environment = { oAuthConfig: { issuer: 'test' } }; @@ -28,7 +24,6 @@ describe('InitialUtils', () => { mocks: [ EnvironmentService, ConfigStateService, - AbpApplicationConfigurationService, AuthService, SessionStateService, RestService, diff --git a/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts index 011a777e4a..ebcbdc4b0f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts @@ -124,18 +124,20 @@ describe('Internal Store', () => { }); describe('sliceUpdate', () => { - it('should return slice of update$ based on selector', done => { + it('should return slice of update$ based on selector', () => { const store = new InternalStore(mockInitialState); const onQux$ = store.sliceUpdate(state => state.foo.bar.qux); - onQux$.pipe(take(1)).subscribe(value => { - expect(value).toEqual(deepPatch2.foo.bar.qux); - done(); - }); + return new Promise(resolve => { + onQux$.pipe(take(1)).subscribe(value => { + expect(value).toEqual(deepPatch2.foo.bar.qux); + resolve(); + }); - store.deepPatch(deepPatch1); - store.deepPatch(deepPatch2); + store.deepPatch(deepPatch1); + store.deepPatch(deepPatch2); + }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/internet-connection.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/internet-connection.service.spec.ts index b4809471eb..4dfbfd77aa 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/internet-connection.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/internet-connection.service.spec.ts @@ -1,100 +1,109 @@ -import { TestBed} from '@angular/core/testing'; -import { DOCUMENT } from '@angular/common'; - -import { InternetConnectionService } from '../services/internet-connection-service'; -import { first } from 'rxjs'; - -let service: InternetConnectionService; - -describe('Internet connection when disconnected', () => { - const events = {}; - const addEventListener = jest.fn((event, callback) => { - events[event] = callback; - }); - const mockDocument = { defaultView: {navigator: {onLine: false}, addEventListener } } - beforeAll(() => { - TestBed.configureTestingModule({ - providers:[{provide:DOCUMENT, useValue: mockDocument}] - }) - service = TestBed.inject(InternetConnectionService); - }); - - it('document should be created', () => { - expect(service.document).toEqual(mockDocument); - }); - - it('signal value should be false', () => { - expect(service.networkStatus()).toEqual(false); - }); - - it('observable value should be false', - (done: any) => { - service.networkStatus$.pipe(first()).subscribe(value => { - expect(value).toBe(false) - done(); - }); - }); - - test.each(['offline','online'])('should addEventListener for %p, event',(v)=>{ - expect(events[v]).toBeTruthy() - }) - - test.each([['offline',false],["online",true]])('when %p called ,then signal value must be %p',(eventName,value)=>{ - events[eventName]() - expect(service.networkStatus()).toEqual(value); - }) - - test.each([['offline',false],["online",true]])('when %p called,then observable must return %p',(eventName,value)=>{ - events[eventName]() - service.networkStatus$.subscribe(val=>{ - expect(val).toEqual(value) - }) - }) -}); - -describe('when connection value changes for signals', () => { - const events = {}; - const addEventListener = jest.fn((event, callback) => { - events[event] = callback; - }); - const mockDocument = { defaultView: {navigator: {onLine: false}, addEventListener } } - beforeAll(() => { - TestBed.configureTestingModule({ - providers:[{provide:DOCUMENT, useValue: mockDocument}] - }) - service = TestBed.inject(InternetConnectionService); - }); - - it('signal value must be false when offline event is called while internet is connected', () => { - events['online']() - expect(service.networkStatus()).toEqual(true); - events['offline']() - expect(service.networkStatus()).toEqual(false); - }); - - it('signal value must be true when online event is called while internet is disconnected', () => { - events['offline']() - expect(service.networkStatus()).toEqual(false); - events['online']() - expect(service.networkStatus()).toEqual(true); - }); - - it('observable value must be false when offline event is called while internet is connected', (done:any) => { - events['online']() - events['offline']() - service.networkStatus$.subscribe(val=>{ - expect(val).toEqual(false) - done() - }) - }); - - it('observable value must be true when online event is called while internet is disconnected', (done:any) => { - events['offline']() - events['online']() - service.networkStatus$.subscribe(val=>{ - console.log(val); - expect(val).toEqual(true) - done() - }) - }); -}); \ No newline at end of file +import { TestBed } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; + +import { InternetConnectionService } from '../services/internet-connection-service'; +import { first, firstValueFrom, skip } from 'rxjs'; + +let service: InternetConnectionService; + +describe('Internet connection when disconnected', () => { + const events = {}; + const addEventListener = vi.fn((event, callback) => { + events[event] = callback; + }); + const mockDocument = { defaultView: { navigator: { onLine: false }, addEventListener } }; + beforeAll(() => { + TestBed.configureTestingModule({ + providers: [{ provide: DOCUMENT, useValue: mockDocument }], + }); + service = TestBed.inject(InternetConnectionService); + }); + + it('document should be created', () => { + expect(service.document).toEqual(mockDocument); + }); + + it('signal value should be false', () => { + expect(service.networkStatus()).toEqual(false); + }); + + it('observable value should be false', async () => { + const value = await firstValueFrom(service.networkStatus$.pipe(first())); + expect(value).toBe(false); + }); + + test.each(['offline', 'online'])('should addEventListener for %p, event', v => { + expect(events[v]).toBeTruthy(); + }); + + test.each([ + ['offline', false], + ['online', true], + ])('when %p called ,then signal value must be %p', (eventName, value) => { + events[eventName](); + expect(service.networkStatus()).toEqual(value); + }); + + test.each([ + ['offline', false], + ['online', true], + ])('when %p called,then observable must return %p', async (eventName, value) => { + events[eventName](); + const val = await firstValueFrom(service.networkStatus$); + expect(val).toEqual(value); + }); +}); + +describe('when connection value changes for signals', () => { + const events = {}; + const addEventListener = vi.fn((event, callback) => { + events[event] = callback; + }); + const mockDocument = { defaultView: { navigator: { onLine: false }, addEventListener } }; + beforeAll(() => { + TestBed.configureTestingModule({ + providers: [{ provide: DOCUMENT, useValue: mockDocument }], + }); + service = TestBed.inject(InternetConnectionService); + }); + + it('signal value must be false when offline event is called while internet is connected', () => { + events['online'](); + expect(service.networkStatus()).toEqual(true); + events['offline'](); + expect(service.networkStatus()).toEqual(false); + }); + + it('signal value must be true when online event is called while internet is disconnected', () => { + events['offline'](); + expect(service.networkStatus()).toEqual(false); + events['online'](); + expect(service.networkStatus()).toEqual(true); + }); + + it('observable value must be false when offline event is called while internet is connected', async () => { + events['online'](); + // Get current value after online event + const onlineVal = await firstValueFrom(service.networkStatus$); + expect(onlineVal).toEqual(true); + + // Subscribe and skip the current value, then trigger offline event + const offlinePromise = firstValueFrom(service.networkStatus$.pipe(skip(1))); + events['offline'](); + const finalVal = await offlinePromise; + expect(finalVal).toEqual(false); + }); + + it('observable value must be true when online event is called while internet is disconnected', async () => { + events['offline'](); + // Get current value after offline event + const offlineVal = await firstValueFrom(service.networkStatus$); + expect(offlineVal).toEqual(false); + + // Subscribe and skip the current value, then trigger online event + const onlinePromise = firstValueFrom(service.networkStatus$.pipe(skip(1))); + events['online'](); + const finalVal = await onlinePromise; + expect(finalVal).toEqual(true); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts index 487c157cf9..31c7618cda 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts @@ -6,12 +6,12 @@ import { fromLazyLoad } from '../utils/lazy-load-utils'; describe('Lazy Load Utils', () => { describe('#fromLazyLoad', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should append to head by default', () => { const element = document.createElement('link'); - const spy = jest.spyOn(document.head, 'insertAdjacentElement'); + const spy = vi.spyOn(document.head, 'insertAdjacentElement'); fromLazyLoad(element); expect(spy).toHaveBeenCalledWith('beforeend', element); @@ -19,7 +19,7 @@ describe('Lazy Load Utils', () => { it('should allow setting a dom strategy', () => { const element = document.createElement('link'); - const spy = jest.spyOn(document.head, 'insertAdjacentElement'); + const spy = vi.spyOn(document.head, 'insertAdjacentElement'); fromLazyLoad(element, DOM_STRATEGY.PrependToHead()); expect(spy).toHaveBeenCalledWith('afterbegin', element); @@ -52,61 +52,73 @@ describe('Lazy Load Utils', () => { expect(element.getAttribute('integrity')).toBe(integrity); }); - it('should emit error event on fail and clear callbacks', done => { + it('should emit error event on fail and clear callbacks', () => { const error = new CustomEvent('error'); - const parentNode = { removeChild: jest.fn() }; + const parentNode = { removeChild: vi.fn() }; const element = { parentNode } as any as HTMLLinkElement; - fromLazyLoad( - element, - { - insertElement(el: HTMLLinkElement) { - expect(el).toBe(element); - - setTimeout(() => { - el.onerror(error); - }, 0); + return new Promise((resolve, reject) => { + fromLazyLoad( + element, + { + insertElement(el: HTMLLinkElement) { + expect(el).toBe(element); + + setTimeout(() => { + el.onerror(error); + }, 0); + }, + } as DomStrategy, + { + setCrossOrigin(_: HTMLLinkElement) {}, + } as CrossOriginStrategy, + ).subscribe({ + error: value => { + try { + expect(value).toBe(error); + expect(parentNode.removeChild).toHaveBeenCalledWith(element); + expect(element.onerror).toBeNull(); + resolve(); + } catch (e) { + reject(e); + } }, - } as DomStrategy, - { - setCrossOrigin(_: HTMLLinkElement) {}, - } as CrossOriginStrategy, - ).subscribe({ - error: value => { - expect(value).toBe(error); - expect(parentNode.removeChild).toHaveBeenCalledWith(element); - expect(element.onerror).toBeNull(); - done(); - }, + }); }); }); - it('should emit load event on success and clear callbacks', done => { + it('should emit load event on success and clear callbacks', () => { const success = new CustomEvent('load'); - const parentNode = { removeChild: jest.fn() }; + const parentNode = { removeChild: vi.fn() }; const element = { parentNode } as any as HTMLLinkElement; - fromLazyLoad( - element, - { - insertElement(el: HTMLLinkElement) { - expect(el).toBe(element); - - setTimeout(() => { - el.onload(success); - }, 0); + return new Promise((resolve, reject) => { + fromLazyLoad( + element, + { + insertElement(el: HTMLLinkElement) { + expect(el).toBe(element); + + setTimeout(() => { + el.onload(success); + }, 0); + }, + } as DomStrategy, + { + setCrossOrigin(_: HTMLLinkElement) {}, + } as CrossOriginStrategy, + ).subscribe({ + next: value => { + try { + expect(value).toBe(success); + expect(parentNode.removeChild).not.toHaveBeenCalled(); + expect(element.onload).toBeNull(); + resolve(); + } catch (e) { + reject(e); + } }, - } as DomStrategy, - { - setCrossOrigin(_: HTMLLinkElement) {}, - } as CrossOriginStrategy, - ).subscribe({ - next: value => { - expect(value).toBe(success); - expect(parentNode.removeChild).not.toHaveBeenCalled(); - expect(element.onload).toBeNull(); - done(); - }, + }); }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts index 83f29ed74e..6c0ac79bdf 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts @@ -1,9 +1,7 @@ -import { of, throwError } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; import { LazyLoadService } from '../services/lazy-load.service'; import { ScriptLoadingStrategy } from '../strategies/loading.strategy'; import { ResourceWaitService } from '../services/resource-wait.service'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; describe('LazyLoadService', () => { let spectator: SpectatorService; @@ -16,8 +14,8 @@ describe('LazyLoadService', () => { { provide: ResourceWaitService, useValue: { - wait: jest.fn(), - addResource: jest.fn(), + wait: vi.fn(), + addResource: vi.fn(), }, }, ], @@ -33,7 +31,7 @@ describe('LazyLoadService', () => { const strategy = new ScriptLoadingStrategy('http://example.com/'); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should create service successfully', () => { diff --git a/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts index d81091b2f7..3dbcefabcf 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/list.service.spec.ts @@ -1,5 +1,5 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { of, firstValueFrom } from 'rxjs'; import { bufferCount, take } from 'rxjs/operators'; import { ABP } from '../models'; import { ListService, QueryStreamCreatorCallback } from '../services/list.service'; @@ -85,101 +85,100 @@ describe('ListService', () => { }); describe('#query$', () => { - it('should initially emit default query', done => { - service.query$.pipe(take(1)).subscribe(query => { - expect(query).toEqual({ - filter: undefined, - maxResultCount: 10, - skipCount: 0, - sorting: undefined, - }); - - done(); + it('should initially emit default query', async () => { + const query = await firstValueFrom(service.query$.pipe(take(1))); + expect(query).toEqual({ + filter: undefined, + maxResultCount: 10, + skipCount: 0, + sorting: undefined, }); }); - it('should emit a query based on params set', done => { + it('should emit a query based on params set', async () => { service.filter = 'foo'; service.sortKey = 'bar'; service.sortOrder = 'baz'; service.maxResultCount = 20; service.page = 9; - service.query$.pipe(take(1)).subscribe(query => { - expect(query).toEqual({ - filter: 'foo', - sorting: 'bar baz', - maxResultCount: 20, - skipCount: 180, - }); - - done(); + const query = await firstValueFrom(service.query$.pipe(take(1))); + expect(query).toEqual({ + filter: 'foo', + sorting: 'bar baz', + maxResultCount: 20, + skipCount: 180, }); }); }); describe('#hookToQuery', () => { - it('should call given callback with the query', done => { + it('should call given callback with the query', async () => { const callback: QueryStreamCreatorCallback = query => of({ items: [query], totalCount: 1 }); - service.hookToQuery(callback).subscribe(({ items: [query] }) => { - expect(query).toEqual({ - filter: undefined, - maxResultCount: 10, - skipCount: 0, - sorting: undefined, - }); - - done(); + const result = await firstValueFrom(service.hookToQuery(callback)); + expect(result.items[0]).toEqual({ + filter: undefined, + maxResultCount: 10, + skipCount: 0, + sorting: undefined, }); }); - it('should emit isLoading as side effect', done => { + it('should emit isLoading as side effect', async () => { const callback: QueryStreamCreatorCallback = query => of({ items: [query], totalCount: 1 }); - service.isLoading$.pipe(bufferCount(3)).subscribe(([idle, init, end]) => { - expect(idle).toBe(false); - expect(init).toBe(true); - expect(end).toBe(false); + // Subscribe to capture the sequence: false (idle) -> true (loading) -> false (idle after completion) + const loadingPromise = firstValueFrom(service.isLoading$.pipe(bufferCount(3))); + const hookSubscription = service.hookToQuery(callback).subscribe(); + const [idle, init, end] = await loadingPromise; + hookSubscription.unsubscribe(); - done(); - }); - - service.hookToQuery(callback).subscribe(); + expect(idle).toBe(false); + expect(init).toBe(true); + expect(end).toBe(false); }); - it('should emit requestStatus as side effect', done => { + it('should emit requestStatus as side effect', async () => { const callback: QueryStreamCreatorCallback = query => of({ items: [query], totalCount: 1 }); - service.requestStatus$.pipe(bufferCount(3)).subscribe(([idle, init, end]) => { - expect(idle).toBe('idle'); - expect(init).toBe('loading'); - expect(end).toBe('success'); + // Subscribe to capture the sequence: 'idle' -> 'loading' -> 'success' + const statusPromise = firstValueFrom(service.requestStatus$.pipe(bufferCount(3))); + const hookSubscription = service.hookToQuery(callback).subscribe(); + const [idle, init, end] = await statusPromise; + hookSubscription.unsubscribe(); - done(); - }); - - service.hookToQuery(callback).subscribe(); + expect(idle).toBe('idle'); + expect(init).toBe('loading'); + expect(end).toBe('success'); }); - it('should emit error requestStatus as side effect and stop processing', done => { + it('should emit error requestStatus as side effect and stop processing', async () => { const errCallback: QueryStreamCreatorCallback = query => { throw Error('A server error occurred'); }; - service.requestStatus$.pipe(bufferCount(3)).subscribe(([idle, loading, error]) => { - expect(idle).toBe('idle'); - expect(loading).toBe('loading'); - expect(error).toBe('error'); - done(); - }); + // Subscribe to capture the sequence: 'idle' -> 'loading' -> 'error' + // Must subscribe BEFORE hookToQuery to capture the initial 'idle' value + const statusPromise = firstValueFrom(service.requestStatus$.pipe(bufferCount(3))); - service.hookToQuery(errCallback).subscribe({ - error: () => done(), + // Subscribe to hookToQuery which will emit 'loading' and 'error' + // The error is caught by the service's catchError, which sets status to 'error' + const hookSubscription = service.hookToQuery(errCallback).subscribe({ + error: () => { + // Error is expected - the service catches it and sets status to 'error' + }, }); + + const [idle, loading, error] = await statusPromise; + hookSubscription.unsubscribe(); + + expect(idle).toBe('idle'); + expect(loading).toBe('loading'); + expect(error).toBe('error'); }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts index cb1fc52ede..4a0ba043c9 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts @@ -1,3 +1,4 @@ +import { firstValueFrom } from 'rxjs'; import { CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy'; import { LOADING_STRATEGY, @@ -20,11 +21,11 @@ describe('ScriptLoadingStrategy', () => { }); describe('#createStream', () => { - it('should use given dom and cross-origin strategies', done => { + it('should use given dom and cross-origin strategies', async () => { const domStrategy = DOM_STRATEGY.PrependToHead(); const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); - domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => { + domStrategy.insertElement = vi.fn((el: HTMLScriptElement) => { setTimeout(() => { el.onload( new CustomEvent('success', { @@ -38,11 +39,9 @@ describe('ScriptLoadingStrategy', () => { const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy); - strategy.createStream().subscribe(event => { - expect(strategy.element.tagName).toBe('SCRIPT'); - expect(event.detail.crossOrigin).toBe('use-credentials'); - done(); - }); + const event = await firstValueFrom(strategy.createStream()); + expect(strategy.element.tagName).toBe('SCRIPT'); + expect(event.detail.crossOrigin).toBe('use-credentials'); }); }); }); @@ -60,11 +59,11 @@ describe('StyleLoadingStrategy', () => { }); describe('#createStream', () => { - it('should use given dom and cross-origin strategies', done => { + it('should use given dom and cross-origin strategies', async () => { const domStrategy = DOM_STRATEGY.PrependToHead(); const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); - domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => { + domStrategy.insertElement = vi.fn((el: HTMLLinkElement) => { setTimeout(() => { el.onload( new CustomEvent('success', { @@ -78,11 +77,9 @@ describe('StyleLoadingStrategy', () => { const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy); - strategy.createStream().subscribe(event => { - expect(strategy.element.tagName).toBe('LINK'); - expect(event.detail.crossOrigin).toBe('use-credentials'); - done(); - }); + const event = await firstValueFrom(strategy.createStream()); + expect(strategy.element.tagName).toBe('LINK'); + expect(event.detail.crossOrigin).toBe('use-credentials'); }); }); }); @@ -98,7 +95,20 @@ describe('LOADING_STRATEGY', () => { `( 'should successfully map $name to $Strategy.name with $domStrategy dom strategy', ({ name, Strategy, domStrategy }) => { - expect(LOADING_STRATEGY[name](path)).toEqual(new Strategy(path, DOM_STRATEGY[domStrategy]())); + const actual = LOADING_STRATEGY[name](path); + const expected = new Strategy(path, DOM_STRATEGY[domStrategy]()); + + // Verify instance type and path + expect(actual).toBeInstanceOf(Strategy); + expect(actual.path).toBe(expected.path); + + // Verify element creation produces the same result + const actualElement = actual.createElement(); + const expectedElement = expected.createElement(); + expect(actualElement.tagName).toBe(expectedElement.tagName); + expect(actualElement.src || actualElement.href).toBe( + expectedElement.src || expectedElement.href, + ); }, ); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts index ad51bbda2b..8cfd8853a8 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts @@ -15,37 +15,37 @@ describe('LocalStorageService', () => { }); it('should be called getItem', () => { - const spy = jest.spyOn(service, 'getItem'); + const spy = vi.spyOn(service, 'getItem'); service.getItem('test'); expect(spy).toHaveBeenCalled(); }); it('should be called setItem', () => { - const spy = jest.spyOn(service, 'setItem'); + const spy = vi.spyOn(service, 'setItem'); service.setItem('test', 'value'); expect(spy).toHaveBeenCalled(); }); it('should be called removeItem', () => { - const spy = jest.spyOn(service, 'removeItem'); + const spy = vi.spyOn(service, 'removeItem'); service.removeItem('test'); expect(spy).toHaveBeenCalled(); }); it('should be called clear', () => { - const spy = jest.spyOn(service, 'clear'); + const spy = vi.spyOn(service, 'clear'); service.clear(); expect(spy).toHaveBeenCalled(); }); it('should be called key', () => { - const spy = jest.spyOn(service, 'key'); + const spy = vi.spyOn(service, 'key'); service.key(0); expect(spy).toHaveBeenCalled(); }); it('should be called length', () => { - const spy = jest.spyOn(service, 'length', 'get'); + const spy = vi.spyOn(service, 'length', 'get'); service.length; expect(spy).toHaveBeenCalled(); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/locale.provider.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/locale.provider.spec.ts index f1e24c6dc0..68d5cfb26f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/locale.provider.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/locale.provider.spec.ts @@ -1,5 +1,5 @@ import { Component, LOCALE_ID } from '@angular/core'; -import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/vitest'; import { differentLocales } from '../constants/different-locales'; import { LocaleId } from '../providers/locale.provider'; import { LocalizationService } from '../services/localization.service'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/localization.pipe.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/localization.pipe.spec.ts index 4eed4b65b9..82446b473a 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/localization.pipe.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/localization.pipe.spec.ts @@ -1,4 +1,4 @@ -import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/vitest'; import { LocalizationPipe } from '../pipes/localization.pipe'; import { LocalizationService } from '../services/localization.service'; @@ -19,7 +19,7 @@ describe('LocalizationPipe', () => { }); it('should call getLocalization selector', () => { - const translateSpy = jest.spyOn(localizationService, 'instant'); + const translateSpy = vi.spyOn(localizationService, 'instant'); pipe.transform('test', '1', '2'); pipe.transform('test2', ['3', '4'] as any); diff --git a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts index 364bbb65ff..dee26425ea 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts @@ -1,4 +1,4 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { Subject } from 'rxjs'; import { LocalizationService } from '../services/localization.service'; import { SessionStateService } from '../services/session-state.service'; @@ -18,26 +18,26 @@ describe('LocalizationService', () => { { provide: SessionStateService, useValue: { - getLanguage: jest.fn(() => 'en'), - setLanguage: jest.fn(), - getLanguage$: jest.fn(() => new Subject()), - onLanguageChange$: jest.fn(() => new Subject()), + getLanguage: vi.fn(() => 'en'), + setLanguage: vi.fn(), + getLanguage$: vi.fn(() => new Subject()), + onLanguageChange$: vi.fn(() => new Subject()), }, }, { provide: ConfigStateService, useValue: { - getOne: jest.fn(), - refreshAppState: jest.fn(), - getDeep: jest.fn(), - getDeep$: jest.fn(() => new Subject()), - getOne$: jest.fn(() => new Subject()), + getOne: vi.fn(), + refreshAppState: vi.fn(), + getDeep: vi.fn(), + getDeep$: vi.fn(() => new Subject()), + getOne$: vi.fn(() => new Subject()), }, }, { provide: Injector, useValue: { - get: jest.fn(), + get: vi.fn(), }, }, ], diff --git a/npm/ng-packs/packages/core/src/lib/tests/multi-tenancy-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/multi-tenancy-utils.spec.ts index 5e49b64d22..b68e992f36 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/multi-tenancy-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/multi-tenancy-utils.spec.ts @@ -1,5 +1,6 @@ -import { Component } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { Component, PLATFORM_ID } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import clone from 'just-clone'; import { of } from 'rxjs'; @@ -10,6 +11,7 @@ import { import { EnvironmentService, MultiTenancyService } from '../services'; import { parseTenantFromUrl } from '../utils'; import { TENANT_KEY } from '../tokens'; +import { TENANT_NOT_FOUND_BY_NAME } from '../tokens/tenant-not-found-by-name'; const environment = { production: false, @@ -68,9 +70,9 @@ describe('MultiTenancyUtils', () => { test('should get the tenancyName, set replaced environment and call the findTenantByName method of AbpTenantService', async () => { const environmentService = spectator.inject(EnvironmentService); const multiTenancyService = spectator.inject(MultiTenancyService); - const setTenantByName = jest.spyOn(multiTenancyService, 'setTenantByName'); - const getEnvironmentSpy = jest.spyOn(environmentService, 'getEnvironment'); - const setStateSpy = jest.spyOn(environmentService, 'setState'); + const setTenantByName = vi.spyOn(multiTenancyService, 'setTenantByName'); + const getEnvironmentSpy = vi.spyOn(environmentService, 'getEnvironment'); + const setStateSpy = vi.spyOn(environmentService, 'setState'); getEnvironmentSpy.mockReturnValue(clone(environment)); @@ -85,10 +87,23 @@ describe('MultiTenancyUtils', () => { setTenantByName.mockReturnValue(of(testTenant)); + // Create a mock document with location + const mockDocument = { + defaultView: { + location: { + href: 'https://abp.volosoft.com/', + }, + }, + }; + const mockInjector = { get: arg => { if (arg === EnvironmentService) return environmentService; if (arg === MultiTenancyService) return multiTenancyService; + if (arg === PLATFORM_ID) return 'browser'; + if (arg === DOCUMENT) return mockDocument; + if (arg === TENANT_NOT_FOUND_BY_NAME) return null; + return null; }, }; await parseTenantFromUrl(mockInjector); diff --git a/npm/ng-packs/packages/core/src/lib/tests/ng-model.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/ng-model.component.spec.ts index d447001d4f..587d8c0adc 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/ng-model.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/ng-model.component.spec.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { AbstractNgModelComponent } from '../abstracts'; @Component({ diff --git a/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts index a2f07e7be9..9d5a9c586d 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts @@ -1,8 +1,8 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { ChangeDetectorRef } from '@angular/core'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { Subject } from 'rxjs'; import { PermissionDirective } from '../directives/permission.directive'; import { PermissionService } from '../services/permission.service'; -import { ChangeDetectorRef } from '@angular/core'; import { QUEUE_MANAGER } from '../tokens/queue.token'; describe('PermissionDirective', () => { @@ -13,16 +13,29 @@ describe('PermissionDirective', () => { directive: PermissionDirective, providers: [ { provide: PermissionService, useValue: { getGrantedPolicy$: () => grantedPolicy$ } }, - { provide: QUEUE_MANAGER, useValue: { add: jest.fn() } }, - { provide: ChangeDetectorRef, useValue: { detectChanges: jest.fn() } }, + { provide: QUEUE_MANAGER, useValue: { add: vi.fn() } }, + { provide: ChangeDetectorRef, useValue: { detectChanges: vi.fn() } }, ], }); beforeEach(() => { - spectator = createDirective('
', { - hostProps: { permission: 'test', runCD: false }, - }); + spectator = createDirective( + '
', + { + hostProps: { permission: 'test', runCD: false }, + }, + ); directive = spectator.directive; + grantedPolicy$.next(false); + spectator.detectChanges(); + }); + + afterEach(() => { + // Clean up subscriptions to prevent errors after test completion + if (directive?.subscription) { + directive.subscription.unsubscribe(); + } + grantedPolicy$.next(false); }); it('should create directive', () => { @@ -30,14 +43,20 @@ describe('PermissionDirective', () => { }); it('should handle permission input', () => { - spectator.setHostInput({ permission: 'new-permission' }); - spectator.detectChanges(); + grantedPolicy$.next(false); + directive.condition = 'new-permission'; + directive.ngOnChanges(); + grantedPolicy$.next(true); expect(directive).toBeTruthy(); + expect(directive.condition).toBe('new-permission'); }); it('should handle runChangeDetection input', () => { - spectator.setHostInput({ runCD: true }); - spectator.detectChanges(); + grantedPolicy$.next(false); + directive.runChangeDetection = true; + directive.ngOnChanges(); + grantedPolicy$.next(true); expect(directive).toBeTruthy(); + expect(directive.runChangeDetection).toBe(true); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts index 45f3b46d80..6486a7bf41 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts @@ -2,14 +2,14 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient } from '@angular/common/http'; import { Component } from '@angular/core'; import { provideRouter, Route, Router } from '@angular/router'; -import { createSpyObject, SpyObject } from '@ngneat/spectator/jest'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { TestBed } from '@angular/core/testing'; +import { createSpyObject, SpyObject } from '@ngneat/spectator/vitest'; import { of } from 'rxjs'; import { permissionGuard } from '../guards/permission.guard'; import { HttpErrorReporterService } from '../services/http-error-reporter.service'; import { PermissionService } from '../services/permission.service'; import { provideAbpCore, withOptions } from '../providers'; -import { TestBed } from '@angular/core/testing'; -import { RouterTestingHarness } from '@angular/router/testing'; import { AuthService } from '../abstracts'; @Component({ template: '' }) @@ -62,28 +62,37 @@ describe('authGuard', () => { { provide: PermissionService, useValue: permissionService }, { provide: HttpErrorReporterService, useValue: httpErrorReporter }, provideRouter(routes), - provideAbpCore(withOptions({ - environment: { - apis: { - default: { + provideAbpCore( + withOptions({ + environment: { + apis: { + default: { + url: 'http://localhost:4200', + }, + }, + application: { + baseUrl: 'http://localhost:4200', + name: 'TestApp', + }, + remoteEnv: { url: 'http://localhost:4200', + mergeStrategy: 'deepmerge', }, }, - application: { - baseUrl: 'http://localhost:4200', - name: 'TestApp', - }, - remoteEnv: { - url: 'http://localhost:4200', - mergeStrategy: 'deepmerge', - }, - }, - registerLocaleFn: () => Promise.resolve(), - })), + registerLocaleFn: () => Promise.resolve(), + skipGetAppConfiguration: true, + }), + ), ], }); }); + afterEach(async () => { + // Wait for any pending async operations to complete before teardown + await new Promise(resolve => setTimeout(resolve, 0)); + TestBed.resetTestingModule(); + }); + it('should return true when the grantedPolicy is true', async () => { permissionService.getGrantedPolicy$.andReturn(of(true)); await RouterTestingHarness.create('/dummy'); diff --git a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts index 6d8f8cf3a4..a24173aa18 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/projection.strategy.spec.ts @@ -6,7 +6,7 @@ import { ViewChild, ViewContainerRef, } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import { ComponentProjectionStrategy, PROJECTION_STRATEGY, @@ -73,7 +73,7 @@ describe('RootComponentProjectionStrategy', () => { baz = 'baz'; } - @Component({ + @Component({ template: '', imports: [], }) @@ -208,9 +208,13 @@ describe('PROJECTION_STRATEGY', () => { `( 'should successfully map $name to $Strategy.name with $domStrategy.name dom strategy', ({ name, Strategy, domStrategy }) => { - expect(PROJECTION_STRATEGY[name](content, context)).toEqual( - new Strategy(content, CONTEXT_STRATEGY.None(), domStrategy()), - ); + const result = PROJECTION_STRATEGY[name](content, context); + const expected = new Strategy(content, CONTEXT_STRATEGY.None(), domStrategy()); + + expect(result).toBeInstanceOf(Strategy); + expect(result.content).toEqual(expected.content); + expect(result['contextStrategy']).toBeInstanceOf(expected['contextStrategy'].constructor); + expect(result['domStrategy'].position).toBe(expected['domStrategy'].position); }, ); @@ -239,9 +243,14 @@ describe('PROJECTION_STRATEGY', () => { 'should successfully map $name to $Strategy.name with $contextStrategy.name context strategy and $domStrategy.name dom strategy', ({ name, Strategy, domStrategy, contextStrategy }) => { context = { x: true }; - expect(PROJECTION_STRATEGY[name](content, context)).toEqual( - new Strategy(content, contextStrategy(context), domStrategy()), - ); + const result = PROJECTION_STRATEGY[name](content, context); + const expected = new Strategy(content, contextStrategy(context), domStrategy()); + + expect(result).toBeInstanceOf(Strategy); + expect(result.content).toEqual(expected.content); + expect(result['contextStrategy']).toBeInstanceOf(expected['contextStrategy'].constructor); + expect(result['contextStrategy'].context).toEqual(expected['contextStrategy'].context); + expect(result['domStrategy'].position).toBe(expected['domStrategy'].position); }, ); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts index cb435baf56..c678088264 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { BehaviorSubject } from 'rxjs'; import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component'; import { ReplaceableComponentsService } from '../services/replaceable-components.service'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts index 3bf3a6d8bb..08075ee7f4 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; import { Router } from '@angular/router'; -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { BehaviorSubject } from 'rxjs'; import { ReplaceableTemplateDirective } from '../directives/replaceable-template.directive'; import { ReplaceableComponents } from '../models/replaceable-components'; @@ -50,8 +50,8 @@ describe('ReplaceableTemplateDirective', () => { }); describe('without external component', () => { - const twoWayChange = jest.fn(a => a); - const someOutput = jest.fn(a => a); + const twoWayChange = vi.fn(a => a); + const someOutput = vi.fn(a => a); beforeEach(() => { spectator = createDirective( @@ -88,8 +88,8 @@ describe('ReplaceableTemplateDirective', () => { hostProps: { oneWay: { label: 'Test' }, twoWay: false, - twoWayChange: jest.fn(), - someOutput: jest.fn(), + twoWayChange: vi.fn(), + someOutput: vi.fn(), }, }, ); diff --git a/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts index 5cbd56328a..9ac795c24f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts @@ -1,4 +1,4 @@ -import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/jest'; +import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -74,17 +74,25 @@ describe('HttpClient testing', () => { spectator.expectOne('bar' + '/test', HttpMethod.GET); }); - test('should complete upon successful request', done => { - const complete = jest.fn(done); + test('should complete upon successful request', async () => { + const request$ = spectator.service.request({ method: HttpMethod.GET, url: '/test' }); - spectator.service.request({ method: HttpMethod.GET, url: '/test' }).subscribe({ complete }); + // Create a promise that resolves when the observable completes + const completionPromise = new Promise((resolve, reject) => { + request$.subscribe({ + complete: () => resolve(), + error: err => reject(err), + }); + }); const req = spectator.expectOne(api + '/test', HttpMethod.GET); spectator.flushAll([req], [{}]); + + await completionPromise; }); test('should handle the error', () => { - const spy = jest.spyOn(httpErrorReporter, 'reportError'); + const spy = vi.spyOn(httpErrorReporter, 'reportError'); spectator.service .request({ method: HttpMethod.GET, url: '/test' }, { observe: Rest.Observe.Events }) @@ -102,7 +110,7 @@ describe('HttpClient testing', () => { }); test('should not handle the error when skipHandleError is true', () => { - const spy = jest.spyOn(httpErrorReporter, 'reportError'); + const spy = vi.spyOn(httpErrorReporter, 'reportError'); spectator.service .request( diff --git a/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts index 3ee908251a..f8443a1804 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/vitest'; import { RouterOutletComponent } from '../components/router-outlet.component'; import { RoutesService } from '../services/routes.service'; import { findRoute, getRoutePath } from '../utils/route-utils'; @@ -23,7 +23,7 @@ describe('Route Utils', () => { `( 'should find $expected in $count turns when path is $path', async ({ path, expected, count }) => { - const find = jest.fn(cb => (cb(node) ? node : null)); + const find = vi.fn(cb => (cb(node) ? node : null)); const routes = { find } as any as RoutesService; const route = findRoute(routes, path); expect(route).toBe(expected); diff --git a/npm/ng-packs/packages/core/src/lib/tests/router-events.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/router-events.service.spec.ts index bc22065aa7..95f199f381 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/router-events.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/router-events.service.spec.ts @@ -1,6 +1,6 @@ import { Router, RouterEvent, NavigationStart, ResolveStart, NavigationError, NavigationEnd, ResolveEnd, NavigationCancel } from '@angular/router'; import { Subject } from 'rxjs'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { take } from 'rxjs/operators'; import { NavigationEventKey, RouterEvents } from '../services/router-events.service'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/router-outlet.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/router-outlet.component.spec.ts index c76876f77a..00ec796e55 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/router-outlet.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/router-outlet.component.spec.ts @@ -1,4 +1,4 @@ -import { Spectator, createComponentFactory } from '@ngneat/spectator/jest'; +import { Spectator, createComponentFactory } from '@ngneat/spectator/vitest'; import { provideRouter } from '@angular/router'; import { RouterOutletComponent } from '../components/router-outlet.component'; diff --git a/npm/ng-packs/packages/core/src/lib/tests/routes.handler.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/routes.handler.spec.ts index 853331539d..32ea1e4324 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/routes.handler.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/routes.handler.spec.ts @@ -1,7 +1,7 @@ import { Router } from '@angular/router'; import { RoutesHandler } from '../handlers/routes.handler'; import { RoutesService } from '../services/routes.service'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; describe('Routes Handler', () => { let spectator: SpectatorService; @@ -15,7 +15,7 @@ describe('Routes Handler', () => { { provide: RoutesService, useValue: { - add: jest.fn(), + add: vi.fn(), }, }, { diff --git a/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts index 71e71945d0..158c9ca644 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts @@ -1,5 +1,5 @@ import { RoutesService } from '../services/routes.service'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { CORE_OPTIONS } from '../tokens/options.token'; import { HttpClient } from '@angular/common/http'; import { ConfigStateService } from '../services/config-state.service'; @@ -33,51 +33,51 @@ describe('Routes Service', () => { { provide: HttpClient, useValue: { - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - delete: jest.fn(), + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), }, }, { provide: ConfigStateService, useValue: { - getOne: jest.fn(), - getDeep: jest.fn(), - getDeep$: jest.fn(() => ({ subscribe: jest.fn() })), - createOnUpdateStream: jest.fn(() => ({ - subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })) + getOne: vi.fn(), + getDeep: vi.fn(), + getDeep$: vi.fn(() => ({ subscribe: vi.fn() })), + createOnUpdateStream: vi.fn(() => ({ + subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })) })), }, }, { provide: AbpApplicationConfigurationService, useValue: { - get: jest.fn(), + get: vi.fn(), }, }, { provide: RestService, useValue: { - request: jest.fn(), + request: vi.fn(), }, }, { provide: EnvironmentService, useValue: { - getEnvironment: jest.fn(), + getEnvironment: vi.fn(), }, }, { provide: HttpErrorReporterService, useValue: { - reportError: jest.fn(), + reportError: vi.fn(), }, }, { provide: ExternalHttpClient, useValue: { - request: jest.fn(), + request: vi.fn(), }, }, { diff --git a/npm/ng-packs/packages/core/src/lib/tests/safe-html.pipe.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/safe-html.pipe.spec.ts index 69ecfeb3a2..a36b2062bc 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/safe-html.pipe.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/safe-html.pipe.spec.ts @@ -1,4 +1,4 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { SafeHtmlPipe } from '../pipes'; describe('SafeHtmlPipe', () => { @@ -27,7 +27,7 @@ describe('SafeHtmlPipe', () => { }); it('should sanitize unsafe HTML content', () => { - const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); const input = `

Click here!

`; const result = pipe.transform(input); expect(result).toBe(`

Click here!

`); diff --git a/npm/ng-packs/packages/core/src/lib/tests/show-password-directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/show-password-directive.spec.ts index a1e766dc51..e1266094cf 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/show-password-directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/show-password-directive.spec.ts @@ -1,55 +1,63 @@ -import { Component, DebugElement } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { ShowPasswordDirective } from '../directives'; -import { By } from '@angular/platform-browser'; - -@Component({ - standalone:true, - template: ` - - - - `, - imports:[ShowPasswordDirective] -}) -class TestComponent { - showPassword = false -} - -describe('ShowPasswordDirective',()=>{ - let fixture: ComponentFixture;; - let des : DebugElement[]; - let desAll : DebugElement[]; - let bareInput; - - beforeEach(()=>{ - fixture = TestBed.configureTestingModule({ - imports: [ TestComponent ] - }).createComponent(TestComponent) - - fixture.detectChanges(); - - des = fixture.debugElement.queryAll(By.directive(ShowPasswordDirective)); - - desAll = fixture.debugElement.queryAll(By.all()); - - bareInput = fixture.debugElement.query(By.css('input:not([abpShowPassword])')); - }) - - it('should have three input has ShowPasswordDirective elements', () => { - expect(des.length).toBe(3); - }); - - test.each([[0,'text'],[1,'password'],[2,'text'],[3,'password']])('%p. input type must be %p)', (index,inpType) => { - const inputType = desAll[index].nativeElement.type; - expect(inputType).toBe(inpType); - }); - - it('should have three input has ShowPasswordDirective elements', () => { - const input = des[2].nativeElement - expect(input.type).toBe('password') - fixture.componentInstance.showPassword = true - fixture.detectChanges() - expect(input.type).toBe('text') - }); - }); \ No newline at end of file +import { Component, DebugElement, ChangeDetectorRef } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ShowPasswordDirective } from '../directives'; + +@Component({ + template: ` + + + `, + imports: [ShowPasswordDirective], +}) +class TestComponent { + showPassword = false; +} + +describe('ShowPasswordDirective', () => { + let fixture: ComponentFixture; + let des: DebugElement[]; + let desAll: DebugElement[]; + let bareInput; + + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [TestComponent], + }).createComponent(TestComponent); + + fixture.detectChanges(); + + des = fixture.debugElement.queryAll(By.directive(ShowPasswordDirective)); + + desAll = fixture.debugElement.queryAll(By.all()); + + bareInput = fixture.debugElement.query(By.css('input:not([abpShowPassword])')); + }); + + it('should have three input has ShowPasswordDirective elements', () => { + expect(des.length).toBe(3); + }); + + test.each([ + [0, 'text'], + [1, 'password'], + [2, 'text'], + [3, 'password'], + ])('%p. input type must be %p)', (index, inpType) => { + const inputType = desAll[index].nativeElement.type; + expect(inputType).toBe(inpType); + }); + + it('should toggle input type when showPassword changes', () => { + const input = des[2].nativeElement; + expect(input.type).toBe('password'); + + fixture.componentInstance.showPassword = true; + + const cdr = fixture.componentRef.injector.get(ChangeDetectorRef); + cdr.markForCheck(); + cdr.detectChanges(); + + expect(input.type).toBe('text'); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/sort.pipe.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/sort.pipe.spec.ts index 0d60ab0b21..7cc830990c 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/sort.pipe.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/sort.pipe.spec.ts @@ -1,4 +1,4 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { SortPipe } from '../pipes/sort.pipe'; describe('SortPipe', () => { diff --git a/npm/ng-packs/packages/core/src/lib/tests/stop-propagation.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/stop-propagation.directive.spec.ts index f054601efa..8e0595c3c3 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/stop-propagation.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/stop-propagation.directive.spec.ts @@ -1,12 +1,12 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { StopPropagationDirective } from '../directives/stop-propagation.directive'; describe('StopPropagationDirective', () => { let spectator: SpectatorDirective; let directive: StopPropagationDirective; let link: HTMLAnchorElement; - const childClickEventFn = jest.fn(() => null); - const parentClickEventFn = jest.fn(() => null); + const childClickEventFn = vi.fn(() => null); + const parentClickEventFn = vi.fn(() => null); const createDirective = createDirectiveFactory({ directive: StopPropagationDirective, }); @@ -28,12 +28,11 @@ describe('StopPropagationDirective', () => { expect(directive).toBeTruthy(); }); - test('should not call click event of parent when child element is clicked', done => { + test('should not call click event of parent when child element is clicked', () => { spectator.setHostInput({ parentClickEventFn, childClickEventFn }); spectator.click('a'); spectator.detectChanges(); expect(childClickEventFn).toHaveBeenCalled(); expect(parentClickEventFn).not.toHaveBeenCalled(); - done(); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts index d0c8c4754f..191c5868c3 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/subscription.service.spec.ts @@ -14,8 +14,8 @@ describe('SubscriptionService', () => { describe('#addOne', () => { it('should subscribe to given observable with next and error functions and return the Subscription instance', () => { - const next = jest.fn(); - const error = jest.fn(); + const next = vi.fn(); + const error = vi.fn(); const subscription = service.addOne(of(null), next, error); expect(subscription).toBeInstanceOf(Subscription); expect(next).toHaveBeenCalledWith(null); @@ -24,7 +24,7 @@ describe('SubscriptionService', () => { }); it('should subscribe to given observable with observer and return the Subscription instance', () => { - const observer = { next: jest.fn(), complete: jest.fn() }; + const observer = { next: vi.fn(), complete: vi.fn() }; const subscription = service.addOne(of(null), observer); expect(subscription).toBeInstanceOf(Subscription); expect(observer.next).toHaveBeenCalledWith(null); diff --git a/npm/ng-packs/packages/core/src/test-setup.ts b/npm/ng-packs/packages/core/src/test-setup.ts index 13874ec714..1d4c608e96 100644 --- a/npm/ng-packs/packages/core/src/test-setup.ts +++ b/npm/ng-packs/packages/core/src/test-setup.ts @@ -1,7 +1,11 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; -setupZoneTestEnv(); +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; + +// Initialize Angular testing environment +getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); -// Mock window.location for test environment Object.defineProperty(window, 'location', { value: { href: 'http://localhost:4200', diff --git a/npm/ng-packs/packages/core/tsconfig.lib.json b/npm/ng-packs/packages/core/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/core/tsconfig.lib.json +++ b/npm/ng-packs/packages/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/core/tsconfig.spec.json b/npm/ng-packs/packages/core/tsconfig.spec.json index be72f24e9b..8c496b20df 100644 --- a/npm/ng-packs/packages/core/tsconfig.spec.json +++ b/npm/ng-packs/packages/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": ["**/*.ts"] + "include": [ + "vite.config.ts", + "vitest.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/core/vitest.config.mts b/npm/ng-packs/packages/core/vitest.config.mts new file mode 100644 index 0000000000..f3c41ed210 --- /dev/null +++ b/npm/ng-packs/packages/core/vitest.config.mts @@ -0,0 +1,22 @@ +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/core', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'core', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/core', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/feature-management/.eslintrc.json b/npm/ng-packs/packages/feature-management/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/feature-management/.eslintrc.json +++ b/npm/ng-packs/packages/feature-management/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/feature-management/jest.config.ts b/npm/ng-packs/packages/feature-management/jest.config.ts index be424e36af..7740aa6d8a 100644 --- a/npm/ng-packs/packages/feature-management/jest.config.ts +++ b/npm/ng-packs/packages/feature-management/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: 'feature-management', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/feature-management/package.json b/npm/ng-packs/packages/feature-management/package.json index cb15079951..628c9d0251 100644 --- a/npm/ng-packs/packages/feature-management/package.json +++ b/npm/ng-packs/packages/feature-management/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.feature-management", - "version": "10.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.theme.shared": "~10.0.1", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/feature-management/project.json b/npm/ng-packs/packages/feature-management/project.json index 7712d1b67b..e34c0d6b16 100644 --- a/npm/ng-packs/packages/feature-management/project.json +++ b/npm/ng-packs/packages/feature-management/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/feature-management"], - "options": { - "jestConfig": "packages/feature-management/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/feature-management" + } } } } diff --git a/npm/ng-packs/packages/feature-management/tsconfig.lib.json b/npm/ng-packs/packages/feature-management/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/feature-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/feature-management/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/feature-management/tsconfig.spec.json b/npm/ng-packs/packages/feature-management/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/feature-management/tsconfig.spec.json +++ b/npm/ng-packs/packages/feature-management/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/feature-management/vitest.config.mts b/npm/ng-packs/packages/feature-management/vitest.config.mts new file mode 100644 index 0000000000..bfe50da685 --- /dev/null +++ b/npm/ng-packs/packages/feature-management/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/feature-management', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'feature-management', + 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/feature-management', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/generators/.eslintrc.json b/npm/ng-packs/packages/generators/.eslintrc.json index 99664e583f..18e0359cbb 100644 --- a/npm/ng-packs/packages/generators/.eslintrc.json +++ b/npm/ng-packs/packages/generators/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], diff --git a/npm/ng-packs/packages/generators/jest.config.ts b/npm/ng-packs/packages/generators/jest.config.ts index cd4c894fe7..0f5f3017ef 100644 --- a/npm/ng-packs/packages/generators/jest.config.ts +++ b/npm/ng-packs/packages/generators/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: 'generators', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/generators/package.json b/npm/ng-packs/packages/generators/package.json index 4f5a133f36..ff1932f9e0 100644 --- a/npm/ng-packs/packages/generators/package.json +++ b/npm/ng-packs/packages/generators/package.json @@ -1,6 +1,6 @@ { "name": "@abp/nx.generators", - "version": "10.0.1", + "version": "10.1.0-rc.2", "homepage": "https://abp.io", "generators": "./generators.json", "type": "commonjs", diff --git a/npm/ng-packs/packages/generators/project.json b/npm/ng-packs/packages/generators/project.json index 5924f7caac..e0215aa306 100644 --- a/npm/ng-packs/packages/generators/project.json +++ b/npm/ng-packs/packages/generators/project.json @@ -46,10 +46,10 @@ "outputs": ["{options.outputFile}"] }, "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], "options": { - "jestConfig": "packages/generators/jest.config.ts" + "reportsDirectory": "../../coverage/packages/generators" } } } diff --git a/npm/ng-packs/packages/generators/src/generators/change-theme/generator.spec.ts b/npm/ng-packs/packages/generators/src/generators/change-theme/generator.spec.ts index 42c6753bbf..22900a606b 100644 --- a/npm/ng-packs/packages/generators/src/generators/change-theme/generator.spec.ts +++ b/npm/ng-packs/packages/generators/src/generators/change-theme/generator.spec.ts @@ -1,11 +1,11 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { Tree, readProjectConfiguration } from '@nx/devkit'; +import { Tree } from '@nx/devkit'; import { changeThemeGenerator } from './generator'; import { ChangeThemeGeneratorSchema } from './schema'; -jest.mock('@nx/devkit/ngcli-adapter', () => ({ - wrapAngularDevkitSchematic: jest.fn(() => jest.fn()), +vi.mock('@nx/devkit/ngcli-adapter', () => ({ + wrapAngularDevkitSchematic: vi.fn(() => vi.fn()), })); describe('change-theme generator', () => { diff --git a/npm/ng-packs/packages/generators/tsconfig.lib.json b/npm/ng-packs/packages/generators/tsconfig.lib.json index 33eca2c2cd..b1cf8952fc 100644 --- a/npm/ng-packs/packages/generators/tsconfig.lib.json +++ b/npm/ng-packs/packages/generators/tsconfig.lib.json @@ -6,5 +6,19 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] } diff --git a/npm/ng-packs/packages/generators/tsconfig.spec.json b/npm/ng-packs/packages/generators/tsconfig.spec.json index f6d8ffcc9f..ba1ad41a90 100644 --- a/npm/ng-packs/packages/generators/tsconfig.spec.json +++ b/npm/ng-packs/packages/generators/tsconfig.spec.json @@ -2,8 +2,21 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"] }, - "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.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" + ] } diff --git a/npm/ng-packs/packages/generators/vitest.config.mts b/npm/ng-packs/packages/generators/vitest.config.mts new file mode 100644 index 0000000000..299cea484b --- /dev/null +++ b/npm/ng-packs/packages/generators/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/generators', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'generators', + 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/generators', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/identity/.eslintrc.json b/npm/ng-packs/packages/identity/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/identity/.eslintrc.json +++ b/npm/ng-packs/packages/identity/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/identity/jest.config.ts b/npm/ng-packs/packages/identity/jest.config.ts index 469457531d..640c748717 100644 --- a/npm/ng-packs/packages/identity/jest.config.ts +++ b/npm/ng-packs/packages/identity/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: 'identity', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/identity/package.json b/npm/ng-packs/packages/identity/package.json index e965b85f3c..a9d91b2d36 100644 --- a/npm/ng-packs/packages/identity/package.json +++ b/npm/ng-packs/packages/identity/package.json @@ -1,15 +1,15 @@ { "name": "@abp/ng.identity", - "version": "10.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.components": "~10.0.1", - "@abp/ng.permission-management": "~10.0.1", - "@abp/ng.theme.shared": "~10.0.1", + "@abp/ng.components": "~10.1.0-rc.2", + "@abp/ng.permission-management": "~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/identity/project.json b/npm/ng-packs/packages/identity/project.json index e67a92ba2a..cbded9c762 100644 --- a/npm/ng-packs/packages/identity/project.json +++ b/npm/ng-packs/packages/identity/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/identity"], - "options": { - "jestConfig": "packages/identity/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/identity" + } } } } diff --git a/npm/ng-packs/packages/identity/tsconfig.lib.json b/npm/ng-packs/packages/identity/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/identity/tsconfig.lib.json +++ b/npm/ng-packs/packages/identity/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/identity/tsconfig.spec.json b/npm/ng-packs/packages/identity/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/identity/tsconfig.spec.json +++ b/npm/ng-packs/packages/identity/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/identity/vitest.config.mts b/npm/ng-packs/packages/identity/vitest.config.mts new file mode 100644 index 0000000000..89b2ebcb0e --- /dev/null +++ b/npm/ng-packs/packages/identity/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/identity', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'identity', + 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/identity', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/oauth/.eslintrc.json b/npm/ng-packs/packages/oauth/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/oauth/.eslintrc.json +++ b/npm/ng-packs/packages/oauth/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/oauth/jest.config.ts b/npm/ng-packs/packages/oauth/jest.config.ts index eb598713d5..1ff693025e 100644 --- a/npm/ng-packs/packages/oauth/jest.config.ts +++ b/npm/ng-packs/packages/oauth/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: 'oauth', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/oauth/package.json b/npm/ng-packs/packages/oauth/package.json index 99c4a06485..98bc02a772 100644 --- a/npm/ng-packs/packages/oauth/package.json +++ b/npm/ng-packs/packages/oauth/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.oauth", - "version": "10.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/utils": "~10.0.1", + "@abp/ng.core": "~10.1.0-rc.2", + "@abp/utils": "~10.1.0-rc.2", "angular-oauth2-oidc": "^20.0.0", "just-clone": "^6.0.0", "just-compare": "^2.0.0", diff --git a/npm/ng-packs/packages/oauth/project.json b/npm/ng-packs/packages/oauth/project.json index 858ec5947c..a1948047b1 100644 --- a/npm/ng-packs/packages/oauth/project.json +++ b/npm/ng-packs/packages/oauth/project.json @@ -23,15 +23,15 @@ }, "defaultConfiguration": "production" }, + "lint": { + "executor": "@nx/eslint:lint" + }, "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], "options": { - "jestConfig": "packages/oauth/jest.config.ts" + "reportsDirectory": "../../coverage/packages/oauth" } - }, - "lint": { - "executor": "@nx/eslint:lint" } } } diff --git a/npm/ng-packs/packages/oauth/src/lib/tests/api.interceptor.spec.ts b/npm/ng-packs/packages/oauth/src/lib/tests/api.interceptor.spec.ts index 04a7652300..247b79b798 100644 --- a/npm/ng-packs/packages/oauth/src/lib/tests/api.interceptor.spec.ts +++ b/npm/ng-packs/packages/oauth/src/lib/tests/api.interceptor.spec.ts @@ -1,8 +1,8 @@ import { HttpRequest } from '@angular/common/http'; import { SpyObject } from '@ngneat/spectator'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; -import { Subject, timer } from 'rxjs'; +import { Subject } from 'rxjs'; import { HttpWaitService, SessionStateService, TENANT_KEY } from '@abp/ng.core'; import { OAuthApiInterceptor } from '../interceptors'; @@ -29,7 +29,7 @@ describe('ApiInterceptor', () => { httpWaitService = spectator.inject(HttpWaitService); }); - it('should add headers to http request', done => { + it('should add headers to http request', () => { oauthService.getAccessToken.andReturn('ey892mkwa8^2jk'); sessionState.getLanguage.andReturn('tr'); sessionState.getTenant.andReturn({ id: 'Volosoft', name: 'Volosoft' }); @@ -42,7 +42,6 @@ describe('ApiInterceptor', () => { expect(req.headers.get('Authorization')).toEqual('Bearer ey892mkwa8^2jk'); expect(req.headers.get('Accept-Language')).toEqual('tr'); expect(req.headers.get(testTenantKey)).toEqual('Volosoft'); - done(); return handleRes$; }, }; @@ -53,9 +52,9 @@ describe('ApiInterceptor', () => { handleRes$.complete(); }); - it('should call http wait services add request and delete request', done => { - const spyAddRequest = jest.spyOn(httpWaitService, 'addRequest'); - const spyDeleteRequest = jest.spyOn(httpWaitService, 'deleteRequest'); + it('should call http wait services add request and delete request', () => { + const spyAddRequest = vi.spyOn(httpWaitService, 'addRequest'); + const spyDeleteRequest = vi.spyOn(httpWaitService, 'deleteRequest'); const request = new HttpRequest('GET', 'https://abp.io'); const handleRes$ = new Subject(); @@ -71,10 +70,7 @@ describe('ApiInterceptor', () => { handleRes$.next(); handleRes$.complete(); - timer(0).subscribe(() => { - expect(spyAddRequest).toHaveBeenCalled(); - expect(spyDeleteRequest).toHaveBeenCalled(); - done(); - }); + expect(spyAddRequest).toHaveBeenCalled(); + expect(spyDeleteRequest).toHaveBeenCalled(); }); }); diff --git a/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts b/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts index 2b6603f39c..ccb4658999 100644 --- a/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts +++ b/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts @@ -1,4 +1,4 @@ -import { createServiceFactory, SpectatorService, createSpyObject } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService, createSpyObject } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; import { AbpOAuthGuard, abpOAuthGuard } from '../guards/oauth.guard'; import { AuthService } from '@abp/ng.core'; @@ -40,7 +40,7 @@ describe('AuthGuard', () => { it('should execute the navigateToLogin method of the authService', () => { const authService = spectator.inject(AuthService); spectator.inject(OAuthService).hasValidAccessToken.andReturn(false); - const navigateToLoginSpy = jest.spyOn(authService, 'navigateToLogin'); + const navigateToLoginSpy = vi.spyOn(authService, 'navigateToLogin'); expect(guard.canActivate(route, state)).toBe(false); expect(navigateToLoginSpy).toHaveBeenCalled(); diff --git a/npm/ng-packs/packages/oauth/src/lib/tests/initial-utils.spec.ts b/npm/ng-packs/packages/oauth/src/lib/tests/initial-utils.spec.ts index cd2dfbcac5..a8a4372817 100644 --- a/npm/ng-packs/packages/oauth/src/lib/tests/initial-utils.spec.ts +++ b/npm/ng-packs/packages/oauth/src/lib/tests/initial-utils.spec.ts @@ -1,6 +1,6 @@ import { Component, Injector } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; -import { OAuthService } from 'angular-oauth2-oidc'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; import { CORE_OPTIONS, @@ -42,6 +42,15 @@ describe('InitialUtils', () => { skipGetAppConfiguration: false, }, }, + { + provide: OAuthStorage, + useValue: { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + }, + }, ], }); @@ -53,8 +62,8 @@ describe('InitialUtils', () => { let clearOAuthStorageSpy; beforeEach(() => { injector = spectator.inject(Injector); - injectorSpy = jest.spyOn(injector, 'get'); - clearOAuthStorageSpy = jest.spyOn(clearOAuthStorageDefault, 'clearOAuthStorage'); + injectorSpy = vi.spyOn(injector, 'get'); + clearOAuthStorageSpy = vi.spyOn(clearOAuthStorageDefault, 'clearOAuthStorage'); clearOAuthStorageSpy.mockReset(); }); diff --git a/npm/ng-packs/packages/oauth/src/lib/tests/oauth-error-filter.service.spec.ts b/npm/ng-packs/packages/oauth/src/lib/tests/oauth-error-filter.service.spec.ts index c8af0f934d..a112e847ec 100644 --- a/npm/ng-packs/packages/oauth/src/lib/tests/oauth-error-filter.service.spec.ts +++ b/npm/ng-packs/packages/oauth/src/lib/tests/oauth-error-filter.service.spec.ts @@ -1,14 +1,13 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; -import { OAuthErrorFilterService } from '../services'; -import { AuthErrorEvent, AuthErrorFilter } from '@abp/ng.core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { OAuthErrorEvent } from 'angular-oauth2-oidc'; +import { AuthErrorEvent, AuthErrorFilter } from '@abp/ng.core'; +import { OAuthErrorFilterService } from '../services'; const ids = { firstFilter: 'firstFilter', secondFilter: 'secondFilter', }; -type Reason = object & { error: { grant_type: string | undefined; }; }; - +type Reason = object & { error: { grant_type: string | undefined } }; describe('AuthService', () => { let spectator: SpectatorService; @@ -37,11 +36,11 @@ describe('AuthService', () => { const { error: { grant_type }, } = (reason || {}); - + return !!grant_type && grant_type === ids.firstFilter; }, }; - + secondFilter = { id: ids.secondFilter, executable: true, @@ -50,7 +49,7 @@ describe('AuthService', () => { const { error: { grant_type }, } = (reason || {}); - + return !!grant_type && grant_type === ids.secondFilter; }, }; @@ -118,4 +117,4 @@ describe('AuthService', () => { expect(oAuthErrorFilterService.run(event)).toBe(false); }); -}); \ No newline at end of file +}); diff --git a/npm/ng-packs/packages/oauth/src/lib/tests/remember-me.service.spec.ts b/npm/ng-packs/packages/oauth/src/lib/tests/remember-me.service.spec.ts index 0ef7116e92..c6d7b81e01 100644 --- a/npm/ng-packs/packages/oauth/src/lib/tests/remember-me.service.spec.ts +++ b/npm/ng-packs/packages/oauth/src/lib/tests/remember-me.service.spec.ts @@ -1,8 +1,6 @@ -import { SpectatorService, SpyObject, createServiceFactory } from "@ngneat/spectator/jest"; -import { RememberMeService } from "../services/remember-me.service"; -import { AbpLocalStorageService } from "@abp/ng.core"; - - +import { SpectatorService, SpyObject, createServiceFactory } from '@ngneat/spectator/vitest'; +import { AbpLocalStorageService } from '@abp/ng.core'; +import { RememberMeService } from '../services/remember-me.service'; describe('RememberMeService', () => { const key = 'remember_me'; @@ -12,10 +10,9 @@ describe('RememberMeService', () => { const createService = createServiceFactory({ service: RememberMeService, - mocks: [AbpLocalStorageService] + mocks: [AbpLocalStorageService], }); - beforeEach(() => { spectator = createService(); rememberMeService = spectator.inject(RememberMeService); @@ -55,17 +52,16 @@ describe('RememberMeService', () => { }); it('should return true when parsed token is setted to true', () => { - const data = { "remember_me": "True" }; + const data = { remember_me: 'True' }; const base64_encoded = btoa(JSON.stringify(data)); - const tokenWithValueTrue = "random." + base64_encoded + ".random"; + const tokenWithValueTrue = 'random.' + base64_encoded + '.random'; expect(rememberMeService.getFromToken(tokenWithValueTrue)).toBe(true); }); it('should return false when value is not setted(undefined)', () => { const data = {}; const base64_encoded = btoa(JSON.stringify(data)); - const tokenWithValueTrue = "random." + base64_encoded + ".random"; + const tokenWithValueTrue = 'random.' + base64_encoded + '.random'; expect(rememberMeService.getFromToken(tokenWithValueTrue)).toBe(false); }); - -}); \ No newline at end of file +}); diff --git a/npm/ng-packs/packages/oauth/src/lib/utils/check-access-token.ts b/npm/ng-packs/packages/oauth/src/lib/utils/check-access-token.ts index 84457c4c3c..38111dc3e9 100644 --- a/npm/ng-packs/packages/oauth/src/lib/utils/check-access-token.ts +++ b/npm/ng-packs/packages/oauth/src/lib/utils/check-access-token.ts @@ -7,6 +7,6 @@ export const checkAccessToken: CheckAuthenticationStateFn = function (injector: const configState = injector.get(ConfigStateService); const oAuth = injector.get(OAuthService); if (oAuth.hasValidAccessToken() && !configState.getDeep('currentUser.id')) { - clearOAuthStorage(this.injector); + clearOAuthStorage(injector); } }; diff --git a/npm/ng-packs/packages/oauth/src/test-setup.ts b/npm/ng-packs/packages/oauth/src/test-setup.ts index 4555f138a7..b72ff2d424 100644 --- a/npm/ng-packs/packages/oauth/src/test-setup.ts +++ b/npm/ng-packs/packages/oauth/src/test-setup.ts @@ -1,2 +1,19 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; -setupZoneTestEnv(); +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; + +// Initialize Angular testing environment +getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + +// Mock window.location for test environment +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:4200', + origin: 'http://localhost:4200', + pathname: '/', + search: '', + hash: '', + }, + writable: true, +}); diff --git a/npm/ng-packs/packages/oauth/tsconfig.lib.json b/npm/ng-packs/packages/oauth/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/oauth/tsconfig.lib.json +++ b/npm/ng-packs/packages/oauth/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/oauth/tsconfig.spec.json b/npm/ng-packs/packages/oauth/tsconfig.spec.json index be72f24e9b..fc61345bb3 100644 --- a/npm/ng-packs/packages/oauth/tsconfig.spec.json +++ b/npm/ng-packs/packages/oauth/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": ["**/*.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/oauth/vitest.config.mts b/npm/ng-packs/packages/oauth/vitest.config.mts new file mode 100644 index 0000000000..292f5376e2 --- /dev/null +++ b/npm/ng-packs/packages/oauth/vitest.config.mts @@ -0,0 +1,22 @@ +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/oauth', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'oauth', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/oauth', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/permission-management/.eslintrc.json b/npm/ng-packs/packages/permission-management/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/permission-management/.eslintrc.json +++ b/npm/ng-packs/packages/permission-management/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/permission-management/jest.config.ts b/npm/ng-packs/packages/permission-management/jest.config.ts index 78cb7d6c8f..bc3f3993e8 100644 --- a/npm/ng-packs/packages/permission-management/jest.config.ts +++ b/npm/ng-packs/packages/permission-management/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: 'permission-management', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/permission-management/package.json b/npm/ng-packs/packages/permission-management/package.json index 05da08386a..7ffca4e312 100644 --- a/npm/ng-packs/packages/permission-management/package.json +++ b/npm/ng-packs/packages/permission-management/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.permission-management", - "version": "10.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.theme.shared": "~10.0.1", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, "publishConfig": { diff --git a/npm/ng-packs/packages/permission-management/project.json b/npm/ng-packs/packages/permission-management/project.json index 3e3287256d..ddd8ad11af 100644 --- a/npm/ng-packs/packages/permission-management/project.json +++ b/npm/ng-packs/packages/permission-management/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/permission-management"], - "options": { - "jestConfig": "packages/permission-management/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/permission-management" + } } } } diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/permission-checkbox-list/permission-checkbox-list.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/permission-checkbox-list/permission-checkbox-list.component.html index 07db143b00..aeaefdff20 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/permission-checkbox-list/permission-checkbox-list.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/permission-checkbox-list/permission-checkbox-list.component.html @@ -1,25 +1,33 @@
- @if (showTitle()) { + @if (showTitle()) {
{{ title() | abpLocalization }}
- } -
- -
+ } +
+ diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html index 41740a1f64..b04e1552c9 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.html @@ -1,4 +1,10 @@ - \ No newline at end of file + diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts index 0aceed0c5f..d8379890fb 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/provider-key-search/provider-key-search.component.ts @@ -1,4 +1,11 @@ -import { Component, input, inject, OnInit, ChangeDetectionStrategy, DestroyRef } from '@angular/core'; +import { + Component, + input, + inject, + OnInit, + ChangeDetectionStrategy, + DestroyRef, +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { PermissionsService } from '@abp/ng.permission-management/proxy'; import { LookupSearchComponent, LookupItem } from '@abp/ng.components/lookup'; @@ -6,56 +13,55 @@ import { Observable, map } from 'rxjs'; import { ResourcePermissionStateService } from '../../../services/resource-permission-state.service'; interface ProviderKeyLookupItem extends LookupItem { - providerKey: string; - providerDisplayName?: string; + providerKey: string; + providerDisplayName?: string; } @Component({ - selector: 'abp-provider-key-search', - templateUrl: './provider-key-search.component.html', - imports: [LookupSearchComponent], - changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'abp-provider-key-search', + templateUrl: './provider-key-search.component.html', + imports: [LookupSearchComponent], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProviderKeySearchComponent implements OnInit { - readonly state = inject(ResourcePermissionStateService); - private readonly service = inject(PermissionsService); - private readonly destroyRef = inject(DestroyRef); + readonly state = inject(ResourcePermissionStateService); + private readonly service = inject(PermissionsService); + private readonly destroyRef = inject(DestroyRef); - readonly resourceName = input.required(); + readonly resourceName = input.required(); - searchFn: (filter: string) => Observable = () => new Observable(); + searchFn: (filter: string) => Observable = () => new Observable(); - ngOnInit() { - this.searchFn = (filter: string) => this.loadProviderKeys(filter); - } + ngOnInit() { + this.searchFn = (filter: string) => this.loadProviderKeys(filter); + } - onItemSelected(item: ProviderKeyLookupItem) { - // State is already updated via displayValue and selectedValue bindings - // This handler can be used for additional side effects if needed - } + onItemSelected(item: ProviderKeyLookupItem) { + // State is already updated via displayValue and selectedValue bindings + // This handler can be used for additional side effects if needed + } - private loadProviderKeys(filter: string): Observable { - const providerName = this.state.selectedProviderName(); - if (!providerName) { - return new Observable(subscriber => { - subscriber.next([]); - subscriber.complete(); - }); - } - - return this.service.searchResourceProviderKey( - this.resourceName(), - providerName, - filter, - 1 - ).pipe( - map(res => (res.keys || []).map(k => ({ - key: k.providerKey || '', - displayName: k.providerDisplayName || k.providerKey || '', - providerKey: k.providerKey || '', - providerDisplayName: k.providerDisplayName || undefined, - }))), - takeUntilDestroyed(this.destroyRef) - ); + private loadProviderKeys(filter: string): Observable { + const providerName = this.state.selectedProviderName(); + if (!providerName) { + return new Observable(subscriber => { + subscriber.next([]); + subscriber.complete(); + }); } + + return this.service + .searchResourceProviderKey(this.resourceName(), providerName, filter, 1) + .pipe( + map(res => + (res.keys || []).map(k => ({ + key: k.providerKey || '', + displayName: k.providerDisplayName || k.providerKey || '', + providerKey: k.providerKey || '', + providerDisplayName: k.providerDisplayName || undefined, + })), + ), + takeUntilDestroyed(this.destroyRef), + ); + } } diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-form/resource-permission-form.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-form/resource-permission-form.component.html index ed1feb243c..50d6370185 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-form/resource-permission-form.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-form/resource-permission-form.component.html @@ -1,28 +1,37 @@ @if (mode() === eResourcePermissionViewModes.Add) { -
+
- @for (provider of state.providers(); track provider.name; let i = $index) { + @for (provider of state.providers(); track provider.name; let i = $index) {
- - + +
- } + }
-
+
- + } @else { -
+

{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}

- -
-} \ No newline at end of file + +
+} diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html index 571880a25f..aa26f12bb3 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.html @@ -1,60 +1,67 @@ - - - + - - @if (!state.hasResourcePermission() || !state.hasProviderKeyLookupService()) { - + + @if (!state.hasResourcePermission() || !state.hasProviderKeyLookupService()) { + + } @else { + @switch (state.viewMode()) { @case (eResourcePermissionViewModes.List) { - + } @case (eResourcePermissionViewModes.Add) { - + } @case (eResourcePermissionViewModes.Edit) { - - } - } + } - + } + } + - - @if (state.isListMode()) { - - } @else { - - - {{ 'AbpUi::Save' | abpLocalization }} - - } - - \ No newline at end of file + + @if (state.isListMode()) { + + } @else { + + + {{ 'AbpUi::Save' | abpLocalization }} + + } + + diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts index fc534f128d..49437e183e 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts +++ b/npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management/resource-permission-management.component.ts @@ -1,26 +1,17 @@ import { ListService, LocalizationPipe } from '@abp/ng.core'; import { - ButtonComponent, - Confirmation, - ConfirmationService, - ModalCloseDirective, - ModalComponent, - ToasterService, + ButtonComponent, + Confirmation, + ConfirmationService, + ModalCloseDirective, + ModalComponent, + ToasterService, } from '@abp/ng.theme.shared'; import { - PermissionsService, - ResourcePermissionGrantInfoDto, + PermissionsService, + ResourcePermissionGrantInfoDto, } from '@abp/ng.permission-management/proxy'; -import { - Component, - inject, - input, - model, - OnInit, - effect, - untracked, - signal, -} from '@angular/core'; +import { Component, inject, input, model, OnInit, effect, untracked, signal } from '@angular/core'; import { finalize, switchMap, of } from 'rxjs'; import { ResourcePermissionStateService } from '../../services/resource-permission-state.service'; import { ResourcePermissionListComponent } from './resource-permission-list/resource-permission-list.component'; @@ -31,187 +22,195 @@ import { eResourcePermissionViewModes } from '../../enums/view-modes'; const DEFAULT_MAX_RESULT_COUNT = 10; @Component({ - selector: 'abp-resource-permission-management', - templateUrl: './resource-permission-management.component.html', - exportAs: 'abpResourcePermissionManagement', - providers: [ResourcePermissionStateService, ListService], - imports: [ - ModalComponent, - LocalizationPipe, - ButtonComponent, - ModalCloseDirective, - ResourcePermissionListComponent, - ResourcePermissionFormComponent, - ], + selector: 'abp-resource-permission-management', + templateUrl: './resource-permission-management.component.html', + exportAs: 'abpResourcePermissionManagement', + providers: [ResourcePermissionStateService, ListService], + imports: [ + ModalComponent, + LocalizationPipe, + ButtonComponent, + ModalCloseDirective, + ResourcePermissionListComponent, + ResourcePermissionFormComponent, + ], }) export class ResourcePermissionManagementComponent implements OnInit { - readonly eResourcePermissionViewModes = eResourcePermissionViewModes; - - protected readonly service = inject(PermissionsService); - protected readonly toasterService = inject(ToasterService); - protected readonly confirmationService = inject(ConfirmationService); - protected readonly state = inject(ResourcePermissionStateService); - private readonly list = inject(ListService); - - readonly resourceName = input.required(); - readonly resourceKey = input.required(); - readonly resourceDisplayName = input(); - - readonly visible = model(false); - - private readonly previousVisible = signal(false); - - constructor() { - effect(() => { - const resourceName = this.resourceName(); - const resourceKey = this.resourceKey(); - const resourceDisplayName = this.resourceDisplayName(); - - untracked(() => { - this.state.resourceName.set(resourceName); - this.state.resourceKey.set(resourceKey); - this.state.resourceDisplayName.set(resourceDisplayName); - }); - }); - - effect(() => { - const isVisible = this.visible(); - const wasVisible = this.previousVisible(); - if (isVisible && !wasVisible) { - this.openModal(); - } else if (!isVisible && wasVisible) { - this.state.reset(); - } - untracked(() => this.previousVisible.set(isVisible)); - }); - } - - ngOnInit() { - this.list.maxResultCount = DEFAULT_MAX_RESULT_COUNT; - - this.list.hookToQuery(query => { - const allData = this.state.allResourcePermissions(); - const skipCount = query.skipCount || 0; - const maxResultCount = query.maxResultCount || DEFAULT_MAX_RESULT_COUNT; - - const paginatedData = allData.slice(skipCount, skipCount + maxResultCount); - - return of({ - items: paginatedData, - totalCount: allData.length - }); - }).subscribe(result => { - this.state.resourcePermissions.set(result.items); - this.state.totalCount.set(result.totalCount); + readonly eResourcePermissionViewModes = eResourcePermissionViewModes; + + protected readonly service = inject(PermissionsService); + protected readonly toasterService = inject(ToasterService); + protected readonly confirmationService = inject(ConfirmationService); + protected readonly state = inject(ResourcePermissionStateService); + private readonly list = inject(ListService); + + readonly resourceName = input.required(); + readonly resourceKey = input.required(); + readonly resourceDisplayName = input(); + + readonly visible = model(false); + + private readonly previousVisible = signal(false); + + constructor() { + effect(() => { + const resourceName = this.resourceName(); + const resourceKey = this.resourceKey(); + const resourceDisplayName = this.resourceDisplayName(); + + untracked(() => { + this.state.resourceName.set(resourceName); + this.state.resourceKey.set(resourceKey); + this.state.resourceDisplayName.set(resourceDisplayName); + }); + }); + + effect(() => { + const isVisible = this.visible(); + const wasVisible = this.previousVisible(); + if (isVisible && !wasVisible) { + this.openModal(); + } else if (!isVisible && wasVisible) { + this.state.reset(); + } + untracked(() => this.previousVisible.set(isVisible)); + }); + } + + ngOnInit() { + this.list.maxResultCount = DEFAULT_MAX_RESULT_COUNT; + + this.list + .hookToQuery(query => { + const allData = this.state.allResourcePermissions(); + const skipCount = query.skipCount || 0; + const maxResultCount = query.maxResultCount || DEFAULT_MAX_RESULT_COUNT; + + const paginatedData = allData.slice(skipCount, skipCount + maxResultCount); + + return of({ + items: paginatedData, + totalCount: allData.length, }); - } - - openModal() { - this.state.modalBusy.set(true); - - this.service.getResource(this.resourceName(), this.resourceKey()).pipe( - switchMap(permRes => { - this.state.setResourceData(permRes.permissions || []); - this.list.get(); - return this.service.getResourceProviderKeyLookupServices(this.resourceName()); - }), - switchMap(providerRes => { - this.state.setProviders(providerRes.providers || []); - return this.service.getResourceDefinitions(this.resourceName()); - }), - finalize(() => this.state.modalBusy.set(false)) - ).subscribe({ - next: defRes => { - this.state.setDefinitions(defRes.permissions || []); - }, - error: () => { - this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions'); - } - }); - } - - onAddClicked() { - this.state.goToAddMode(); - } - - onEditClicked(grant: ResourcePermissionGrantInfoDto) { - this.state.prepareEditMode(grant); - this.state.modalBusy.set(true); - - this.service.getResourceByProvider( - this.resourceName(), - this.resourceKey(), - grant.providerName || '', - grant.providerKey || '' - ).pipe( - finalize(() => this.state.modalBusy.set(false)) - ).subscribe({ - next: res => { - this.state.setEditModePermissions(res.permissions || []); - } - }); - } - - onDeleteClicked(grant: ResourcePermissionGrantInfoDto) { - this.confirmationService - .warn( - 'AbpPermissionManagement::PermissionDeletionConfirmationMessage', - 'AbpPermissionManagement::AreYouSure', - { - messageLocalizationParams: [grant.providerKey || ''], - } + }) + .subscribe(result => { + this.state.resourcePermissions.set(result.items); + this.state.totalCount.set(result.totalCount); + }); + } + + openModal() { + this.state.modalBusy.set(true); + + this.service + .getResource(this.resourceName(), this.resourceKey()) + .pipe( + switchMap(permRes => { + this.state.setResourceData(permRes.permissions || []); + this.list.get(); + return this.service.getResourceProviderKeyLookupServices(this.resourceName()); + }), + switchMap(providerRes => { + this.state.setProviders(providerRes.providers || []); + return this.service.getResourceDefinitions(this.resourceName()); + }), + finalize(() => this.state.modalBusy.set(false)), + ) + .subscribe({ + next: defRes => { + this.state.setDefinitions(defRes.permissions || []); + }, + error: () => { + this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions'); + }, + }); + } + + onAddClicked() { + this.state.goToAddMode(); + } + + onEditClicked(grant: ResourcePermissionGrantInfoDto) { + this.state.prepareEditMode(grant); + this.state.modalBusy.set(true); + + this.service + .getResourceByProvider( + this.resourceName(), + this.resourceKey(), + grant.providerName || '', + grant.providerKey || '', + ) + .pipe(finalize(() => this.state.modalBusy.set(false))) + .subscribe({ + next: res => { + this.state.setEditModePermissions(res.permissions || []); + }, + }); + } + + onDeleteClicked(grant: ResourcePermissionGrantInfoDto) { + this.confirmationService + .warn( + 'AbpPermissionManagement::ResourcePermissionDeletionConfirmationMessage', + 'AbpPermissionManagement::AreYouSure', + { + messageLocalizationParams: [grant.providerKey || ''], + }, + ) + .subscribe((status: Confirmation.Status) => { + if (status === Confirmation.Status.confirm) { + this.state.modalBusy.set(true); + this.service + .deleteResource( + this.resourceName(), + this.resourceKey(), + grant.providerName || '', + grant.providerKey || '', ) - .subscribe((status: Confirmation.Status) => { - if (status === Confirmation.Status.confirm) { - this.state.modalBusy.set(true); - this.service.deleteResource( - this.resourceName(), - this.resourceKey(), - grant.providerName || '', - grant.providerKey || '' - ).pipe( - switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), - finalize(() => this.state.modalBusy.set(false)) - ).subscribe({ - next: res => { - this.state.setResourceData(res.permissions || []); - this.list.get(); - this.toasterService.success('AbpUi::SuccessfullyDeleted'); - } - }); - } + .pipe( + switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), + finalize(() => this.state.modalBusy.set(false)), + ) + .subscribe({ + next: res => { + this.state.setResourceData(res.permissions || []); + this.list.get(); + this.toasterService.success('AbpUi::DeletedSuccessfully'); + }, }); - } - - savePermission() { - const isEdit = this.state.isEditMode(); - const providerName = isEdit ? this.state.editProviderName() : this.state.selectedProviderName(); - const providerKey = isEdit ? this.state.editProviderKey() : this.state.selectedProviderKey(); - - if (!isEdit && !this.state.canSave()) { - this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions'); - return; } + }); + } - this.state.modalBusy.set(true); - this.service.updateResource( - this.resourceName(), - this.resourceKey(), - { - providerName, - providerKey, - permissions: this.state.selectedPermissions() - } - ).pipe( - switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), - finalize(() => this.state.modalBusy.set(false)) - ).subscribe({ - next: res => { - this.state.setResourceData(res.permissions || []); - this.list.get(); - this.toasterService.success('AbpUi::SavedSuccessfully'); - this.state.goToListMode(); - } - }); + savePermission() { + const isEdit = this.state.isEditMode(); + const providerName = isEdit ? this.state.editProviderName() : this.state.selectedProviderName(); + const providerKey = isEdit ? this.state.editProviderKey() : this.state.selectedProviderKey(); + + if (!isEdit && !this.state.canSave()) { + this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions'); + return; } + + this.state.modalBusy.set(true); + this.service + .updateResource(this.resourceName(), this.resourceKey(), { + providerName, + providerKey, + permissions: this.state.selectedPermissions(), + }) + .pipe( + switchMap(() => this.service.getResource(this.resourceName(), this.resourceKey())), + finalize(() => this.state.modalBusy.set(false)), + ) + .subscribe({ + next: res => { + this.state.setResourceData(res.permissions || []); + this.list.get(); + this.toasterService.success('AbpUi::SavedSuccessfully'); + this.state.goToListMode(); + }, + }); + } } diff --git a/npm/ng-packs/packages/permission-management/tsconfig.lib.json b/npm/ng-packs/packages/permission-management/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/permission-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/permission-management/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/permission-management/tsconfig.spec.json b/npm/ng-packs/packages/permission-management/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/permission-management/tsconfig.spec.json +++ b/npm/ng-packs/packages/permission-management/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/permission-management/vitest.config.mts b/npm/ng-packs/packages/permission-management/vitest.config.mts new file mode 100644 index 0000000000..7471e1446f --- /dev/null +++ b/npm/ng-packs/packages/permission-management/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/permission-management', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'permission-management', + 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/permission-management', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/schematics/.eslintrc.json b/npm/ng-packs/packages/schematics/.eslintrc.json index a291bd52a8..12efb6a283 100644 --- a/npm/ng-packs/packages/schematics/.eslintrc.json +++ b/npm/ng-packs/packages/schematics/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/schematics/jest.config.ts b/npm/ng-packs/packages/schematics/jest.config.ts index 70fa6f0001..03eb6a87a7 100644 --- a/npm/ng-packs/packages/schematics/jest.config.ts +++ b/npm/ng-packs/packages/schematics/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: 'schematics', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/schematics/package.json b/npm/ng-packs/packages/schematics/package.json index 8a7836e8ba..199be73f20 100644 --- a/npm/ng-packs/packages/schematics/package.json +++ b/npm/ng-packs/packages/schematics/package.json @@ -1,6 +1,6 @@ { "name": "@abp/ng.schematics", - "version": "10.0.1", + "version": "10.1.0-rc.2", "author": "", "schematics": "./collection.json", "dependencies": { diff --git a/npm/ng-packs/packages/schematics/project.json b/npm/ng-packs/packages/schematics/project.json index 74a01e9f03..d12fff2738 100644 --- a/npm/ng-packs/packages/schematics/project.json +++ b/npm/ng-packs/packages/schematics/project.json @@ -6,16 +6,16 @@ "prefix": "abp", "tags": [], "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/schematics"], - "options": { - "jestConfig": "packages/schematics/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/schematics" + } } } } diff --git a/npm/ng-packs/packages/schematics/src/test-setup.ts b/npm/ng-packs/packages/schematics/src/test-setup.ts deleted file mode 100644 index 1100b3e8a6..0000000000 --- a/npm/ng-packs/packages/schematics/src/test-setup.ts +++ /dev/null @@ -1 +0,0 @@ -import 'jest-preset-angular/setup-jest'; diff --git a/npm/ng-packs/packages/schematics/src/tests/parse-generic-type.spec.ts b/npm/ng-packs/packages/schematics/src/tests/parse-generic-type.spec.ts index 400fcdf6eb..4bf520e108 100644 --- a/npm/ng-packs/packages/schematics/src/tests/parse-generic-type.spec.ts +++ b/npm/ng-packs/packages/schematics/src/tests/parse-generic-type.spec.ts @@ -1,7 +1,6 @@ +import { expect, describe, it, test } from 'vitest'; import { parseBaseTypeWithGenericTypes } from '../utils/model'; -import { parseGenerics } from '../utils/tree'; - -import {test} from '@jest/globals'; +import { parseGenerics } from '../utils/tree'; const cases: Array<[string, string[]]> = [ [ @@ -17,7 +16,7 @@ const cases: Array<[string, string[]]> = [ [ 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto', 'string', - 'Volo.Abp.Identity.IdentityUserDto' + 'Volo.Abp.Identity.IdentityUserDto', ], ], [ @@ -39,41 +38,38 @@ const cases: Array<[string, string[]]> = [ 'System.String', ], ], - [ - 'AuditedEntityWithUserDto', - ['AuditedEntityWithUserDto'], - ], + ['AuditedEntityWithUserDto', ['AuditedEntityWithUserDto']], ]; test.each(cases)('should parse %s', (inputStr, expected) => { - const parsed = parseBaseTypeWithGenericTypes(inputStr); - expect(parsed).toEqual(expected); -}) + const parsed = parseBaseTypeWithGenericTypes(inputStr); + expect(parsed).toEqual(expected); +}); describe('parseGenerics', () => { - - it('should work with simple type', function() { + it('should work with simple type', function () { const node = parseGenerics('System.String'); expect(node.data).toEqual('System.String'); expect(node.index).toBe(0); expect(node.parent).toBe(null); }); - it('should work with simple Array type', function() { + it('should work with simple Array type', function () { const node = parseGenerics('System.String[]'); expect(node.data).toEqual('System.String[]'); expect(node.index).toBe(0); expect(node.parent).toBe(null); }); - it('should work with simple', function() { + it('should work with simple', function () { const node = parseGenerics('Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'); expect(node.data).toEqual('Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'); expect(node.index).toBe(0); expect(node.parent).toBe(null); }); - it('should work with `Volo.Abp.Application.Dtos.AuditedEntityWithUserDto`', function() { - const type = 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'; + it('should work with `Volo.Abp.Application.Dtos.AuditedEntityWithUserDto`', function () { + const type = + 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'; const node = parseGenerics(type); expect(node.data).toEqual('Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'); @@ -83,37 +79,37 @@ describe('parseGenerics', () => { expect(child.parent).toBe(node); }); - - it('should work with `Volo.Abp.Application.Dtos.AuditedEntityWithUserDto`', function() { - const type = 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'; + it('should work with `Volo.Abp.Application.Dtos.AuditedEntityWithUserDto`', function () { + const type = + 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'; const node = parseGenerics(type); expect(node.data).toEqual('Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'); expect(node.children.length).toBe(2); expect(node.children[0].data).toEqual('System.string'); - expect(node.children[0].index).toBe(0) + expect(node.children[0].index).toBe(0); expect(node.children[1].data).toEqual('Volo.Abp.Identity.IdentityUserDto'); expect(node.children[1].index).toBe(1); - }); - it('should Volo.Abp.Application.Dtos.AuditedEntityWithUserDto>', function() { - const type = 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto>'; + it('should Volo.Abp.Application.Dtos.AuditedEntityWithUserDto>', function () { + const type = + 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto>'; const node = parseGenerics(type); expect(node.data).toEqual('Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'); expect(node.children.length).toBe(2); expect(node.children[0].data).toEqual('System.string'); - expect((node.children[0]).parent).toBe(node); + expect(node.children[0].parent).toBe(node); expect(node.children[1].data).toEqual('Volo.Abp.Identity.IdentityUserDto'); expect(node.children[1].children.length).toBe(1); expect(node.children[1].children[0].data).toEqual('System.Int'); - expect(node.children[1].children[0].parent).toBe(node.children[1]) - expect(node.children[1].children[0].index).toBe(0) - + expect(node.children[1].children[0].parent).toBe(node.children[1]); + expect(node.children[1].children[0].index).toBe(0); }); - it('should Volo.Abp.Application.Dtos.AuditedEntityWithUserDto,System.string>', function() { - const type = 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto,System.string>'; + it('should Volo.Abp.Application.Dtos.AuditedEntityWithUserDto,System.string>', function () { + const type = + 'Volo.Abp.Application.Dtos.AuditedEntityWithUserDto,System.string>'; const node = parseGenerics(type); expect(node.data).toEqual('Volo.Abp.Application.Dtos.AuditedEntityWithUserDto'); expect(node.children.length).toBe(2); @@ -121,9 +117,5 @@ describe('parseGenerics', () => { expect(node.children[0].children.length).toBe(1); expect(node.children[0].children[0].data).toEqual('System.Int'); expect(node.children[1].data).toEqual('System.string'); - }); }); - - - diff --git a/npm/ng-packs/packages/schematics/src/utils/methods.ts b/npm/ng-packs/packages/schematics/src/utils/methods.ts index d3fc935819..6d31903a78 100644 --- a/npm/ng-packs/packages/schematics/src/utils/methods.ts +++ b/npm/ng-packs/packages/schematics/src/utils/methods.ts @@ -19,4 +19,9 @@ export const getParamValueName = (paramName: string, descriptorName: string) => export function isDictionaryType(type?: string, typeSimple?: string): boolean { const haystacks = [type || '', typeSimple || '']; return haystacks.some(t => /(^|\b)(System\.Collections\.Generic\.)?(I)?Dictionary\s* /(^|\b)(System\.Collections\.Generic\.)?(I)?(List|Enumerable|Collection)\s* `${x}[]`).some(x => x === type); + // Check for array types like Volo.Abp.Content.IRemoteStreamContent[] + if (VOLO_REMOTE_STREAM_CONTENT.map(x => `${x}[]`).some(x => x === type)) { + return true; + } + + // Check for collection types like List, IEnumerable, ICollection, Collection, IList + // This matches any generic type from System.Collections.Generic that implements IEnumerable + if (isCollectionType(type)) { + const { generics } = extractGenerics(type); + if (generics.length > 0 && VOLO_REMOTE_STREAM_CONTENT.includes(generics[0])) { + return true; + } + } + + return false; } function getMethodNameFromAction(action: Action): string { diff --git a/npm/ng-packs/packages/schematics/tsconfig.spec.json b/npm/ng-packs/packages/schematics/tsconfig.spec.json index a42c4b02fb..fc61345bb3 100644 --- a/npm/ng-packs/packages/schematics/tsconfig.spec.json +++ b/npm/ng-packs/packages/schematics/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"] + "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/schematics/vitest.config.mts b/npm/ng-packs/packages/schematics/vitest.config.mts new file mode 100644 index 0000000000..6398ad426d --- /dev/null +++ b/npm/ng-packs/packages/schematics/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/schematics', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'schematics', + 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/schematics', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/setting-management/.eslintrc.json b/npm/ng-packs/packages/setting-management/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/setting-management/.eslintrc.json +++ b/npm/ng-packs/packages/setting-management/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/setting-management/jest.config.ts b/npm/ng-packs/packages/setting-management/jest.config.ts index 17d04089d0..be1cc254ed 100644 --- a/npm/ng-packs/packages/setting-management/jest.config.ts +++ b/npm/ng-packs/packages/setting-management/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: 'setting-management', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/setting-management/package.json b/npm/ng-packs/packages/setting-management/package.json index 7bb01af08f..fd8977f924 100644 --- a/npm/ng-packs/packages/setting-management/package.json +++ b/npm/ng-packs/packages/setting-management/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.setting-management", - "version": "10.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.components": "~10.0.1", - "@abp/ng.theme.shared": "~10.0.1", + "@abp/ng.components": "~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/setting-management/project.json b/npm/ng-packs/packages/setting-management/project.json index 57f72234fb..fa07efdfc3 100644 --- a/npm/ng-packs/packages/setting-management/project.json +++ b/npm/ng-packs/packages/setting-management/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/setting-management"], - "options": { - "jestConfig": "packages/setting-management/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/setting-management" + } } } } diff --git a/npm/ng-packs/packages/setting-management/tsconfig.lib.json b/npm/ng-packs/packages/setting-management/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/setting-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/setting-management/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/setting-management/tsconfig.spec.json b/npm/ng-packs/packages/setting-management/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/setting-management/tsconfig.spec.json +++ b/npm/ng-packs/packages/setting-management/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/setting-management/vitest.config.mts b/npm/ng-packs/packages/setting-management/vitest.config.mts new file mode 100644 index 0000000000..d1670d87fe --- /dev/null +++ b/npm/ng-packs/packages/setting-management/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/setting-management', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'setting-management', + 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/setting-management', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/tenant-management/.eslintrc.json b/npm/ng-packs/packages/tenant-management/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/tenant-management/.eslintrc.json +++ b/npm/ng-packs/packages/tenant-management/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/tenant-management/jest.config.ts b/npm/ng-packs/packages/tenant-management/jest.config.ts index e69152e9ab..a5d63bbaaf 100644 --- a/npm/ng-packs/packages/tenant-management/jest.config.ts +++ b/npm/ng-packs/packages/tenant-management/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: 'tenant-management', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/tenant-management/package.json b/npm/ng-packs/packages/tenant-management/package.json index e34f4f7270..1ced045022 100644 --- a/npm/ng-packs/packages/tenant-management/package.json +++ b/npm/ng-packs/packages/tenant-management/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.tenant-management", - "version": "10.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.feature-management": "~10.0.1", - "@abp/ng.theme.shared": "~10.0.1", + "@abp/ng.feature-management": "~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/tenant-management/project.json b/npm/ng-packs/packages/tenant-management/project.json index 2394793380..9bbd10d632 100644 --- a/npm/ng-packs/packages/tenant-management/project.json +++ b/npm/ng-packs/packages/tenant-management/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/tenant-management"], - "options": { - "jestConfig": "packages/tenant-management/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/tenant-management" + } } } } diff --git a/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts b/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts index 7b38035d2f..236613bbdd 100644 --- a/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts +++ b/npm/ng-packs/packages/tenant-management/src/lib/tenant-management.routes.ts @@ -1,4 +1,5 @@ import { Routes } from '@angular/router'; +import { Provider } from '@angular/core'; import { TenantManagementConfigOptions } from './models/config-options'; import { TENANT_MANAGEMENT_CREATE_FORM_PROP_CONTRIBUTORS, @@ -19,7 +20,7 @@ import { TenantsComponent } from './components'; import { eTenantManagementComponents } from './enums'; import { tenantManagementExtensionsResolver } from './resolvers'; -export function provideTenantManagement(options: TenantManagementConfigOptions = {}) { +export function provideTenantManagement(options: TenantManagementConfigOptions = {}): Provider[] { return [ { provide: TENANT_MANAGEMENT_ENTITY_ACTION_CONTRIBUTORS, diff --git a/npm/ng-packs/packages/tenant-management/tsconfig.lib.json b/npm/ng-packs/packages/tenant-management/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/tenant-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/tenant-management/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/tenant-management/tsconfig.spec.json b/npm/ng-packs/packages/tenant-management/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/tenant-management/tsconfig.spec.json +++ b/npm/ng-packs/packages/tenant-management/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/tenant-management/vitest.config.mts b/npm/ng-packs/packages/tenant-management/vitest.config.mts new file mode 100644 index 0000000000..9125d40292 --- /dev/null +++ b/npm/ng-packs/packages/tenant-management/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/tenant-management', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'tenant-management', + 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/tenant-management', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/theme-basic/.eslintrc.json b/npm/ng-packs/packages/theme-basic/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/theme-basic/.eslintrc.json +++ b/npm/ng-packs/packages/theme-basic/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/theme-basic/jest.config.ts b/npm/ng-packs/packages/theme-basic/jest.config.ts index 5e30d9abd6..2c6efb6a7e 100644 --- a/npm/ng-packs/packages/theme-basic/jest.config.ts +++ b/npm/ng-packs/packages/theme-basic/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: 'theme-basic', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/theme-basic/package.json b/npm/ng-packs/packages/theme-basic/package.json index e1d17f802f..e6e08aa95f 100644 --- a/npm/ng-packs/packages/theme-basic/package.json +++ b/npm/ng-packs/packages/theme-basic/package.json @@ -1,14 +1,14 @@ { "name": "@abp/ng.theme.basic", - "version": "10.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/theme-basic/project.json b/npm/ng-packs/packages/theme-basic/project.json index be0464cc18..ac155e0fe7 100644 --- a/npm/ng-packs/packages/theme-basic/project.json +++ b/npm/ng-packs/packages/theme-basic/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/theme-basic"], - "options": { - "jestConfig": "packages/theme-basic/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/theme-basic" + } } } } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html index 9f4fdd900d..28e408655d 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html @@ -12,7 +12,7 @@ @if (route.iconClass) { } - {{ route.name | abpLazyLocalization | async }} + {{ route.name | abpAsyncLocalization | async }} @@ -39,7 +39,7 @@ @if (route.iconClass) { } - {{ route.name | abpLazyLocalization | async }} + {{ route.name | abpAsyncLocalization | async }}
{ let spectator: SpectatorService; let handler: LazyStyleHandler; - let lazyLoad: LazyLoadService; + let lazyLoad: any; + + beforeAll(async () => { + await setupComponentResources( + '../components/breadcrumb', + import.meta.url + ); + }); + + const dir$ = new Subject<'ltr' | 'rtl'>(); const createService = createServiceFactory({ service: LazyStyleHandler, providers: [ - DocumentDirHandlerService, + { + provide: DOCUMENT, + useValue: document, + }, + { + provide: LAZY_STYLES, + useValue: [BOOTSTRAP], + }, + { + provide: LazyLoadService, + useValue: { + loaded: new Map(), + load: vi.fn(() => of(null)), + remove: vi.fn(), + }, + }, + { + provide: DocumentDirHandlerService, + useValue: { + dir$, + }, + }, { provide: LocalizationService, - useValue: { currentLang: 'en', currentLang$ }, + useValue: { + currentLang: 'en', + currentLang$: of({ payload: 'en' }), + }, }, ], }); @@ -25,7 +61,7 @@ describe('LazyStyleHandler', () => { beforeEach(() => { spectator = createService(); handler = spectator.service; - lazyLoad = handler['lazyLoad']; + lazyLoad = spectator.inject(LazyLoadService); }); describe('#dir', () => { @@ -36,15 +72,19 @@ describe('LazyStyleHandler', () => { it('should set bootstrap to rtl', () => { const oldHref = createLazyStyleHref(BOOTSTRAP, 'ltr'); const newHref = createLazyStyleHref(BOOTSTRAP, 'rtl'); - lazyLoad.loaded.set(newHref, null); // avoid actual loading - const load = jest.spyOn(lazyLoad, 'load'); - const remove = jest.spyOn(lazyLoad, 'remove'); - const strategy = LOADING_STRATEGY.PrependAnonymousStyleToHead(newHref); + + lazyLoad.loaded.set(newHref, null); + + const loadSpy = vi.spyOn(lazyLoad, 'load'); + const removeSpy = vi.spyOn(lazyLoad, 'remove'); handler.dir = 'rtl'; - expect(load).toHaveBeenCalledWith(strategy); - expect(remove).toHaveBeenCalledWith(oldHref); + expect(loadSpy).toHaveBeenCalledTimes(1); + const [strategy] = loadSpy.mock.calls[0]; + expect((strategy as LoadingStrategy).path).toBe(newHref); + + expect(removeSpy).toHaveBeenCalledWith(oldHref); }); }); }); diff --git a/npm/ng-packs/packages/theme-basic/src/lib/tests/utils/index.ts b/npm/ng-packs/packages/theme-basic/src/lib/tests/utils/index.ts new file mode 100644 index 0000000000..73b11724ec --- /dev/null +++ b/npm/ng-packs/packages/theme-basic/src/lib/tests/utils/index.ts @@ -0,0 +1 @@ +export * from './setup-component-resources'; \ No newline at end of file diff --git a/npm/ng-packs/packages/theme-basic/src/lib/tests/utils/setup-component-resources.ts b/npm/ng-packs/packages/theme-basic/src/lib/tests/utils/setup-component-resources.ts new file mode 100644 index 0000000000..0795a5e32b --- /dev/null +++ b/npm/ng-packs/packages/theme-basic/src/lib/tests/utils/setup-component-resources.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Sets up component resource resolution for Angular component tests. + * This is needed when components have external templates or stylesheets. + * + * @param componentDirPath - The path to the component directory relative to the test file. + * For example: '../components/loader-bar' or './components/my-component' + * @param testFileUrl - The import.meta.url from the test file. Defaults to the caller's location. + * + * @example + * ```typescript + * + * import { setupComponentResources } from './utils'; + * + * beforeAll(() => setupComponentResources('../components/loader-bar', import.meta.url)); + * ``` + */ +export async function setupComponentResources( + componentDirPath: string, + testFileUrl: string = import.meta.url, +): Promise { + try { + if (typeof process !== 'undefined' && process.versions?.node) { + const { ɵresolveComponentResources: resolveComponentResources } = await import('@angular/core'); + + // Get the test file directory path + const testFileDir = dirname(fileURLToPath(testFileUrl)); + const componentDir = resolve(testFileDir, componentDirPath); + + await resolveComponentResources((url: string) => { + // For SCSS/SASS files, return empty CSS since jsdom can't parse SCSS + if (url.endsWith('.scss') || url.endsWith('.sass')) { + return Promise.resolve(''); + } + + // For other files (HTML, CSS, etc.), read the actual content + try { + // Resolve relative paths like './component.scss' or 'component.scss' + const normalizedUrl = url.replace(/^\.\//, ''); + const filePath = resolve(componentDir, normalizedUrl); + return Promise.resolve(readFileSync(filePath, 'utf-8')); + } catch (error) { + // If file not found, return empty string + return Promise.resolve(''); + } + }); + } + } catch (error) { + console.warn('Failed to set up component resource resolver:', error); + } +} diff --git a/npm/ng-packs/packages/theme-basic/src/test-setup.ts b/npm/ng-packs/packages/theme-basic/src/test-setup.ts index 4555f138a7..b349bcf687 100644 --- a/npm/ng-packs/packages/theme-basic/src/test-setup.ts +++ b/npm/ng-packs/packages/theme-basic/src/test-setup.ts @@ -1,2 +1,20 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; -setupZoneTestEnv(); +import '@angular/compiler'; +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; + +// Initialize Angular testing environment +getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + +// Mock window.location for test environment +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:4200', + origin: 'http://localhost:4200', + pathname: '/', + search: '', + hash: '', + }, + writable: true, +}); diff --git a/npm/ng-packs/packages/theme-basic/tsconfig.lib.json b/npm/ng-packs/packages/theme-basic/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/theme-basic/tsconfig.lib.json +++ b/npm/ng-packs/packages/theme-basic/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/theme-basic/tsconfig.spec.json b/npm/ng-packs/packages/theme-basic/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/theme-basic/tsconfig.spec.json +++ b/npm/ng-packs/packages/theme-basic/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/theme-basic/vitest.config.mts b/npm/ng-packs/packages/theme-basic/vitest.config.mts new file mode 100644 index 0000000000..f76013d8f4 --- /dev/null +++ b/npm/ng-packs/packages/theme-basic/vitest.config.mts @@ -0,0 +1,22 @@ +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/theme-basic', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'theme-basic', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/theme-basic', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/packages/theme-shared/.eslintrc.json b/npm/ng-packs/packages/theme-shared/.eslintrc.json index 5e303d20bb..afa6cfec5f 100644 --- a/npm/ng-packs/packages/theme-shared/.eslintrc.json +++ b/npm/ng-packs/packages/theme-shared/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/vitest.config.*.timestamp*"], "overrides": [ { "files": ["*.ts"], diff --git a/npm/ng-packs/packages/theme-shared/jest.config.ts b/npm/ng-packs/packages/theme-shared/jest.config.ts index af6440fc3e..c03a94ad03 100644 --- a/npm/ng-packs/packages/theme-shared/jest.config.ts +++ b/npm/ng-packs/packages/theme-shared/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: 'theme-shared', preset: '../../jest.preset.js', diff --git a/npm/ng-packs/packages/theme-shared/package.json b/npm/ng-packs/packages/theme-shared/package.json index 8f4fe7888a..dc42e452a3 100644 --- a/npm/ng-packs/packages/theme-shared/package.json +++ b/npm/ng-packs/packages/theme-shared/package.json @@ -1,13 +1,13 @@ { "name": "@abp/ng.theme.shared", - "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.core": "~10.1.0-rc.2", "@fortawesome/fontawesome-free": "^6.0.0", "@ng-bootstrap/ng-bootstrap": "~20.0.0", "@ngx-validate/core": "^0.2.0", diff --git a/npm/ng-packs/packages/theme-shared/project.json b/npm/ng-packs/packages/theme-shared/project.json index a77551187c..2bfa7e6574 100644 --- a/npm/ng-packs/packages/theme-shared/project.json +++ b/npm/ng-packs/packages/theme-shared/project.json @@ -23,16 +23,16 @@ }, "defaultConfiguration": "production" }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/packages/theme-shared"], - "options": { - "jestConfig": "packages/theme-shared/jest.config.ts" - } - }, "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../coverage/packages/theme-shared" + } } } } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts index 6e60a77669..a5a1a4d112 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts @@ -6,7 +6,7 @@ import { ABP, LocalizationPipe } from '@abp/ng.core'; @Component({ selector: 'abp-breadcrumb-items', templateUrl: './breadcrumb-items.component.html', - imports: [ NgTemplateOutlet, RouterLink, LocalizationPipe], + imports: [NgTemplateOutlet, RouterLink, LocalizationPipe], }) export class BreadcrumbItemsComponent { @Input() items: Partial[] = []; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts index 5f84edac4f..d0d204dceb 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts @@ -1,31 +1,25 @@ import { ABP, - CORE_OPTIONS, LocalizationPipe, RouterOutletComponent, RoutesService, - LocalizationService, + provideAbpCore, + withOptions, + RestService, + AbpApplicationConfigurationService, + ConfigStateService, } from '@abp/ng.core'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { RouterModule } from '@angular/router'; -import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/vitest'; +import { of } from 'rxjs'; import { BreadcrumbComponent, BreadcrumbItemsComponent } from '../components'; -import { OTHERS_GROUP } from '@abp/ng.core'; -import { SORT_COMPARE_FUNC } from '@abp/ng.core'; +import { setupComponentResources } from './utils'; const mockRoutes: ABP.Route[] = [ { name: '_::Identity', path: '/identity' }, { name: '_::Users', path: '/identity/users', parentName: '_::Identity' }, ]; -// Simple compare function that doesn't use inject() -const simpleCompareFunc = (a: any, b: any) => { - const aNumber = a.order || 0; - const bNumber = b.order || 0; - return aNumber - bNumber; -}; - describe('BreadcrumbComponent', () => { let spectator: SpectatorRouting; let routes: RoutesService; @@ -34,39 +28,58 @@ describe('BreadcrumbComponent', () => { component: RouterOutletComponent, stubsEnabled: false, detectChanges: false, + imports: [ + RouterModule, + LocalizationPipe, + BreadcrumbComponent, + BreadcrumbItemsComponent, + ], providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { - provide: CORE_OPTIONS, - useValue: { + provideAbpCore( + withOptions({ environment: { apis: { default: { url: 'http://localhost:4200', }, }, + application: { + name: 'TestApp', + baseUrl: 'http://localhost:4200', + }, }, - } + registerLocaleFn: () => Promise.resolve(), + skipGetAppConfiguration: true, + }), + ), + { + provide: RestService, + useValue: { + request: vi.fn(), + handleError: vi.fn(), + }, }, - RoutesService, - LocalizationService, { - provide: OTHERS_GROUP, - useValue: 'AbpUi::OthersGroup', + provide: AbpApplicationConfigurationService, + useValue: { + get: vi.fn(), + }, }, { - provide: SORT_COMPARE_FUNC, - useValue: simpleCompareFunc, + provide: ConfigStateService, + useValue: { + getOne: vi.fn(), + getAll: vi.fn(() => ({})), + getAll$: vi.fn(() => of({})), + getDeep: vi.fn(), + getDeep$: vi.fn(() => of(undefined)), + createOnUpdateStream: vi.fn(() => ({ + subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })) + })), + refreshAppState: vi.fn(), + }, }, ], - declarations: [], - imports: [ - RouterModule, - LocalizationPipe, - BreadcrumbComponent, - BreadcrumbItemsComponent, - ], routes: [ { path: '', @@ -85,6 +98,8 @@ describe('BreadcrumbComponent', () => { ], }); + beforeAll(() => setupComponentResources('../components/breadcrumb', import.meta.url)); + beforeEach(() => { spectator = createRouting(); routes = spectator.inject(RoutesService); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts index bf834d182d..2986e7c14e 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { ButtonComponent } from '../components'; describe('ButtonComponent', () => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts index fa474640f6..9990b33676 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/card.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { CardComponent, CardBodyComponent, diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts index e2fba6d0da..4ea0946c01 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/checkbox.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { FormCheckboxComponent } from '../components/checkbox/checkbox.component'; describe('FormCheckboxComponent', () => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts index 029becaa96..d03e69733d 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts @@ -1,27 +1,24 @@ import { CoreTestingModule } from '@abp/ng.core/testing'; -import { NgModule } from '@angular/core'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { timer } from 'rxjs'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { firstValueFrom, timer } from 'rxjs'; import { ConfirmationComponent } from '../components'; import { Confirmation } from '../models'; import { ConfirmationService } from '../services'; import { CONFIRMATION_ICONS, DEFAULT_CONFIRMATION_ICONS } from '../tokens/confirmation-icons.token'; - -@NgModule({ - exports: [ConfirmationComponent], - declarations: [], - imports: [CoreTestingModule.withConfig(), ConfirmationComponent], - providers: [{ provide: CONFIRMATION_ICONS, useValue: DEFAULT_CONFIRMATION_ICONS }], -}) -export class MockModule {} +import { setupComponentResources } from './utils'; describe('ConfirmationService', () => { let spectator: SpectatorService; let service: ConfirmationService; + const createService = createServiceFactory({ service: ConfirmationService, - imports: [CoreTestingModule.withConfig(), MockModule], + imports: [CoreTestingModule.withConfig(), ConfirmationComponent], + providers: [{ provide: CONFIRMATION_ICONS, useValue: DEFAULT_CONFIRMATION_ICONS }], + }); + + beforeAll(async () => { + await setupComponentResources('../components/confirmation', import.meta.url); }); beforeEach(() => { @@ -33,16 +30,16 @@ describe('ConfirmationService', () => { clearElements(); }); - test('should display a confirmation popup', fakeAsync(() => { + test('should display a confirmation popup', async () => { service.show('_::MESSAGE', '_::TITLE'); - tick(); + await firstValueFrom(timer(10)); expect(selectConfirmationContent('.title')).toBe('TITLE'); expect(selectConfirmationContent('.message')).toBe('MESSAGE'); - })); + }); - test('should display HTML string in title, message, and buttons', fakeAsync(() => { + test('should display HTML string in title, message, and buttons', async () => { service.show( '_::MESSAGE', '_::TITLE', @@ -53,24 +50,24 @@ describe('ConfirmationService', () => { }, ); - tick(); + await firstValueFrom(timer(10)); expect(selectConfirmationContent('.custom-title')).toBe('TITLE'); expect(selectConfirmationContent('.custom-message')).toBe('MESSAGE'); expect(selectConfirmationContent('.custom-cancel')).toBe('CANCEL'); expect(selectConfirmationContent('.custom-yes')).toBe('YES'); - })); + }); - test('should display custom FA icon', fakeAsync(() => { + test('should display custom FA icon', async () => { service.show('_::MESSAGE', '_::TITLE', undefined, { icon: 'fa fa-info', }); - tick(); + await firstValueFrom(timer(10)); expect(selectConfirmationElement('.icon').className).toBe('icon fa fa-info'); - })); + }); - test('should display custom icon as html element', fakeAsync(() => { + test('should display custom icon as html element', async () => { const className = 'custom-icon'; const selector = '.' + className; @@ -78,12 +75,14 @@ describe('ConfirmationService', () => { iconTemplate: `I am icon`, }); - tick(); + await firstValueFrom(timer(10)); const element = selectConfirmationElement(selector); expect(element).toBeTruthy(); expect(element.innerHTML).toBe('I am icon'); - })); + }); + + test.each` type | selector | icon ${'info'} | ${'.info'} | ${'.fa-info-circle'} @@ -93,7 +92,7 @@ describe('ConfirmationService', () => { `('should display $type confirmation popup', async ({ type, selector, icon }) => { service[type]('_::MESSAGE', '_::TITLE'); - await timer(0).toPromise(); + await firstValueFrom(timer(10)); expect(selectConfirmationContent('.title')).toBe('TITLE'); expect(selectConfirmationContent('.message')).toBe('MESSAGE'); @@ -101,31 +100,18 @@ describe('ConfirmationService', () => { expect(selectConfirmationElement(icon)).toBeTruthy(); }); - // test('should close with ESC key', (done) => { - // service - // .info('', '') - // .pipe(take(1)) - // .subscribe((status) => { - // expect(status).toBe(Confirmation.Status.dismiss); - // done(); - // }); - // const escape = new KeyboardEvent('keyup', { key: 'Escape' }); - // document.dispatchEvent(escape); - // }); - - test('should close when click cancel button', done => { + test('should close when click cancel button', async () => { service.info('_::', '_::', { yesText: '_::Sure', cancelText: '_::Exit' }).subscribe(status => { expect(status).toBe(Confirmation.Status.reject); - done(); }); - timer(0).subscribe(() => { - expect(selectConfirmationContent('button#cancel')).toBe('Exit'); - expect(selectConfirmationContent('button#confirm')).toBe('Sure'); + await firstValueFrom(timer(10)); - (document.querySelector('button#cancel') as HTMLButtonElement).click(); - }); + expect(selectConfirmationContent('button#cancel')).toBe('Exit'); + expect(selectConfirmationContent('button#confirm')).toBe('Sure'); + + (document.querySelector('button#cancel') as HTMLButtonElement).click(); }); test.each` @@ -135,9 +121,9 @@ describe('ConfirmationService', () => { `( 'should call the listenToEscape method $count times when dismissible is $dismissible', ({ dismissible, count }) => { - const spy = jest.spyOn(service as any, 'listenToEscape'); + const spy = vi.spyOn(service as any, 'listenToEscape'); - service.info('_::', '_::', { dismissible }); + service.info('_::', '_::', { dismissible }); expect(spy).toHaveBeenCalledTimes(count); }, diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts index 727a1c5312..e9b0e45a0d 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/ellipsis.directive.spec.ts @@ -1,4 +1,4 @@ -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; import { EllipsisDirective } from '../directives/ellipsis.directive'; describe('EllipsisDirective', () => { @@ -39,17 +39,61 @@ describe('EllipsisDirective', () => { expect(directive.title).toBe('test title'); }); - test('should have element innerText as title if not specified', () => { - spectator.setHostInput({ title: undefined }); + test('should add abp-ellipsis-inline class to element if width is given', () => { + expect(el).toHaveClass('abp-ellipsis-inline'); + }); +}); + +describe('EllipsisDirective when title is not specified', () => { + let spectator: SpectatorDirective; + let directive: EllipsisDirective; + let el: HTMLDivElement; + const createDirective = createDirectiveFactory({ + directive: EllipsisDirective, + }); + + beforeEach(() => { + spectator = createDirective( + '
test content
', + { + hostProps: { + title: undefined, + width: '100px', + }, + }, + ); + directive = spectator.directive; + el = spectator.query('div') as HTMLDivElement; + }); + + test('should have element innerText as title', () => { expect(directive.title).toBe(el.innerText); }); +}); - test('should add abp-ellipsis-inline class to element if width is given', () => { - expect(el).toHaveClass('abp-ellipsis-inline'); +describe('EllipsisDirective when width is not given', () => { + let spectator: SpectatorDirective; + let directive: EllipsisDirective; + let el: HTMLDivElement; + const createDirective = createDirectiveFactory({ + directive: EllipsisDirective, + }); + + beforeEach(() => { + spectator = createDirective( + '
test content
', + { + hostProps: { + title: 'test title', + width: undefined, + }, + }, + ); + directive = spectator.directive; + el = spectator.query('div') as HTMLDivElement; }); - test('should add abp-ellipsis class to element if width is not given', () => { - spectator.setHostInput({ width: undefined }); + test('should add abp-ellipsis class to element', () => { expect(el).toHaveClass('abp-ellipsis'); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts index 93b8d32f0f..d1cb9a5940 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts @@ -1,49 +1,84 @@ -import { CORE_OPTIONS, LocalizationPipe } from '@abp/ng.core'; -import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { ElementRef, Renderer2 } from '@angular/core'; -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { DOCUMENT } from '@angular/common'; +import { Router } from '@angular/router'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { Pipe, PipeTransform } from '@angular/core'; import { Subject } from 'rxjs'; +import { vi } from 'vitest'; + import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; +import { setupComponentResources } from './utils'; + +/** + * Mock pipe to avoid ABP DI chain + */ +@Pipe({ name: 'abpLocalization'}) +class MockLocalizationPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + +describe('HttpErrorWrapperComponent', () => { + let spectator: Spectator; + let createComponent: ReturnType>; -describe('ErrorComponent', () => { - let spectator: SpectatorHost; - const createHost = createHostFactory({ - component: HttpErrorWrapperComponent, - declarations: [], - mocks: [HttpClient], - providers: [ - { provide: CORE_OPTIONS, useValue: {} }, - { provide: Renderer2, useValue: { removeChild: () => null } }, - { - provide: ElementRef, - useValue: { nativeElement: document.createElement('div') }, - }, - ], - imports: [HttpClientModule, LocalizationPipe], + beforeAll(async () => { + await setupComponentResources( + '../components/http-error-wrapper', + import.meta.url, + ); }); - beforeEach(() => { - spectator = createHost( - '', - ); - spectator.component.destroy$ = new Subject(); - }); - - describe('#destroy', () => { - it('should be call when pressed the esc key', done => { - spectator.component.destroy$.subscribe(() => { - done(); - }); + beforeEach(() => { + if (!createComponent) { + createComponent = createComponentFactory({ + component: HttpErrorWrapperComponent, + detectChanges: false, - spectator.keyboard.pressEscape(); - }); + overrideComponents: [ + [ + HttpErrorWrapperComponent, + { + set: { + template: '
', + imports: [MockLocalizationPipe], + }, + }, + ], + ], - it('should be call when clicked the close button', done => { - spectator.component.destroy$.subscribe(() => { - done(); + providers: [ + { + provide: DOCUMENT, + useValue: document, + }, + { + provide: Router, + useValue: { + navigateByUrl: vi.fn(), + }, + }, + ], }); + } + + spectator = createComponent(); + + spectator.component.destroy$ = new Subject(); + spectator.component.title = '_::Oops!'; + spectator.component.details = '_::Sorry, an error has occured.'; + }); + + it('should create component', () => { + expect(spectator.component).toBeTruthy(); + }); + + it('should emit destroy$ when destroy is called', () => { + const spy = vi.fn(); + spectator.component.destroy$.subscribe(spy); + + spectator.component.destroy(); - spectator.click('#abp-close-button'); - }); + expect(spy).toHaveBeenCalled(); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts index 75726103d2..d57f75af28 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts @@ -2,46 +2,48 @@ import { HttpErrorReporterService } from '@abp/ng.core'; import { CoreTestingModule } from '@abp/ng.core/testing'; import { APP_BASE_HREF } from '@angular/common'; import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of, Subject } from 'rxjs'; -import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; import { ErrorHandler } from '../handlers'; import { ConfirmationService } from '../services'; +import { CreateErrorComponentService } from '../services/create-error-component.service'; +import { RouterErrorHandlerService } from '../services/router-error-handler.service'; import { CUSTOM_ERROR_HANDLERS, HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; import { CustomHttpErrorHandlerService } from '../models'; const customHandlerMock: CustomHttpErrorHandlerService = { priority: 100, - canHandle: jest.fn().mockReturnValue(true), - execute: jest.fn(), + canHandle: vi.fn().mockReturnValue(true), + execute: vi.fn(), }; const reporter$ = new Subject(); -@NgModule({ - exports: [HttpErrorWrapperComponent], - declarations: [], - imports: [CoreTestingModule, HttpErrorWrapperComponent], -}) -class MockModule {} - let spectator: SpectatorService; let service: ErrorHandler; let httpErrorReporter: HttpErrorReporterService; -const errorConfirmation: jest.Mock = jest.fn(() => of(null)); -const CONFIRMATION_BUTTONS = { - hideCancelBtn: true, - yesText: 'AbpAccount::Close', -}; +const errorConfirmation = vi.fn(() => of(null)); + describe('ErrorHandler', () => { const createService = createServiceFactory({ service: ErrorHandler, - imports: [CoreTestingModule.withConfig(), MockModule], + imports: [CoreTestingModule.withConfig()], mocks: [OAuthService], providers: [ + { + provide: RouterErrorHandlerService, + useValue: { + listen: vi.fn(), + }, + }, + { + provide: CreateErrorComponentService, + useValue: { + execute: vi.fn(), + }, + }, { provide: HttpErrorReporterService, useValue: { @@ -65,7 +67,10 @@ describe('ErrorHandler', () => { }, { provide: HTTP_ERROR_CONFIG, - useFactory: () => ({}), + useValue: { + skipHandledErrorCodes: [], + errorScreen: {}, + }, }, ], }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts index cf4eb4aac3..e6ae275a95 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/form-input.component.spec.ts @@ -1,4 +1,4 @@ -import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; import { FormInputComponent } from '../components/form-input/form-input.component'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts index e2904e33f1..a825cbf7db 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/loader-bar.component.spec.ts @@ -1,26 +1,32 @@ -import { HttpWaitService, LOADER_DELAY, SubscriptionService } from '@abp/ng.core'; +import { HttpWaitService, LOADER_DELAY, RouterWaitService, SubscriptionService } from '@abp/ng.core'; import { HttpRequest } from '@angular/common/http'; -import { NavigationEnd, NavigationError, NavigationStart, Router } from '@angular/router'; -import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator/jest'; -import { Subject, timer } from 'rxjs'; +import { NavigationStart, Router } from '@angular/router'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { combineLatest, firstValueFrom, Subject, timer } from 'rxjs'; import { LoaderBarComponent } from '../components/loader-bar/loader-bar.component'; +import { setupComponentResources } from './utils'; describe('LoaderBarComponent', () => { let spectator: Spectator; let router: Router; + let createComponent: ReturnType>; const events$ = new Subject(); - const createComponent = createComponentFactory({ - component: LoaderBarComponent, - detectChanges: false, - providers: [ - SubscriptionService, - { provide: Router, useValue: { events: events$ } }, - { provide: LOADER_DELAY, useValue: 0 }, - ], - }); + beforeAll(() => setupComponentResources('../components/loader-bar', import.meta.url)); beforeEach(() => { + if (!createComponent) { + createComponent = createComponentFactory({ + component: LoaderBarComponent, + detectChanges: false, + providers: [ + SubscriptionService, + { provide: Router, useValue: { events: events$ } }, + { provide: LOADER_DELAY, useValue: 0 }, + ], + }); + } + spectator = createComponent({}); spectator.component.intervalPeriod = 1; spectator.component.stopDelay = 1; @@ -32,66 +38,127 @@ describe('LoaderBarComponent', () => { expect(spectator.component.color).toBe('#77b6ff'); }); - it('should increase the progressLevel', done => { + it('should increase the progressLevel', async () => { spectator.detectChanges(); const httpWaitService = spectator.inject(HttpWaitService); httpWaitService.addRequest(new HttpRequest('GET', 'test')); spectator.detectChanges(); - setTimeout(() => { - expect(spectator.component.progressLevel > 0).toBeTruthy(); - done(); - }, 10); + + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(spectator.component.progressLevel > 0).toBeTruthy(); }); - it('should be interval unsubscribed', done => { - const request = new HttpRequest('GET', 'test'); + it('should be interval unsubscribed', async () => { + const request = new HttpRequest('GET', 'test'); spectator.detectChanges(); const httpWaitService = spectator.inject(HttpWaitService); + + await firstValueFrom(combineLatest([ + httpWaitService.getLoading$(), + spectator.inject(RouterWaitService).getLoading$() + ])); + httpWaitService.addRequest(request); + spectator.detectChanges(); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } + expect(spectator.component.interval.closed).toBe(false); + httpWaitService.deleteRequest(request); - timer(400).subscribe(() => { - expect(spectator.component.interval.closed).toBe(true); - done(); - }); + spectator.detectChanges(); + + await firstValueFrom(timer(400)); + + expect(spectator.component.interval.closed).toBe(true); }); - it('should start and stop the loading with navigation', done => { + + it('should start and stop the loading with navigation', async () => { + spectator.detectChanges(); + const routerWaitService = spectator.inject(RouterWaitService); + + routerWaitService.setLoading(true); spectator.detectChanges(); - events$.next(new NavigationStart(1, 'test')); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.interval.closed).toBe(false); - events$.next(new NavigationEnd(1, 'test', 'test')); - events$.next(new NavigationError(1, 'test', 'test')); + routerWaitService.setLoading(false); + spectator.detectChanges(); + + attempts = 0; + while (spectator.component.progressLevel !== 100 && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.progressLevel).toBe(100); - timer(2).subscribe(() => { - expect(spectator.component.progressLevel).toBe(0); - done(); - }); + await firstValueFrom(timer(spectator.component.stopDelay + 10)); + expect(spectator.component.progressLevel).toBe(0); }); - it('should stop the loading with navigation', done => { + it('should stop the loading with navigation', async () => { + spectator.detectChanges(); + const routerWaitService = spectator.inject(RouterWaitService); + + routerWaitService.setLoading(true); spectator.detectChanges(); - events$.next(new NavigationStart(1, 'test')); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.interval.closed).toBe(false); - events$.next(new NavigationEnd(1, 'testend', 'testend')); + routerWaitService.setLoading(false); + spectator.detectChanges(); + + attempts = 0; + while (spectator.component.progressLevel !== 100 && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } expect(spectator.component.progressLevel).toBe(100); - timer(2).subscribe(() => { - expect(spectator.component.progressLevel).toBe(0); - done(); - }); + await firstValueFrom(timer(spectator.component.stopDelay + 10)); + expect(spectator.component.progressLevel).toBe(0); }); describe('#startLoading', () => { - it('should return when isLoading is true', done => { + it('should return when isLoading is true', async () => { spectator.detectChanges(); + events$.next(new NavigationStart(1, 'test')); + spectator.detectChanges(); + + let attempts = 0; + while (spectator.component.interval.closed && attempts < 50) { + await new Promise(resolve => setTimeout(resolve, 10)); + spectator.detectChanges(); + attempts++; + } + events$.next(new NavigationStart(1, 'test')); - done(); + spectator.detectChanges(); + + expect(spectator.component).toBeTruthy(); }); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts index 91589f2155..f80388d478 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts @@ -1,4 +1,4 @@ -import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/vitest'; import { LoadingDirective } from '../directives'; import { LoadingComponent } from '../components'; import { Component } from '@angular/core'; @@ -29,10 +29,11 @@ describe('LoadingDirective', () => { expect(spectator.directive).toBeTruthy(); }); - it('should handle loading input', () => { - spectator.setHostInput({ loading: false }); - spectator.detectChanges(); + it('should handle loading input', async () => { + spectator.directive.loading = false; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); + expect(spectator.directive.loading).toBe(false); }); }); @@ -53,19 +54,19 @@ describe('LoadingDirective', () => { expect(spectator.directive.targetElement).toBe(mockTarget); }); - it('should handle delay input', () => { - spectator.setHostInput({ delay: 100 }); - spectator.detectChanges(); + it('should handle delay input', async () => { + spectator.directive.delay = 100; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); }); - it('should handle loading state changes', () => { - spectator.setHostInput({ loading: false }); - spectator.detectChanges(); + it('should handle loading state changes', async() => { + spectator.directive.loading = false; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); - spectator.setHostInput({ loading: true }); - spectator.detectChanges(); + spectator.directive.loading = true; + await new Promise(resolve => setTimeout(resolve, 10)); expect(spectator.directive).toBeTruthy(); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts index dc79c47a9c..0caef230ca 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts @@ -1,10 +1,11 @@ import { ConfirmationService } from '@abp/ng.theme.shared'; import { CoreTestingModule } from '@abp/ng.core/testing'; import { Component, EventEmitter, Input } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import { Confirmation } from '@abp/ng.theme.shared'; -import { Subject, timer } from 'rxjs'; +import { firstValueFrom, Subject, timer } from 'rxjs'; import { ModalComponent } from '../components/modal/modal.component'; +import { setupComponentResources } from './utils'; @Component({ template: ` @@ -27,25 +28,34 @@ class TestHostComponent { } const mockConfirmation$ = new Subject(); -const disappearFn = jest.fn(); +const disappearFn = vi.fn(); describe('ModalComponent', () => { let spectator: Spectator; + let createComponent: ReturnType>; - const createComponent = createComponentFactory({ - component: TestHostComponent, - imports: [CoreTestingModule.withConfig()], - providers: [ - { - provide: ConfirmationService, - useValue: { - warn: jest.fn(() => mockConfirmation$), - }, - }, - ], - }); + beforeAll(() => setupComponentResources('../components/modal', import.meta.url)); beforeEach(() => { + // Create component factory in beforeEach to ensure beforeAll has run + if (!createComponent) { + createComponent = createComponentFactory({ + component: TestHostComponent, + imports: [ + CoreTestingModule.withConfig(), + ModalComponent, + ], + providers: [ + { + provide: ConfirmationService, + useValue: { + warn: vi.fn(() => mockConfirmation$), + }, + }, + ], + }); + } + spectator = createComponent(); disappearFn.mockClear(); }); @@ -71,10 +81,10 @@ describe('ModalComponent', () => { }); }); -async function wait0ms() { - await timer(0).toPromise(); +async function wait0ms() { + await firstValueFrom(timer(0)); } -async function wait300ms() { - await timer(300).toPromise(); +async function wait300ms() { + await firstValueFrom(timer(300)); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts index ae008d73ee..401da50070 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts @@ -1,23 +1,25 @@ -import { CoreTestingModule } from '@abp/ng.core/testing'; -import { NgModule } from '@angular/core'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { ContentProjectionService } from '@abp/ng.core'; +import { ComponentRef } from '@angular/core'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import { ToastContainerComponent } from '../components/toast-container/toast-container.component'; -import { ToastComponent } from '../components/toast/toast.component'; import { ToasterService } from '../services/toaster.service'; -@NgModule({ - exports: [ToastContainerComponent], - declarations: [], - imports: [CoreTestingModule.withConfig(), ToastContainerComponent, ToastComponent], -}) -export class MockModule {} - describe('ToasterService', () => { let spectator: SpectatorService; let service: ToasterService; + const mockComponentRef = { + changeDetectorRef: { detectChanges: vi.fn() }, + instance: {} as ToastContainerComponent, + } as unknown as ComponentRef; + + const contentProjectionService = { + projectContent: vi.fn().mockReturnValue(mockComponentRef), + } satisfies Partial; + const createService = createServiceFactory({ service: ToasterService, - imports: [CoreTestingModule.withConfig(), MockModule], + providers: [{ provide: ContentProjectionService, useValue: contentProjectionService }], }); beforeEach(() => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/utils/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/utils/index.ts new file mode 100644 index 0000000000..73b11724ec --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/utils/index.ts @@ -0,0 +1 @@ +export * from './setup-component-resources'; \ No newline at end of file diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/utils/setup-component-resources.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/utils/setup-component-resources.ts new file mode 100644 index 0000000000..0795a5e32b --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/utils/setup-component-resources.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Sets up component resource resolution for Angular component tests. + * This is needed when components have external templates or stylesheets. + * + * @param componentDirPath - The path to the component directory relative to the test file. + * For example: '../components/loader-bar' or './components/my-component' + * @param testFileUrl - The import.meta.url from the test file. Defaults to the caller's location. + * + * @example + * ```typescript + * + * import { setupComponentResources } from './utils'; + * + * beforeAll(() => setupComponentResources('../components/loader-bar', import.meta.url)); + * ``` + */ +export async function setupComponentResources( + componentDirPath: string, + testFileUrl: string = import.meta.url, +): Promise { + try { + if (typeof process !== 'undefined' && process.versions?.node) { + const { ɵresolveComponentResources: resolveComponentResources } = await import('@angular/core'); + + // Get the test file directory path + const testFileDir = dirname(fileURLToPath(testFileUrl)); + const componentDir = resolve(testFileDir, componentDirPath); + + await resolveComponentResources((url: string) => { + // For SCSS/SASS files, return empty CSS since jsdom can't parse SCSS + if (url.endsWith('.scss') || url.endsWith('.sass')) { + return Promise.resolve(''); + } + + // For other files (HTML, CSS, etc.), read the actual content + try { + // Resolve relative paths like './component.scss' or 'component.scss' + const normalizedUrl = url.replace(/^\.\//, ''); + const filePath = resolve(componentDir, normalizedUrl); + return Promise.resolve(readFileSync(filePath, 'utf-8')); + } catch (error) { + // If file not found, return empty string + return Promise.resolve(''); + } + }); + } + } catch (error) { + console.warn('Failed to set up component resource resolver:', error); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts index 9f263779e2..1fcd5c3591 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts @@ -1,8 +1,9 @@ import { AbpApplicationConfigurationService, ConfigStateService } from '@abp/ng.core'; import { CoreTestingModule } from '@abp/ng.core/testing'; +import { AbpApplicationLocalizationService } from '@abp/ng.core'; import { HttpClient } from '@angular/common/http'; import { Component, Injector } from '@angular/core'; -import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of } from 'rxjs'; import { getPasswordValidators, validatePassword } from '../utils'; @@ -32,6 +33,44 @@ describe('ValidationUtils', () => { 'Abp.Identity.Password.RequireDigit': 'True', }, }, + localization: { + values: {}, + languages: [], + currentCulture: { + cultureName: 'en', + displayName: 'English', + englishName: 'English', + threeLetterIsoLanguageName: 'eng', + twoLetterIsoLanguageName: 'en', + isRightToLeft: false, + name: 'en', + nativeName: 'English', + dateTimeFormat: { + calendarAlgorithmType: 'SolarCalendar', + dateTimeFormatLong: 'dddd, MMMM d, yyyy', + shortDatePattern: 'M/d/yyyy', + fullDateTimePattern: 'dddd, MMMM d, yyyy h:mm:ss tt', + dateSeparator: '/', + shortTimePattern: 'h:mm tt', + longTimePattern: 'h:mm:ss tt', + }, + }, + defaultResourceName: null, + resources: {}, + languagesMap: {}, + languageFilesMap: {}, + }, + }), + }, + }, + { + provide: AbpApplicationLocalizationService, + useValue: { + get: () => + of({ + resources: { + Default: { texts: {}, baseResources: [] }, + }, }), }, }, diff --git a/npm/ng-packs/packages/theme-shared/src/test-setup.ts b/npm/ng-packs/packages/theme-shared/src/test-setup.ts index 2cf1ac7191..3eaa53b84d 100644 --- a/npm/ng-packs/packages/theme-shared/src/test-setup.ts +++ b/npm/ng-packs/packages/theme-shared/src/test-setup.ts @@ -1,10 +1,28 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; -setupZoneTestEnv(); - -const originalError = console.error; -console.error = (...args: any[]) => { - if (args[0]?.includes?.('ExpressionChangedAfterItHasBeenCheckedError')) { - return; - } - originalError.apply(console, args); -}; +import '@angular/compiler'; +import 'zone.js'; +import 'zone.js/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { + ɵgetCleanupHook as getCleanupHook, + getTestBed +} from '@angular/core/testing'; + + +beforeEach(getCleanupHook(false)); +afterEach(getCleanupHook(true)); + +// Initialize Angular testing environment +getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + + +// Mock window.location for test environment +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:4200', + origin: 'http://localhost:4200', + pathname: '/', + search: '', + hash: '', + }, + writable: true, +}); diff --git a/npm/ng-packs/packages/theme-shared/tsconfig.lib.json b/npm/ng-packs/packages/theme-shared/tsconfig.lib.json index 22d2695db8..80ebc37002 100644 --- a/npm/ng-packs/packages/theme-shared/tsconfig.lib.json +++ b/npm/ng-packs/packages/theme-shared/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/theme-shared/tsconfig.spec.json b/npm/ng-packs/packages/theme-shared/tsconfig.spec.json index 023d7d0b51..fc61345bb3 100644 --- a/npm/ng-packs/packages/theme-shared/tsconfig.spec.json +++ b/npm/ng-packs/packages/theme-shared/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/theme-shared/vitest.config.mts b/npm/ng-packs/packages/theme-shared/vitest.config.mts new file mode 100644 index 0000000000..d9496b26fd --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/vitest.config.mts @@ -0,0 +1,22 @@ +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/theme-shared', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'theme-shared', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['src/test-setup.ts'], + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/theme-shared', + provider: 'v8' as const, + }, + }, +})); diff --git a/npm/ng-packs/tsconfig.base.json b/npm/ng-packs/tsconfig.base.json index 4194690430..3c38c46d4c 100644 --- a/npm/ng-packs/tsconfig.base.json +++ b/npm/ng-packs/tsconfig.base.json @@ -21,6 +21,7 @@ "@abp/ng.account/config": ["packages/account/config/src/public-api.ts"], "@abp/ng.components": ["packages/components/src/public-api.ts"], "@abp/ng.components/chart.js": ["packages/components/chart.js/src/public-api.ts"], + "@abp/ng.components/dynamic-form": ["packages/components/dynamic-form/src/public-api.ts"], "@abp/ng.components/extensible": ["packages/components/extensible/src/public-api.ts"], "@abp/ng.components/lookup": ["packages/components/lookup/src/public-api.ts"], "@abp/ng.components/page": ["packages/components/page/src/public-api.ts"], diff --git a/npm/ng-packs/vitest.config.mts b/npm/ng-packs/vitest.config.mts new file mode 100644 index 0000000000..f239d0bfce --- /dev/null +++ b/npm/ng-packs/vitest.config.mts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + projects: [ + './packages/core/vitest.config.mts', + './packages/theme-basic/vitest.config.mts', + './packages/theme-shared/vitest.config.mts', + './packages/oauth/vitest.config.mts', + './packages/generators/vitest.config.mts', + './packages/schematics/vitest.config.mts', + ], + }, +}); \ No newline at end of file diff --git a/npm/packs/anchor-js/package.json b/npm/packs/anchor-js/package.json index a9b41a7503..66c70614a8 100644 --- a/npm/packs/anchor-js/package.json +++ b/npm/packs/anchor-js/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/anchor-js", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "anchor-js": "^5.0.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/aspnetcore.components.server.basictheme/package.json b/npm/packs/aspnetcore.components.server.basictheme/package.json index c4a08f558d..d566c60e3a 100644 --- a/npm/packs/aspnetcore.components.server.basictheme/package.json +++ b/npm/packs/aspnetcore.components.server.basictheme/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/aspnetcore.components.server.basictheme", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/aspnetcore.components.server.theming": "~10.0.1" + "@abp/aspnetcore.components.server.theming": "~10.1.0-rc.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/aspnetcore.components.server.theming/package.json b/npm/packs/aspnetcore.components.server.theming/package.json index 99aaf1a817..5b98884eb5 100644 --- a/npm/packs/aspnetcore.components.server.theming/package.json +++ b/npm/packs/aspnetcore.components.server.theming/package.json @@ -1,12 +1,12 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/aspnetcore.components.server.theming", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/bootstrap": "~10.0.1", - "@abp/font-awesome": "~10.0.1" + "@abp/bootstrap": "~10.1.0-rc.2", + "@abp/font-awesome": "~10.1.0-rc.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/aspnetcore.mvc.ui.theme.basic/package.json b/npm/packs/aspnetcore.mvc.ui.theme.basic/package.json index bd45348e48..be30febb4a 100644 --- a/npm/packs/aspnetcore.mvc.ui.theme.basic/package.json +++ b/npm/packs/aspnetcore.mvc.ui.theme.basic/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/aspnetcore.mvc.ui.theme.basic", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.shared": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.shared": "~10.1.0-rc.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json b/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json index 30edce64c1..3143c0fc0a 100644 --- a/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json +++ b/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/aspnetcore.mvc.ui.theme.shared", "repository": { "type": "git", @@ -10,21 +10,21 @@ "access": "public" }, "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.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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/aspnetcore.mvc.ui/package-lock.json b/npm/packs/aspnetcore.mvc.ui/package-lock.json index 12dc9cb774..3ec3578119 100644 --- a/npm/packs/aspnetcore.mvc.ui/package-lock.json +++ b/npm/packs/aspnetcore.mvc.ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "@abp/aspnetcore.mvc.ui", - "version": "10.0.1", + "version": "10.1.0-rc.2", "lockfileVersion": 1, "requires": true, "packages": { diff --git a/npm/packs/aspnetcore.mvc.ui/package.json b/npm/packs/aspnetcore.mvc.ui/package.json index 77ea761b29..52fe871c52 100644 --- a/npm/packs/aspnetcore.mvc.ui/package.json +++ b/npm/packs/aspnetcore.mvc.ui/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/aspnetcore.mvc.ui", "repository": { "type": "git", diff --git a/npm/packs/blogging/package.json b/npm/packs/blogging/package.json index 53a4d0e7b1..94be08cdb9 100644 --- a/npm/packs/blogging/package.json +++ b/npm/packs/blogging/package.json @@ -1,14 +1,14 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/blogging", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/bootstrap-datepicker/package.json b/npm/packs/bootstrap-datepicker/package.json index e44a1f90c8..48241bfb07 100644 --- a/npm/packs/bootstrap-datepicker/package.json +++ b/npm/packs/bootstrap-datepicker/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/bootstrap-datepicker", "repository": { "type": "git", diff --git a/npm/packs/bootstrap-daterangepicker/package.json b/npm/packs/bootstrap-daterangepicker/package.json index 3d70ce087c..d027c0f0bd 100644 --- a/npm/packs/bootstrap-daterangepicker/package.json +++ b/npm/packs/bootstrap-daterangepicker/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/bootstrap-daterangepicker", "repository": { "type": "git", diff --git a/npm/packs/bootstrap/package.json b/npm/packs/bootstrap/package.json index 919a85ffba..479f04b7a1 100644 --- a/npm/packs/bootstrap/package.json +++ b/npm/packs/bootstrap/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/bootstrap", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "bootstrap": "^5.3.8" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/chart.js/package.json b/npm/packs/chart.js/package.json index f286990cf3..6f3148b0e3 100644 --- a/npm/packs/chart.js/package.json +++ b/npm/packs/chart.js/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/chart.js", "publishConfig": { "access": "public" diff --git a/npm/packs/clipboard/package.json b/npm/packs/clipboard/package.json index f85d7c94fe..64c7810dda 100644 --- a/npm/packs/clipboard/package.json +++ b/npm/packs/clipboard/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/clipboard", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "clipboard": "^2.0.11" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/cms-kit.admin/package.json b/npm/packs/cms-kit.admin/package.json index f438c85ccc..3c9f0b9058 100644 --- a/npm/packs/cms-kit.admin/package.json +++ b/npm/packs/cms-kit.admin/package.json @@ -1,16 +1,16 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/cms-kit.admin", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/cms-kit.public/package.json b/npm/packs/cms-kit.public/package.json index 3ba98678f2..b7fcf1b540 100644 --- a/npm/packs/cms-kit.public/package.json +++ b/npm/packs/cms-kit.public/package.json @@ -1,12 +1,12 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/cms-kit.public", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/cms-kit/package.json b/npm/packs/cms-kit/package.json index 3059ab084a..86686216d4 100644 --- a/npm/packs/cms-kit/package.json +++ b/npm/packs/cms-kit/package.json @@ -1,12 +1,12 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/cms-kit", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/codemirror/package.json b/npm/packs/codemirror/package.json index e33ab104da..808bc652ef 100644 --- a/npm/packs/codemirror/package.json +++ b/npm/packs/codemirror/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/codemirror", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "codemirror": "^5.65.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/core/package.json b/npm/packs/core/package.json index afe7c43659..1b0fea5b69 100644 --- a/npm/packs/core/package.json +++ b/npm/packs/core/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/core", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/utils": "~10.0.1" + "@abp/utils": "~10.1.0-rc.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/cropperjs/package.json b/npm/packs/cropperjs/package.json index 2b861e039e..8732cf5faf 100644 --- a/npm/packs/cropperjs/package.json +++ b/npm/packs/cropperjs/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/cropperjs", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "cropperjs": "^1.6.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/datatables.net-bs4/package.json b/npm/packs/datatables.net-bs4/package.json index ff30e3e85a..18b00a2cf9 100644 --- a/npm/packs/datatables.net-bs4/package.json +++ b/npm/packs/datatables.net-bs4/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/datatables.net-bs4", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/datatables.net": "~10.0.1", + "@abp/datatables.net": "~10.1.0-rc.2", "datatables.net-bs4": "^2.3.4" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/datatables.net-bs5/package.json b/npm/packs/datatables.net-bs5/package.json index 20819ec6c0..19c71ea013 100644 --- a/npm/packs/datatables.net-bs5/package.json +++ b/npm/packs/datatables.net-bs5/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/datatables.net-bs5", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/datatables.net": "~10.0.1", + "@abp/datatables.net": "~10.1.0-rc.2", "datatables.net-bs5": "^2.3.4" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/datatables.net/package.json b/npm/packs/datatables.net/package.json index 6d297761dc..d0f0c240e4 100644 --- a/npm/packs/datatables.net/package.json +++ b/npm/packs/datatables.net/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/datatables.net", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "datatables.net": "^2.3.4" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/docs/package.json b/npm/packs/docs/package.json index ec0afb054a..cafd0f48bb 100644 --- a/npm/packs/docs/package.json +++ b/npm/packs/docs/package.json @@ -1,15 +1,15 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/docs", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/flag-icon-css/package.json b/npm/packs/flag-icon-css/package.json index 6a243d896f..cd5cdec40f 100644 --- a/npm/packs/flag-icon-css/package.json +++ b/npm/packs/flag-icon-css/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/flag-icon-css", "publishConfig": { "access": "public" diff --git a/npm/packs/flag-icons/package.json b/npm/packs/flag-icons/package.json index 325156e811..fba66cb6bb 100644 --- a/npm/packs/flag-icons/package.json +++ b/npm/packs/flag-icons/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/flag-icons", "publishConfig": { "access": "public" diff --git a/npm/packs/font-awesome/package.json b/npm/packs/font-awesome/package.json index e97d036ab3..41f0c12daf 100644 --- a/npm/packs/font-awesome/package.json +++ b/npm/packs/font-awesome/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/font-awesome", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "@fortawesome/fontawesome-free": "^7.0.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/highlight.js/package.json b/npm/packs/highlight.js/package.json index 3817f5177d..922abc3da8 100644 --- a/npm/packs/highlight.js/package.json +++ b/npm/packs/highlight.js/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/highlight.js", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "@highlightjs/cdn-assets": "~11.11.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/jquery-form/package.json b/npm/packs/jquery-form/package.json index 1102de0842..9682e9c275 100644 --- a/npm/packs/jquery-form/package.json +++ b/npm/packs/jquery-form/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/jquery-form", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "jquery-form": "^4.3.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/jquery-validation-unobtrusive/package.json b/npm/packs/jquery-validation-unobtrusive/package.json index bd19a28899..34ce45bf1c 100644 --- a/npm/packs/jquery-validation-unobtrusive/package.json +++ b/npm/packs/jquery-validation-unobtrusive/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/jquery-validation-unobtrusive", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/jquery-validation": "~10.0.1", + "@abp/jquery-validation": "~10.1.0-rc.2", "jquery-validation-unobtrusive": "^4.0.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/jquery-validation/package.json b/npm/packs/jquery-validation/package.json index e6c72c0efa..4cad463ff2 100644 --- a/npm/packs/jquery-validation/package.json +++ b/npm/packs/jquery-validation/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/jquery-validation", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "jquery-validation": "^1.21.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/jquery/package.json b/npm/packs/jquery/package.json index c6d451ddad..4a4f7f0aa7 100644 --- a/npm/packs/jquery/package.json +++ b/npm/packs/jquery/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/jquery", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "jquery": "~3.7.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/jstree/package.json b/npm/packs/jstree/package.json index fca895cc19..24e1786cb7 100644 --- a/npm/packs/jstree/package.json +++ b/npm/packs/jstree/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/jstree", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "jstree": "^3.3.17" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/lodash/package.json b/npm/packs/lodash/package.json index c700c6f6f7..64050e87fc 100644 --- a/npm/packs/lodash/package.json +++ b/npm/packs/lodash/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/lodash", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "lodash": "^4.17.21" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/luxon/package.json b/npm/packs/luxon/package.json index 626ae466c2..29d4dbb782 100644 --- a/npm/packs/luxon/package.json +++ b/npm/packs/luxon/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/luxon", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "luxon": "^3.7.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/malihu-custom-scrollbar-plugin/package.json b/npm/packs/malihu-custom-scrollbar-plugin/package.json index e0c902da01..1f291f26ca 100644 --- a/npm/packs/malihu-custom-scrollbar-plugin/package.json +++ b/npm/packs/malihu-custom-scrollbar-plugin/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/malihu-custom-scrollbar-plugin", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "malihu-custom-scrollbar-plugin": "^3.1.5" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/markdown-it/package.json b/npm/packs/markdown-it/package.json index 88c0bbeae8..fb554e3883 100644 --- a/npm/packs/markdown-it/package.json +++ b/npm/packs/markdown-it/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/markdown-it", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "markdown-it": "^14.1.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/moment/package.json b/npm/packs/moment/package.json index 33546ac420..b0f2c78e4e 100644 --- a/npm/packs/moment/package.json +++ b/npm/packs/moment/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/moment", "repository": { "type": "git", diff --git a/npm/packs/owl.carousel/package.json b/npm/packs/owl.carousel/package.json index e4b0891cb2..5044c2fbe0 100644 --- a/npm/packs/owl.carousel/package.json +++ b/npm/packs/owl.carousel/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/owl.carousel", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "owl.carousel": "^2.3.4" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/popper.js/package.json b/npm/packs/popper.js/package.json index daea70e352..cc86b2e2ea 100644 --- a/npm/packs/popper.js/package.json +++ b/npm/packs/popper.js/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/popper.js", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "@popperjs/core": "^2.11.8" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/prismjs/package.json b/npm/packs/prismjs/package.json index 1f09c08d62..0e83b34e7f 100644 --- a/npm/packs/prismjs/package.json +++ b/npm/packs/prismjs/package.json @@ -1,12 +1,12 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/prismjs", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/qrcode/package.json b/npm/packs/qrcode/package.json index a5399e9c61..ad766dc992 100644 --- a/npm/packs/qrcode/package.json +++ b/npm/packs/qrcode/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/qrcode", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1" + "@abp/core": "~10.1.0-rc.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/select2/package.json b/npm/packs/select2/package.json index abad97dd0d..caf990f633 100644 --- a/npm/packs/select2/package.json +++ b/npm/packs/select2/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/select2", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "select2": "^4.0.13" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/signalr/package.json b/npm/packs/signalr/package.json index 45b3fc37f7..c22f2b18b2 100644 --- a/npm/packs/signalr/package.json +++ b/npm/packs/signalr/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/signalr", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "@microsoft/signalr": "~9.0.6" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/slugify/package.json b/npm/packs/slugify/package.json index df5af3552a..9d596d6589 100644 --- a/npm/packs/slugify/package.json +++ b/npm/packs/slugify/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/slugify", "publishConfig": { "access": "public" diff --git a/npm/packs/star-rating-svg/package.json b/npm/packs/star-rating-svg/package.json index 5a976d07fb..00531dcdec 100644 --- a/npm/packs/star-rating-svg/package.json +++ b/npm/packs/star-rating-svg/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/star-rating-svg", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "star-rating-svg": "^3.5.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/sweetalert2/package.json b/npm/packs/sweetalert2/package.json index bb83bbbbef..236fa38a7e 100644 --- a/npm/packs/sweetalert2/package.json +++ b/npm/packs/sweetalert2/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/sweetalert2", "publishConfig": { "access": "public" @@ -10,7 +10,7 @@ "directory": "npm/packs/sweetalert2" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "sweetalert2": "^11.23.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/timeago/package.json b/npm/packs/timeago/package.json index 3cf6543693..22f0d3ced5 100644 --- a/npm/packs/timeago/package.json +++ b/npm/packs/timeago/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/timeago", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "timeago": "^1.6.7" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/toastr/package.json b/npm/packs/toastr/package.json index af0d86a82e..c18fcd70e9 100644 --- a/npm/packs/toastr/package.json +++ b/npm/packs/toastr/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/toastr", "repository": { "type": "git", @@ -10,7 +10,7 @@ "access": "public" }, "dependencies": { - "@abp/jquery": "~10.0.1", + "@abp/jquery": "~10.1.0-rc.2", "toastr": "^2.1.4" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/tui-editor/package.json b/npm/packs/tui-editor/package.json index 17223d8679..1d18b7aab0 100644 --- a/npm/packs/tui-editor/package.json +++ b/npm/packs/tui-editor/package.json @@ -1,12 +1,12 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/tui-editor", "publishConfig": { "access": "public" }, "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" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/uppy/package.json b/npm/packs/uppy/package.json index 006b7181a1..6443c16bf9 100644 --- a/npm/packs/uppy/package.json +++ b/npm/packs/uppy/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/uppy", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "uppy": "^5.1.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/utils/package.json b/npm/packs/utils/package.json index 16205b1057..9a059db2d8 100644 --- a/npm/packs/utils/package.json +++ b/npm/packs/utils/package.json @@ -1,6 +1,6 @@ { "name": "@abp/utils", - "version": "10.0.1", + "version": "10.1.0-rc.2", "scripts": { "prepublishOnly": "yarn install --ignore-scripts && node prepublish.js", "ng": "ng", diff --git a/npm/packs/vee-validate/package.json b/npm/packs/vee-validate/package.json index 2d56683daf..30c837454b 100644 --- a/npm/packs/vee-validate/package.json +++ b/npm/packs/vee-validate/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/vee-validate", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/vue": "~10.0.1", + "@abp/vue": "~10.1.0-rc.2", "vee-validate": "~3.4.4" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/npm/packs/virtual-file-explorer/package.json b/npm/packs/virtual-file-explorer/package.json index ba4e0ea8c4..0672075f5b 100644 --- a/npm/packs/virtual-file-explorer/package.json +++ b/npm/packs/virtual-file-explorer/package.json @@ -1,12 +1,12 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/virtual-file-explorer", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/clipboard": "~10.0.1", - "@abp/prismjs": "~10.0.1" + "@abp/clipboard": "~10.1.0-rc.2", + "@abp/prismjs": "~10.1.0-rc.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/vue/package.json b/npm/packs/vue/package.json index 5a0358acdc..fb878bd9cf 100644 --- a/npm/packs/vue/package.json +++ b/npm/packs/vue/package.json @@ -1,5 +1,5 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/vue", "publishConfig": { "access": "public" diff --git a/npm/packs/zxcvbn/package.json b/npm/packs/zxcvbn/package.json index fc6197b952..882d8db35f 100644 --- a/npm/packs/zxcvbn/package.json +++ b/npm/packs/zxcvbn/package.json @@ -1,11 +1,11 @@ { - "version": "10.0.1", + "version": "10.1.0-rc.2", "name": "@abp/zxcvbn", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.0.1", + "@abp/core": "~10.1.0-rc.2", "zxcvbn": "^4.4.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", diff --git a/source-code/Volo.Abp.Account.SourceCode/Volo.Abp.Account.SourceCode.zip b/source-code/Volo.Abp.Account.SourceCode/Volo.Abp.Account.SourceCode.zip index 08b4f7f0e7..5b64147a89 100644 Binary files a/source-code/Volo.Abp.Account.SourceCode/Volo.Abp.Account.SourceCode.zip and b/source-code/Volo.Abp.Account.SourceCode/Volo.Abp.Account.SourceCode.zip differ diff --git a/source-code/Volo.Abp.AuditLogging.SourceCode/Volo.Abp.AuditLogging.SourceCode.zip b/source-code/Volo.Abp.AuditLogging.SourceCode/Volo.Abp.AuditLogging.SourceCode.zip index d06a9fad14..0919ab4bab 100644 Binary files a/source-code/Volo.Abp.AuditLogging.SourceCode/Volo.Abp.AuditLogging.SourceCode.zip and b/source-code/Volo.Abp.AuditLogging.SourceCode/Volo.Abp.AuditLogging.SourceCode.zip differ diff --git a/source-code/Volo.Abp.BackgroundJobs.SourceCode/Volo.Abp.BackgroundJobs.SourceCode.zip b/source-code/Volo.Abp.BackgroundJobs.SourceCode/Volo.Abp.BackgroundJobs.SourceCode.zip index 3723ba5bd8..8322813f0e 100644 Binary files a/source-code/Volo.Abp.BackgroundJobs.SourceCode/Volo.Abp.BackgroundJobs.SourceCode.zip and b/source-code/Volo.Abp.BackgroundJobs.SourceCode/Volo.Abp.BackgroundJobs.SourceCode.zip differ diff --git a/source-code/Volo.Abp.BlobStoring.Database.SourceCode/Volo.Abp.BlobStoring.Database.SourceCode.zip b/source-code/Volo.Abp.BlobStoring.Database.SourceCode/Volo.Abp.BlobStoring.Database.SourceCode.zip index 74a4943b6c..edac4d8f20 100644 Binary files a/source-code/Volo.Abp.BlobStoring.Database.SourceCode/Volo.Abp.BlobStoring.Database.SourceCode.zip and b/source-code/Volo.Abp.BlobStoring.Database.SourceCode/Volo.Abp.BlobStoring.Database.SourceCode.zip differ diff --git a/source-code/Volo.Abp.FeatureManagement.SourceCode/Volo.Abp.FeatureManagement.SourceCode.zip b/source-code/Volo.Abp.FeatureManagement.SourceCode/Volo.Abp.FeatureManagement.SourceCode.zip index 57339950ba..fcc3980ea7 100644 Binary files a/source-code/Volo.Abp.FeatureManagement.SourceCode/Volo.Abp.FeatureManagement.SourceCode.zip and b/source-code/Volo.Abp.FeatureManagement.SourceCode/Volo.Abp.FeatureManagement.SourceCode.zip differ diff --git a/source-code/Volo.Abp.Identity.SourceCode/Volo.Abp.Identity.SourceCode.zip b/source-code/Volo.Abp.Identity.SourceCode/Volo.Abp.Identity.SourceCode.zip index c821db8dad..30d07ea91a 100644 Binary files a/source-code/Volo.Abp.Identity.SourceCode/Volo.Abp.Identity.SourceCode.zip and b/source-code/Volo.Abp.Identity.SourceCode/Volo.Abp.Identity.SourceCode.zip differ diff --git a/source-code/Volo.Abp.IdentityServer.SourceCode/Volo.Abp.IdentityServer.SourceCode.zip b/source-code/Volo.Abp.IdentityServer.SourceCode/Volo.Abp.IdentityServer.SourceCode.zip index 98ad610ab2..19dc79216c 100644 Binary files a/source-code/Volo.Abp.IdentityServer.SourceCode/Volo.Abp.IdentityServer.SourceCode.zip and b/source-code/Volo.Abp.IdentityServer.SourceCode/Volo.Abp.IdentityServer.SourceCode.zip differ diff --git a/source-code/Volo.Abp.OpenIddict.SourceCode/Volo.Abp.OpenIddict.SourceCode.zip b/source-code/Volo.Abp.OpenIddict.SourceCode/Volo.Abp.OpenIddict.SourceCode.zip index 6022b003a2..3a79b434bc 100644 Binary files a/source-code/Volo.Abp.OpenIddict.SourceCode/Volo.Abp.OpenIddict.SourceCode.zip and b/source-code/Volo.Abp.OpenIddict.SourceCode/Volo.Abp.OpenIddict.SourceCode.zip differ diff --git a/source-code/Volo.Abp.PermissionManagement.SourceCode/Volo.Abp.PermissionManagement.SourceCode.zip b/source-code/Volo.Abp.PermissionManagement.SourceCode/Volo.Abp.PermissionManagement.SourceCode.zip index 3907870d7a..570d22f3a9 100644 Binary files a/source-code/Volo.Abp.PermissionManagement.SourceCode/Volo.Abp.PermissionManagement.SourceCode.zip and b/source-code/Volo.Abp.PermissionManagement.SourceCode/Volo.Abp.PermissionManagement.SourceCode.zip differ diff --git a/source-code/Volo.Abp.SettingManagement.SourceCode/Volo.Abp.SettingManagement.SourceCode.zip b/source-code/Volo.Abp.SettingManagement.SourceCode/Volo.Abp.SettingManagement.SourceCode.zip index cfb986ddc2..469b0b5584 100644 Binary files a/source-code/Volo.Abp.SettingManagement.SourceCode/Volo.Abp.SettingManagement.SourceCode.zip and b/source-code/Volo.Abp.SettingManagement.SourceCode/Volo.Abp.SettingManagement.SourceCode.zip differ diff --git a/source-code/Volo.Abp.TenantManagement.SourceCode/Volo.Abp.TenantManagement.SourceCode.zip b/source-code/Volo.Abp.TenantManagement.SourceCode/Volo.Abp.TenantManagement.SourceCode.zip index 3c9d5c6885..44d7739eff 100644 Binary files a/source-code/Volo.Abp.TenantManagement.SourceCode/Volo.Abp.TenantManagement.SourceCode.zip and b/source-code/Volo.Abp.TenantManagement.SourceCode/Volo.Abp.TenantManagement.SourceCode.zip differ diff --git a/source-code/Volo.Abp.Users.SourceCode/Volo.Abp.Users.SourceCode.zip b/source-code/Volo.Abp.Users.SourceCode/Volo.Abp.Users.SourceCode.zip index 2910c37396..1925c724ea 100644 Binary files a/source-code/Volo.Abp.Users.SourceCode/Volo.Abp.Users.SourceCode.zip and b/source-code/Volo.Abp.Users.SourceCode/Volo.Abp.Users.SourceCode.zip differ diff --git a/source-code/Volo.Abp.VirtualFileExplorer.SourceCode/Volo.Abp.VirtualFileExplorer.SourceCode.zip b/source-code/Volo.Abp.VirtualFileExplorer.SourceCode/Volo.Abp.VirtualFileExplorer.SourceCode.zip index c9c6d19ee9..56f1cd2acf 100644 Binary files a/source-code/Volo.Abp.VirtualFileExplorer.SourceCode/Volo.Abp.VirtualFileExplorer.SourceCode.zip and b/source-code/Volo.Abp.VirtualFileExplorer.SourceCode/Volo.Abp.VirtualFileExplorer.SourceCode.zip differ diff --git a/source-code/Volo.Blogging.SourceCode/Volo.Blogging.SourceCode.zip b/source-code/Volo.Blogging.SourceCode/Volo.Blogging.SourceCode.zip index 603f3b3168..6081d1e39b 100644 Binary files a/source-code/Volo.Blogging.SourceCode/Volo.Blogging.SourceCode.zip and b/source-code/Volo.Blogging.SourceCode/Volo.Blogging.SourceCode.zip differ diff --git a/source-code/Volo.CmsKit.SourceCode/Volo.CmsKit.SourceCode.zip b/source-code/Volo.CmsKit.SourceCode/Volo.CmsKit.SourceCode.zip index 9188d5a55d..c508876724 100644 Binary files a/source-code/Volo.CmsKit.SourceCode/Volo.CmsKit.SourceCode.zip and b/source-code/Volo.CmsKit.SourceCode/Volo.CmsKit.SourceCode.zip differ diff --git a/source-code/Volo.Docs.SourceCode/Volo.Docs.SourceCode.zip b/source-code/Volo.Docs.SourceCode/Volo.Docs.SourceCode.zip index 35cfec78e1..d80581ad13 100644 Binary files a/source-code/Volo.Docs.SourceCode/Volo.Docs.SourceCode.zip and b/source-code/Volo.Docs.SourceCode/Volo.Docs.SourceCode.zip differ diff --git a/templates/app-nolayers/angular/package.json b/templates/app-nolayers/angular/package.json index 6a8c89da4e..cc5bb073f6 100644 --- a/templates/app-nolayers/angular/package.json +++ b/templates/app-nolayers/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.identity": "~10.0.1", - "@abp/ng.oauth": "~10.0.1", - "@abp/ng.setting-management": "~10.0.1", - "@abp/ng.tenant-management": "~10.0.1", - "@abp/ng.theme.lepton-x": "~5.0.1", - "@abp/ng.theme.shared": "~10.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.identity": "~10.1.0-rc.2", + "@abp/ng.oauth": "~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.lepton-x": "~5.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "@angular/animations": "~21.0.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", @@ -36,7 +36,7 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@abp/ng.schematics": "~10.0.1", + "@abp/ng.schematics": "~10.1.0-rc.2", "@angular-eslint/builder": "~21.0.0", "@angular-eslint/eslint-plugin": "~21.0.0", "@angular-eslint/eslint-plugin-template": "~21.0.0", diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj index c8898f2ecd..d5bca102f5 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj @@ -81,7 +81,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/package.json index 5976d69c79..3369e91103 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.0.1", - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.2", + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index bf85ca3338..fe5c55621a 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -82,11 +82,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/package.json index 4d49e5cb83..05b853bd30 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2", + "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj index dd3805cffa..34e9a39044 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj index a8bf31d346..4cfca64cd8 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj @@ -8,7 +8,7 @@ - + @@ -78,7 +78,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj index 06d3ef4d96..d2dcc81851 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj @@ -8,7 +8,7 @@ - + @@ -79,11 +79,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj index ad7126578b..f47e419fd4 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj @@ -29,7 +29,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj index 01791b9243..481266b6ce 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj @@ -73,7 +73,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj index ff10498ccf..c8c8c9b534 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj @@ -74,11 +74,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj index ddd2eba8a4..22ddbecc6c 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj @@ -76,7 +76,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj index fb57cc387a..6eb8bd408e 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj @@ -77,11 +77,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/package.json b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/package.json +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app/angular/package.json b/templates/app/angular/package.json index 57e5e6d457..558852f6da 100644 --- a/templates/app/angular/package.json +++ b/templates/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.identity": "~10.0.1", - "@abp/ng.oauth": "~10.0.1", - "@abp/ng.setting-management": "~10.0.1", - "@abp/ng.tenant-management": "~10.0.1", - "@abp/ng.theme.lepton-x": "~5.0.1", - "@abp/ng.theme.shared": "~10.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.identity": "~10.1.0-rc.2", + "@abp/ng.oauth": "~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.lepton-x": "~5.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "@angular/animations": "~21.0.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", @@ -36,7 +36,7 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@abp/ng.schematics": "~10.0.1", + "@abp/ng.schematics": "~10.1.0-rc.2", "@angular-eslint/builder": "~21.0.0", "@angular-eslint/eslint-plugin": "~21.0.0", "@angular-eslint/eslint-plugin-template": "~21.0.0", diff --git a/templates/app/angular/src/app/home/home.component.html b/templates/app/angular/src/app/home/home.component.html index e3e73e48ae..edcdc66bc0 100644 --- a/templates/app/angular/src/app/home/home.component.html +++ b/templates/app/angular/src/app/home/home.component.html @@ -1,4 +1,5 @@
+
diff --git a/templates/app/angular/src/app/home/home.component.ts b/templates/app/angular/src/app/home/home.component.ts index 420edd4724..ee6bd838c9 100644 --- a/templates/app/angular/src/app/home/home.component.ts +++ b/templates/app/angular/src/app/home/home.component.ts @@ -1,16 +1,89 @@ import {AuthService, LocalizationPipe} from '@abp/ng.core'; import { Component, inject } from '@angular/core'; import {NgTemplateOutlet} from "@angular/common"; +import {DynamicFormComponent, FormFieldConfig} from "@abp/ng.components/dynamic-form"; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], - imports: [NgTemplateOutlet, LocalizationPipe] + imports: [NgTemplateOutlet, LocalizationPipe, DynamicFormComponent] }) export class HomeComponent { private authService = inject(AuthService); + formFields: FormFieldConfig[] = [ + { + key: 'firstName', + type: 'text', + label: 'First Name', + placeholder: 'Enter first name', + value: 'erdemc', + required: true, + validators: [ + { type: 'required', message: 'First name is required' }, + { type: 'minLength', value: 2, message: 'Minimum 2 characters required' } + ], + gridSize: 6, + order: 1 + }, + { + key: 'lastName', + type: 'text', + label: 'Last Name', + placeholder: 'Enter last name', + required: true, + validators: [ + { type: 'required', message: 'Last name is required' } + ], + gridSize: 12, + order: 3 + }, + { + key: 'email', + type: 'email', + label: 'Email Address', + placeholder: 'Enter email', + required: true, + validators: [ + { type: 'required', message: 'Email is required' }, + { type: 'email', message: 'Please enter a valid email' } + ], + gridSize: 6, + order: 2 + }, + { + key: 'userType', + type: 'select', + label: 'User Type', + required: true, + options: [ + { key: 'admin', value: 'Administrator' }, + { key: 'user', value: 'Regular User' }, + { key: 'guest', value: 'Guest User' } + ], + validators: [ + { type: 'required', message: 'Please select user type' } + ], + order: 4 + }, + { + key: 'adminNotes', + type: 'textarea', + label: 'Admin Notes', + placeholder: 'Enter admin-specific notes', + conditionalLogic: [ + { + dependsOn: 'userType', + condition: 'equals', + value: 'admin', + action: 'show' + } + ], + order: 5 + } + ]; + get hasLoggedIn(): boolean { return this.authService.isAuthenticated; } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index 52ce942043..06c7bbfb73 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -41,7 +41,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/package.json index caf290bbd5..f8c0f62c92 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/package.json @@ -3,6 +3,6 @@ "name": "my-app-authserver", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj index f098f3f8bb..051d683e84 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj index d6ecb3f8a5..8dca10c391 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj @@ -18,7 +18,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/package.json index 4d49e5cb83..05b853bd30 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2", + "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index 447561b8d7..40e4ec7305 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -14,7 +14,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/package.json index 4d49e5cb83..05b853bd30 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2", + "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj index 4cf0f1600a..fe1817a967 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj index 84b1706103..bfb297aa01 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj index 27146946a7..6e8d593847 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj @@ -15,12 +15,12 @@ - + - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/package.json index 4d49e5cb83..05b853bd30 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2", + "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj index 3347686ce2..538acc4dff 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj @@ -15,7 +15,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/package.json index 4d49e5cb83..05b853bd30 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2", + "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj index ef1012d89d..b42902d66c 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj index 1388a59d4e..8e30d5120e 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj @@ -22,7 +22,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj index 80397424e3..5bff54da57 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj @@ -26,7 +26,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj index c01bedb15b..3d77bbf91a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index 39f61b8785..558f1085e8 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index fa7d6b10d3..fd67ed8d0b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -19,7 +19,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package.json b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package.json index e63e947d67..ca7fd1e83f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package.json +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.0.1" + "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.2" } } diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj index 792c5b7fec..8d93386afe 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index a900401ddb..491b99fe17 100644 --- a/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index 8095c28eb3..6cbeb43b83 100644 --- a/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -35,7 +35,7 @@ - + diff --git a/templates/module/angular/package.json b/templates/module/angular/package.json index 6e66db9a0d..e87956a956 100644 --- a/templates/module/angular/package.json +++ b/templates/module/angular/package.json @@ -13,15 +13,15 @@ }, "private": true, "dependencies": { - "@abp/ng.account": "~10.0.1", - "@abp/ng.components": "~10.0.1", - "@abp/ng.core": "~10.0.1", - "@abp/ng.identity": "~10.0.1", - "@abp/ng.oauth": "~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.theme.shared": "~10.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.identity": "~10.1.0-rc.2", + "@abp/ng.oauth": "~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.basic": "~10.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "@angular/animations": "~21.0.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", @@ -36,7 +36,7 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@abp/ng.schematics": "~10.0.1", + "@abp/ng.schematics": "~10.1.0-rc.2", "@angular-eslint/builder": "~21.0.0", "@angular-eslint/eslint-plugin": "~21.0.0", "@angular-eslint/eslint-plugin-template": "~21.0.0", diff --git a/templates/module/angular/projects/my-project-name/package.json b/templates/module/angular/projects/my-project-name/package.json index 14730c74d9..5b2413bd3d 100644 --- a/templates/module/angular/projects/my-project-name/package.json +++ b/templates/module/angular/projects/my-project-name/package.json @@ -4,8 +4,8 @@ "peerDependencies": { "@angular/common": "~19.1.0", "@angular/core": "~19.1.0", - "@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.1.0" diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index ff603f750c..6978bafb8b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/package.json b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/package.json index 9f348f690c..214f77a570 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/package.json +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/package.json @@ -3,6 +3,6 @@ "name": "my-app-authserver", "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/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj index a9f5661242..f4911b3d7b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj index 8f18fa029d..2b54b7e2b9 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj index b541e4584a..0d4aec5273 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj @@ -17,7 +17,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/package.json b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/package.json index 325a32edab..11a6338595 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/package.json +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.0.1", - "@abp/aspnetcore.components.server.basictheme": "~10.0.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/aspnetcore.components.server.basictheme": "~10.1.0-rc.2" } } diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index 42176927d9..6413362b0b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index 9c835c4fc6..5511bf8218 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/package.json b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/package.json index a963ff1451..52a1fc6c21 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/package.json +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.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/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj index beccf5a523..8c51a41819 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/package.json b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/package.json index a963ff1451..52a1fc6c21 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/package.json +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/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/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj index 9b51ce4e8e..245a02ab0b 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj @@ -15,7 +15,7 @@ - + diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj index 14a13170b4..73c445396f 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj @@ -22,7 +22,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj index 7cfe198e10..738eb9185a 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj index 2388cf0094..d979841c6a 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj @@ -23,7 +23,7 @@ - + diff --git a/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index 32b11637fb..c578865d23 100644 --- a/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -14,7 +14,7 @@ - +