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 4cb89fd21d..559bdb637e 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -26,7 +26,6 @@ jobs: 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. - reviewers: maliming draft: true token: ${{ github.token }} - name: Merge Pull Request 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 4db15d41ec..36d35793d3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,8 +31,9 @@ + - + @@ -57,63 +58,63 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -122,7 +123,7 @@ - + @@ -141,7 +142,7 @@ - + @@ -169,17 +170,17 @@ - + - - + + - - - + + + @@ -194,6 +195,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/delete-bin-obj.ps1 b/delete-bin-obj.ps1 index 6a6741b767..e4a0fe9437 100644 --- a/delete-bin-obj.ps1 +++ b/delete-bin-obj.ps1 @@ -10,4 +10,3 @@ Get-ChildItem -Path . -Include bin,obj -Recurse -Directory | ForEach-Object { } Write-Host "BIN and OBJ folders have been successfully deleted." -ForegroundColor Green - diff --git a/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md b/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md new file mode 100644 index 0000000000..4890b42174 --- /dev/null +++ b/docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md @@ -0,0 +1,17 @@ +We are thrilled to announce that **ABP.IO will be sponsoring [NDC London 2026](https://ndclondon.com/),** making the start of 2026 a very exciting time for us\! + +NDC London is going to take place from **26th-30th January 2026 at Queen Elizabeth II Center.** This 5-Day event for software developers will have over 90 speakers and 100 sessions. We are excited to be a part of this amazing event once more as devoted supporters of the software development community\! + +## Conference Tracks, Topics, and What Developers Can Expect + +Developers attending **NDC London 2026** can expect five focused tracks packed with practical, real-world sessions. The conference covers the modern development stack, including **.NET, JavaScript, Cloud, DevOps, Security, Testing, UX, Web**, and emerging technologies, delivered by industry experts with actionable insights developers can apply immediately. + +## Discover Previous NDC Events + +We have shared **our takeaways from past NDC events** and other conferences [**here**](https://abp.io/community/events/sponsored#gsc.tab=0). You can check them out to learn what we discovered along the way\! + +## Stop By Our Booth and Say Hello + +We can’t wait to meet fellow developers at NDC London 2026, have meaningful conversations and connect in person. **If you are stopping by our booth, don’t miss our raffle\!** We will be giving away a nice surprise during the event\! + +We are looking forward to meeting you there and sharing a few great days focused on software development. See you there\! diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png new file mode 100644 index 0000000000..0a2b72ab1c Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png new file mode 100644 index 0000000000..ef93db3d48 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md new file mode 100644 index 0000000000..ff613b0aed --- /dev/null +++ b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md @@ -0,0 +1,153 @@ +# Which Open-Source PDF Libraries Are Recently Popular ? A Data-Driven Look At PDF Topic + +So you're looking for a PDF library in .NET, right? Here's the thing - just because something has a million downloads doesn't mean it's what you should use *today*. I'm looking at **recent download momentum** (how many people are actually using it NOW via NuGet) and **GitHub activity** (are they still maintaining this thing or did they abandon it?). + +I pulled data from the last ~90 days for the main players in the .NET PDF space. Here's what's actually happening: + +## Popularity Comparison of .NET PDF Libraries (*ordered by score*) + +| Library | GitHub Stars | Avg Daily NuGet Downloads | Total NuGet Downloads | **Popularity Score** | +|---------|---------------|-----------------------------|----------------------------|---------------------| +| **[Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet)** | [2.9k](https://github.com/microsoft/playwright-dotnet) | [23k](https://www.nuget.org/packages/Microsoft.Playwright) | 39M | **71/100** | +| **[QuestPDF](https://github.com/QuestPDF/QuestPDF)** | [13.7k](https://github.com/QuestPDF/QuestPDF) | [8.2k](https://www.nuget.org/packages/QuestPDF) | 15M | **54/100** | +| **[PDFsharp](https://github.com/empira/PDFsharp)** | [862](https://github.com/empira/PDFsharp) | [9k](https://www.nuget.org/packages/PdfSharp) | 47M | **48/100** | +| **[iText](https://github.com/itext/itext-dotnet)** | [1.9k](https://github.com/itext/itext-dotnet) | [17.2k](https://www.nuget.org/packages/itext) | 16M | **44/100** | +| **[PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)** | [3.8k](https://github.com/hardkoded/puppeteer-sharp) | [8.7k](https://www.nuget.org/packages/PuppeteerSharp) | 26M | **40/100** | + +**How I calculated the score:** I weighted GitHub Stars (30%), Daily Downloads (40% - because that's what matters NOW), and Total Downloads (30% - for historical context). Everything normalized to 0-100 before weighting. Higher = better momentum overall. + +## The Breakdown - What You Actually Need to Know + +### [PDFsharp](https://docs.pdfsharp.net/) + +![pdfsharp](pdfsharp.png) + +**NuGet:** [PdfSharp](https://www.nuget.org/packages/PdfSharp) | **GitHub:** [empira/PDFsharp](https://github.com/empira/PDFsharp) + +**What it does:** Code-first PDF stuff - drawing, manipulating, merging, that kind of thing. Not for HTML/browser rendering though, so don't try to convert your React app to PDF with this. + +**What's the vibe?** **Stable, but kinda old school.** It's got the biggest total download count (47M!) but only pulling ~9k/day now. They updated it 2 weeks ago (Jan 6) so it's alive, and it supports .NET 8-10 which is nice. The GitHub stars (862) are pretty low compared to the shiny new kids, but honestly? It's been around forever and people still use it. It's the reliable old workhorse. + +**Pick this if:** +- You need to build PDFs from scratch with code (not HTML) +- You want to draw graphics, manipulate existing PDFs, merge files +- You don't want browser engines anywhere near your project + +--- + +### [iText](https://itextpdf.com/) + +![iText Logo](itext.jpg) + +**NuGet:** [itext](https://www.nuget.org/packages/itext/) | **GitHub:** [itext/itext-dotnet](https://github.com/itext/itext-dotnet) + +**What it does:** The enterprise beast. Digital signatures, PDF compliance (PDF/A, PDF/UA), forms, all that fancy stuff. Can do HTML-to-PDF too if you need it. + +**What's the vibe?** **Actually doing pretty well!** ~17.2k downloads/day (highest for code-first libs), updated literally yesterday (Jan 18). They're moving fast. 1.9k stars isn't huge but the community seems active. The catch? This is the enterprise option - check the licensing before you commit if you're doing commercial work. + +**Pick this if:** +- You need digital signatures, PDF compliance, or advanced form stuff +- Your company is cool with licensing fees (or you're doing open source) +- You need serious PDF manipulation features +- You want HTML-to-PDF AND code-based generation in one package + +--- + +### [Microsoft.Playwright](https://playwright.dev/dotnet/) + +![Playwright Logo](playwright.png) + +**NuGet:** [Microsoft.Playwright](https://www.nuget.org/packages/Microsoft.Playwright) | **GitHub:** [microsoft/playwright-dotnet](https://github.com/microsoft/playwright-dotnet) + +**What it does:** Browser automation that can turn HTML/CSS/JS into PDFs. Uses real browser engines (Chromium, WebKit, Firefox) so your PDFs look exactly like they would in a browser. + +**What's the vibe?** **Killing it.** ~23k downloads/day (highest in this whole list!). It's Microsoft-backed so you know they're not gonna abandon it anytime soon. Last commit was December 3rd but honestly that's fine, they're actively maintaining. 2.9k stars and climbing. If you need to turn web pages into PDFs, this is probably your best bet right now. + +**Pick this if:** +- You need to convert HTML/CSS/JS to PDF and want it to look EXACTLY like the browser +- You're working with SPAs, dynamic content, or web templates +- You also need browser automation/testing (bonus!) +- Layout accuracy is critical (forms, dashboards, etc.) + +--- + +### [PuppeteerSharp](https://www.puppeteersharp.com/) + +![PuppeteerSharp Logo](PuppeteerSharp.png) + +**NuGet:** [PuppeteerSharp](https://www.nuget.org/packages/PuppeteerSharp) | **GitHub:** [hardkoded/puppeteer-sharp](https://github.com/hardkoded/puppeteer-sharp) + +**What it does:** Basically Playwright's older sibling. Uses headless Chromium to turn HTML into PDFs. Same idea, different API. + +**What's the vibe?** **Stable but losing ground.** Got updated last week (Jan 12) so it's maintained, but ~8.7k/day is way less than Playwright's ~23k. 3.8k stars is decent though. It works fine, but Playwright is eating its lunch. Still, if you know Puppeteer already or only need Chromium, this might be fine. + +**Pick this if:** +- You already know Puppeteer from Node.js and want the same vibe in .NET +- You only need Chromium (don't care about Firefox/WebKit) +- You have existing Puppeteer code you're porting + +--- + + + +### [QuestPDF](https://github.com/QuestPDF/QuestPDF) + +![QuestPDF Logo](QuestPDF.png) + +**NuGet:** [QuestPDF](https://www.nuget.org/packages/QuestPDF) | **GitHub:** [QuestPDF/QuestPDF](https://github.com/QuestPDF/QuestPDF) + +**What it does:** Build PDFs with fluent C# APIs. Think of it like building a UI layout, but for PDFs. No HTML needed - it's all code, all .NET. + +**What's the vibe?** **The community favorite.** 13.7k stars (most by far!), updated yesterday (Jan 18). ~8.2k downloads/day isn't the highest but the community is clearly excited about it. Modern API, active dev, people seem to actually enjoy using it. If you're building reports/invoices from code and want something that feels modern, this is it. + +**Pick this if:** +- You want to build PDFs with code (not HTML) and you like fluent APIs +- You're generating reports, invoices, structured documents +- You want zero browser dependencies +- You care about type safety and maintainable code +- You want something that feels modern and well-designed + + + +## Who's Winning Right Now? + +Here's what the numbers are telling us: + +### Code-First Libraries (Building PDFs with Code) + +**[QuestPDF](https://github.com/QuestPDF/QuestPDF)** - Score: 54/100 +The people's choice. Most GitHub stars (13.7k), updated yesterday, community loves it. Downloads aren't the highest but the engagement is real. This is what people are excited about. + +**[iText](https://github.com/itext/itext-dotnet)** - Score: 44/100 +Actually pulling the most daily downloads (~17.2k/day) for code-first libs, also updated yesterday. The enterprise crowd is still using this heavily. Just watch that licensing. + +**[PDFsharp](https://github.com/empira/PDFsharp)** - Score: 48/100 +The old reliable. 47M total downloads but only ~9k/day now. It works, it's stable, but it's not where the momentum is. Still a solid choice if you need something battle-tested. + +### HTML/Browser-Based Libraries (Turning Web Pages into PDFs) + +**[Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet)** - Score: 71/100 +Winner winner. ~23k downloads/day (highest overall), Microsoft backing, actively maintained. If you need HTML-to-PDF, this is probably the move. + +**[PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)** - Score: 40/100 +Still kicking around at ~8.7k/day but Playwright is clearly the future. Updated last week so it's not dead, just... less popular. + + + +## TL;DR - What Should You Actually Use? + +**Building PDFs from code (not HTML):** +- **QuestPDF** - If you want something modern and the community is raving about it (13.7k stars!) +- **iText** - If you need enterprise features and can handle the licensing +- **PDFsharp** - If you want the battle-tested option that's been around forever + +**Converting HTML/web pages to PDF:** +- **Playwright** - Just use this. It's winning right now (~23k/day), Microsoft-backed, actively maintained. Game over. +- **PuppeteerSharp** - Only if you really need Chromium-only or you're migrating from Node.js Puppeteer + +**Bottom line:** For HTML-to-PDF, Playwright is dominating. For code-first, QuestPDF has the hype but iText has the downloads. Choose your fighter. + +--- + +*Numbers from GitHub and NuGet as of January 19, 2026. Daily downloads are from the last 90 days.* + diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png new file mode 100644 index 0000000000..bd40d5d2d8 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg new file mode 100644 index 0000000000..f0b2df04e2 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png new file mode 100644 index 0000000000..b9f1c0c203 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png differ diff --git a/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png new file mode 100644 index 0000000000..71a50d9e82 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png differ diff --git a/docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/POST.md b/docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/POST.md new file mode 100644 index 0000000000..398cb41f73 --- /dev/null +++ b/docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/POST.md @@ -0,0 +1,167 @@ +# How AI Is Changing Developers + +In the last few years, AI has moved from “nice to have” to “hard to live without” for developers. At first it was just code completion and smart hints. Now it’s getting deep into how we build software: the methods, the toolchain, and even the job itself. + +Here are some structured thoughts on how AI is affecting developers, based on trends and personal experience. + +## Every library will have AI-first docs + +Future libraries and frameworks won’t just have docs for humans. They’ll also have a manual for AI: + +- How to use +- Why it is designed this way +- What NOT to do +- Conventions & Best Practices + +Once these rules are written in a structured way, AI can onboard to a library faster and more consistently than a junior developer. + +Docs won’t just be knowledge anymore. They’ll be instructions AI can execute. + +## AI will be a must-have for developers + +Soon, “writing code without AI” will feel as strange as “writing code without an IDE.” + +- It won’t be about whether you use AI +- It’ll be about how well you use it and where + +AI will become: + +- A standard productivity tool +- An extension of a developer’s thinking +- A second brain + +Developers who don’t use AI will fall behind in both speed and understanding. + +## As AI gets smarter, it replaces “time” + +AI isn’t replacing developers right away. It’s replacing: + +- Lots of repetitive time +- Basic development costs +- Higher output per hour + +Boilerplate, CRUD, basic validation, simple logic — all of that will get swallowed fast. + +It’s not people being replaced. It’s waste. + +## Orchestrating multiple AIs becomes real + +The future isn’t “one AI does everything.” It’s more like: + +- Claude writes core code +- Copilot generates and maintains unit tests +- Codex and similar tools write docs and examples +- Other AIs handle refactoring, performance analysis, security checks + +The dev process itself becomes an AI orchestration system. + +The developer’s role looks more like: + +Architect + conductor + quality gatekeeper + +## Only great infrastructure gets amplified by AI + +Even if AI can teach you “how to use it correctly,” it still can’t invent mature infrastructure for you. + +We still rely on: + +- Stable base frameworks (like [ABP](https://abp.io)) +- Engineering capability proven by many projects +- Long-term maintenance and evolution + +AI is an accelerator, not the foundation. + +For open source, AI is actually a better companion: + +- Helps understand the source code +- Helps learn design thinking +- Helps ship faster + +The stronger the infrastructure, the more value AI can amplify. + +## Frontend feels mature; backend still evolving + +From personal experience: + +- AI is already very strong in frontend work (Bootstrap / UI components, layout, styling, interaction) +- Backend is still learning and improving (business boundaries, architecture trade-offs, implicit constraints) + +This shows: the clearer the rules and the faster the feedback, the faster AI improves. + +## Writing rules for AI is productivity itself + +In the ABP libraries, we’ve already written lots of rules for AI: + +- Conventions +- Usage limits +- Recommended patterns + +As rules grow: + +- AI becomes more stable +- More predictable +- Base development work can be largely automated + +Future engineering skill will be, in large part: how to design a rules system for AI. + +## The real advantage is better feedback loops + +AI gets much stronger when there’s clear feedback: + +- Tests that run fast and fail loudly +- Logs and metrics that explain behavior +- Code review that checks for edge cases and security + +The teams that win are the ones who can quickly verify, correct, and learn. + +## About a developer’s career + +Sometimes I think: I’m glad I didn’t enter the software industry just in the last few years. + +If you’re just starting out, you really feel: + +- The barrier is lower +- The competition is tougher + +But whenever I see AI generate confident but wrong code, I’m reminded: + +- The industry still has a future +- It still needs judgment, taste, and experience + +There will always be people who love coding. If AI does it and we watch, that’s fine too. + +## Chaos everywhere, but the experience is moving fast + +Big companies, platforms, tools: + +- GitHub +- OpenAI +- Claude +- All kinds of IDEs / agents + +New AI tools, apps, and platforms keep popping up. New concepts show up almost every week. It’s noisy, but the big picture is clear: AI keeps getting better, and the overall developer experience is improving fast. + +## Get ready for the AI revolution + +Looking back at personal experience: + +- Before: Google +- Now: ChatGPT +- Before: manual translation +- Now: fully automatic +- Before: writing unit tests by hand +- Now: AI does it all +- Before: human replies to customers +- Now: AI-assisted or even AI-led + +From code completion to agents running tasks, and now deep IDE integration — the pace is shocking. + +## Closing + +AI is not the end of software engineering. It is: + +- A leap in cognition +- A restructure of how work gets done +- An upgrade of roles + +What matters most isn’t how much code AI can write, but how we redefine the value of “developers” in the AI era. diff --git a/docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/image.png b/docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/image.png new file mode 100644 index 0000000000..03a2e61455 Binary files /dev/null and b/docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/image.png differ diff --git a/docs/en/Community-Articles/2026-02-02-ndc-london-article/post.md b/docs/en/Community-Articles/2026-02-02-ndc-london-article/post.md new file mode 100644 index 0000000000..eeab7b79e1 --- /dev/null +++ b/docs/en/Community-Articles/2026-02-02-ndc-london-article/post.md @@ -0,0 +1,50 @@ + +The software development world converged on the **Queen Elizabeth II Centre** in Westminster from **January 26-30** for **NDC London 2026**. As one of the most anticipated tech conferences in Europe, this year’s event delivered a masterclass in the future of the stack. + +We have spent five days immersed in workshops and sessions. Here is our comprehensive recap of the highlights and the technical shifts that will define 2026\. + +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BBjsk292Ejh%2b5X2yeS2pD9uibmq8qxh50b9eOg5U5Ib2jAFaeCHItbTyOpajIeaUzNKg/p0WHohjf1iac2%2bVL6kT/Y3ORSKpRQrdE22QJTwAxBMUryUgTQJ989hYtsvF%2bkReDR03k0gIl4ApUaji6Tg) + +## **1\. High-Performance .NET and C\# Evolution** + +A major focus this year was the continued evolution of the .NET ecosystem. Experts delivered standout sessions on high-performance coding patterns, it’s clear that efficiency and "Native AOT" (Ahead-of-Time compilation) are no longer niche topics, they are becoming industry standards. + +## **1\. Moving Beyond the AI Hype** + +If 2025 was about experimenting with LLMs, NDC London 2026 was about AI integration. Sessions from experts showcased how developers are moving past simple chatbots and integrating AI directly into the CI/CD pipeline and automated testing suites. + +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BDxx%2FqqZ08tgIxCPsAnDDD2w5yJPjVXwUJrbGHpSln3npfpJEBQ78chKoSlZS1cz1nbigNQtRq60dlbyMLwnAgE52tBwUJz481PcBgNtyFMW7rm7oKhFV9c7tK8bEcK%2FscRudaV8w7%2FPO8U5KJv%2BQal) + +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BBdNXgjnu7HIGgX//VJrh3XzjPns4ODHMUhZ%2bDQCcZa2Nc0%2b%2bshyt2UXqaIKEJMPHh6JIDGBtUrdQZ1EzmGn3pingGKiw7YTbh0Z%2bLRZSmcY6pEXkd1S/7VVncmICIHrQgjg%2b7eb2uO28qadIWGbD99) + +## **3\. The "Hallway Track" and Community Networking** + +One of the biggest draws of **NDC London** is the community. Between the 100+ sessions, the exhibitor hall was buzzing with live demos and networking. + +Watch the video: + +[![Watch the Hallway Track video](https://img.youtube.com/vi/yb-FILkqL7U/hqdefault.jpg)](https://www.youtube.com/watch?v=yb-FILkqL7U) + +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BCLbkSK3YZDZZhBGi/IBZOCXgcWHwTyS/s5v6U%2bSeQnY5yCTzMJFTu/mA4xX%2bL5tjbMPfEI8gvCwmVEfSymGFIiJLtAbP8T2zFZev%2bm74sTsQ%2b4sdsLKbdijiae3G%2b45ijWep7yFJx9BWMgV263zzvI) +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BCrCACVWDlDjOgl9ASMeZNMVBGye%2bfya4aO6UW5Kyg9MCVLswzckRWS%2bT71AcQuWMGfiousZlSCrKNAGrosPXzuWAsxnNai3xBcj061TWjGAGX4u1AtrD0eknRxuKe2ba%2bVO7r0sZqle%2bUyZa305hhO) + +## **4\. The Big Giveaway: Our Xbox Series S Raffle** + +One of our favorite moments of the week was our Raffle Session. We love giving back to the community that inspires us, and this year, the energy at our booth was higher than ever. + +We were thrilled to give away a brand-new Xbox Series S to one lucky winner\! It was fantastic to meet so many of you who stopped by to enter, chat about your current projects, and share your thoughts on the future of the industry. + +**Congratulations again to our 2026 winner\!** We hope you enjoy some well-deserved gaming time after a long week of learning. + +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BBozHxXhCL7qMtx5LAxvafvPOKaZJepGlR7tgHVvw6wGpuR4Ervipym%2busZ7eMl3uook15K1874RYEwUenBfoZSJBm33MdaHFduha9iJ7tnfTmW12QbdYM77yqfVJ7EonuJsRrNySdYrQuRI0H2RkZr) + +Watch the video: + +[![Watch the Xbox Series S giveaway](https://img.youtube.com/vi/W5HRwys8dpE/hqdefault.jpg)](https://www.youtube.com/watch?v=W5HRwys8dpE) + + +## **Final Thoughts: See You at NDC London 2027\!** + +NDC London 2026 proved once again why it is a cornerstone event for the global developer community. We are returning to our projects with a refreshed roadmap and a deeper understanding of the tools shaping our industry. + +![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BDJq%2bG7yg1jtoY3gGH8mFMZen%2bncuL%2bKrQHY4/FPOF2KXcLyEjJymhk0JAVwJ76lPeqBchrfsAK3TOUTKY15tC7jm3uwgcH9IWRxCM2ouqxVGqGPd8YIRdG7H7QgyuknBkS4wsdYI9gl1EGqgPtTXJd) diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/0.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/0.png new file mode 100644 index 0000000000..a0cf7c166d Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/0.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/1.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/1.png new file mode 100644 index 0000000000..da57dc3bb9 Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/1.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/2.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/2.png new file mode 100644 index 0000000000..95634840d7 Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/2.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/3.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/3.png new file mode 100644 index 0000000000..398d89e6de Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/3.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4.png new file mode 100644 index 0000000000..d2ba557ec7 Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_1.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_1.png new file mode 100644 index 0000000000..09e3faacb7 Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_1.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_2.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_2.png new file mode 100644 index 0000000000..fd0965bb99 Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_2.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/5.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/5.png new file mode 100644 index 0000000000..70863a30df Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/5.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/Post.md b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/Post.md new file mode 100644 index 0000000000..ec5e423330 --- /dev/null +++ b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/Post.md @@ -0,0 +1,239 @@ +# What NDC London 2026 Looked Like From a Developer’s Perspective + +![](0.png) + +This year we attended NDC London as a sponsor for [ABP](https://abp.io). The conference was held at the same place [Queen Elizabeth II](https://qeiicentre.london/). I guess this is the best conf for .NET developers around the world (thanks to the NDC team). It was 3 full days started from 28 to 30 January 2026. As exhibitor we talked a lot with the attendees who stopped by our booth or while we were eating or in the conf rooms. + +This is the best opportunity to know what everyone is doing in software society. While I was explaining ABP to the people who first time heard, I also ask about what they do in their work. Developers mostly work on web platforms. And as you know there's an AI transformation in our sector. That's why I wonder if other people also stick to the latest AI trend! Well... not as I expected. In Volosoft, we are tightly following AI trends, using in our daily development, injecting this new technology to our product and trying to benefit this as much as possible. + +![1](1.png) + +This new AI trend is same as the invention of printing (by Johannes Gutenberg in 1450) or it's similar to invention of calculators (by William S. Burroughs in 1886). The countries who benefit these inventions got a huge increase in their welfare level. So, we welcome this new AI invention in software development, design, devops and testing. I also see this as a big wave in the ocean, if you are prepared and developed your skills, you can play with it 🌊 and it's called surfing or you'll die in the ocean. But not all the companies react this transformation quickly. Many developers use it like ChatGpt conversation (copy-paste from it) or using GitHub Co-Pilot in a limited manner. But as I heard from Steven Sanderson's session and other Microsoft employees, they are already using it to reproduce the bugs reported in the issues or creating even feature PRs via Co-Pilot. That's a good news for me! + +Here're some pictures from the conf and that's me on the left side with brown shoes :) + +![2](2.png) + +Another thing I see, there's a decrease in the number of attendees'. I don't know the real reason but probably the IT companies cut the budget for conferences. As you also hear, many companies layoff because of the AI replaces some of the positions. + +The food was great during the conference. It was more like eating sessions for me. Lots of good meals from different countries' kitchen. In the second day, there was a party. People grabbed their beers, wines, beverages and made some more networking. + +I was expecting more AI oriented sessions but it was less then my expectations. Even though I was an exhibitor, I tried to attend some of the session. I'll tell you my notes about these. + +## Sessions / Talks + +### The dangers of probably-working software | Damian Brady + +The first session and keynote was from Damian Brady. He's part of Developer Advocacy team at GitHub. And the topic was "The dangers of probably-working software". He started with some negative impact of how generative AI is killing software, and he ended like this a not so bad, we can benefit from the AI transformation. First time I hear "sleepwalking" term for the development. He was telling when we generate code via AI, and we don't review well-enough, we're sleepwalkers. And that's correct! and good analogy for that case. This talk centers on a powerful lesson: *“**Don’t ship code you don’t truly understand.**”* + Damian tells a personal story from his early .NET days when he implemented a **Huffman compression algorithm** based largely on Wikipedia. The code **“worked” in small tests** but **failed in production**. The experience forced him to deeply understand the algorithm rather than relying on copied solutions. Through this story, he explores themes of trust, complexity, testing, and mental models in software engineering. + +#### What I learnt from this session + +- “It seems to work” is not the same as “I understand it.” +- Code copied from Wikipedia or StackOverflow is inherently risky in production. +- Passing tests on small datasets does not guarantee real-world reliability. +- Performance issues often surface only in edge cases. +- Delivery pressure can discourage deep understanding — to the detriment of quality. +- Always ask: “**When does this fail?**” — not just “**Why does this work?**” + +![3](3.png) + +### Playing the long game | Sheena O'Connell + +Sheena is a former software engineer who now trains and supports tech educators. She talks about AI tools... +AI tools are everywhere but poorly understood; there’s hype, risks, and mixed results. The key question is how individuals and organisations should play the long game so skilled human engineers—especially juniors—can still grow and thrive. +She showed some statistics about how job postings on Indeed platform dramatically decreasing for software developers. About AI generated-code, she tells, it's less secure, there might be logical problems or interesting bugs, human might not read code very well and understanding/debugging code might sometimes take much longer time. + +![4](4.png) + + + + + +Being an engineer is about much more than a job title — it requires systems thinking, clear communication, dealing with uncertainty, continuous learning, discipline, and good knowledge management. The job market is shifting: demand for AI-skilled workers is rising quickly and paying premiums, and required skills are changing faster in AI-exposed roles. There’s strength in using a diversity of models instead of locking into one provider, and guardrails improve reliability. + +AI is creating new roles (like AI security, observability, and operations) and new kinds of work, while routine attrition also opens opportunities. At the same time, heavy AI use can have negative cognitive effects: people may think less, feel lonelier, and prefer talking to AI over humans. + +Organizations are becoming more dynamic and project-based, with shorter planning cycles, higher trust, and more experimentation — but also risk of “shiny new toy” syndrome. Research shows AI can boost productivity by 15–20% in many cases, especially in simpler, greenfield projects and popular languages, but it can actually reduce productivity on very complex work. Overall, the recommendation is to focus on using AI well (not just the newest model), add monitoring and guardrails, keep flexibility, and build tools that allow safe experimentation. + +![4_1](4_1.png) + + + +We’re in a messy, fast-moving AI era where LLM tools are everywhere but poorly understood. There’s a lot of hype and marketing noise, making it hard even for technical people to separate reality from fantasy. Different archetypes have emerged — from AI-optimists to skeptics — and both extremes have risks. AI is great for quick prototyping but unreliable for complex work, so teams need guardrails, better practices, and a focus on learning rather than “writing more code faster.” The key question is how individuals and organizations can play the long game so strong human engineers — especially juniors — can still grow and thrive in an AI-driven world. + +![4_2](4_2.png) + +### Crafting Intelligent Agents with Context Engineering | Carly Richmond + +Carly is a Developer Advocate Lead at Elastic in London with deep experience in web development and agile delivery from her years in investment banking. A practical UI engineer. She brings a clear, hands-on perspective to building real-world AI systems. In her talk on **“Crafting Intelligent Agents with Context Engineering,”** she argues that prompt engineering isn’t enough — and shows how carefully shaping context across data, tools, and systems is key to creating reliable, useful AI agents. She mentioned about the context of an AI process. The context consists of Instructions, Short Memory, Long Memory, RAG, User Prompts, Tools, Structured Output. + +![5](5.png) + + + +### Modular Monoliths | Kevlin Henney + +Kevlin frames the “microservices vs monolith” debate as a false dichotomy. His core argument is simple but powerful: problems rarely come from *being a monolith* — they come from being a **poorly structured one**. Modularity is not a deployment choice; it is an architectural discipline. + +## **Notes from the Talk** + +- A monolith is not inherently bad; a tangled monolith is. +- Architecture is mostly about **boundaries**, not boxes. +- If you cannot draw clean internal boundaries, you are not ready for microservices. +- Dependencies reveal your real architecture better than diagrams. +- Teams shape systems more than tools do (a modern reading of Conway’s Law). +- Splitting systems prematurely increases complexity without increasing clarity. +- Good modular design makes systems **easier to change, not just easier to scale**. + +## **Lessons for Developers** + +- Start with a well-structured modular monolith before considering microservices. +- Treat modules as real first-class citizens: clear ownership, clear contracts. +- Make dependency direction explicit — no circular graphs. +- Use internal architectural tests to prevent boundary violations. +- Organize code by *capability*, not by technical layer. +- Optimize for **cognitive load**, not deployment topology. +- If your team structure is messy, your architecture will be messy — fix people, not tech. + +--- + +### AI Coding Agents & Skills | Steve Sanderson + +![steve-sanderson-talk](D:\github\volosoft\abp\docs\en\Community-Articles\2026-02-03-Impressions-of-NDC-London-2026\steve-sanderson-talk.png) + +In this session, Steve started how Microsoft is excessively using AI tools for PRs, reproducing bug reports etc... He says, we use brains and hands less then anytime. And he summarized the AI assisted development into 10 outlines. These are Subagents, Plan Mode, Skills, Delegate, Memories, Hooks, MCP, Infinite Sessions, Plugins and Git Workflow. Let's see his ideas for each of these headings: + +## **1. Subagents** + +- Break big problems into smaller, specialized agents. +- Each subagent should have a clear responsibility and limited scope. +- Parallel work is better than one “smart but slow” agent. +- Reduces hallucination by narrowing context per agent. +- Easier to debug: you can inspect each agent’s output separately. + +------ + +## **2. Plan Mode** + +- Always start with a plan before generating code. +- The plan should be explicit, human-readable, and reviewable. +- Helps align expectations between you and the AI. +- Prevents wasted effort on wrong directions. +- Encourages structured thinking instead of trial-and-error coding. + +------ + +## **3. Skills** + +- Skills are reusable capabilities for AI agents. +- Treat skills like APIs: versioned, documented, and shareable. +- Prefer many small skills over one monolithic skill. +- Store skills in Git, not in chat history. +- Skills should integrate with real tools (CI, GitHub, browsers, etc.). + +------ + +## **4. Delegate** + +- Don’t micromanage — delegate well-defined tasks. +- Give clear inputs, constraints, and success criteria. +- Let the AI own the implementation details. +- Review outcomes instead of every intermediate step. +- Use delegation for repetitive or mechanical work. + +------ + +## **5. Memories** + +- Long-term memory should capture decisions, not chat noise. +- Store *why* something was done, not every detail of *how*. +- Keep memory sparse and structured. +- Treat memory like documentation that evolves over time. +- Be careful about leaking sensitive data into persistent memory. + +------ + +## **6. Hooks** + +- Hooks connect AI actions to your real workflow. +- Examples: pre-commit checks, PR reviews, test triggers. +- Hooks make AI proactive instead of reactive. +- They reduce manual context switching for developers. +- Best hooks are lightweight and predictable. + +------ + +## **7. MCP (Model Context Protocol)** + +- Standard way for models to talk to external tools. +- Enables safe, controlled access to systems (files, APIs, databases). +- Prevents random tool usage; everything is explicit. +- Encourages ecosystem of interoperable tools. +- Critical for production-grade AI assistants. + +------ + +## **8. Infinite Sessions** + +- AI should remember the “project context,” not just the last message. +- Reduces repetition and re-explaining. +- Enables deeper reasoning over time. +- Works best when combined with structured memory. +- Still requires periodic cleanup to avoid context bloat. + +------ + +## **9. Plugins** + +- Extend AI capabilities beyond core model features. +- Plugins should solve real workflow problems, not demos. +- Prefer composable plugins over custom hacks. +- Security matters — don’t give plugins unlimited access. +- Treat plugins like dependencies: review and maintain them. + +------ + +## **10. Git Workflow** + +- AI should operate inside your existing Git process. +- Generate small, focused commits — not giant changes. +- Use AI for PR descriptions and code reviews. +- Keep humans in the loop for design decisions. +- Branching strategy still matters; AI doesn’t replace it. + +**Lessons for Developers from Steve's Talk** + +- Coding agents work best when you treat them like programmable teammates, not autocomplete tools. +- “Skills” are the right abstraction for scaling AI assistants across a team. +- A skill is fundamentally a structured Markdown file + metadata + optional scripts/tools. +- Load **descriptions first, details later** — this keeps LLM context small and reliable. +- Treat skills like shared APIs: version them, review them, and store them in source control. +- Skills can be installed from Git repos (marketplaces), not just created locally. +- Slash commands make skills fast, explicit, and reproducible in daily workflow. +- Use skills to bridge AI ↔ real systems (e.g., GitHub Actions, Playwright, build status). +- Automation skills are most valuable when they handle end-to-end flows (browser + app + data). +- Let the agent *discover* the right skill rather than hard-coding every step. +- Prefer small, composable skills over one “god skill.” +- Skills reduce hallucination risk by constraining what the agent is allowed to do. + +--- + +### My Personal Notes about AI + +- This is your code tech stack for a basic .NET project: + + - Assembly > MSIL > C# > ASP.NET Core > NuGet + NPM > Your Handmade Business Code + + When we ask a development to an AI assisted IDE, AI never starts from Assembly or even it's not writing an existing NPM package. It basically uses what's there on the market. So we know frameworks like ASP.NET Core, ABP will always be there after AI evolution. + +- Software engineer is not just writing correct syntax code to explain a program to computer. As an engineer you need to understand the requirements, design the problem, make proper decisions and fix the uncertainty. Asking AI the right questions is very critical these days. + +- Tesla cars already started to go autonomous. As a driver, you don't need to care about how the car is driven. You need to choose the right way to go in the shortest time without hussle. + +- Nowadays, **developers big new issue is Reviewing the AI generated-code.** In the future, developers who use AI, who inspect AI generated code well and who tells the AI exactly what's needed will be the most important topics. Others (who's typing only code) will be naturally eliminated. Invest your time for these topics. + +- We see that our brain is getting lazier, our coding muscles gets weaker day by day. Just like after calculator invention, we stopped calculate big numbers. We'll eventually forget coding. But maybe that's what it needs to be! + +- Also I don't think AI will replace developers. Think about washing machines. Since they came out, they still need humans to put the clothes in the machine, pick the best program, take out from the machine and iron. From now on, AI is our assistance in every aspect of our life from shopping, medical issues, learning to coding. Let's benefit from it. + diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/cover.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/cover.png new file mode 100644 index 0000000000..1cae98768d Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/cover.png differ diff --git a/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/steve-sanderson-talk.png b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/steve-sanderson-talk.png new file mode 100644 index 0000000000..0fe03dd06b Binary files /dev/null and b/docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/steve-sanderson-talk.png differ 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/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 6edcee78f1..6c13f5d61d 100644 --- a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md +++ b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md @@ -71,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. 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/data-table-column-extensions.md b/docs/en/framework/ui/angular/data-table-column-extensions.md index 0365336d81..81312cdd75 100644 --- a/docs/en/framework/ui/angular/data-table-column-extensions.md +++ b/docs/en/framework/ui/angular/data-table-column-extensions.md @@ -342,4 +342,5 @@ export const identityEntityPropContributors = { ## See Also +- [Extensible Table Row Detail](extensible-table-row-detail.md) - [Customizing Application Modules Guide](../../architecture/modularity/extending/customizing-application-modules-guide.md) diff --git a/docs/en/framework/ui/angular/extensible-table-row-detail.md b/docs/en/framework/ui/angular/extensible-table-row-detail.md new file mode 100644 index 0000000000..0ae24e69e1 --- /dev/null +++ b/docs/en/framework/ui/angular/extensible-table-row-detail.md @@ -0,0 +1,189 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to add expandable row details to data tables using the Extensible Table Row Detail component in ABP Framework Angular UI." +} +``` + +# Extensible Table Row Detail for Angular UI + +## Introduction + +The `` component allows you to add expandable row details to any ``. When users click the expand icon, additional content is revealed below the row. + +Extensible Table Row Detail Example + +## Quick Start + +### Step 1. Import the Component + +Import `ExtensibleTableRowDetailComponent` in your component: + +```typescript +import { + ExtensibleTableComponent, + ExtensibleTableRowDetailComponent +} from '@abp/ng.components/extensible'; + +@Component({ + // ... + imports: [ + ExtensibleTableComponent, + ExtensibleTableRowDetailComponent, + ], +}) +export class MyComponent { } +``` + +### Step 2. Add Row Detail Template + +Place `` inside `` with an `ng-template`: + +```html + + + +
+
{%{{{ row.name }}}%}
+

ID: {%{{{ row.id }}}%}

+

Status: {%{{{ row.isActive ? 'Active' : 'Inactive' }}}%}

+
+
+
+
+``` + +An expand/collapse chevron icon will automatically appear in the first column of each row. + +## API + +### ExtensibleTableRowDetailComponent + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `rowHeight` | `string` | `number` | `'100%'` | Height of the expanded row detail area | + +### Template Context Variables + +| Variable | Type | Description | +|----------|------|-------------| +| `row` | `R` | The current row data object | +| `expanded` | `boolean` | Whether the row is currently expanded | + +## Usage Examples + +### Basic Example + +Display additional information when a row is expanded: + +```html + + + +
+ Details for: {%{{{ row.name }}}%} +
{%{{{ row | json }}}%}
+
+
+
+
+``` + +### With Custom Row Height + +Specify a fixed height for the detail area: + +```html + + +
Fixed 200px height content
+
+
+``` + +### Using Expanded State + +Apply conditional styling based on expansion state: + +```html + + +
+

This row is {%{{{ expanded ? 'expanded' : 'collapsed' }}}%}

+
+
+
+``` + +### With Badges and Localization + +```html + + +
+
+
+

{%{{{ 'MyModule::Name' | abpLocalization }}}%}

+

{%{{{ row.name }}}%}

+
+
+

{%{{{ 'MyModule::Status' | abpLocalization }}}%}

+

+ @if (row.isActive) { + {%{{{ 'AbpUi::Yes' | abpLocalization }}}%} + } @else { + {%{{{ 'AbpUi::No' | abpLocalization }}}%} + } +

+
+
+
+
+
+``` + +## Alternative: Direct Template Input + +For simpler use cases, you can use the `rowDetailTemplate` input on `` directly: + +```html + + + +
{%{{{ row.name }}}%}
+
+``` + +## Events + +### rowDetailToggle + +The `rowDetailToggle` output emits when a row is expanded or collapsed: + +```html + + + ... + + +``` + +```typescript +onRowToggle(row: MyDto) { + console.log('Row toggled:', row); +} +``` + +## See Also + +- [Data Table Column Extensions](data-table-column-extensions.md) +- [Entity Action Extensions](entity-action-extensions.md) +- [Extensions Overview](extensions-overall.md) diff --git a/docs/en/framework/ui/angular/images/row-detail-image.png b/docs/en/framework/ui/angular/images/row-detail-image.png new file mode 100644 index 0000000000..c99ff98ca0 Binary files /dev/null and b/docs/en/framework/ui/angular/images/row-detail-image.png differ diff --git a/docs/en/framework/ui/angular/localization.md b/docs/en/framework/ui/angular/localization.md index 9f3122529c..d50484c83a 100644 --- a/docs/en/framework/ui/angular/localization.md +++ b/docs/en/framework/ui/angular/localization.md @@ -12,7 +12,7 @@ Before exploring _the localization pipe_ and _the localization service_, you sho The localization key format consists of two sections which are **Resource Name** and **Key**. `ResourceName::Key` -> If you do not specify the resource name, the `defaultResourceName` which is declared in `environment.ts` will be considered as default. +> If you do not specify the resource name, the `defaultResourceName` will be used. The value is first retrieved from the backend API response. If the backend does not provide a `defaultResourceName`, the value declared in `environment.ts` will be used as a fallback. ```ts const environment = { 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/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/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/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 7e894027f6..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 @@ -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/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/microservice.md b/docs/en/get-started/microservice.md index 56f275c258..d30e3926fc 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -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) 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/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/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/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/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/kubernetes.md b/docs/en/studio/kubernetes.md index 758a7cf55c..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" } } ```` @@ -210,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 083a0d384f..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`: diff --git a/docs/en/studio/overview.md b/docs/en/studio/overview.md index 228f10afce..256389f79d 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) ### AI Assistant diff --git a/docs/en/studio/running-applications.md b/docs/en/studio/running-applications.md index adec07ad66..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,7 +82,7 @@ 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`. @@ -83,7 +90,7 @@ We can add 4 different item types to *Profile* for defining the tree structure. ![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: @@ -100,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. @@ -108,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*. @@ -124,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. @@ -134,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. @@ -142,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. @@ -154,40 +160,40 @@ 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`, `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 / Stop / Restart +#### Start / Stop / Restart - **Start**: Starts the selected application. - **Stop**: Stops the running application. @@ -195,13 +201,13 @@ The .NET icon indicates that the application is a C# project. After we [add](#c- > 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`, `Health Status`, `Requests`, `Exceptions`, `Events` and `Logs`. @@ -216,11 +222,11 @@ When the C# application is connected to ABP Studio, it starts sending telemetry - `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) - **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. @@ -234,7 +240,7 @@ We can open the *Application Properties* window to change *Launch url*, *Health ![csharp-application-context-menu-run-connection](images/solution-runner/csharp-application-context-menu-run-connection.png) -### Open with +#### Open with The *Open with* submenu provides options to open the application project in external tools: @@ -244,13 +250,17 @@ The *Open with* submenu provides options to open the application project in exte - **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 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. @@ -264,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. @@ -316,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) @@ -326,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 edbac3b471..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" } } 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/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/docs/en/ui-themes/lepton-x-lite/angular.md b/docs/en/ui-themes/lepton-x-lite/angular.md index 1ab5212462..9e18bf994b 100644 --- a/docs/en/ui-themes/lepton-x-lite/angular.md +++ b/docs/en/ui-themes/lepton-x-lite/angular.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Discover how to install and use the LeptonX Lite Angular UI theme for ABP Framework, enhancing your project's professional look effortlessly." + "Description": "Discover how to install and use the LeptonX Lite Angular UI theme for ABP Framework, enhancing your project's professional look effortlessly." } ``` @@ -69,12 +69,12 @@ export const appConfig: ApplicationConfig = { To change the logos and brand color of `LeptonX`, you have two options: -1) Provide logo and application name via the Theme Shared provider (recommended) +1. Provide logo and application name via the Theme Shared provider (recommended) ```ts // app.config.ts -import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared'; -import { environment } from './environments/environment'; +import { provideLogo, withEnvironmentOptions } from "@abp/ng.theme.shared"; +import { environment } from "./environments/environment"; export const appConfig: ApplicationConfig = { providers: [ @@ -91,15 +91,15 @@ Ensure your environment contains the logo url and app name: export const environment = { // ... application: { - name: 'MyProjectName', - logoUrl: '/assets/images/logo.png', + name: "MyProjectName", + logoUrl: "/assets/images/logo.png", }, }; ``` The LeptonX brand component reads these values automatically from `@abp/ng.theme.shared`. -2) Or override via CSS variables in `styles.scss` +2. Or override via CSS variables in `styles.scss` ```css :root { @@ -296,10 +296,11 @@ The Mobile User-Profile component key is `eThemeLeptonXComponents.MobileUserProf ![Angular Footer Component](../../images/angular-footer.png) -The Footer is the section of content at the very bottom of the site. This section of the content can be modified. -Inject **FooterLinksService** and use the **setFooterInfo** method of **FooterLinksService** +The Footer is the section of content at the very bottom of the site. This section of the content can be modified. The ABP Studio templates serve this option by default. You can reach the configurations under `angular/src/app/footer` directory that has a component and a configuration file. + +If you still prefer overriding it by yourself, remove the default configuration. Inject **FooterLinksService** and use the **setFooterInfo** method of **FooterLinksService** to assign path or link and description. -**descUrl** and **footerLinks** are nullable. Constant **footerLinks** are on the right side of footer. +The **descUrl** and **footerLinks** are nullable. Constant **footerLinks** are on the right side of footer. ```js ///... diff --git a/docs/en/ui-themes/lepton-x/angular.md b/docs/en/ui-themes/lepton-x/angular.md index efafbe442d..0d0195d39f 100644 --- a/docs/en/ui-themes/lepton-x/angular.md +++ b/docs/en/ui-themes/lepton-x/angular.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Learn how to integrate the LeptonX Angular UI into your project with step-by-step instructions and essential configuration tips." + "Description": "Learn how to integrate the LeptonX Angular UI into your project with step-by-step instructions and essential configuration tips." } ``` @@ -16,14 +16,12 @@ To add `LeptonX` into your existing projects, follow the steps below. Add theme-specific styles into the `styles` array of the file. Check the [Theme Configurations](../../framework/ui/angular/theme-configurations.md#lepton-x-commercial) documentation for more information. - - At last, remove `provideThemeLepton` from `app.config.ts`, and add the following providers in `app.config.ts` - ```ts -import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x'; -import { provideSideMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts'; -// import { provideThemeLepton } from '@volo/abp.ng.theme.lepton'; +import { provideThemeLeptonX } from "@volosoft/abp.ng.theme.lepton-x"; +import { provideSideMenuLayout } from "@volosoft/abp.ng.theme.lepton-x/layouts"; +// import { provideThemeLepton } from '@volo/abp.ng.theme.lepton'; export const appConfig: ApplicationConfig = { providers: [ @@ -37,14 +35,11 @@ export const appConfig: ApplicationConfig = { If you want to use the **`Top Menu`** instead of the **`Side Menu`**, add `provideTopMenuLayout` as below,and [this style imports](https://docs.abp.io/en/abp/7.4/UI/Angular/Theme-Configurations#lepton-x-commercial) ```ts -import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x'; -import { provideTopMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts'; +import { provideThemeLeptonX } from "@volosoft/abp.ng.theme.lepton-x"; +import { provideTopMenuLayout } from "@volosoft/abp.ng.theme.lepton-x/layouts"; export const appConfig: ApplicationConfig = { - providers: [ - provideTopMenuLayout(), - provideThemeLeptonX(), - ], + providers: [provideTopMenuLayout(), provideThemeLeptonX()], }; ``` @@ -74,6 +69,12 @@ export const appConfig: ApplicationConfig = { If everything is ok, you can remove the `@volo/abp.ng.theme.lepton` in package.json +## Customizing the Footer Section + +You can follow the [component replacement](../../framework/ui/angular/component-replacement.md) documentation to customize the footer part. However, the ABP Studio templates serve this by default. You can reach the footer under `angular/src/app/footer` directory that has a component and a configuration file. + +![angular-footer-files](./images/angular-footer-files.png) + ## Server Side In order to migrate to LeptonX on your server side projects (Host and/or IdentityServer projects), please follow [Server Side Migration](https://docs.abp.io/en/commercial/latest/themes/lepton-x/mvc) document. diff --git a/docs/en/ui-themes/lepton-x/images/angular-footer-files.png b/docs/en/ui-themes/lepton-x/images/angular-footer-files.png new file mode 100644 index 0000000000..384584b788 Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/angular-footer-files.png differ diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs index 247e6e354e..dfe79d7e01 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs @@ -29,5 +29,13 @@ public class ChatClientAccessor : IChatClientAccessor ChatClient = serviceProvider.GetKeyedService( AbpAIWorkspaceOptions.GetChatClientServiceKeyName( WorkspaceNameAttribute.GetWorkspaceName())); + + // Fallback to default chat client if not configured for the workspace. + if (ChatClient is null) + { + ChatClient = serviceProvider.GetKeyedService( + AbpAIWorkspaceOptions.GetChatClientServiceKeyName( + AbpAIModule.DefaultWorkspaceName)); + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs index 72ccfdaf38..9a52f7edc9 100644 --- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs +++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs @@ -9,9 +9,13 @@ public class TypedChatClient : DelegatingChatClient, IChatClient( + serviceProvider.GetKeyedService( AbpAIWorkspaceOptions.GetChatClientServiceKeyName( WorkspaceNameAttribute.GetWorkspaceName())) + ?? + serviceProvider.GetRequiredKeyedService( + AbpAIWorkspaceOptions.GetChatClientServiceKeyName( + AbpAIModule.DefaultWorkspaceName)) ) { } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/AbpBlazorClientHttpMessageHandler.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpBlazorClientHttpMessageHandler.cs similarity index 94% rename from framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/AbpBlazorClientHttpMessageHandler.cs rename to framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpBlazorClientHttpMessageHandler.cs index 5fa70baabe..88855e3fc5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/AbpBlazorClientHttpMessageHandler.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpBlazorClientHttpMessageHandler.cs @@ -4,13 +4,15 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; using Volo.Abp.AspNetCore.Components.Progression; +using Volo.Abp.AspNetCore.Components.Web; using Volo.Abp.DependencyInjection; using Volo.Abp.Timing; -namespace Volo.Abp.AspNetCore.Components.Web; +namespace Volo.Abp.AspNetCore.Components.WebAssembly; public class AbpBlazorClientHttpMessageHandler : DelegatingHandler, ITransientDependency { @@ -51,6 +53,7 @@ public class AbpBlazorClientHttpMessageHandler : DelegatingHandler, ITransientDe options.Type = UiPageProgressType.Info; }); + request.SetBrowserRequestStreamingEnabled(true); await SetLanguageAsync(request, cancellationToken); await SetAntiForgeryTokenAsync(request); await SetTimeZoneAsync(request); 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.Auditing/Volo/Abp/Auditing/AuditingDisabledState.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingDisabledState.cs new file mode 100644 index 0000000000..c4a2384316 --- /dev/null +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingDisabledState.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.Auditing; + +public class AuditingDisabledState +{ + public bool IsDisabled { get; private set; } + + public AuditingDisabledState(bool isDisabled) + { + IsDisabled = isDisabled; + } +} diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs index aadbeccabb..028096d0ac 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingHelper.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Options; using Volo.Abp.Clients; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; using Volo.Abp.Timing; using Volo.Abp.Tracing; using Volo.Abp.Users; @@ -26,6 +27,7 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency protected IAuditSerializer AuditSerializer; protected IServiceProvider ServiceProvider; protected ICorrelationIdProvider CorrelationIdProvider { get; } + protected IAmbientScopeProvider AuditingDisabledState { get; } public AuditingHelper( IAuditSerializer auditSerializer, @@ -37,7 +39,8 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency IAuditingStore auditingStore, ILogger logger, IServiceProvider serviceProvider, - ICorrelationIdProvider correlationIdProvider) + ICorrelationIdProvider correlationIdProvider, + IAmbientScopeProvider auditingDisabledState) { Options = options.Value; AuditSerializer = auditSerializer; @@ -50,6 +53,7 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency Logger = logger; ServiceProvider = serviceProvider; CorrelationIdProvider = correlationIdProvider; + AuditingDisabledState = auditingDisabledState; } public virtual bool ShouldSaveAudit(MethodInfo? methodInfo, bool defaultValue = false, bool ignoreIntegrationServiceAttribute = false) @@ -64,6 +68,11 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency return false; } + if (!IsAuditingEnabled()) + { + return false; + } + if (methodInfo.IsDefined(typeof(AuditedAttribute), true)) { return true; @@ -178,6 +187,19 @@ public class AuditingHelper : IAuditingHelper, ITransientDependency return actionInfo; } + private const string AuditingDisabledScopeKey = "Volo.Abp.Auditing.DisabledScope"; + + public virtual IDisposable DisableAuditing() + { + return AuditingDisabledState.BeginScope(AuditingDisabledScopeKey, new AuditingDisabledState(true)); + } + + public virtual bool IsAuditingEnabled() + { + var state = AuditingDisabledState.GetValue(AuditingDisabledScopeKey); + return state == null || !state.IsDisabled; + } + protected virtual void ExecutePreContributors(AuditLogInfo auditLogInfo) { using (var scope = ServiceProvider.CreateScope()) diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs index e6fa20bc90..f0a36da5b4 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingHelper.cs @@ -26,4 +26,23 @@ public interface IAuditingHelper MethodInfo method, IDictionary arguments ); + + /// + /// Creates a scope in which auditing is temporarily disabled. + /// + /// + /// An that restores the previous auditing state + /// when disposed. This method supports nested scopes; disposing a scope + /// restores the auditing state that was active before that scope was created. + /// + IDisposable DisableAuditing(); + + /// + /// Determines whether auditing is currently enabled. + /// + /// + /// true if auditing is enabled in the current context; otherwise, false. + /// This reflects any active scopes created by . + /// + bool IsAuditingEnabled(); } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs index 65b7e1b390..159b72ad66 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs @@ -47,6 +47,7 @@ public class AbpAuthorizationModule : AbpModule options.ResourceValueProviders.Add(); options.ResourceValueProviders.Add(); + options.ResourceValueProviders.Add(); }); Configure(options => diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs index 13c7981063..d3ac6870d3 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/ClientPermissionValueProvider.cs @@ -44,7 +44,7 @@ public class ClientPermissionValueProvider : PermissionValueProvider var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value; if (clientId == null) { - return new MultiplePermissionGrantResult(permissionNames); ; + return new MultiplePermissionGrantResult(permissionNames); } using (CurrentTenant.Change(null)) diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs new file mode 100644 index 0000000000..2a73292528 --- /dev/null +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ClientResourcePermissionValueProvider.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Security.Claims; + +namespace Volo.Abp.Authorization.Permissions.Resources; + +public class ClientResourcePermissionValueProvider : ResourcePermissionValueProvider +{ + public const string ProviderName = "C"; + + public override string Name => ProviderName; + + protected ICurrentTenant CurrentTenant { get; } + + public ClientResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore, ICurrentTenant currentTenant) + : base(resourcePermissionStore) + { + CurrentTenant = currentTenant; + } + + public override async Task CheckAsync(ResourcePermissionValueCheckContext context) + { + var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value; + + if (clientId == null) + { + return PermissionGrantResult.Undefined; + } + + using (CurrentTenant.Change(null)) + { + return await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, clientId) + ? PermissionGrantResult.Granted + : PermissionGrantResult.Undefined; + } + } + + public override async Task CheckAsync(ResourcePermissionValuesCheckContext context) + { + var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToArray(); + Check.NotNullOrEmpty(permissionNames, nameof(permissionNames)); + + var clientId = context.Principal?.FindFirst(AbpClaimTypes.ClientId)?.Value; + if (clientId == null) + { + return new MultiplePermissionGrantResult(permissionNames); + } + + using (CurrentTenant.Change(null)) + { + return await ResourcePermissionStore.IsGrantedAsync(permissionNames, context.ResourceName, context.ResourceKey, Name, clientId); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs index bdc28c7ca0..1e2280b78a 100644 --- a/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs +++ b/framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs @@ -12,6 +12,7 @@ using RabbitMQ.Client.Events; using Volo.Abp.ExceptionHandling; using Volo.Abp.RabbitMQ; using Volo.Abp.Threading; +using Volo.Abp.Tracing; namespace Volo.Abp.BackgroundJobs.RabbitMQ; @@ -33,6 +34,7 @@ public class JobQueue : IJobQueue protected IBackgroundJobExecuter JobExecuter { get; } protected IServiceScopeFactory ServiceScopeFactory { get; } protected IExceptionNotifier ExceptionNotifier { get; } + protected ICorrelationIdProvider CorrelationIdProvider { get; } protected SemaphoreSlim SyncObj = new SemaphoreSlim(1, 1); protected bool IsDiposed { get; private set; } @@ -44,7 +46,8 @@ public class JobQueue : IJobQueue IRabbitMqSerializer serializer, IBackgroundJobExecuter jobExecuter, IServiceScopeFactory serviceScopeFactory, - IExceptionNotifier exceptionNotifier) + IExceptionNotifier exceptionNotifier, + ICorrelationIdProvider correlationIdProvider) { AbpBackgroundJobOptions = backgroundJobOptions.Value; AbpRabbitMqBackgroundJobOptions = rabbitMqAbpBackgroundJobOptions.Value; @@ -52,6 +55,7 @@ public class JobQueue : IJobQueue JobExecuter = jobExecuter; ServiceScopeFactory = serviceScopeFactory; ExceptionNotifier = exceptionNotifier; + CorrelationIdProvider = correlationIdProvider; ChannelPool = channelPool; JobConfiguration = AbpBackgroundJobOptions.GetJob(typeof(TArgs)); @@ -168,7 +172,8 @@ public class JobQueue : IJobQueue var routingKey = QueueConfiguration.QueueName; var basicProperties = new BasicProperties { - Persistent = true + Persistent = true, + CorrelationId = CorrelationIdProvider.Get() }; if (delay.HasValue) @@ -201,7 +206,10 @@ public class JobQueue : IJobQueue try { - await JobExecuter.ExecuteAsync(context); + using (CorrelationIdProvider.Change(ea.BasicProperties.CorrelationId)) + { + await JobExecuter.ExecuteAsync(context); + } await ChannelAccessor!.Channel.BasicAckAsync(deliveryTag: ea.DeliveryTag, multiple: false); } catch (BackgroundJobExecutionException) 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/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.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs index 81d2993c4f..c35406b45b 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs @@ -20,7 +20,7 @@ public static class EntityCacheServiceCollectionExtensions services.Configure(options => { - options.ConfigureCache(cacheOptions ?? GetDefaultCacheOptions()); + options.ConfigureCache>(cacheOptions ?? GetDefaultCacheOptions()); }); services.Configure(options => @@ -42,7 +42,7 @@ public static class EntityCacheServiceCollectionExtensions services.Configure(options => { - options.ConfigureCache(cacheOptions ?? GetDefaultCacheOptions()); + options.ConfigureCache>(cacheOptions ?? GetDefaultCacheOptions()); }); return services; @@ -59,7 +59,7 @@ public static class EntityCacheServiceCollectionExtensions services.Configure(options => { - options.ConfigureCache(cacheOptions ?? GetDefaultCacheOptions()); + options.ConfigureCache>(cacheOptions ?? GetDefaultCacheOptions()); }); return services; 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.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 6a5b608c76..07ea443fdf 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -310,7 +310,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); } } - else if (entityEntry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + else if (GetAllPropertyEntries(entityEntry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { if (IsOnlyForeignKeysModified(entityEntry)) { @@ -446,7 +446,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; case EntityState.Modified: - if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + if (GetAllPropertyEntries(entry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { if (IsOnlyForeignKeysModified(entry)) { @@ -454,7 +454,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; } - var modifiedProperties = entry.Properties.Where(x => x.IsModified).ToList(); + var modifiedProperties = GetAllPropertyEntries(entry).Where(x => x.IsModified).ToList(); var disableAuditingAttributes = modifiedProperties.Select(x => x.Metadata.PropertyInfo?.GetCustomAttribute()).ToList(); if (disableAuditingAttributes.Any(x => x == null || x.UpdateModificationProps)) { @@ -501,9 +501,36 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } + protected virtual IEnumerable GetAllPropertyEntries(EntityEntry entry) + { + return entry.Properties.Concat(GetAllComplexPropertyEntries(entry.ComplexProperties)); + } + + protected virtual IEnumerable GetAllComplexPropertyEntries(IEnumerable complexPropertyEntries) + { + foreach (var complexPropertyEntry in complexPropertyEntries) + { + var complexPropertyInfo = complexPropertyEntry.Metadata.PropertyInfo; + if (complexPropertyInfo != null && complexPropertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + continue; + } + + foreach (var propertyEntry in complexPropertyEntry.Properties) + { + yield return propertyEntry; + } + + foreach (var nestedPropertyEntry in GetAllComplexPropertyEntries(complexPropertyEntry.ComplexProperties)) + { + yield return nestedPropertyEntry; + } + } + } + protected virtual bool IsOnlyForeignKeysModified(EntityEntry entry) { - return entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && + return GetAllPropertyEntries(entry).Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString())); } @@ -662,7 +689,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, bool forceApply = false) { if (forceApply || - entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + GetAllPropertyEntries(entry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { IncrementEntityVersionProperty(entry); SetModificationAuditProperties(entry); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index 75e82976b4..9a1e8e3cdf 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -184,6 +184,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency var properties = entityEntry.Metadata.GetProperties(); var isCreated = IsCreated(entityEntry); var isDeleted = IsDeleted(entityEntry); + var isSoftDeleted = IsSoftDeleted(entityEntry); foreach (var property in properties) { @@ -193,7 +194,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } var propertyEntry = entityEntry.Property(property.Name); - if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry)) + if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !isSoftDeleted) { var propertyType = DeterminePropertyTypeFromEntry(property, propertyEntry); @@ -207,6 +208,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } + foreach (var complexPropertyEntry in entityEntry.ComplexProperties) + { + AddComplexPropertyChanges( + complexPropertyEntry, + propertyChanges, + isCreated, + isDeleted, + isSoftDeleted, + parentPath: null); + } + if (AbpEfCoreNavigationHelper == null) { return propertyChanges; @@ -250,6 +262,52 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + protected virtual void AddComplexPropertyChanges( + ComplexPropertyEntry complexPropertyEntry, + List propertyChanges, + bool isCreated, + bool isDeleted, + bool isSoftDeleted, + string? parentPath) + { + var complexPropertyInfo = complexPropertyEntry.Metadata.PropertyInfo; + if (complexPropertyInfo != null && complexPropertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + return; + } + + var complexPropertyPath = parentPath == null + ? complexPropertyEntry.Metadata.Name + : $"{parentPath}.{complexPropertyEntry.Metadata.Name}"; + + foreach (var propertyEntry in complexPropertyEntry.Properties) + { + if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !isSoftDeleted) + { + var propertyType = DeterminePropertyTypeFromEntry(propertyEntry.Metadata, propertyEntry); + + propertyChanges.Add(new EntityPropertyChangeInfo + { + NewValue = isDeleted ? null : JsonSerializer.Serialize(propertyEntry.CurrentValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), + OriginalValue = isCreated ? null : JsonSerializer.Serialize(propertyEntry.OriginalValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), + PropertyName = $"{complexPropertyPath}.{propertyEntry.Metadata.Name}", + PropertyTypeFullName = propertyType.FullName! + }); + } + } + + foreach (var nestedComplexPropertyEntry in complexPropertyEntry.ComplexProperties) + { + AddComplexPropertyChanges( + nestedComplexPropertyEntry, + propertyChanges, + isCreated, + isDeleted, + isSoftDeleted, + complexPropertyPath); + } + } + /// /// Determines the CLR type of a property based on its EF Core metadata and the values in the given . /// @@ -262,7 +320,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency /// . If both values are null, the declared CLR type /// (which may remain ) is returned. /// - protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) + protected virtual Type DeterminePropertyTypeFromEntry(IReadOnlyPropertyBase property, PropertyEntry propertyEntry) { var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); 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.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs index 800b73544d..1ab79125e6 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs @@ -61,6 +61,7 @@ public class AbpAuditingTestModule : AbpModule ); options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty))); + options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithComplexProperty), type => type == typeof(AppEntityWithComplexProperty))); }); context.Services.AddType(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs new file mode 100644 index 0000000000..be399318cd --- /dev/null +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs @@ -0,0 +1,37 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Volo.Abp.Auditing.App.Entities; + +public class AppEntityWithComplexProperty : FullAuditedAggregateRoot +{ + public string Name { get; set; } + + public AppEntityContactInformation ContactInformation { get; set; } + + [DisableAuditing] + public AppEntityContactInformation DisabledContactInformation { get; set; } + + public AppEntityWithComplexProperty() + { + } + + public AppEntityWithComplexProperty(Guid id, string name) + : base(id) + { + Name = name; + } +} + +public class AppEntityContactInformation +{ + public string Street { get; set; } = string.Empty; + + public AppEntityContactLocation Location { get; set; } = new(); +} + +public class AppEntityContactLocation +{ + public string City { get; set; } = string.Empty; +} diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs index e8950880d7..a4d0564e90 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs @@ -31,6 +31,7 @@ public class AbpAuditingTestDbContext : AbpDbContext public DbSet AppEntityWithNavigationChildOneToMany { get; set; } public DbSet AppEntityWithNavigationsAndDisableAuditing { get; set; } public DbSet EntitiesWithObjectProperty { get; set; } + public DbSet AppEntitiesWithComplexProperty { get; set; } public AbpAuditingTestDbContext(DbContextOptions options) : base(options) @@ -77,5 +78,27 @@ public class AbpAuditingTestDbContext : AbpDbContext ); }); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.ComplexProperty(x => x.ContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, lb => + { + lb.Property(x => x.City).IsRequired(); + }); + }); + + b.ComplexProperty(x => x.DisabledContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, lb => + { + lb.Property(x => x.City).IsRequired(); + }); + }); + }); } } diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs new file mode 100644 index 0000000000..f1f82a4459 --- /dev/null +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingHelper_Tests.cs @@ -0,0 +1,139 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NSubstitute; +using Volo.Abp.DependencyInjection; +using Xunit; + +namespace Volo.Abp.Auditing; + +public class AuditingHelper_Tests : AbpAuditingTestBase +{ + private readonly IAuditingHelper _auditingHelper; + protected IAuditingStore AuditingStore; + + public AuditingHelper_Tests() + { + _auditingHelper = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + AuditingStore = Substitute.For(); + services.Replace(ServiceDescriptor.Singleton(AuditingStore)); + } + + [Fact] + public async Task Should_Write_AuditLog_Without_DisableAuditing() + { + var myAuditedObject = GetRequiredService(); + + await myAuditedObject.DoItAsync(); + + await AuditingStore.Received().SaveAsync(Arg.Any()); + } + + [Fact] + public async Task Should_Not_Write_AuditLog_With_DisableAuditing() + { + var myAuditedObject = GetRequiredService(); + + using (_auditingHelper.DisableAuditing()) + { + await myAuditedObject.DoItAsync(); + } + + await AuditingStore.DidNotReceive().SaveAsync(Arg.Any()); + } + + [Fact] + public async Task Should_Write_AuditLog_After_DisableAuditing_Scope_Disposed() + { + var myAuditedObject = GetRequiredService(); + + using (_auditingHelper.DisableAuditing()) + { + await myAuditedObject.DoItAsync(); + } + + AuditingStore.ClearReceivedCalls(); + + await myAuditedObject.DoItAsync(); + + await AuditingStore.Received().SaveAsync(Arg.Any()); + } + + [Fact] + public async Task Should_Not_Write_AuditLog_With_Nested_DisableAuditing() + { + var myAuditedObject = GetRequiredService(); + + using (_auditingHelper.DisableAuditing()) + { + await myAuditedObject.DoItAsync(); + + using (_auditingHelper.DisableAuditing()) + { + await myAuditedObject.DoItAsync(); + } + + await myAuditedObject.DoItAsync(); + } + + await AuditingStore.DidNotReceive().SaveAsync(Arg.Any()); + } + + [Fact] + public void Should_Return_True_When_Auditing_Is_Enabled() + { + Assert.True(_auditingHelper.IsAuditingEnabled()); + } + + [Fact] + public void Should_Return_False_When_Auditing_Is_Disabled() + { + using (_auditingHelper.DisableAuditing()) + { + Assert.False(_auditingHelper.IsAuditingEnabled()); + } + } + + [Fact] + public void Should_Return_True_After_DisableAuditing_Scope_Disposed() + { + using (_auditingHelper.DisableAuditing()) + { + Assert.False(_auditingHelper.IsAuditingEnabled()); + } + + Assert.True(_auditingHelper.IsAuditingEnabled()); + } + + [Fact] + public void Should_Return_False_With_Nested_DisableAuditing() + { + using (_auditingHelper.DisableAuditing()) + { + Assert.False(_auditingHelper.IsAuditingEnabled()); + + using (_auditingHelper.DisableAuditing()) + { + Assert.False(_auditingHelper.IsAuditingEnabled()); + } + + Assert.False(_auditingHelper.IsAuditingEnabled()); + } + } + + public interface IMyAuditedObject : ITransientDependency, IAuditingEnabled + { + } + + public class MyAuditedObject : IMyAuditedObject + { + public virtual Task DoItAsync() + { + return Task.CompletedTask; + } + } +} diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs index 637b9b4d97..d9667f23e2 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; +using Shouldly; using Volo.Abp.Auditing.App.Entities; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; @@ -820,6 +821,167 @@ public class Auditing_Tests : AbpAuditingTestBase AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 } + + [Fact] + public async Task Should_Write_AuditLog_For_Complex_Property_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = new AppEntityWithComplexProperty(entityId, "Test Entity") + { + ContactInformation = new AppEntityContactInformation + { + Street = "First Street", + Location = new AppEntityContactLocation + { + City = "First City" + } + }, + DisabledContactInformation = new AppEntityContactInformation + { + Street = "Disabled Street", + Location = new AppEntityContactLocation + { + City = "Disabled City" + } + } + }; + + await repository.InsertAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithComplexProperty).FullName && + x.EntityChanges[0].PropertyChanges.Count == 3 && + x.EntityChanges[0].PropertyChanges.Any(pc => + pc.PropertyName == nameof(AppEntityWithComplexProperty.Name) && + pc.OriginalValue == null && + pc.NewValue == "\"Test Entity\"" && + pc.PropertyTypeFullName == typeof(string).FullName) && + x.EntityChanges[0].PropertyChanges.Any(pc => + pc.PropertyName == "ContactInformation.Street" && + pc.OriginalValue == null && + pc.NewValue == "\"First Street\"" && + pc.PropertyTypeFullName == typeof(string).FullName) && + x.EntityChanges[0].PropertyChanges.Any(pc => + pc.PropertyName == "ContactInformation.Location.City" && + pc.OriginalValue == null && + pc.NewValue == "\"First City\"" && + pc.PropertyTypeFullName == typeof(string).FullName) && + x.EntityChanges[0].PropertyChanges.All(pc => + !pc.PropertyName.StartsWith(nameof(AppEntityWithComplexProperty.DisabledContactInformation))))); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.ContactInformation.Location.City = "Updated City"; + entity.DisabledContactInformation.Street = "Updated Disabled Street"; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithComplexProperty).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == "ContactInformation.Location.City" && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"First City\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Updated City\"" && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + } + + [Fact] + public async Task Should_Not_Update_Modification_Audit_Properties_When_Only_Disabled_Complex_Property_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = new AppEntityWithComplexProperty(entityId, "Test Entity") + { + ContactInformation = new AppEntityContactInformation + { + Street = "First Street", + Location = new AppEntityContactLocation + { + City = "First City" + } + }, + DisabledContactInformation = new AppEntityContactInformation + { + Street = "Disabled Street", + Location = new AppEntityContactLocation + { + City = "Disabled City" + } + } + }; + + await repository.InsertAsync(entity); + + await uow.CompleteAsync(); + } + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.Name = "Updated Test Entity"; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + } + + DateTime? lastModificationTime; + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + lastModificationTime = entity.LastModificationTime; + lastModificationTime.ShouldNotBeNull(); + await uow.CompleteAsync(); + } + + await Task.Delay(10); + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.DisabledContactInformation.Street = "Updated Disabled Street"; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + } + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.LastModificationTime.ShouldBe(lastModificationTime); + await uow.CompleteAsync(); + } + } } public class Auditing_DisableLogActionInfo_Tests : Auditing_Tests 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.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs index a54b1656bf..3fe863c046 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs @@ -6,6 +6,7 @@ using NSubstitute; using Shouldly; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.TestApp; +using Volo.Abp.TestApp.Domain; using Volo.Abp.TestApp.Testing; using Xunit; @@ -85,6 +86,33 @@ public class Auditing_Tests : Auditing_Tests })); } + [Fact] + public async Task Should_Set_Modification_If_Complex_Properties_Changed() + { + var city = Guid.NewGuid().ToString(); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.City = city; + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.ContactInformation.ShouldNotBeNull(); + douglas.ContactInformation!.Location.City.ShouldBe(city); + douglas.LastModificationTime.ShouldNotBeNull(); + douglas.LastModificationTime.Value.ShouldBeLessThanOrEqualTo(Clock.Now); + douglas.LastModifierId.ShouldBe(CurrentUserId); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + [Fact] public async Task Should_Not_Set_Modification_If_Properties_HasDisableAuditing_UpdateModificationProps() { @@ -106,6 +134,50 @@ public class Auditing_Tests : Auditing_Tests EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); } + [Fact] + public async Task Should_Not_Set_Modification_If_ComplexProperties_HasDisableAuditing_UpdateModificationProps() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.DisableAuditingUpdateModificationPropsProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldBeNull(); + douglas.LastModifierId.ShouldBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + + [Fact] + public async Task Should_Not_Set_Modification_If_Nested_ComplexProperties_HasDisableAuditing_UpdateModificationProps() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.DisableAuditingUpdateModificationPropsProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldBeNull(); + douglas.LastModifierId.ShouldBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + [Fact] public async Task Should_Not_PublishEntityEvent_If_Properties_HasDisableAuditing_PublishEntityEventProperty() { @@ -126,6 +198,48 @@ public class Auditing_Tests : Auditing_Tests EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any()); } + [Fact] + public async Task Should_Not_PublishEntityEvent_If_ComplexProperties_HasDisableAuditing_PublishEntityEventProperty() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.DisableAuditingPublishEntityEventProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any()); + } + + [Fact] + public async Task Should_Not_PublishEntityEvent_If_Nested_ComplexProperties_HasDisableAuditing_PublishEntityEventProperty() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.DisableAuditingPublishEntityEventProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any()); + } + [Fact] public async Task Should_Set_Modification_And_PublishEntityEvent_If_Properties_HasDisableAuditing() @@ -146,4 +260,46 @@ public class Auditing_Tests : Auditing_Tests EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); } + + [Fact] + public async Task Should_Set_Modification_And_PublishEntityEvent_If_ComplexProperties_HasDisableAuditing() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.DisableAuditingProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + + [Fact] + public async Task Should_Set_Modification_And_PublishEntityEvent_If_Nested_ComplexProperties_HasDisableAuditing() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.DisableAuditingProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 4c6efa5a02..96f4b1a2e1 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -78,6 +78,14 @@ public class TestMigrationsDbContext : AbpDbContext b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now); b.Property(x => x.TenantId).HasColumnName("Tenant_Id"); b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted"); + b.ComplexProperty(x => x.ContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, locationBuilder => + { + locationBuilder.Property(x => x.City).IsRequired(); + }); + }); }); modelBuilder.Entity(b => diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index a8e68dfca8..2b95877bed 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -92,6 +92,14 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now); b.Property(x => x.TenantId).HasColumnName("Tenant_Id"); b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted"); + b.ComplexProperty(x => x.ContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, locationBuilder => + { + locationBuilder.Property(x => x.City).IsRequired(); + }); + }); }); modelBuilder diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index c05d0013d3..9da2357d70 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs @@ -32,6 +32,8 @@ public class Person : FullAuditedAggregateRoot, IMultiTenant, IHasEntityVe public virtual DateTime HasDefaultValue { get; set; } + public virtual PersonContactInformation? ContactInformation { get; set; } + public int EntityVersion { get; set; } [DisableAuditing(UpdateModificationProps = false)] @@ -84,3 +86,33 @@ public class Person : FullAuditedAggregateRoot, IMultiTenant, IHasEntityVe ); } } + +public class PersonContactInformation +{ + public string Street { get; set; } = string.Empty; + + public PersonContactLocation Location { get; set; } = new(); + + [DisableAuditing(UpdateModificationProps = false)] + public string? DisableAuditingUpdateModificationPropsProperty { get; set; } + + [DisableAuditing(PublishEntityEvent = false)] + public string? DisableAuditingPublishEntityEventProperty { get; set; } + + [DisableAuditing] + public string? DisableAuditingProperty { get; set; } +} + +public class PersonContactLocation +{ + public string City { get; set; } = string.Empty; + + [DisableAuditing(UpdateModificationProps = false)] + public string? DisableAuditingUpdateModificationPropsProperty { get; set; } + + [DisableAuditing(PublishEntityEvent = false)] + public string? DisableAuditingPublishEntityEventProperty { get; set; } + + [DisableAuditing] + public string? DisableAuditingProperty { get; set; } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs index f30774838e..438bf4b3b9 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs @@ -30,8 +30,15 @@ public class TestAppModule : AbpModule context.Services.AddHttpContextAccessor(); context.Services.Replace(ServiceDescriptor.Singleton()); - context.Services.AddEntityCache(); - context.Services.AddEntityCache(); + context.Services.AddEntityCache(new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(7) + }); + context.Services.AddEntityCache(new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(9) + }); + context.Services.AddEntityCache(); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs index de2e95895c..d46762615c 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs @@ -76,6 +76,10 @@ public class TestDataBuilder : ITransientDependency private async Task AddPeople() { var douglas = new Person(UserDouglasId, "Douglas", 42, cityId: LondonCityId); + douglas.ContactInformation = new PersonContactInformation + { + Street = "Test Street" + }; douglas.Phones.Add(new Phone(douglas.Id, "123456789")); douglas.Phones.Add(new Phone(douglas.Id, "123456780", PhoneType.Home)); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs index 5e321736d4..6ef8da9b7c 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs @@ -1,5 +1,7 @@ using System; +using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; using Shouldly; using Volo.Abp.Caching; using Volo.Abp.Domain.Entities; @@ -116,6 +118,49 @@ public abstract class EntityCache_Tests : TestAppTestBase, Guid>>(); + + var productOptions = GetDefaultCachingOptions(productCache); + productOptions.AbsoluteExpirationRelativeToNow.ShouldBe(TimeSpan.FromMinutes(2)); + productOptions.SlidingExpiration.ShouldBeNull(); + } + + [Fact] + public void EntityCache_Configured_Options_Should_Be_Applied() + { + var productCache = GetRequiredService, Guid>>(); + var productCacheItemCache = GetRequiredService, Guid>>(); + + var productOptions = GetDefaultCachingOptions(productCache); + productOptions.AbsoluteExpirationRelativeToNow.ShouldBe(TimeSpan.FromMinutes(7)); + productOptions.SlidingExpiration.ShouldBeNull(); + + var productCacheItemOptions = GetDefaultCachingOptions(productCacheItemCache); + productCacheItemOptions.AbsoluteExpirationRelativeToNow.ShouldBe(TimeSpan.FromMinutes(9)); + productCacheItemOptions.SlidingExpiration.ShouldBeNull(); + } + + private static DistributedCacheEntryOptions GetDefaultCachingOptions(object instance) + { + var internalCacheProperty = instance + .GetType() + .GetProperty("InternalCache", BindingFlags.Instance | BindingFlags.Public); + + if (internalCacheProperty != null) + { + instance = internalCacheProperty.GetValue(instance); + } + + var defaultOptionsField = instance + .GetType() + .GetField("DefaultCacheOptions", BindingFlags.Instance | BindingFlags.NonPublic); + + return (DistributedCacheEntryOptions)defaultOptionsField.GetValue(instance); + } } [Serializable] @@ -148,3 +193,14 @@ public class ProductCacheItem public decimal Price { get; set; } } + +[Serializable] +[CacheName("ProductCacheItem2")] +public class ProductCacheItem2 +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public decimal Price { get; set; } +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json index 841cd18775..07309d4922 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json @@ -34,14 +34,14 @@ "DisplayName:PhoneNumber": "Telefon numarası", "PersonalSettings": "Kişisel ayarlar", "PersonalSettingsSaved": "Kişisel ayarlar kaydedildi", - "PersonalSettingsChangedConfirmationModalTitle": "Personal info changed", - "PersonalSettingsChangedConfirmationModalDescription": "If you want to apply these changes, you have to login. Do you want to log out?", + "PersonalSettingsChangedConfirmationModalTitle": "Kişisel bilgiler değişti", + "PersonalSettingsChangedConfirmationModalDescription": "Değişikliklerin yansıtılması için tekrar giriş yapmanız gerekmektedir. Şimdi oturumu kapatmak istiyor musunuz?", "PasswordChanged": "Şifre değiştirildi", "NewPasswordConfirmFailed": "Lütfen yeni şifreyi onaylayın.", "NewPasswordSameAsOld": "Yeni şifre eski şifre ile aynı olamaz.", - "Manage": "Manage", + "Manage": "Yönet", "MyAccount": "Hesabım", - "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "self-registration etkin mi ?", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Kendi kendine kayıt etkin mi?", "Description:Abp.Account.IsSelfRegistrationEnabled": "Bir kullanıcının hesabı kendisi tarafından kaydedip kaydedememesidir.", "DisplayName:Abp.Account.EnableLocalLogin": "Yerel bir hesapla kimlik doğrulaması", "Description:Abp.Account.EnableLocalLogin": "Sunucunun, kullanıcıların yerel bir hesapla kimlik doğrulamasına izin verip vermeyeceğini belirtir.", diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.abppkg.analyze.json b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.abppkg.analyze.json index 26b2fd35ee..92c808c088 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.abppkg.analyze.json +++ b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.abppkg.analyze.json @@ -412,6 +412,28 @@ "contentType": "applicationService", "name": "ProfileAppService", "summary": null + }, + { + "defaultValue": "true", + "displayName": "Is self-registration enabled", + "description": "Whether a user can register the account by him or herself.", + "isVisibleToClient": true, + "isInherited": true, + "isEncrypted": false, + "contentType": "setting", + "name": "Abp.Account.IsSelfRegistrationEnabled", + "summary": null + }, + { + "defaultValue": "true", + "displayName": "Authenticate with a local account", + "description": "Indicates if the server will allow users to authenticate with a local account.", + "isVisibleToClient": true, + "isInherited": true, + "isEncrypted": false, + "contentType": "setting", + "name": "Abp.Account.EnableLocalLogin", + "summary": null } ] } \ No newline at end of file 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/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 21a4374fba..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.1.0-rc.1", - "@abp/prismjs": "~10.1.0-rc.1", - "@abp/highlight.js": "~10.1.0-rc.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 090a72c506..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.1.tgz#ebb677e689372d5c0d07a5814b61eae45397a3c8" - integrity sha512-iJpKYQJy5mN1O8n0+4DsWoWKAOasLuUE9Cja01Yl7YTb6XwYCGozBAVZbi+YouhYmsSr2zM4ozKxXPbXXt0u1w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.1.0-rc.1.tgz#52667c1b0242b710d1b8ea65cf9c8dce50c2a213" - integrity sha512-LlcVW/R+WPy8DqWmCfkbxwsWVHdo4jqn1/wvcSuxtPYsQZcBLocM+IgmT1Be3Ti1gn5Gu2h89UsdYlDfLMllqQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.1.tgz#26ef6c368dbfdd27d2836f1dd8cd9e8cf630f0ab" - integrity sha512-6OpD/w7NOSI7OpjubMx45gXsZY5K6ggynJ9uSqv3NDxSbj9l6c8cRTX1i56ATT10rhyYxZIw83FRhMlxOiLfjg== +"@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.1.0-rc.1" - "@abp/core" "~10.1.0-rc.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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 9c12be2aa6..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.1.0-rc.1", - "@abp/prismjs": "~10.1.0-rc.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 891127c606..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.1.tgz#ebb677e689372d5c0d07a5814b61eae45397a3c8" - integrity sha512-iJpKYQJy5mN1O8n0+4DsWoWKAOasLuUE9Cja01Yl7YTb6XwYCGozBAVZbi+YouhYmsSr2zM4ozKxXPbXXt0u1w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.1.tgz#26ef6c368dbfdd27d2836f1dd8cd9e8cf630f0ab" - integrity sha512-6OpD/w7NOSI7OpjubMx45gXsZY5K6ggynJ9uSqv3NDxSbj9l6c8cRTX1i56ATT10rhyYxZIw83FRhMlxOiLfjg== +"@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.1.0-rc.1" - "@abp/core" "~10.1.0-rc.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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/app/Volo.BloggingTestApp/package.json b/modules/blogging/app/Volo.BloggingTestApp/package.json index 5a8ef2d655..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.1.0-rc.1", - "@abp/blogging": "~10.1.0-rc.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 7b82124f60..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.1.0-rc.1.tgz#2438e08f4ceeb47b5448ca7e46de00ecd825112b" - integrity sha512-4D2JVdoXF0duIByQ6xeV3sU44ZLFQtkKgIjylDhlVuXUfCg8yKBqjK9MBNpV2Ltxj32HnKQjnPkj06P+zZL1ZA== +"@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.1.0-rc.1" - "@abp/owl.carousel" "~10.1.0-rc.1" - "@abp/prismjs" "~10.1.0-rc.1" - "@abp/tui-editor" "~10.1.0-rc.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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.1.tgz#ebb677e689372d5c0d07a5814b61eae45397a3c8" - integrity sha512-iJpKYQJy5mN1O8n0+4DsWoWKAOasLuUE9Cja01Yl7YTb6XwYCGozBAVZbi+YouhYmsSr2zM4ozKxXPbXXt0u1w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.1.0-rc.1.tgz#180edebe4fbeaf8fb9c468f6104c0ca14c9040a7" - integrity sha512-udlMHMqoqhUBP0Xy1pJ9XBWHKjgNDxdXeW/QqgJwb1tFE1iyHpL73gaJLSfA1RzwDnWiweWDTcm7zzXhz2UpHQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" owl.carousel "^2.3.4" -"@abp/prismjs@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.1.tgz#26ef6c368dbfdd27d2836f1dd8cd9e8cf630f0ab" - integrity sha512-6OpD/w7NOSI7OpjubMx45gXsZY5K6ggynJ9uSqv3NDxSbj9l6c8cRTX1i56ATT10rhyYxZIw83FRhMlxOiLfjg== +"@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.1.0-rc.1" - "@abp/core" "~10.1.0-rc.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/tui-editor@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.1.0-rc.1.tgz#fd9d014ea5bbdc9c93b3160040928cc27ba50d5e" - integrity sha512-X6tlZs3/soUbl/L/aiQfIOZuiZRfmlUpEukIduJBlDsSaNR2hJgEa98RsQNvQ2yvSU/H7+9kwIwdizhQdimujA== +"@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.1.0-rc.1" - "@abp/prismjs" "~10.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" + "@abp/prismjs" "~10.1.0-rc.2" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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/client-simulation/demo/Volo.ClientSimulation.Demo/package.json b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json index 10810ab195..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.1.0-rc.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 55a960f9ab..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== +"@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.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== +"@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.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== + "@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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 9badfe81b0..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.1.0-rc.1", - "@abp/ng.identity": "~10.1.0-rc.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.1", - "@abp/ng.theme.basic": "~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 6aa3f4aee0..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.1.0-rc.1", - "@abp/ng.theme.shared": ">=10.1.0-rc.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 5160154a29..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.1.0-rc.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 55a960f9ab..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== +"@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.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== +"@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.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== + "@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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 881359366e..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.1.0-rc.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 55a960f9ab..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== +"@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.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== +"@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.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== + "@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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 3bfc3b1e83..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.1.0-rc.1", - "@abp/cms-kit": "10.1.0-rc.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 ffae765b95..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.1.tgz#ebb677e689372d5c0d07a5814b61eae45397a3c8" - integrity sha512-iJpKYQJy5mN1O8n0+4DsWoWKAOasLuUE9Cja01Yl7YTb6XwYCGozBAVZbi+YouhYmsSr2zM4ozKxXPbXXt0u1w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/cms-kit.admin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.1.0-rc.1.tgz#800ab806e343c0464eb775ef844e77a0e56f07ce" - integrity sha512-RkMTl9eDXqOC/fmun4gsm+glWUCmGJUtv6DnLu8m3wBuwLGyac64o9shpie/6TFvF99agtUp+4se5c8PO4m0tA== +"@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.1.0-rc.1" - "@abp/jstree" "~10.1.0-rc.1" - "@abp/markdown-it" "~10.1.0-rc.1" - "@abp/slugify" "~10.1.0-rc.1" - "@abp/tui-editor" "~10.1.0-rc.1" - "@abp/uppy" "~10.1.0-rc.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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.1.0-rc.1.tgz#1f38e4f848da7898d37e66263e807a7cefe8df95" - integrity sha512-owy45DYVAxnu0UYhpqPctIfbM7m/NxYREjnhq+41BaXXiTDLOXEsSoucwObDevnvwthYDuB+4CXK3NYz7x/8+g== +"@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.1.0-rc.1" - "@abp/star-rating-svg" "~10.1.0-rc.1" + "@abp/highlight.js" "~10.1.0-rc.2" + "@abp/star-rating-svg" "~10.1.0-rc.2" -"@abp/cms-kit@10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.1.0-rc.1.tgz#c359a8b392644d17cea1a000169a728dc8ea42f4" - integrity sha512-YSnmmygVa7NZShoI/brrBIiM2SLdSLwCEmxC7Igml0j0BfO7MYBQ0BlY2dcdun9jtwv52V9OZQvtmnuObSolkw== +"@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.1.0-rc.1" - "@abp/cms-kit.public" "~10.1.0-rc.1" + "@abp/cms-kit.admin" "~10.1.0-rc.2" + "@abp/cms-kit.public" "~10.1.0-rc.2" -"@abp/codemirror@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.1.0-rc.1.tgz#6a1a6d7ed56a33385f7fbc48cbf49697dde485f8" - integrity sha512-XoVp2Jh1xwkLL8ZPuWo3kxrr/bgU56WwG3fegNCfJY1RsFckNGI8VKvka/+AQaCxfq44hYWurm/3c2JjkI392A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" codemirror "^5.65.1" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.1.0-rc.1.tgz#52667c1b0242b710d1b8ea65cf9c8dce50c2a213" - integrity sha512-LlcVW/R+WPy8DqWmCfkbxwsWVHdo4jqn1/wvcSuxtPYsQZcBLocM+IgmT1Be3Ti1gn5Gu2h89UsdYlDfLMllqQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/jstree@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.1.0-rc.1.tgz#1a0ee3ed28a84f959df41b5192199e18f8a8b60c" - integrity sha512-37XpZscnQyAK3oiz27dcAnyqGkcIs38Coxr4D65USULm84f/YGEV9QbKwqYIvdtj5Xac4IXQ5K2jANj+TRIuBg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jstree "^3.3.17" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/markdown-it@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.1.0-rc.1.tgz#277508743134503937f56bc764e6554d7f37f402" - integrity sha512-JBPhX9t6Err9kcZTwFC8ib3QaNGRKs6ucme+h7GY592WUFDzcxOIAoOPguFvn3x1nQCXNOEqy5NFO2BVRfN6qw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" markdown-it "^14.1.0" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.1.tgz#26ef6c368dbfdd27d2836f1dd8cd9e8cf630f0ab" - integrity sha512-6OpD/w7NOSI7OpjubMx45gXsZY5K6ggynJ9uSqv3NDxSbj9l6c8cRTX1i56ATT10rhyYxZIw83FRhMlxOiLfjg== +"@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.1.0-rc.1" - "@abp/core" "~10.1.0-rc.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/slugify@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.1.0-rc.1.tgz#86aa65da4e80d1b651fb029e9366e84ceefde201" - integrity sha512-MlIJ+7ee6IT1pH+5IUe4w+jfdZZIn1NAOSE0vmM8iz82MIgYcPp8PdV1qBeO+v8BlgfGJebKCul0VX98debbxw== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.1.0-rc.1.tgz#48b87590495258264d02443a577ee82f0ac4c15b" - integrity sha512-DLr6NpGfhmF94uaXYdMFDpusMZ4SVHEOu2z4tJ+h8LelaOBvQSKBMjx2yc2R7fByReiujRc1JZeIhZKboZfVSA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" star-rating-svg "^3.5.0" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/tui-editor@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.1.0-rc.1.tgz#fd9d014ea5bbdc9c93b3160040928cc27ba50d5e" - integrity sha512-X6tlZs3/soUbl/L/aiQfIOZuiZRfmlUpEukIduJBlDsSaNR2hJgEa98RsQNvQ2yvSU/H7+9kwIwdizhQdimujA== +"@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.1.0-rc.1" - "@abp/prismjs" "~10.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" + "@abp/prismjs" "~10.1.0-rc.2" -"@abp/uppy@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.1.0-rc.1.tgz#aa849234c078238c9bf6af8c181a1432ac628d22" - integrity sha512-0utLga9u7Do3BH1Nou7t01a9f89fhe5A6NqCvQg91opKmMFYOke5PiGAUOvbbbBh6kZDlPw9dc9E6fOhIBXh2w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" uppy "^5.1.2" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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.Public.Web/Pages/CmsKitPageRouteValueTransformer.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKitPageRouteValueTransformer.cs index 996a449489..82750943c0 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKitPageRouteValueTransformer.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKitPageRouteValueTransformer.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Volo.Abp.Auditing; using Volo.Abp.Caching; using Volo.Abp.Features; using Volo.Abp.MultiTenancy; @@ -16,18 +17,21 @@ public class CmsKitPageRouteValueTransformer : CmsKitDynamicRouteValueTransforme protected IFeatureChecker FeatureChecker { get; } protected IPagePublicAppService PagePublicAppService { get; } protected IDistributedCache PageCache { get; } + protected IAuditingHelper AuditingHelper { get; } public CmsKitPageRouteValueTransformer( ICurrentTenant currentTenant, ITenantConfigurationProvider tenantConfigurationProvider, IFeatureChecker featureChecker, IPagePublicAppService pagePublicAppService, - IDistributedCache pageCache) + IDistributedCache pageCache, + IAuditingHelper auditingHelper) : base(currentTenant, tenantConfigurationProvider) { FeatureChecker = featureChecker; PagePublicAppService = pagePublicAppService; PageCache = pageCache; + AuditingHelper = auditingHelper; } protected async override ValueTask DoTransformAsync(HttpContext httpContext, RouteValueDictionary values) @@ -44,7 +48,10 @@ public class CmsKitPageRouteValueTransformer : CmsKitDynamicRouteValueTransforme var exist = await PageCache.GetAsync(PageCacheItem.GetKey(slug)) != null; if (!exist) { - exist = await PagePublicAppService.DoesSlugExistAsync(slug); + using (AuditingHelper.DisableAuditing()) + { + exist = await PagePublicAppService.DoesSlugExistAsync(slug); + } } if (exist) diff --git a/modules/docs/app/VoloDocs.Web/package.json b/modules/docs/app/VoloDocs.Web/package.json index d802372318..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.1.0-rc.1", - "@abp/docs": "~10.1.0-rc.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 89b737d361..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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.1.0-rc.1.tgz#976f553f72e8fe6d9dca3886906c12a7d980f8f0" - integrity sha512-pbgMsNGPhS8IKvSAzrF3d+8KxKqEYi1pssnfMvw+5LvfW2gdRnofjYuTFTaFzCQnYzUFlhVKSHd9yjz7RrWrvw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" anchor-js "^5.0.0" -"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.1.0-rc.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/clipboard@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.1.0-rc.1.tgz#ebb677e689372d5c0d07a5814b61eae45397a3c8" - integrity sha512-iJpKYQJy5mN1O8n0+4DsWoWKAOasLuUE9Cja01Yl7YTb6XwYCGozBAVZbi+YouhYmsSr2zM4ozKxXPbXXt0u1w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" clipboard "^2.0.11" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/docs@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.1.0-rc.1.tgz#b06d4a878de9d5149fcd96e8efa20be02179c2e9" - integrity sha512-+8wV8y1+VO55BHJHZsxW0/OTp7TpauCf/h/QkWjeCdGJ/+sE73FOkZ31Xr+jNCstteF/lwNbK8zFSNQVtQH87g== +"@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.1.0-rc.1" - "@abp/clipboard" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/popper.js" "~10.1.0-rc.1" - "@abp/prismjs" "~10.1.0-rc.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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.1.0-rc.1.tgz#db1fcbd11df41a328456c2a6ae38283f9683611e" - integrity sha512-CJOrNB2T2fC3IiKw46eRr/NDzCTlNwsH6ooEovp4wy0O+t1kk4U/XnguN8wWxCl/SvW1VOAPOrRm6Ar2OABu+w== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.1.0-rc.1.tgz#26ef6c368dbfdd27d2836f1dd8cd9e8cf630f0ab" - integrity sha512-6OpD/w7NOSI7OpjubMx45gXsZY5K6ggynJ9uSqv3NDxSbj9l6c8cRTX1i56ATT10rhyYxZIw83FRhMlxOiLfjg== +"@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.1.0-rc.1" - "@abp/core" "~10.1.0-rc.1" + "@abp/clipboard" "~10.1.0-rc.2" + "@abp/core" "~10.1.0-rc.2" prismjs "^1.30.0" -"@abp/select2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@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/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo.Abp.FeatureManagement.Application.Contracts.abppkg.analyze.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo.Abp.FeatureManagement.Application.Contracts.abppkg.analyze.json index 73fc20b72c..fe7a26b081 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo.Abp.FeatureManagement.Application.Contracts.abppkg.analyze.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo.Abp.FeatureManagement.Application.Contracts.abppkg.analyze.json @@ -73,6 +73,13 @@ "contentType": "abpModule", "name": "AbpFeatureManagementApplicationContractsModule", "summary": null + }, + { + "displayName": "Manage host features", + "isEnabled": true, + "contentType": "permission", + "name": "FeatureManagement.ManageHostFeatures", + "summary": null } ] } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo.Abp.Identity.Application.Contracts.abppkg.analyze.json b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo.Abp.Identity.Application.Contracts.abppkg.analyze.json index 8f70114751..ed9bd2c045 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo.Abp.Identity.Application.Contracts.abppkg.analyze.json +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo.Abp.Identity.Application.Contracts.abppkg.analyze.json @@ -73,6 +73,90 @@ "contentType": "abpModule", "name": "AbpIdentityApplicationContractsModule", "summary": null + }, + { + "displayName": "Role management", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Roles", + "summary": null + }, + { + "displayName": "Create", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Roles.Create", + "summary": null + }, + { + "displayName": "Edit", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Roles.Update", + "summary": null + }, + { + "displayName": "Delete", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Roles.Delete", + "summary": null + }, + { + "displayName": "Change permissions", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Roles.ManagePermissions", + "summary": null + }, + { + "displayName": "User management", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Users", + "summary": null + }, + { + "displayName": "Create", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Users.Create", + "summary": null + }, + { + "displayName": "Edit", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Users.Update", + "summary": null + }, + { + "displayName": "Manage roles", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Users.Update.ManageRoles", + "summary": null + }, + { + "displayName": "Delete", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Users.Delete", + "summary": null + }, + { + "displayName": "Change permissions", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.Users.ManagePermissions", + "summary": null + }, + { + "displayName": "User lookup", + "isEnabled": true, + "contentType": "permission", + "name": "AbpIdentity.UserLookup", + "summary": null } ] } \ No newline at end of file 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 a5b4156d2b..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,18 +2,23 @@ using System.Linq; using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.Localization; namespace Volo.Abp.Identity; -public class AbpIdentityResultException : BusinessException +public class AbpIdentityResultException : BusinessException, ILocalizeErrorMessage { 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 string LocalizeMessage(LocalizationContext context) + { + return Message; + } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs index 48d8b01a73..633bbd1ed9 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/UserRoleFinder.cs @@ -35,10 +35,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency { page = page < 1 ? 1 : page; var users = await IdentityUserRepository.GetListAsync(filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10); - return users.Select(user => new UserFinderResult + return users.Select(x => new UserFinderResult { - Id = user.Id, - UserName = user.UserName + Id = x.Id, + UserName = x.UserName }).ToList(); } } @@ -49,10 +49,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency { page = page < 1 ? 1 : page; var roles = await IdentityRoleRepository.GetListAsync(filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10); - return roles.Select(user => new RoleFinderResult + return roles.Select(x => new RoleFinderResult { - Id = user.Id, - RoleName = user.Name + Id = x.Id, + RoleName = x.Name }).ToList(); } } @@ -62,10 +62,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency using (IdentityUserRepository.DisableTracking()) { var users = await IdentityUserRepository.GetListByIdsAsync(ids); - return users.Select(user => new UserFinderResult + return users.Select(x => new UserFinderResult { - Id = user.Id, - UserName = user.UserName + Id = x.Id, + UserName = x.UserName }).ToList(); } } @@ -75,10 +75,10 @@ public class UserRoleFinder : IUserRoleFinder, ITransientDependency using (IdentityUserRepository.DisableTracking()) { var roles = await IdentityRoleRepository.GetListAsync(names); - return roles.Select(user => new RoleFinderResult + return roles.Select(x => new RoleFinderResult { - Id = user.Id, - RoleName = user.Name + Id = x.Id, + RoleName = x.Name }).ToList(); } } diff --git a/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs b/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs index fa9fc31f79..ab1824e13d 100644 --- a/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs +++ b/modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs @@ -30,9 +30,9 @@ public class RoleResourcePermissionProviderKeyLookupService : IResourcePermissio return roles.Select(r => new ResourcePermissionProviderKeyInfo(r.RoleName, r.RoleName)).ToList(); } - public virtual async Task> SearchAsync(string[] keys, CancellationToken cancellationToken = default) + public virtual Task> SearchAsync(string[] keys, CancellationToken cancellationToken = default) { - var roles = await UserRoleFinder.SearchRoleByNamesAsync(keys.Distinct().ToArray()); - return roles.Select(r => new ResourcePermissionProviderKeyInfo(r.RoleName, r.RoleName)).ToList(); + // Keys are role names + return Task.FromResult(keys.Select(x => new ResourcePermissionProviderKeyInfo(x, x)).ToList()); } } 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 new file mode 100644 index 0000000000..fb45c5190e --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/AbpIdentityResultException_Tests.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Identity; +using Shouldly; +using Volo.Abp.AspNetCore.ExceptionHandling; +using Volo.Abp.Localization; +using Xunit; + +namespace Volo.Abp.Identity; + +public class AbpIdentityResultException_Tests : AbpIdentityDomainTestBase +{ + [Fact] + public void Should_Localize_Messages() + { + 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 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("Ş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("zh-Hans")) + { + 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("密码长度必须大于 6 字符。"); + message.ShouldContain("密码必须至少包含一个非字母数字字符。"); + message.ShouldContain("发生了一个未知错误。"); + } + } +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs new file mode 100644 index 0000000000..d0c42968c7 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/ClientFinderResult.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.IdentityServer.Clients; + +public class ClientFinderResult +{ + public Guid Id { get; set; } + + public string ClientId { get; set; } +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs new file mode 100644 index 0000000000..be96379902 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Clients/IClientFinder.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.IdentityServer.Clients; + +public interface IClientFinder +{ + Task> SearchAsync(string filter, int page = 1); +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json index 1d62307724..27edc5d80a 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/FR.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nom d'utilisateur ou mot de passe invalide!", "InvalidAuthenticatorCode": "Code d'authentification invalide !", "InvalidRecoveryCode": "Code de récupération invalide !", - "TheTargetUserIsNotLinkedToYou": "L'utilisateur cible n'est pas lié à vous!" + "TheTargetUserIsNotLinkedToYou": "L'utilisateur cible n'est pas lié à vous!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json index 3329275161..ee9fd11913 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ar.json @@ -11,6 +11,7 @@ "InvalidUsername": "اسم المستخدم أو كلمة المرور غير صالحة!", "InvalidAuthenticatorCode": "كود المصدق غير صالح!", "InvalidRecoveryCode": "رمز الاسترداد غير صالح!", - "TheTargetUserIsNotLinkedToYou": "المستخدم المستهدف غير مرتبط بك!" + "TheTargetUserIsNotLinkedToYou": "المستخدم المستهدف غير مرتبط بك!", + "ClientResourcePermissionProviderKeyLookupService": "العميل" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json index 1c5cb2bbf6..f77e12665b 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/cs.json @@ -11,6 +11,7 @@ "InvalidUsername": "Neplatné uživatelské jméno či heslo!", "InvalidAuthenticatorCode": "Neplatný ověřovací kód!", "InvalidRecoveryCode": "Neplatný kód pro obnovení!", - "TheTargetUserIsNotLinkedToYou": "Cílový uživatel s vámi není spojen!" + "TheTargetUserIsNotLinkedToYou": "Cílový uživatel s vámi není spojen!", + "ClientResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json index b0fc6bd640..3738380e70 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/de.json @@ -11,6 +11,7 @@ "InvalidUsername": "Ungültiger Benutzername oder Passwort!", "InvalidAuthenticatorCode": "Ungültiger Authentifizierungscode!", "InvalidRecoveryCode": "Ungültiger Wiederherstellungscode!", - "TheTargetUserIsNotLinkedToYou": "Der Zielbenutzer ist nicht mit Ihnen verknüpft!" + "TheTargetUserIsNotLinkedToYou": "Der Zielbenutzer ist nicht mit Ihnen verknüpft!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json index 246db314c7..9528ee45de 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/el.json @@ -10,6 +10,7 @@ "LoginIsNotAllowed": "Δεν επιτρέπεται να συνδεθείτε! Ο λογαριασμός σας είναι ανενεργός ή χρειάζεται να επιβεβαιώσετε το email/τον αριθμό τηλεφώνου σας.", "InvalidUsername": "Μη έγκυρο όνομα ή κωδικός!", "InvalidAuthenticatorCode": "Μη έγκυρος κωδικός ελέγχου ταυτότητας!", - "TheTargetUserIsNotLinkedToYou": "Ο χρήστης-στόχος δεν είναι συνδεδεμένος με εσάς!" + "TheTargetUserIsNotLinkedToYou": "Ο χρήστης-στόχος δεν είναι συνδεδεμένος με εσάς!", + "ClientResourcePermissionProviderKeyLookupService": "Πελάτης" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json index 173379249c..aaf48fefee 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en-GB.json @@ -9,6 +9,7 @@ "InvalidUserNameOrPassword": "Invalid username or password!", "LoginIsNotAllowed": "You are not allowed to login! Your account is inactive or needs to confirm your email/phone number.", "InvalidUsername": "Invalid username or password!", - "TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!" + "TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json index 82d4be83b0..71ee543544 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json @@ -11,6 +11,7 @@ "InvalidUsername": "Invalid username or password!", "InvalidAuthenticatorCode": "Invalid authenticator code!", "InvalidRecoveryCode": "Invalid recovery code!", - "TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!" + "TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json index 4c2ecd92ee..9cd39dea54 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/es.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nombre de usuario icorrecto", "InvalidAuthenticatorCode": "¡Código de autenticador no válido!", "InvalidRecoveryCode": "¡Código de recuperación no válido!", - "TheTargetUserIsNotLinkedToYou": "El usuario de destino no está asociado a usted." + "TheTargetUserIsNotLinkedToYou": "El usuario de destino no está asociado a usted.", + "ClientResourcePermissionProviderKeyLookupService": "Cliente" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json index 8713511f43..ec7f8d355b 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fa.json @@ -11,6 +11,7 @@ "InvalidUsername": "نام کاربری یا رمز عبور نامعتبر!", "InvalidAuthenticatorCode": "کد احراز هویت نامعتبر!", "InvalidRecoveryCode": "کد بازیابی نامعتبر!", - "TheTargetUserIsNotLinkedToYou": "کاربر هدف به شما پیوند داده نشده است!" + "TheTargetUserIsNotLinkedToYou": "کاربر هدف به شما پیوند داده نشده است!", + "ClientResourcePermissionProviderKeyLookupService": "کلاینت" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json index 94779f34b3..fe3f0747a9 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/fi.json @@ -11,6 +11,7 @@ "InvalidUsername": "Väärä käyttäjänimi tai salasana!", "InvalidAuthenticatorCode": "Virheellinen todennuskoodi!", "InvalidRecoveryCode": "Virheellinen palautuskoodi!", - "TheTargetUserIsNotLinkedToYou": "Kohdekäyttäjä ei ole linkitetty sinuun!" + "TheTargetUserIsNotLinkedToYou": "Kohdekäyttäjä ei ole linkitetty sinuun!", + "ClientResourcePermissionProviderKeyLookupService": "Asiakas" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json index 2df86f55bc..c1e2dbe4b5 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hi.json @@ -11,6 +11,7 @@ "InvalidUsername": "अमान्य उपयोगकर्ता नाम या पासवर्ड!", "InvalidAuthenticatorCode": "अमान्य प्रमाणक कोड!", "InvalidRecoveryCode": "अमान्य पुनर्प्राप्ति कोड!", - "TheTargetUserIsNotLinkedToYou": "लक्ष्य उपयोगकर्ता आपसे जुड़ा नहीं है!" + "TheTargetUserIsNotLinkedToYou": "लक्ष्य उपयोगकर्ता आपसे जुड़ा नहीं है!", + "ClientResourcePermissionProviderKeyLookupService": "क्लाइंट" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json index da79db308c..693bd3513e 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hr.json @@ -11,6 +11,7 @@ "InvalidUsername": "Neispravno korisničko ime ili lozinka!", "InvalidAuthenticatorCode": "Nevažeći kod autentifikatora!", "InvalidRecoveryCode": "Nevažeći kod za oporavak!", - "TheTargetUserIsNotLinkedToYou": "Ciljani korisnik nije povezan s vama!" + "TheTargetUserIsNotLinkedToYou": "Ciljani korisnik nije povezan s vama!", + "ClientResourcePermissionProviderKeyLookupService": "Klijent" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json index e41b61c736..505e034724 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/hu.json @@ -11,6 +11,7 @@ "InvalidUsername": "Érvénytelen felhasználónév vagy jelszó!", "InvalidAuthenticatorCode": "Érvénytelen hitelesítő kód!", "InvalidRecoveryCode": "Érvénytelen helyreállítási kód!", - "TheTargetUserIsNotLinkedToYou": "A célfelhasználó nincs hozzád kapcsolódva!" + "TheTargetUserIsNotLinkedToYou": "A célfelhasználó nincs hozzád kapcsolódva!", + "ClientResourcePermissionProviderKeyLookupService": "Kliens" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json index 8f97265691..fc4b11af18 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/is.json @@ -11,6 +11,7 @@ "InvalidUsername": "Ógilt notendanafn eða lykilorð!", "InvalidAuthenticatorCode": "Ógildur auðkenningarkóði!", "InvalidRecoveryCode": "Ógildur endurheimtarkóði!", - "TheTargetUserIsNotLinkedToYou": "Marknotandinn er ekki tengdur þér!" + "TheTargetUserIsNotLinkedToYou": "Marknotandinn er ekki tengdur þér!", + "ClientResourcePermissionProviderKeyLookupService": "Biðlari" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json index 1795289fa3..eb0e87c346 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/it.json @@ -11,6 +11,7 @@ "InvalidUsername": "Username o password non validi!", "InvalidAuthenticatorCode": "Codice autenticatore non valido!", "InvalidRecoveryCode": "Codice di ripristino non valido!", - "TheTargetUserIsNotLinkedToYou": "L'utente indicato non è collegato a te!" + "TheTargetUserIsNotLinkedToYou": "L'utente indicato non è collegato a te!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json index e8a7a5c0b1..baaca72b61 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/nl.json @@ -11,6 +11,7 @@ "InvalidUsername": "Ongeldige gebruikersnaam of wachtwoord!", "InvalidAuthenticatorCode": "Ongeldige authenticatiecode!", "InvalidRecoveryCode": "Ongeldige herstelcode!", - "TheTargetUserIsNotLinkedToYou": "De beoogde gebruiker is niet aan jou gekoppeld!" + "TheTargetUserIsNotLinkedToYou": "De beoogde gebruiker is niet aan jou gekoppeld!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json index a231a6ec3f..59a831a878 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pl-PL.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nieprawidłowa nazwa użytkownika lub hasło!", "InvalidAuthenticatorCode": "Nieprawidłowy kod uwierzytelniający!", "InvalidRecoveryCode": "Nieprawidłowy kod odzyskiwania!", - "TheTargetUserIsNotLinkedToYou": "Docelowy użytkownik nie jest z Tobą powiązany!" + "TheTargetUserIsNotLinkedToYou": "Docelowy użytkownik nie jest z Tobą powiązany!", + "ClientResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json index db5d2fb00e..35ab5aa711 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/pt-BR.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nome de usuário ou senha inválidos!", "InvalidAuthenticatorCode": "Código de autenticador inválido!", "InvalidRecoveryCode": "Código de recuperação inválido!", - "TheTargetUserIsNotLinkedToYou": "O usuário-alvo não está vinculado a você!" + "TheTargetUserIsNotLinkedToYou": "O usuário-alvo não está vinculado a você!", + "ClientResourcePermissionProviderKeyLookupService": "Cliente" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json index a1af796373..9ab41e64bc 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ro-RO.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nume de utilizator sau parolă invalidă!", "InvalidAuthenticatorCode": "Cod de autentificare invalid!", "InvalidRecoveryCode": "Cod de recuperare nevalid!", - "TheTargetUserIsNotLinkedToYou": "Utilizatorul ţintă nu este conectat la dumneavoastră!" + "TheTargetUserIsNotLinkedToYou": "Utilizatorul ţintă nu este conectat la dumneavoastră!", + "ClientResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json index 0c8a0f9fe5..90db053cd6 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ru.json @@ -11,6 +11,7 @@ "InvalidUsername": "Неверное имя пользователя или пароль!", "InvalidAuthenticatorCode": "Неверный код аутентификатора!", "InvalidRecoveryCode": "Неверный код восстановления!", - "TheTargetUserIsNotLinkedToYou": "Целевой пользователь не связан с вами!" + "TheTargetUserIsNotLinkedToYou": "Целевой пользователь не связан с вами!", + "ClientResourcePermissionProviderKeyLookupService": "Клиент" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json index e0e31056fd..181ae21fa2 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sk.json @@ -11,6 +11,7 @@ "InvalidUsername": "Nesprávne používateľské meno alebo heslo!", "InvalidAuthenticatorCode": "Neplatný overovací kód!", "InvalidRecoveryCode": "Neplatný kód na obnovenie!", - "TheTargetUserIsNotLinkedToYou": "Cieľový používateľ nie je s vami prepojený!" + "TheTargetUserIsNotLinkedToYou": "Cieľový používateľ nie je s vami prepojený!", + "ClientResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json index 527b11b35f..b6d653d406 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sl.json @@ -11,6 +11,7 @@ "InvalidUsername": "Napačno uporabniško ime ali geslo!", "InvalidAuthenticatorCode": "Neveljavna koda za preverjanje pristnosti!", "InvalidRecoveryCode": "Neveljavna obnovitvena koda!", - "TheTargetUserIsNotLinkedToYou": "Ciljni uporabnik ni povezan z vami!" + "TheTargetUserIsNotLinkedToYou": "Ciljni uporabnik ni povezan z vami!", + "ClientResourcePermissionProviderKeyLookupService": "Odjemalec" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json index cdbf5d0dff..f0a1921bdf 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/sv.json @@ -11,6 +11,7 @@ "InvalidUsername": "Ogiltigt användarnamn eller lösenord!", "InvalidAuthenticatorCode": "Ogiltig autentiseringskod!", "InvalidRecoveryCode": "Ogiltig återställningskod!", - "TheTargetUserIsNotLinkedToYou": "Målanvändaren är inte kopplad till dig!" + "TheTargetUserIsNotLinkedToYou": "Målanvändaren är inte kopplad till dig!", + "ClientResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json index ef893f60ef..1d1e9e7c36 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json @@ -11,6 +11,7 @@ "InvalidUsername": "Kullanıcı adı ya da şifre geçersiz!", "InvalidAuthenticatorCode": "Geçersiz kimlik doğrulama kodu!", "InvalidRecoveryCode": "Geçersiz kurtarma kodu!", - "TheTargetUserIsNotLinkedToYou": "Hedef kullanıcı sizinle bağlantılı değil!" + "TheTargetUserIsNotLinkedToYou": "Hedef kullanıcı sizinle bağlantılı değil!", + "ClientResourcePermissionProviderKeyLookupService": "İstemci" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json index 4d62d91611..6eeb3b68cf 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/vi.json @@ -11,6 +11,7 @@ "InvalidUsername": "Sai username hoặc password!", "InvalidAuthenticatorCode": "Mã xác thực không hợp lệ!", "InvalidRecoveryCode": "Mã khôi phục không hợp lệ!", - "TheTargetUserIsNotLinkedToYou": "Người dùng mục tiêu không được liên kết với bạn!" + "TheTargetUserIsNotLinkedToYou": "Người dùng mục tiêu không được liên kết với bạn!", + "ClientResourcePermissionProviderKeyLookupService": "Máy khách" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json index afb5824356..bbe4d7773d 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json @@ -11,6 +11,7 @@ "InvalidUsername": "用户名或密码错误!", "InvalidAuthenticatorCode": "验证码无效!", "InvalidRecoveryCode": "恢复代码无效!", - "TheTargetUserIsNotLinkedToYou": "目标用户与您没有关联!" + "TheTargetUserIsNotLinkedToYou": "目标用户与您没有关联!", + "ClientResourcePermissionProviderKeyLookupService": "客户端" } } \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json index 3678d6b572..5e592e1409 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hant.json @@ -11,6 +11,7 @@ "InvalidUsername": "用戶名或密碼錯誤!", "InvalidAuthenticatorCode": "驗證碼無效!", "InvalidRecoveryCode": "恢復碼無效!", - "TheTargetUserIsNotLinkedToYou": "目標用戶與您無關!" + "TheTargetUserIsNotLinkedToYou": "目標用戶與您無關!", + "ClientResourcePermissionProviderKeyLookupService": "用戶端" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/ClientFinder.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/ClientFinder.cs new file mode 100644 index 0000000000..d64215a047 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/Clients/ClientFinder.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.IdentityServer.Clients; + +public class ClientFinder : IClientFinder, ITransientDependency +{ + protected IClientRepository ClientRepository { get; } + + public ClientFinder(IClientRepository clientRepository) + { + ClientRepository = clientRepository; + } + + public virtual async Task> SearchAsync(string filter, int page = 1) + { + using (ClientRepository.DisableTracking()) + { + page = page < 1 ? 1 : page; + var clients = await ClientRepository.GetListAsync(nameof(Client.ClientName), filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10); + return clients.Select(x => new ClientFinderResult + { + Id = x.Id, + ClientId = x.ClientId + }).ToList(); + } + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/ClientResourcePermissionManagerExtensions.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/ClientResourcePermissionManagerExtensions.cs new file mode 100644 index 0000000000..16e686a2aa --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/ClientResourcePermissionManagerExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Authorization.Permissions; + +namespace Volo.Abp.PermissionManagement; + +public static class ClientResourcePermissionManagerExtensions +{ + public static Task GetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, string permissionName) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task> GetAllForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAllAsync(resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task SetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, [NotNull] string permissionName, bool isGranted) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.SetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId, isGranted); + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs index 3d2d42c28f..2525ffb3d3 100644 --- a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/AbpPermissionManagementDomainIdentityServerModule.cs @@ -1,5 +1,8 @@ -using Volo.Abp.Authorization.Permissions; +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.IdentityServer; +using Volo.Abp.IdentityServer.Clients; using Volo.Abp.Modularity; namespace Volo.Abp.PermissionManagement.IdentityServer; @@ -18,5 +21,17 @@ public class AbpPermissionManagementDomainIdentityServerModule : AbpModule options.ProviderPolicies[ClientPermissionValueProvider.ProviderName] = "IdentityServer.Client.ManagePermissions"; }); + + context.Services.AddAbpOptions().PostConfigure((options, serviceProvider) => + { + // The IClientFinder implementation in identity Server Pro module for tiered application. + if (serviceProvider.GetService() == null) + { + return; + } + + options.ResourceManagementProviders.Add(); + options.ResourcePermissionProviderKeyLookupServices.Add(); + }); } } diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientDeletedEventHandler.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientDeletedEventHandler.cs new file mode 100644 index 0000000000..2a5ebbfb71 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientDeletedEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.IdentityServer.Clients; +using Volo.Abp.Uow; + +namespace Volo.Abp.PermissionManagement.IdentityServer; + +public class ClientDeletedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IPermissionManager PermissionManager { get; } + protected IResourcePermissionManager ResourcePermissionManager { get; } + + public ClientDeletedEventHandler(IPermissionManager permissionManager, IResourcePermissionManager resourcePermissionManager) + { + PermissionManager = permissionManager; + ResourcePermissionManager = resourcePermissionManager; + } + + [UnitOfWork] + public virtual async Task HandleEventAsync(EntityDeletedEto eventData) + { + await PermissionManager.DeleteAsync(ClientPermissionValueProvider.ProviderName, eventData.Entity.ClientId); + await ResourcePermissionManager.DeleteAsync(ClientResourcePermissionValueProvider.ProviderName, eventData.Entity.ClientId); + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs index dd25c6d5c0..618588bdf4 100644 --- a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientPermissionManagementProvider.cs @@ -18,7 +18,6 @@ public class ClientPermissionManagementProvider : PermissionManagementProvider guidGenerator, currentTenant) { - } public override Task CheckAsync(string name, string providerName, string providerKey) @@ -29,6 +28,14 @@ public class ClientPermissionManagementProvider : PermissionManagementProvider } } + public override Task CheckAsync(string[] names, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, providerName, providerKey); + } + } + protected override Task GrantAsync(string name, string providerKey) { using (CurrentTenant.Change(null)) diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs new file mode 100644 index 0000000000..9285742131 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs @@ -0,0 +1,62 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.PermissionManagement.IdentityServer; + +public class ClientResourcePermissionManagementProvider : ResourcePermissionManagementProvider +{ + public override string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ClientResourcePermissionManagementProvider( + IResourcePermissionGrantRepository permissionGrantRepository, + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) + : base( + permissionGrantRepository, + guidGenerator, + currentTenant) + { + } + + public override Task CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(name, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task CheckAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task SetAsync(string name, string resourceName, string resourceKey, string providerKey, bool isGranted) + { + using (CurrentTenant.Change(null)) + { + return base.SetAsync(name, resourceName, resourceKey, providerKey, isGranted); + } + } + + protected override async Task GrantAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + await base.GrantAsync(name, resourceName, resourceKey, providerKey); + } + } + + protected override Task RevokeAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.RevokeAsync(name, resourceName, resourceKey, providerKey); + } + } +} diff --git a/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs new file mode 100644 index 0000000000..7fc813dabb --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IdentityServer.Clients; +using Volo.Abp.IdentityServer.Localization; +using Volo.Abp.Localization; + +namespace Volo.Abp.PermissionManagement.IdentityServer; + +public class ClientResourcePermissionProviderKeyLookupService : IResourcePermissionProviderKeyLookupService, ITransientDependency +{ + public string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ILocalizableString DisplayName { get; } + + protected IClientFinder ClientFinder { get; } + + public ClientResourcePermissionProviderKeyLookupService(IClientFinder clientFinder) + { + ClientFinder = clientFinder; + DisplayName = LocalizableString.Create(nameof(ClientResourcePermissionProviderKeyLookupService)); + } + + public virtual async Task> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default) + { + var clients = await ClientFinder.SearchAsync(filter, page); + return clients.Select(x => new ResourcePermissionProviderKeyInfo(x.ClientId, x.ClientId)).ToList(); + } + + public virtual Task> SearchAsync(string[] keys, CancellationToken cancellationToken = default) + { + // Keys are ClientIds + return Task.FromResult(keys.Select(x => new ResourcePermissionProviderKeyInfo(x, x)).ToList()); + } +} diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/package.json b/modules/openiddict/app/OpenIddict.Demo.Server/package.json index 881359366e..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.1.0-rc.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 037c330682..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.1.0-rc.1", - "@abp/ng.components": "~10.1.0-rc.1", - "@abp/ng.core": "~10.1.0-rc.1", - "@abp/ng.oauth": "~10.1.0-rc.1", - "@abp/ng.identity": "~10.1.0-rc.1", - "@abp/ng.setting-management": "~10.1.0-rc.1", - "@abp/ng.tenant-management": "~10.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.1", - "@abp/ng.theme.lepton-x": "~5.1.0-rc.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.1.0-rc.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.Shared/Volo/Abp/OpenIddict/Applications/ApplicationFinderResult.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/ApplicationFinderResult.cs new file mode 100644 index 0000000000..5ff4d5041b --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/ApplicationFinderResult.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.OpenIddict.Applications; + +public class ApplicationFinderResult +{ + public Guid Id { get; set; } + + public string ClientId { get; set; } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/IApplicationFinder.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/IApplicationFinder.cs new file mode 100644 index 0000000000..0585df8815 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/IApplicationFinder.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.OpenIddict.Applications; + +public interface IApplicationFinder +{ + Task> SearchAsync(string filter, int page = 1); +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationClientIdChangedEto.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationClientIdChangedEto.cs new file mode 100644 index 0000000000..d80e5f35d6 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationClientIdChangedEto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Volo.Abp.OpenIddict.Applications; + +[Serializable] +public class OpenIddictApplicationClientIdChangedEto +{ + public Guid Id { get; set; } + + public string ClientId { get; set; } + + public string OldClientId { get; set; } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationEto.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationEto.cs new file mode 100644 index 0000000000..ff853d2c4a --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationEto.cs @@ -0,0 +1,43 @@ +using System; + +namespace Volo.Abp.OpenIddict.Applications; + +[Serializable] +public class OpenIddictApplicationEto +{ + public Guid Id { get; set; } + + public string ApplicationType { get; set; } + + public string ClientId { get; set; } + + public string ClientSecret { get; set; } + + public string ClientType { get; set; } + + public string ConsentType { get; set; } + + public string DisplayName { get; set; } + + public string DisplayNames { get; set; } + + public string JsonWebKeySet { get; set; } + + public string Permissions { get; set; } + + public string PostLogoutRedirectUris { get; set; } + + public string Properties { get; set; } + + public string RedirectUris { get; set; } + + public string Requirements { get; set; } + + public string Settings { get; set; } + + public string FrontChannelLogoutUri { get; set; } + + public string ClientUri { get; set; } + + public string LogoUri { get; set; } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json index 61c52f63c5..c22bdea508 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ar.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "هل تريد منح {0} حق الوصول إلى بياناتك؟", "ScopesRequested": "النطاقات المطلوبة", "Accept": "قبول", - "Deny": "رفض" + "Deny": "رفض", + "ApplicationResourcePermissionProviderKeyLookupService": "العميل" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json index c219b5dc8f..fda735746b 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/cs.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Chcete uživateli {0} udělit přístup ke svým datům?", "ScopesRequested": "Požadované rozsahy", "Accept": "Akceptovat", - "Deny": "Odmítnout" + "Deny": "Odmítnout", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json index 4c6438de37..affff9da29 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/de.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Möchten Sie {0} Zugriff auf Ihre Daten gewähren?", "ScopesRequested": "Umfänge angefordert", "Accept": "Akzeptieren", - "Deny": "Leugnen" + "Deny": "Leugnen", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json index c10f34fb1d..493c73b2a7 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/el.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Θέλετε να παραχωρήσετε στον χρήστη {0} πρόσβαση στα δεδομένα σας;", "ScopesRequested": "Ζητούνται πεδία εφαρμογής", "Accept": "Αποδοχή", - "Deny": "Άρνηση" + "Deny": "Άρνηση", + "ApplicationResourcePermissionProviderKeyLookupService": "Πελάτης" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json index b6bd02bc15..d742176bd5 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/en.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Do you want to grant {0} access to your data?", "ScopesRequested": "Scopes requested", "Accept": "Accept", - "Deny": "Deny" + "Deny": "Deny", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json index 3bec447a24..ad5a974972 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/es.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "¿Quieres otorgarle a {0} acceso a tus datos?", "ScopesRequested": "Alcances solicitados", "Accept": "Aceptar", - "Deny": "Denegar" + "Deny": "Denegar", + "ApplicationResourcePermissionProviderKeyLookupService": "Cliente" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json index f1649533a9..c0c2d82340 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fa.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "آیا می خواهید به {0} اجازه دسترسی به داده های خود را بدهید؟", "ScopesRequested": "محدوده های درخواستی", "Accept": "پذیرش", - "Deny": "رد" + "Deny": "رد", + "ApplicationResourcePermissionProviderKeyLookupService": "کلاینت" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json index 43dbfb17ac..ce0bb20bfe 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fi.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Haluatko myöntää käyttäjälle {0} pääsyn tietoihisi?", "ScopesRequested": "Laajuudet pyydetty", "Accept": "Hyväksy", - "Deny": "Kiellä" + "Deny": "Kiellä", + "ApplicationResourcePermissionProviderKeyLookupService": "Asiakas" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json index 188116b53c..c11964d21d 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/fr.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Voulez-vous accorder à {0} l'accès à vos données ?", "ScopesRequested": "Périmètres demandés", "Accept": "Accepter", - "Deny": "Refuser" + "Deny": "Refuser", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json index 39b3370ca7..7f2678d7a0 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hi.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "क्या आप {0} को अपने डेटा तक पहुंच प्रदान करना चाहते हैं?", "ScopesRequested": "दायरे का अनुरोध किया गया", "Accept": "स्वीकार करना", - "Deny": "अस्वीकार करना" + "Deny": "अस्वीकार करना", + "ApplicationResourcePermissionProviderKeyLookupService": "क्लाइंट" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json index 2dfc3b3a9d..ee16d2b1e0 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hr.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Želite li {0} odobriti pristup vašim podacima?", "ScopesRequested": "Traženi dometi", "Accept": "Prihvatiti", - "Deny": "poreći" + "Deny": "poreći", + "ApplicationResourcePermissionProviderKeyLookupService": "Klijent" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json index f55ea2ac74..7a5e7b1956 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/hu.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Hozzáférést szeretne adni a(z) {0} számára az adataihoz?", "ScopesRequested": "Kért hatókörök", "Accept": "Elfogad", - "Deny": "Tiltás" + "Deny": "Tiltás", + "ApplicationResourcePermissionProviderKeyLookupService": "Kliens" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json index b9fc6a43ce..b6c5bafca6 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/is.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Viltu veita {0} aðgang að gögnunum þínum?", "ScopesRequested": "Umfang óskað", "Accept": "Samþykkja", - "Deny": "Neita" + "Deny": "Neita", + "ApplicationResourcePermissionProviderKeyLookupService": "Biðlari" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json index 22eb3bbafe..e5b67df57a 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/it.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Vuoi concedere a {0} l'accesso ai tuoi dati?", "ScopesRequested": "Ambiti richiesti", "Accept": "Accettare", - "Deny": "Negare" + "Deny": "Negare", + "ApplicationResourcePermissionProviderKeyLookupService": "Cliente" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json index b3b35c6acb..7eb593fd2d 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/nl.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Wilt u {0} toegang verlenen tot uw gegevens?", "ScopesRequested": "Scopes gevraagd", "Accept": "Aanvaarden", - "Deny": "Ontkennen" + "Deny": "Ontkennen", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json index 2e5e19eae7..bff402519d 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pl-PL.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Czy chcesz przyznać firmie {0} dostęp do swoich danych?", "ScopesRequested": "Poproszono o zakresy", "Accept": "Zaakceptować", - "Deny": "Zaprzeczyć" + "Deny": "Zaprzeczyć", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json index 60c50e9e3e..97f52a7f01 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/pt-BR.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Deseja permitir {0} acessar seus dados?", "ScopesRequested": "Escopo solicitado", "Accept": "Aceitar", - "Deny": "Negar" + "Deny": "Negar", + "ApplicationResourcePermissionProviderKeyLookupService": "Cliente" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json index a9d9eee22e..579df65787 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ro-RO.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Doriți să acordați acces {0} la datele dvs.?", "ScopesRequested": "Domenii de aplicare solicitate", "Accept": "Accept", - "Deny": "Negați" + "Deny": "Negați", + "ApplicationResourcePermissionProviderKeyLookupService": "Client" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json index 4f5b1d0e21..2f22d22da5 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ru.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Вы хотите предоставить пользователю {0} доступ к вашим данным?", "ScopesRequested": "Запрошенные объемы", "Accept": "Принимать", - "Deny": "Отрицать" + "Deny": "Отрицать", + "ApplicationResourcePermissionProviderKeyLookupService": "Клиент" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json index 13e0853b2d..cb017569cc 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sk.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Chcete používateľovi {0} udeliť prístup k svojim údajom?", "ScopesRequested": "Požadované rozsahy", "Accept": "súhlasiť", - "Deny": "Odmietnuť" + "Deny": "Odmietnuť", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json index c8157ac509..49d1bd148b 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sl.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Ali želite aplikaciji {0} omogočiti dostop do vaših podatkov?", "ScopesRequested": "Zahtevani obsegi", "Accept": "Sprejmi", - "Deny": "Zanikati" + "Deny": "Zanikati", + "ApplicationResourcePermissionProviderKeyLookupService": "Odjemalec" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json index 3e2a58578e..063727a745 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/sv.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Vill du ge {0} tillgång till dina data?", "ScopesRequested": "Begärda omfattningar", "Accept": "Acceptera", - "Deny": "Förneka" + "Deny": "Förneka", + "ApplicationResourcePermissionProviderKeyLookupService": "Klient" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json index 907612a6c4..847eb5f482 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/tr.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Do you want to grant {0} access to your data?", "ScopesRequested": "İstenen kapsamlar", "Accept": "Kabul etmek", - "Deny": "Reddetmek" + "Deny": "Reddetmek", + "ApplicationResourcePermissionProviderKeyLookupService": "İstemci" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json index c89d882a39..4435c32298 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/vi.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "Bạn có muốn cấp cho {0} quyền truy cập vào dữ liệu của mình không?", "ScopesRequested": "Phạm vi được yêu cầu", "Accept": "Chấp nhận", - "Deny": "Từ chối" + "Deny": "Từ chối", + "ApplicationResourcePermissionProviderKeyLookupService": "Máy khách" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json index f00a7c9e37..ee7b4a7b0f 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hans.json @@ -10,6 +10,7 @@ "DoYouWantToGrantAccessToYourData": "是否要授予 {0} 访问你的数据的权限?", "ScopesRequested": "要求的Scope", "Accept": "接受", - "Deny": "拒绝" + "Deny": "拒绝", + "ApplicationResourcePermissionProviderKeyLookupService": "客户端" } } \ No newline at end of file diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json index 83aa0c35d1..ca1ceaed22 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/zh-Hant.json @@ -11,6 +11,7 @@ "DoYouWantToGrantAccessToYourData": "是否要授予 {0} 訪問你的數據的權限?", "ScopesRequested": "要求的Scope", "Accept": "接受", - "Deny": "拒絕" + "Deny": "拒絕", + "ApplicationResourcePermissionProviderKeyLookupService": "客戶端" } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainMappers.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainMappers.cs new file mode 100644 index 0000000000..ef750fc3a6 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainMappers.cs @@ -0,0 +1,13 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.OpenIddict.Applications; + +namespace Volo.Abp.OpenIddict; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class OpenIddictApplicationToOpenIddictApplicationEtoMapper : MapperBase +{ + public override partial OpenIddictApplicationEto Map(OpenIddictApplication source); + + public override partial void Map(OpenIddictApplication source, OpenIddictApplicationEto destination); +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs index fd20f71012..e235e16893 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs @@ -8,6 +8,7 @@ using Volo.Abp.BackgroundWorkers; using Volo.Abp.Caching; using Volo.Abp.DistributedLocking; using Volo.Abp.Domain; +using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Guids; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -18,6 +19,7 @@ using Volo.Abp.OpenIddict.Authorizations; using Volo.Abp.OpenIddict.Scopes; using Volo.Abp.OpenIddict.Tokens; using Volo.Abp.Threading; +using Volo.Abp.Users; namespace Volo.Abp.OpenIddict; @@ -36,6 +38,15 @@ public class AbpOpenIddictDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { AddOpenIddictCore(context.Services); + + context.Services.AddMapperlyObjectMapper(); + + Configure(options => + { + options.EtoMappings.Add(typeof(AbpOpenIddictDomainModule)); + + options.AutoEventSelectors.Add(); + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationFinder.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationFinder.cs new file mode 100644 index 0000000000..56c6f9af99 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationFinder.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.OpenIddict.Applications; + +public class AbpApplicationFinder : IApplicationFinder, ITransientDependency +{ + protected IOpenIddictApplicationRepository ApplicationRepository { get; } + + public AbpApplicationFinder(IOpenIddictApplicationRepository applicationRepository) + { + ApplicationRepository = applicationRepository; + } + + public virtual async Task> SearchAsync(string filter, int page = 1) + { + using (ApplicationRepository.DisableTracking()) + { + page = page < 1 ? 1 : page; + var applications = await ApplicationRepository.GetListAsync(nameof(OpenIddictApplication.CreationTime), filter: filter, skipCount: (page - 1) * 10, maxResultCount: 10); + return applications.Select(x => new ApplicationFinderResult + { + Id = x.Id, + ClientId = x.ClientId + }).ToList(); + } + } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs index ea2283ea25..478ccb68f1 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs @@ -6,29 +6,35 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; +using Volo.Abp.EventBus.Distributed; namespace Volo.Abp.OpenIddict.Applications; public class AbpApplicationManager : OpenIddictApplicationManager, IAbpApplicationManager { protected AbpOpenIddictIdentifierConverter IdentifierConverter { get; } + protected IDistributedEventBus DistributedEventBus { get; } public AbpApplicationManager( [NotNull] IOpenIddictApplicationCache cache, [NotNull] ILogger logger, [NotNull] IOptionsMonitor options, [NotNull] IOpenIddictApplicationStore resolver, - AbpOpenIddictIdentifierConverter identifierConverter) + AbpOpenIddictIdentifierConverter identifierConverter, + IDistributedEventBus distributedEventBus) : base(cache, logger, options, resolver) { IdentifierConverter = identifierConverter; + DistributedEventBus = distributedEventBus; } - public async override ValueTask UpdateAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default) + public override async ValueTask UpdateAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default) { + var entity = await Store.FindByIdAsync(IdentifierConverter.ToString(application.Id), cancellationToken); + var oldClientId = entity?.ClientId; + if (!Options.CurrentValue.DisableEntityCaching) { - var entity = await Store.FindByIdAsync(IdentifierConverter.ToString(application.Id), cancellationToken); if (entity != null) { await Cache.RemoveAsync(entity, cancellationToken); @@ -36,9 +42,21 @@ public class AbpApplicationManager : OpenIddictApplicationManager> GetListAsync(string sorting, int skipCount, int maxResultCount, string filter = null, CancellationToken cancellationToken = default); Task GetCountAsync(string filter = null, CancellationToken cancellationToken = default); - + Task FindByClientIdAsync(string clientId, CancellationToken cancellationToken = default); Task> FindByPostLogoutRedirectUriAsync(string address, CancellationToken cancellationToken = default); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs index 867bbe71c5..7b3b0ba826 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.MongoDB/Volo/Abp/OpenIddict/Applications/MongoOpenIddictApplicationRepository.cs @@ -17,7 +17,7 @@ public class MongoOpenIddictApplicationRepository : MongoDbRepository dbContextProvider) : base(dbContextProvider) { } - + public virtual async Task> GetListAsync(string sorting, int skipCount, int maxResultCount, string filter = null, CancellationToken cancellationToken = default) { diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/ClientResourcePermissionManagerExtensions.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/ClientResourcePermissionManagerExtensions.cs new file mode 100644 index 0000000000..16e686a2aa --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/ClientResourcePermissionManagerExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Authorization.Permissions; + +namespace Volo.Abp.PermissionManagement; + +public static class ClientResourcePermissionManagerExtensions +{ + public static Task GetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, string permissionName) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task> GetAllForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.GetAllAsync(resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId); + } + + public static Task SetForClientAsync([NotNull] this IResourcePermissionManager resourcePermissionManager, string resourceName, string resourceKey, string clientId, [NotNull] string permissionName, bool isGranted) + { + Check.NotNull(resourcePermissionManager, nameof(resourcePermissionManager)); + + return resourcePermissionManager.SetAsync(permissionName, resourceName, resourceKey, ClientPermissionValueProvider.ProviderName, clientId, isGranted); + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs index 1bf2bf140d..2cad25a283 100644 --- a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/AbpPermissionManagementDomainOpenIddictModule.cs @@ -1,6 +1,9 @@ -using Volo.Abp.Authorization.Permissions; +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict; +using Volo.Abp.OpenIddict.Applications; namespace Volo.Abp.PermissionManagement.OpenIddict; @@ -17,5 +20,17 @@ public class AbpPermissionManagementDomainOpenIddictModule : AbpModule options.ManagementProviders.Add(); options.ProviderPolicies[ClientPermissionValueProvider.ProviderName] = "OpenIddictPro.Application.ManagePermissions"; }); + + context.Services.AddAbpOptions().PostConfigure((options, serviceProvider) => + { + // The IApplicationFinder implementation in OpenIddict Pro module for tiered application. + if (serviceProvider.GetService() == null) + { + return; + } + + options.ResourceManagementProviders.Add(); + options.ResourcePermissionProviderKeyLookupServices.Add(); + }); } } diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs index 4c4d6fe398..6d07a728e4 100644 --- a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationPermissionManagementProvider.cs @@ -18,7 +18,6 @@ public class ApplicationPermissionManagementProvider : PermissionManagementProvi guidGenerator, currentTenant) { - } public override Task CheckAsync(string name, string providerName, string providerKey) @@ -29,6 +28,14 @@ public class ApplicationPermissionManagementProvider : PermissionManagementProvi } } + public override Task CheckAsync(string[] names, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, providerName, providerKey); + } + } + protected override Task GrantAsync(string name, string providerKey) { using (CurrentTenant.Change(null)) diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs new file mode 100644 index 0000000000..e12fa63d03 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class ApplicationResourcePermissionManagementProvider : ResourcePermissionManagementProvider +{ + public override string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ApplicationResourcePermissionManagementProvider( + IResourcePermissionGrantRepository resourcePermissionGrantRepository, + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) + : base(resourcePermissionGrantRepository, guidGenerator, currentTenant) + { + } + + public override Task CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(name, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task CheckAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.CheckAsync(names, resourceName, resourceKey, providerName, providerKey); + } + } + + public override Task SetAsync(string name, string resourceName, string resourceKey, string providerKey, bool isGranted) + { + using (CurrentTenant.Change(null)) + { + return base.SetAsync(name, resourceName, resourceKey, providerKey, isGranted); + } + } + + protected override async Task GrantAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + await base.GrantAsync(name, resourceName, resourceKey, providerKey); + } + } + + protected override Task RevokeAsync(string name, string resourceName, string resourceKey, string providerKey) + { + using (CurrentTenant.Change(null)) + { + return base.RevokeAsync(name, resourceName, resourceKey, providerKey); + } + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs new file mode 100644 index 0000000000..1423ca9768 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.OpenIddict.Applications; +using Volo.Abp.OpenIddict.Localization; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class ApplicationResourcePermissionProviderKeyLookupService : IResourcePermissionProviderKeyLookupService, ITransientDependency +{ + public string Name => ClientResourcePermissionValueProvider.ProviderName; + + public ILocalizableString DisplayName { get; } + + protected IApplicationFinder ApplicationFinder { get; } + + public ApplicationResourcePermissionProviderKeyLookupService(IApplicationFinder applicationFinder) + { + ApplicationFinder = applicationFinder; + DisplayName = LocalizableString.Create(nameof(ApplicationResourcePermissionProviderKeyLookupService)); + } + + public virtual async Task> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default) + { + var applications = await ApplicationFinder.SearchAsync(filter, page); + return applications.Select(x => new ResourcePermissionProviderKeyInfo(x.ClientId, x.ClientId)).ToList(); + } + + public virtual Task> SearchAsync(string[] keys, CancellationToken cancellationToken = default) + { + // Keys are ClientIds + return Task.FromResult(keys.Select(x => new ResourcePermissionProviderKeyInfo(x, x)).ToList()); + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationClientIdChangedHandler.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationClientIdChangedHandler.cs new file mode 100644 index 0000000000..8a22d1b46d --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationClientIdChangedHandler.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.OpenIddict.Applications; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class OpenIddictApplicationClientIdChangedHandler : + IDistributedEventHandler, + ITransientDependency +{ + protected IPermissionManager PermissionManager { get; } + protected IPermissionGrantRepository PermissionGrantRepository { get; } + protected IResourcePermissionManager ResourcePermissionManager { get; } + protected IResourcePermissionGrantRepository ResourcePermissionGrantRepository { get; } + + public OpenIddictApplicationClientIdChangedHandler( + IPermissionManager permissionManager, + IPermissionGrantRepository permissionGrantRepository, + IResourcePermissionManager resourcePermissionManager, + IResourcePermissionGrantRepository resourcePermissionGrantRepository) + { + PermissionManager = permissionManager; + PermissionGrantRepository = permissionGrantRepository; + ResourcePermissionManager = resourcePermissionManager; + ResourcePermissionGrantRepository = resourcePermissionGrantRepository; + } + + public async Task HandleEventAsync(OpenIddictApplicationClientIdChangedEto eventData) + { + var permissionGrantsInRole = await PermissionGrantRepository.GetListAsync(ClientPermissionValueProvider.ProviderName, eventData.OldClientId); + foreach (var permissionGrant in permissionGrantsInRole) + { + await PermissionManager.UpdateProviderKeyAsync(permissionGrant, eventData.ClientId); + } + + var resourcePermissionGrantsInRole = await ResourcePermissionGrantRepository.GetListAsync(ClientResourcePermissionValueProvider.ProviderName, eventData.OldClientId); + foreach (var resourcePermissionGrant in resourcePermissionGrantsInRole) + { + await ResourcePermissionManager.UpdateProviderKeyAsync(resourcePermissionGrant, eventData.ClientId); + } + } +} diff --git a/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationDeletedEventHandler.cs b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationDeletedEventHandler.cs new file mode 100644 index 0000000000..ac02065e63 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/OpenIddictApplicationDeletedEventHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Authorization.Permissions.Resources; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.OpenIddict.Applications; +using Volo.Abp.Uow; + +namespace Volo.Abp.PermissionManagement.OpenIddict; + +public class OpenIddictApplicationDeletedEventHandler : + IDistributedEventHandler>, + ITransientDependency +{ + protected IPermissionManager PermissionManager { get; } + protected IResourcePermissionManager ResourcePermissionManager { get; } + + public OpenIddictApplicationDeletedEventHandler(IPermissionManager permissionManager, IResourcePermissionManager resourcePermissionManager) + { + PermissionManager = permissionManager; + ResourcePermissionManager = resourcePermissionManager; + } + + [UnitOfWork] + public virtual async Task HandleEventAsync(EntityDeletedEto eventData) + { + await PermissionManager.DeleteAsync(ClientPermissionValueProvider.ProviderName, eventData.Entity.ClientId); + await ResourcePermissionManager.DeleteAsync(ClientResourcePermissionValueProvider.ProviderName, eventData.Entity.ClientId); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs index 90d4e176d3..9682965c67 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs @@ -26,7 +26,7 @@ public abstract class ResourcePermissionManagementProvider : IResourcePermission CurrentTenant = currentTenant; } - public virtual async Task CheckAsync(string name, string resourceName,string resourceKey, string providerName, string providerKey) + public virtual async Task CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) { var multiplePermissionValueProviderGrantInfo = await CheckAsync(new[] { name }, resourceName, resourceKey, providerName, providerKey); @@ -55,7 +55,7 @@ public abstract class ResourcePermissionManagementProvider : IResourcePermission } } - public virtual Task SetAsync(string name, string resourceName,string resourceKey, string providerKey, bool isGranted) + public virtual Task SetAsync(string name, string resourceName, string resourceKey, string providerKey, bool isGranted) { return isGranted ? GrantAsync(name, resourceName, resourceKey, providerKey) diff --git a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json index 7ae41ebfe9..3a8321fed9 100644 --- a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json +++ b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/package.json @@ -3,6 +3,6 @@ "name": "demo-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2" } } diff --git a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock index 55a960f9ab..a2b6e13b29 100644 --- a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock +++ b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/yarn.lock @@ -2,185 +2,185 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.1.0-rc.1.tgz#99407c4108b685eef91f21d038cc5bd2343b5847" - integrity sha512-UbVgYf1K+zUqueNCwOl4QoUAz7nwJyoQJSqq0gi7LA9JBRBbb8nDA8szQjAUVqF4h+k4o61588MUQnerUQlZ6g== +"@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.1" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.1.0-rc.2" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.1.0-rc.1.tgz#7c0f721bdd4ec99f441a49e662a457d98c2c66fa" - integrity sha512-dB6wmR3C43Vn1/hiWY3IkxivUEWTGhO4mj3hffwAeyJqnEUh6kQUW6UfmYNGEhahSjhVIS3lUZ4x+R+yGCpemw== +"@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.1" - "@abp/bootstrap" "~10.1.0-rc.1" - "@abp/bootstrap-datepicker" "~10.1.0-rc.1" - "@abp/bootstrap-daterangepicker" "~10.1.0-rc.1" - "@abp/datatables.net-bs5" "~10.1.0-rc.1" - "@abp/font-awesome" "~10.1.0-rc.1" - "@abp/jquery-form" "~10.1.0-rc.1" - "@abp/jquery-validation-unobtrusive" "~10.1.0-rc.1" - "@abp/lodash" "~10.1.0-rc.1" - "@abp/luxon" "~10.1.0-rc.1" - "@abp/malihu-custom-scrollbar-plugin" "~10.1.0-rc.1" - "@abp/moment" "~10.1.0-rc.1" - "@abp/select2" "~10.1.0-rc.1" - "@abp/sweetalert2" "~10.1.0-rc.1" - "@abp/timeago" "~10.1.0-rc.1" - -"@abp/aspnetcore.mvc.ui@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.1.0-rc.1.tgz#290297353fdc15826a870b35162a0bbf4c9bc948" - integrity sha512-RBR5wPf5ygzuSpfXG5MeOGd/YOtQH5pVyU65T4DMtGpLKHQxgxbh7JK1btyC5zGmzr/Ds5+5Mcd7S66dIckDJQ== + "@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.1.0-rc.1.tgz#54730dc001dab746af18c41d51b73949742e3b2d" - integrity sha512-DhVfA9WjfRQrUwjN2eJTCjLEhAU7rLjWgMlifW1id3HbdMs9emKn0rzQqhZ8MOSUNBwLkbUWcXWnojRUFgUB+w== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.1.0-rc.1.tgz#9af7ffe1d1bd4fc99e1606e30189e6f28bb2f201" - integrity sha512-/Hsge8UwLZFuPNWRZhpf4nVB4urPYsaKAuCz0pSH5OHmS4FWr0Ng3vnUd9fcSeaq9OV2HarcgGWDapYtszttVQ== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.1.0-rc.1.tgz#e937f0a10874c0f3b0f719ff6a316b0ca91e65c5" - integrity sha512-GlRyH5oCjcmFk1eBGCHk6GglL2DJ5RLlDXn7HWU675u48DCQTkBbKP9qA3RcYERQB4qqhcOKEDtYPZVljRVHRQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" bootstrap "^5.3.8" -"@abp/core@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.1.0-rc.1.tgz#d9ce0958407fa39c8015eb2d9328a3a157aaa4f0" - integrity sha512-pFsaQei1W0JRJkhNdlxq+EDP7x+83nT0UHZetTHNzp8tkP4Cn8Ni/ZODIUku8ck1vcKc0X3pXKj42BN+yZ1g3g== +"@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.1.0-rc.1" + "@abp/utils" "~10.1.0-rc.2" -"@abp/datatables.net-bs5@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.1.0-rc.1.tgz#7981bc64c543d24aa5b0ab373f5693187990d5be" - integrity sha512-yaYHuZ9eVHT2cZcH9r680X+imWhOqbhWlOlbLDpJtg7Un0ejDzou+JqxKYHHXmSEKYEPHjfCEhnXkMnyqvSUHQ== +"@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.1.0-rc.1" + "@abp/datatables.net" "~10.1.0-rc.2" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.1.0-rc.1.tgz#f487357690060dd055dbb7f54e1298e8d8a81e40" - integrity sha512-KTKTW73H9vaX6J9ipXUf7d3QdseKaFRzl7e1QKldOoQxBs4pzT2NATGdUyiAnJsfPl4scpSFetzI66Rs7r3idg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" datatables.net "^2.3.4" -"@abp/font-awesome@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.1.0-rc.1.tgz#2ee67b07ce6296c7f0ac5217c9d9677b9c70f05e" - integrity sha512-O/3oLbSu1pMNftQzQW29gSpkWCFJazFKjT7sOYId1Ueric0svyJ42hFr5gHf9RFVmCU5pgyhawcoTyzQ34PxYQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-form@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-10.1.0-rc.1.tgz#60fc98085671a2ff08e8c411847354b6a48cf55e" - integrity sha512-bj70hE7MsH2AoBb2yU2d6ng5Bo/wqjCNUI0kbG0T1T4yp1cToGAVJ6hrd+KOp2Tf6W+NITn1EycxRTCktE9QRA== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.1.0-rc.1.tgz#a820989d8daf53096f838817cb2b3dba21f71653" - integrity sha512-7uXxbvuZ+ecD5suH3nx/ggiFAA8GjXGAB5tuGCdqePdBn1PMzDIDI1ZP9q8sR7sMjo7RKVK6MzX+PazUuJ4L7g== +"@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.1.0-rc.1" + "@abp/jquery-validation" "~10.1.0-rc.2" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.1.0-rc.1.tgz#c8630b0a53ee93218be5c82264085b36b0b07936" - integrity sha512-/eS4vy5gwHIaRr6aHkvO6xWRjL22bVly7S0AqbZw2OxJ0pw1n23n+zipn/NDRSngRZSajTLLm1pYCG5qlJGd3A== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" jquery-validation "^1.21.0" -"@abp/jquery@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.1.0-rc.1.tgz#38d5d5b5a6144bf207773ef26f36d381a257a635" - integrity sha512-YarNlYPG7MfLc0fSnOcVrp9Da1EUxz4zUERnTOQ0/EdOUdcFhGAqI2yfoTFCxqX+TSphQWZEdif/tsfULtkI1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" jquery "~3.7.1" -"@abp/lodash@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.1.0-rc.1.tgz#bf735e34a42ddcdf19e331c8c5cf1a24a87475cb" - integrity sha512-JQV0oFLE90V9GdrD37PTMiOylw/U6yjpDu5PblVW/zlNmw/TzEtukp3/vJl7GJxoKRYWtgJf1nFLgG/+2kjE1A== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" lodash "^4.17.21" -"@abp/luxon@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.1.0-rc.1.tgz#1c883ad3a5653f0c6ce4965e4b4ce21543129534" - integrity sha512-SDtwjIDR4OzrtzRWpgb8atUSK0DwajJ5LC486xHq5ftnbFETRfOwihyIfC2H4oIPPDE5oS+LOnjCkRYVkGt0oQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.1.0-rc.1.tgz#770106795f5eba9d36dd2d4002e450c215c66787" - integrity sha512-EFwQ7rUiPM6mxznn1t23ySzQH+VQ0dB2YxOqBAluFR/yC3qypjb98jGQaZKsJx9+iT4DuG9gGPKOfct1Sqh0KQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.1.0-rc.1.tgz#fe5bfa5bc28bb847cf801523c1b2f118ffb12447" - integrity sha512-P6z9YO/FC/5DhTDWsHkquo5tEzIhSfUnRefLcZtv7XuJtevH/93NFSr23RL6D+F4r3cB5GMXGFUYmXGKWoXGHg== +"@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.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.1.0-rc.1.tgz#19432bb6483f8a3470f02a3037612835b4c54ef4" - integrity sha512-xPvdrgXZzRhEIrEyw9eoITc+hYlbi8fSW3BGQ1ZMBXWgYkjWxhj/1AuLsu2ewFAKdBNz+PewjA0MKP6iBLATHw== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" select2 "^4.0.13" -"@abp/sweetalert2@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.1.0-rc.1.tgz#40f4d6964cd6620dcb81040d84e432f4ae314686" - integrity sha512-E4dUC2k/kClBnadtGK5NWR9YmCGgKq5uOGjEVtzXxJH62MlaCXlcxRrK9wNzX8pzDMDYFKjuOUugoROEgbRKWQ== +"@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.1.0-rc.1" + "@abp/core" "~10.1.0-rc.2" sweetalert2 "^11.23.0" -"@abp/timeago@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.1.0-rc.1.tgz#482e3a645f63ee9adf708ef8ae895fcf2bcb4271" - integrity sha512-UOb2o6bvi3+2kbD7+rUJdqTQvTbgqUD7ktd0U5Hulb3dBOyv6MZEagzMbXA003OROiS23JzzT7M0lLStvixwlg== +"@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.1.0-rc.1" + "@abp/jquery" "~10.1.0-rc.2" timeago "^1.6.7" -"@abp/utils@~10.1.0-rc.1": - version "10.1.0-rc.1" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.1.tgz#e53ef81f39fd620b41ae2bfb69d06ee85ccb28ad" - integrity sha512-xBHIw5rmAttN5PL5C84L2znHkA2HMrP6HaA2aV5k6KU7BtOWJhviZnKJgbUBfufNQNeLwlThCHAfS7aOv/UQkg== +"@abp/utils@~10.1.0-rc.2": + version "10.1.0-rc.2" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.1.0-rc.2.tgz#86a980c6536b3b5ce185d406723b28be421864ac" + integrity sha512-Oz863VNA8fraQ81vTvqM0IqwiaseLwfFU5QNn6iOGOfn5wQrEkPwtZ0jMI+DGNtJgPzoKiq+iKc3K+SiuVgldg== dependencies: just-compare "^2.3.0" diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Application.Contracts/Volo.Abp.SettingManagement.Application.Contracts.abppkg.analyze.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Application.Contracts/Volo.Abp.SettingManagement.Application.Contracts.abppkg.analyze.json index f07fc5a44c..a366802a26 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Application.Contracts/Volo.Abp.SettingManagement.Application.Contracts.abppkg.analyze.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Application.Contracts/Volo.Abp.SettingManagement.Application.Contracts.abppkg.analyze.json @@ -68,6 +68,27 @@ "contentType": "abpModule", "name": "AbpSettingManagementApplicationContractsModule", "summary": null + }, + { + "displayName": "Emailing", + "isEnabled": true, + "contentType": "permission", + "name": "SettingManagement.Emailing", + "summary": null + }, + { + "displayName": "Emailing test", + "isEnabled": true, + "contentType": "permission", + "name": "SettingManagement.Emailing.Test", + "summary": null + }, + { + "displayName": "Time zone", + "isEnabled": true, + "contentType": "permission", + "name": "SettingManagement.TimeZone", + "summary": null } ] } \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json index 44077ac44f..38464ed8c0 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "إدارة الإعداد", "Feature:SettingManagementEnable": "تمكين إدارة الإعداد", "Feature:SettingManagementEnableDescription": "تفعيل إعداد نظام الإدارة في التطبيق.", - "Feature:AllowChangingEmailSettings": "السماح لتغيير إعدادات البريد الإلكتروني.", + "Feature:AllowChangingEmailSettings": "السماح لتغيير إعدادات البريد الإلكتروني", "Feature:AllowChangingEmailSettingsDescription": "السماح لتغيير إعدادات البريد الإلكتروني.", "SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json index 94a6e1f6ea..34a9f6dd2c 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Správa nastavení", "Feature:SettingManagementEnable": "Povolit správu nastavení", "Feature:SettingManagementEnableDescription": "Povolit systém správy nastavení v aplikaci.", - "Feature:AllowChangingEmailSettings": "Povolit změnu nastavení e-mailu.", + "Feature:AllowChangingEmailSettings": "Povolit změnu nastavení e-mailu", "Feature:AllowChangingEmailSettingsDescription": "Povolit změnu nastavení e-mailu.", "SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json index a63b7201f2..a2b2c9ab95 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Einstellungsverwaltung", "Feature:SettingManagementEnable": "Einstellungsverwaltung aktivieren", "Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.", - "Feature:AllowChangingEmailSettings": "Änderung der E-Mail-Einstellungen zulassen.", + "Feature:AllowChangingEmailSettings": "Änderung der E-Mail-Einstellungen zulassen", "Feature:AllowChangingEmailSettingsDescription": "Änderung der E-Mail-Einstellungen zulassen.", "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json index e78aa19fca..fabd73852f 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json @@ -36,7 +36,7 @@ "Feature:SettingManagementGroup": "Einstellungsmanagement", "Feature:SettingManagementEnable": "Aktivieren Sie die Einstellungsverwaltung", "Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.", - "Feature:AllowChangingEmailSettings": "Erlauben Sie das Ändern der E-Mail-Einstellungen.", + "Feature:AllowChangingEmailSettings": "Erlauben Sie das Ändern der E-Mail-Einstellungen", "Feature:AllowChangingEmailSettingsDescription": "Erlauben Sie das Ändern der E-Mail-Einstellungen.", "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json index 424e38e8de..118b1664fe 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json @@ -31,7 +31,7 @@ "Feature:SettingManagementGroup": "Διαχείριση ρυθμίσεων", "Feature:SettingManagementEnable": "Ενεργοποίηση διαχείρισης ρυθμίσεων", "Feature:SettingManagementEnableDescription": "Ενεργοποίηση συστήματος διαχείρισης ρυθμίσεων στην εφαρμογή.", - "Feature:AllowChangingEmailSettings": "Επιτρέψτε την αλλαγή των ρυθμίσεων email.", + "Feature:AllowChangingEmailSettings": "Επιτρέψτε την αλλαγή των ρυθμίσεων email", "Feature:AllowChangingEmailSettingsDescription": "Επιτρέψτε την αλλαγή των ρυθμίσεων email.", "SmtpPasswordPlaceholder": "Εισαγάγετε μια τιμή για ενημέρωση κωδικού πρόσβασης" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json index 832163574b..cffd6f27cd 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Setting management", "Feature:SettingManagementEnable": "Enable setting management", "Feature:SettingManagementEnableDescription": "Enable setting management system in the application.", - "Feature:AllowChangingEmailSettings": "Allow changing email settings.", + "Feature:AllowChangingEmailSettings": "Allow changing email settings", "Feature:AllowChangingEmailSettingsDescription": "Allow changing email settings.", "SmtpPasswordPlaceholder": "Enter a value to update password" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json index 91cc71d532..f158ec3b0d 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Gestión de la configuración", "Feature:SettingManagementEnable": "Habilitar la gestión de la configuración", "Feature:SettingManagementEnableDescription": "Habilite el sistema de gestión de la configuración en la aplicación.", - "Feature:AllowChangingEmailSettings": "Permitir cambiar la configuración de correo electrónico.", + "Feature:AllowChangingEmailSettings": "Permitir cambiar la configuración de correo electrónico", "Feature:AllowChangingEmailSettingsDescription": "Permitir cambiar la configuración de correo electrónico.", "SmtpPasswordPlaceholder": "Ingrese un valor para actualizar la contraseña" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json index 8e7efcb8c6..a2062743fd 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Asetusten hallinta", "Feature:SettingManagementEnable": "Ota asetusten hallinta käyttöön", "Feature:SettingManagementEnableDescription": "Ota asetustenhallintajärjestelmä käyttöön sovelluksessa.", - "Feature:AllowChangingEmailSettings": "Salli sähköpostiasetusten muuttaminen.", + "Feature:AllowChangingEmailSettings": "Salli sähköpostiasetusten muuttaminen", "Feature:AllowChangingEmailSettingsDescription": "Salli sähköpostiasetusten muuttaminen.", "SmtpPasswordPlaceholder": "Syötä arvo päivittääksesi salasana" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json index 1844cc3fbb..957ff62a19 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Gestion des paramètres", "Feature:SettingManagementEnable": "Activer la gestion des paramètres", "Feature:SettingManagementEnableDescription": "Activer le système de gestion des paramètres dans l'application.", - "Feature:AllowChangingEmailSettings": "Autoriser la modification des paramètres de messagerie.", + "Feature:AllowChangingEmailSettings": "Autoriser la modification des paramètres de messagerie", "Feature:AllowChangingEmailSettingsDescription": "Autoriser la modification des paramètres de messagerie.", "SmtpPasswordPlaceholder": "Entrez une valeur pour mettre à jour le mot de passe" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json index c0979707a4..1e68c9ad7e 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json @@ -36,7 +36,7 @@ "Feature:SettingManagementGroup": "Upravljanje postavkama", "Feature:SettingManagementEnable": "Omogući upravljanje postavkama", "Feature:SettingManagementEnableDescription": "Omogućite sustav upravljanja postavkama u aplikaciji.", - "Feature:AllowChangingEmailSettings": "Dopusti promjenu postavki e-pošte.", + "Feature:AllowChangingEmailSettings": "Dopusti promjenu postavki e-pošte", "Feature:AllowChangingEmailSettingsDescription": "Dopusti promjenu postavki e-pošte.", "SmtpPasswordPlaceholder": "Unesite vrijednost za ažuriranje lozinke" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json index efe7876d8d..43b8a53593 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Beállításkezelés", "Feature:SettingManagementEnable": "Beállításkezelés engedélyezése", "Feature:SettingManagementEnableDescription": "A beállításkezelő rendszer engedélyezése az alkalmazásban.", - "Feature:AllowChangingEmailSettings": "Az e-mail beállítások módosításának engedélyezése.", + "Feature:AllowChangingEmailSettings": "Az e-mail beállítások módosításának engedélyezése", "Feature:AllowChangingEmailSettingsDescription": "Az e-mail beállítások módosításának engedélyezése.", "SmtpPasswordPlaceholder": "Adjon meg egy értéket a jelszó frissítéséhez" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json index 73c3c736a9..69a437b66e 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Stillingar", "Feature:SettingManagementEnable": "Virkja stillingar", "Feature:SettingManagementEnableDescription": "Virkja stillingar í forritinu.", - "Feature:AllowChangingEmailSettings": "Leyfa að breyta stillingum tölvupósts.", + "Feature:AllowChangingEmailSettings": "Leyfa að breyta stillingum tölvupósts", "Feature:AllowChangingEmailSettingsDescription": "Leyfa að breyta stillingum tölvupósts.", "SmtpPasswordPlaceholder": "Sláðu inn gildi til að uppfæra lykilorð" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json index cc6669e7c8..01f80cecfb 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Gestione Impostazioni", "Feature:SettingManagementEnable": "Abilita gestione impostazioni", "Feature:SettingManagementEnableDescription": "Abilita sistema gestione impostazioni nell'applicazione", - "Feature:AllowChangingEmailSettings": "Consenti di modificare le loro impostazioni e-mail.", + "Feature:AllowChangingEmailSettings": "Consenti di modificare le loro impostazioni e-mail", "Feature:AllowChangingEmailSettingsDescription": "Consenti di modificare le loro impostazioni e-mail.", "SmtpPasswordPlaceholder": "Inserisci un valore per aggiornare la password" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json index f6d86ab9ba..57a87f550b 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Instellingsbeheer", "Feature:SettingManagementEnable": "Instellingenbeheer inschakelen", "Feature:SettingManagementEnableDescription": "Schakel het instellingsbeheersysteem in de toepassing in.", - "Feature:AllowChangingEmailSettings": "Toestaan om e-mailinstellingen te wijzigen.", + "Feature:AllowChangingEmailSettings": "Toestaan om e-mailinstellingen te wijzigen", "Feature:AllowChangingEmailSettingsDescription": "Toestaan om e-mailinstellingen te wijzigen.", "SmtpPasswordPlaceholder": "Voer een waarde in om het wachtwoord bij te werken" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json index 45c144626d..28c8dd3689 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Zarządzanie ustawieniami", "Feature:SettingManagementEnable": "Włącz zarządzanie ustawieniami", "Feature:SettingManagementEnableDescription": "Włącz system zarządzania ustawieniami w aplikacji.", - "Feature:AllowChangingEmailSettings": "Zezwól na zmianę ustawień poczty e-mail.", + "Feature:AllowChangingEmailSettings": "Zezwól na zmianę ustawień poczty e-mail", "Feature:AllowChangingEmailSettingsDescription": "Zezwól na zmianę ustawień poczty e-mail.", "SmtpPasswordPlaceholder": "Wprowadź wartość, aby zaktualizować hasło" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json index 831d54707e..8b968bacfe 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Gestão de Cenários", "Feature:SettingManagementEnable": "Habilitar gerenciamento de configuração", "Feature:SettingManagementEnableDescription": "Habilite o sistema de gerenciamento de configuração no aplicativo.", - "Feature:AllowChangingEmailSettings": "Permitir alterar as configurações de e-mail.", + "Feature:AllowChangingEmailSettings": "Permitir alterar as configurações de e-mail", "Feature:AllowChangingEmailSettingsDescription": "Permitir alterar as configurações de e-mail.", "SmtpPasswordPlaceholder": "Digite um valor para atualizar a senha" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json index a0f94747ed..83a9b3e2dc 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Administrarea setărilor", "Feature:SettingManagementEnable": "Activează administrarea setărilor", "Feature:SettingManagementEnableDescription": "Activează sistemul de administrare a setărilor în aplicaţie.", - "Feature:AllowChangingEmailSettings": "Permiteți modificarea setărilor de e-mail.", + "Feature:AllowChangingEmailSettings": "Permiteți modificarea setărilor de e-mail", "Feature:AllowChangingEmailSettingsDescription": "Permiteți modificarea setărilor de e-mail.", "SmtpPasswordPlaceholder": "Introduceți o valoare pentru a actualiza parola" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json index 1145595e8e..7026026918 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Управление настройками", "Feature:SettingManagementEnable": "Включить управление настройками", "Feature:SettingManagementEnableDescription": "Включите систему управления настройками в приложении.", - "Feature:AllowChangingEmailSettings": "Разрешить изменение настроек электронной почты.", + "Feature:AllowChangingEmailSettings": "Разрешить изменение настроек электронной почты", "Feature:AllowChangingEmailSettingsDescription": "Разрешить изменение настроек электронной почты.", "SmtpPasswordPlaceholder": "Введите значение для обновления пароля" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json index e8661a3471..9e4ef99363 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Správa nastavení", "Feature:SettingManagementEnable": "Povoliť správu nastavení", "Feature:SettingManagementEnableDescription": "Povoliť systém správy nastavení v aplikácii.", - "Feature:AllowChangingEmailSettings": "Povoliť zmenu nastavení e-mailu.", + "Feature:AllowChangingEmailSettings": "Povoliť zmenu nastavení e-mailu", "Feature:AllowChangingEmailSettingsDescription": "Povoliť zmenu nastavení e-mailu.", "SmtpPasswordPlaceholder": "Zadajte hodnotu pre aktualizáciu hesla" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json index 9bd877053b..e1cce43730 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Upravljanje nastavitev", "Feature:SettingManagementEnable": "Omogoči upravljanje nastavitev", "Feature:SettingManagementEnableDescription": "Omogočite nastavitev sistema upravljanja v aplikaciji.", - "Feature:AllowChangingEmailSettings": "Dovoli spreminjanje e-poštnih nastavitev.", + "Feature:AllowChangingEmailSettings": "Dovoli spreminjanje e-poštnih nastavitev", "Feature:AllowChangingEmailSettingsDescription": "Dovoli spreminjanje e-poštnih nastavitev.", "SmtpPasswordPlaceholder": "Vnesite vrednost za posodobitev gesla" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json index 01719059cc..95462e08b7 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json @@ -34,7 +34,7 @@ "Feature:SettingManagementGroup": "Hantering av inställningar", "Feature:SettingManagementEnable": "Aktivera hantering av inställningar", "Feature:SettingManagementEnableDescription": "Aktivera inställningshanteringssystem i applikationen.", - "Feature:AllowChangingEmailSettings": "Tillåt ändring av e-postinställningar.", + "Feature:AllowChangingEmailSettings": "Tillåt ändring av e-postinställningar", "Feature:AllowChangingEmailSettingsDescription": "Tillåt ändring av e-postinställningar.", "SmtpPasswordPlaceholder": "Ange ett värde för att uppdatera lösenordet" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json index ff4107e21b..8a2db80e58 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Ayar yönetimi", "Feature:SettingManagementEnable": "Ayar yönetimini etkinleştir", "Feature:SettingManagementEnableDescription": "Uygulamada ayar yönetim sistemini etkinleştirin.", - "Feature:AllowChangingEmailSettings": "E-posta ayarlarını değiştirmeye izin verin.", + "Feature:AllowChangingEmailSettings": "E-posta ayarlarını değiştirmeye izin verin", "Feature:AllowChangingEmailSettingsDescription": "E-posta ayarlarını değiştirmeye izin verin.", "SmtpPasswordPlaceholder": "Şifreyi güncellemek için bir değer girin" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json index 38b284a6ca..4c4f74ec2c 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "Cài đặt quản lý", "Feature:SettingManagementEnable": "Bật quản lý cài đặt", "Feature:SettingManagementEnableDescription": "Bật cài đặt hệ thống quản lý trong ứng dụng.", - "Feature:AllowChangingEmailSettings": "Cho phép thay đổi cài đặt email.", + "Feature:AllowChangingEmailSettings": "Cho phép thay đổi cài đặt email", "Feature:AllowChangingEmailSettingsDescription": "Cho phép thay đổi cài đặt email.", "SmtpPasswordPlaceholder": "Nhập một giá trị để cập nhật mật khẩu" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json index b9d0940273..392eb7e4e9 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "设置管理", "Feature:SettingManagementEnable": "启用设置管理", "Feature:SettingManagementEnableDescription": "在应用程序中启用设置管理系统。", - "Feature:AllowChangingEmailSettings": "允许更改邮件设置。", + "Feature:AllowChangingEmailSettings": "允许更改邮件设置", "Feature:AllowChangingEmailSettingsDescription": "允许更改邮件设置。", "SmtpPasswordPlaceholder": "输入一个值以更新密码" } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json index b57a542842..3dea507456 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json @@ -35,7 +35,7 @@ "Feature:SettingManagementGroup": "設定管理", "Feature:SettingManagementEnable": "啟用設定管理", "Feature:SettingManagementEnableDescription": "在應用程序中啟用設定管理系統.", - "Feature:AllowChangingEmailSettings": "允許更改電子郵件設置。", + "Feature:AllowChangingEmailSettings": "允許更改電子郵件設置", "Feature:AllowChangingEmailSettingsDescription": "允許更改電子郵件設置。", "SmtpPasswordPlaceholder": "輸入一個值以更新密碼" } diff --git a/modules/virtual-file-explorer/app/package.json b/modules/virtual-file-explorer/app/package.json index 9087a91d1f..50866814bc 100644 --- a/modules/virtual-file-explorer/app/package.json +++ b/modules/virtual-file-explorer/app/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.1", - "@abp/virtual-file-explorer": "~10.1.0-rc.1" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.1.0-rc.2", + "@abp/virtual-file-explorer": "~10.1.0-rc.2" } } diff --git a/npm/lerna.json b/npm/lerna.json index 870bb88959..ee02c6794e 100644 --- a/npm/lerna.json +++ b/npm/lerna.json @@ -1,5 +1,5 @@ { - "version": "10.1.0-rc.1", + "version": "10.1.0-rc.2", "packages": [ "packs/*" ], diff --git a/npm/ng-packs/.gitignore b/npm/ng-packs/.gitignore index 9d756a8b67..84b48a798c 100644 --- a/npm/ng-packs/.gitignore +++ b/npm/ng-packs/.gitignore @@ -58,3 +58,5 @@ Thumbs.db .angular .nx/ + +vitest.config.*.timestamp* \ No newline at end of file diff --git a/npm/ng-packs/apps/dev-app/project.json b/npm/ng-packs/apps/dev-app/project.json index bea3bf6612..c397d8d449 100644 --- a/npm/ng-packs/apps/dev-app/project.json +++ b/npm/ng-packs/apps/dev-app/project.json @@ -175,10 +175,12 @@ "executor": "@nx/eslint:lint" }, "test": { - "executor": "@nx/jest:jest", + "executor": "@nx/vitest:test", "outputs": ["{workspaceRoot}/coverage/apps/dev-app"], "options": { - "jestConfig": "apps/dev-app/jest.config.ts" + "passWithNoTests": true, + "reportsDirectory": "../../coverage/apps/dev-app", + "silent": false } }, "serve-static": { diff --git a/npm/ng-packs/apps/dev-app/src/app/app.config.ts b/npm/ng-packs/apps/dev-app/src/app/app.config.ts index daa8a1f937..6b8bcf2c05 100644 --- a/npm/ng-packs/apps/dev-app/src/app/app.config.ts +++ b/npm/ng-packs/apps/dev-app/src/app/app.config.ts @@ -31,6 +31,10 @@ export const appConfig: ApplicationConfig = { registerLocaleFn: registerLocaleForEsBuild(), sendNullsAsQueryParam: false, skipGetAppConfiguration: false, + uiLocalization: { + enabled: true, + basePath: '/assets/localization', + }, }), ), provideAbpOAuth(), diff --git a/npm/ng-packs/apps/dev-app/src/app/app.routes.ts b/npm/ng-packs/apps/dev-app/src/app/app.routes.ts index 47462b7f8f..6c59a18bb1 100644 --- a/npm/ng-packs/apps/dev-app/src/app/app.routes.ts +++ b/npm/ng-packs/apps/dev-app/src/app/app.routes.ts @@ -6,6 +6,14 @@ export const appRoutes: Routes = [ pathMatch: 'full', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent), }, + { + path: 'dynamic-form', + loadComponent: () => import('./dynamic-form-page/dynamic-form-page.component').then(m => m.DynamicFormPageComponent), + }, + { + path: 'localization-test', + loadComponent: () => import('./localization-test/localization-test.component').then(m => m.LocalizationTestComponent), + }, { path: 'account', loadChildren: () => import('@abp/ng.account').then(m => m.createRoutes()), diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html new file mode 100644 index 0000000000..c675dca707 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html @@ -0,0 +1,111 @@ +
+
+
+

Dynamic Form Showcase

+

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

+
+
+
+ +
+
+
+
+

+ + User Registration Form +

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

Loading form configuration...

+
+ } +
+
+ + +
+
+
+ + Available Field Types +
+
+
+
+
+
Text Inputs
+
    +
  • Text
  • +
  • Email
  • +
  • Password
  • +
  • Tel
  • +
  • URL
  • +
  • Textarea
  • +
+
+
+
Special Inputs
+
    +
  • Number
  • +
  • Date
  • +
  • DateTime-Local
  • +
  • Time
  • +
  • Range
  • +
  • Color
  • +
+
+
+
Selection
+
    +
  • Select (Dropdown)
  • +
  • Radio
  • +
  • Checkbox
  • +
+
+
+
Files
+
    +
  • File (Single/Multiple)
  • +
+
+
+
Nested Forms
+
    +
  • Group (Nested Fields)
  • +
  • Array (Dynamic List)
  • +
+
+
+
+
+ + Features: Full validation support, conditional logic, nested forms (groups & arrays), + grid-based layout, ARIA accessibility, keyboard navigation, and screen reader support. +
+
+
+ + New: Try the nested forms below! Add/remove phone numbers dynamically, + or fill in your work experience. The Address section shows a grouped form. +
+
+
+
+
+
\ No newline at end of file diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts new file mode 100644 index 0000000000..0955a448e5 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts @@ -0,0 +1,38 @@ +import { Component, inject, OnInit, ViewChild } from '@angular/core'; +import { DynamicFormComponent, FormFieldConfig } from '@abp/ng.components/dynamic-form'; +import { FormConfigService } from './form-config.service'; + +@Component({ + selector: 'app-dynamic-form-page', + templateUrl: './dynamic-form-page.component.html', + imports: [DynamicFormComponent], +}) +export class DynamicFormPageComponent implements OnInit { + @ViewChild(DynamicFormComponent, { static: false }) dynamicFormComponent: DynamicFormComponent; + protected readonly formConfigService = inject(FormConfigService); + + formFields: FormFieldConfig[] = []; + + ngOnInit() { + this.formConfigService.getFormConfig().subscribe(config => { + this.formFields = config; + }); + } + + submit(formData: any) { + console.log('✅ Form Submitted Successfully!', formData); + console.table(formData); + + // Show success message + alert('✅ Form submitted successfully! Check the console for details.'); + + // Reset form after submission + this.dynamicFormComponent.resetForm(); + } + + cancel() { + console.log('❌ Form Cancelled'); + alert('Form cancelled'); + this.dynamicFormComponent.resetForm(); + } +} diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts new file mode 100644 index 0000000000..b88423f945 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/form-config.service.ts @@ -0,0 +1,381 @@ +import { Injectable } from '@angular/core'; +import { FormFieldConfig } from '@abp/ng.components/dynamic-form'; +import { Observable, of } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class FormConfigService { + getFormConfig(): Observable { + const formConfig: FormFieldConfig[] = [ + // Section 1: Basic Text Inputs + { + key: 'firstName', + label: 'First Name', + type: 'text', + placeholder: 'Enter your first name', + required: true, + gridSize: 6, + order: 1, + validators: [{ type: 'required', message: 'First name is required' }], + }, + { + key: 'lastName', + label: 'Last Name', + type: 'text', + placeholder: 'Enter your last name', + required: true, + gridSize: 6, + order: 2, + validators: [{ type: 'required', message: 'Last name is required' }], + }, + + // Section 2: Email & Password + { + key: 'email', + label: 'Email Address', + type: 'email', + placeholder: 'example@domain.com', + required: true, + gridSize: 6, + order: 3, + validators: [ + { type: 'required', message: 'Email is required' }, + { type: 'email', message: 'Invalid email address' }, + ], + }, + { + key: 'password', + label: 'Password', + type: 'password', + placeholder: 'Enter a strong password', + required: true, + gridSize: 6, + order: 4, + minLength: 8, + maxLength: 50, + validators: [ + { type: 'required', message: 'Password is required' }, + { type: 'minLength', value: 8, message: 'Password must be at least 8 characters' }, + ], + }, + + // Section 3: Contact Information + { + key: 'phone', + label: 'Phone Number', + type: 'tel', + placeholder: '555-123-4567', + gridSize: 6, + order: 5, + pattern: '[0-9]{3}-[0-9]{3}-[0-9]{4}', + }, + { + key: 'website', + label: 'Website', + type: 'url', + placeholder: 'https://example.com', + gridSize: 6, + order: 6, + }, + + // Section 4: Numbers & Dates + { + key: 'age', + label: 'Age', + type: 'number', + placeholder: 'Enter your age', + required: true, + gridSize: 4, + order: 7, + min: 18, + max: 100, + validators: [ + { type: 'required', message: 'Age is required' }, + { type: 'min', value: 18, message: 'You must be at least 18 years old' }, + ], + }, + { + key: 'birthdate', + label: 'Birth Date', + type: 'date', + required: true, + gridSize: 4, + order: 8, + max: new Date().toISOString().split('T')[0], + validators: [{ type: 'required', message: 'Birth date is required' }], + }, + { + key: 'appointmentTime', + label: 'Appointment Date & Time', + type: 'datetime-local', + gridSize: 4, + order: 9, + min: new Date().toISOString().slice(0, 16), + }, + + // Section 5: Select & Radio + { + key: 'country', + label: 'Country', + type: 'select', + required: true, + gridSize: 6, + order: 10, + options: { + defaultValues: [ + { key: 'usa', value: 'United States' }, + { key: 'uk', value: 'United Kingdom' }, + { key: 'canada', value: 'Canada' }, + { key: 'germany', value: 'Germany' }, + { key: 'france', value: 'France' }, + ], + }, + validators: [{ type: 'required', message: 'Country is required' }], + }, + { + key: 'gender', + label: 'Gender', + type: 'radio', + required: true, + gridSize: 6, + order: 11, + options: { + defaultValues: [ + { key: 'male', value: 'Male' }, + { key: 'female', value: 'Female' }, + { key: 'other', value: 'Other' }, + { key: 'prefer-not-to-say', value: 'Prefer not to say' }, + ], + }, + validators: [{ type: 'required', message: 'Gender is required' }], + }, + + // Section 6: Conditional Field (shown when country is USA) + { + key: 'state', + label: 'State (USA Only)', + type: 'select', + gridSize: 6, + order: 12, + options: { + defaultValues: [ + { key: 'ca', value: 'California' }, + { key: 'ny', value: 'New York' }, + { key: 'tx', value: 'Texas' }, + { key: 'fl', value: 'Florida' }, + ], + }, + conditionalLogic: [ + { dependsOn: 'country', condition: 'equals', value: 'usa', action: 'show' }, + ], + }, + + // Section 7: Time & Range + { + key: 'preferredTime', + label: 'Preferred Contact Time', + type: 'time', + gridSize: 6, + order: 13, + step: '900', // 15 minutes + }, + { + key: 'experienceLevel', + label: 'Experience Level (0-10)', + type: 'range', + gridSize: 6, + order: 14, + min: 0, + max: 10, + step: 1, + value: 5, + }, + + // Section 8: Color & File + { + key: 'favoriteColor', + label: 'Favorite Color', + type: 'color', + gridSize: 6, + order: 15, + value: '#007bff', + }, + { + key: 'profilePicture', + label: 'Profile Picture', + type: 'file', + gridSize: 6, + order: 16, + accept: 'image/*', + multiple: false, + }, + + // Section 9: Textarea & Checkbox + { + key: 'bio', + label: 'Biography', + type: 'textarea', + placeholder: 'Tell us about yourself...', + gridSize: 12, + order: 17, + maxLength: 500, + }, + { + key: 'newsletter', + label: 'Subscribe to newsletter', + type: 'checkbox', + gridSize: 6, + order: 18, + }, + { + key: 'terms', + label: 'I agree to the terms and conditions', + type: 'checkbox', + required: true, + gridSize: 6, + order: 19, + validators: [{ type: 'requiredTrue', message: 'You must agree to the terms' }], + }, + + // Section 10: NESTED FORM - Phone Numbers (Array) + { + key: 'phoneNumbers', + type: 'array', + label: 'Phone Numbers', + gridSize: 12, + order: 20, + minItems: 1, + maxItems: 5, + children: [ + { + key: 'type', + type: 'select', + label: 'Type', + gridSize: 4, + required: true, + options: { + defaultValues: [ + { key: 'mobile', value: 'Mobile' }, + { key: 'home', value: 'Home' }, + { key: 'work', value: 'Work' }, + { key: 'other', value: 'Other' } + ] + }, + validators: [{ type: 'required', message: 'Phone type is required' }], + }, + { + key: 'number', + type: 'tel', + label: 'Number', + gridSize: 8, + required: true, + placeholder: '555-123-4567', + validators: [{ type: 'required', message: 'Phone number is required' }], + }, + ] + }, + + // Section 11: NESTED FORM - Work Experience (Array with nested group) + { + key: 'workExperience', + type: 'array', + label: 'Work Experience', + gridSize: 12, + order: 21, + minItems: 0, + maxItems: 10, + children: [ + { + key: 'company', + type: 'text', + label: 'Company Name', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'Company name is required' }], + }, + { + key: 'position', + type: 'text', + label: 'Position', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'Position is required' }], + }, + { + key: 'startDate', + type: 'date', + label: 'Start Date', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'Start date is required' }], + }, + { + key: 'endDate', + type: 'date', + label: 'End Date', + gridSize: 6, + }, + { + key: 'currentJob', + type: 'checkbox', + label: 'Currently working here', + gridSize: 12, + }, + { + key: 'description', + type: 'textarea', + label: 'Job Description', + placeholder: 'Describe your responsibilities...', + gridSize: 12, + maxLength: 500, + }, + ] + }, + + // Section 12: NESTED FORM - Address Group (Group type) + { + key: 'address', + type: 'group', + label: 'Address Information', + gridSize: 12, + order: 22, + children: [ + { + key: 'street', + type: 'text', + label: 'Street Address', + gridSize: 8, + placeholder: '123 Main St', + }, + { + key: 'apartment', + type: 'text', + label: 'Apt/Suite', + gridSize: 4, + placeholder: 'Apt 4B', + }, + { + key: 'city', + type: 'text', + label: 'City', + gridSize: 6, + required: true, + validators: [{ type: 'required', message: 'City is required' }], + }, + { + key: 'zipCode', + type: 'text', + label: 'ZIP Code', + gridSize: 6, + required: true, + pattern: '[0-9]{5}', + validators: [{ type: 'required', message: 'ZIP code is required' }], + }, + ] + }, + ]; + + return of(formConfig); + } +} diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html index 1a0d46c2da..d8b08c06ae 100644 --- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html +++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html @@ -1,4 +1,8 @@
+
+

{{ '::Welcome' | abpLocalization }}

{{ '::LongWelcomeMessage' | abpLocalization }}

@if (!hasLoggedIn) { - - {{ 'AbpAccount::Login' | abpLocalization }} - + + {{ 'AbpAccount::Login' | abpLocalization }} + }
@@ -30,8 +29,7 @@
- + "> - + "> - + ">
- + ">

- - + + "> - + style="width: 72px; height: 28px; border: none; display: inline-block">

- + "> - + ">
@@ -199,8 +180,7 @@

- + "> - + "> - + "> - + "> - + "> - + ">
@@ -301,12 +264,11 @@

@if (context.customTemplate) { - + } @for (link of context.links; track $index) { - {{ link.label }} + {{ link.label }} }
@@ -318,9 +280,8 @@
- Details + Details
@@ -340,4 +301,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/app/localization-test/localization-test.component.ts b/npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts new file mode 100644 index 0000000000..187f6d1e37 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts @@ -0,0 +1,58 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { LocalizationPipe, UILocalizationService, SessionStateService } from '@abp/ng.core'; +import { CommonModule } from '@angular/common'; +import { CardComponent, CardBodyComponent } from '@abp/ng.theme.shared'; +import { AsyncPipe } from '@angular/common'; + +@Component({ + selector: 'app-localization-test', + standalone: true, + imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent, AsyncPipe], + template: ` +
+

Hybrid Localization Test

+ + + +
Backend Localization (if available)
+

MyProjectName::Welcome: {{ 'MyProjectName::Welcome' | abpLocalization }}

+

AbpAccount::Login: {{ 'AbpAccount::Login' | abpLocalization }}

+
+
+ + + +
UI Localization (from /assets/localization/{{ currentLanguage$ | async }}.json)
+

MyProjectName::CustomKey: {{ 'MyProjectName::CustomKey' | abpLocalization }}

+

MyProjectName::TestMessage: {{ 'MyProjectName::TestMessage' | abpLocalization }}

+
+
+ + + +
UI Override (UI > Backend Priority)
+

AbpAccount::Login: {{ 'AbpAccount::Login' | abpLocalization }}

+

If backend has "Login", UI version should override it

+
+
+ + + +
Loaded UI Localizations
+
{{ loadedLocalizations | json }}
+
+
+
+ `, +}) +export class LocalizationTestComponent implements OnInit { + private uiLocalizationService = inject(UILocalizationService); + private sessionState = inject(SessionStateService); + + loadedLocalizations: any = {}; + currentLanguage$ = this.sessionState.getLanguage$(); + + ngOnInit() { + this.loadedLocalizations = this.uiLocalizationService.getLoadedLocalizations(); + } +} 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/assets/localization/en.json b/npm/ng-packs/apps/dev-app/src/assets/localization/en.json new file mode 100644 index 0000000000..579709ebd0 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/assets/localization/en.json @@ -0,0 +1,10 @@ +{ + "MyProjectName": { + "Welcome": "Welcome from UI (en.json)", + "CustomKey": "This is a UI-only localization", + "TestMessage": "UI localization is working!" + }, + "AbpAccount": { + "Login": "Sign In (UI Override)" + } +} diff --git a/npm/ng-packs/apps/dev-app/src/assets/localization/tr.json b/npm/ng-packs/apps/dev-app/src/assets/localization/tr.json new file mode 100644 index 0000000000..1d565a14d2 --- /dev/null +++ b/npm/ng-packs/apps/dev-app/src/assets/localization/tr.json @@ -0,0 +1,10 @@ +{ + "MyProjectName": { + "Welcome": "UI'dan Hoş Geldiniz (tr.json)", + "CustomKey": "Bu sadece UI'da olan bir localization", + "TestMessage": "UI localization çalışıyor!" + }, + "AbpAccount": { + "Login": "Giriş Yap (UI Override)" + } +} 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 c5c3b698c7..af6b753ca3 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.1.0-rc.1", - "@abp/utils": "~10.1.0-rc.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", @@ -56,6 +58,7 @@ "@angular-eslint/eslint-plugin-template": "~21.0.0", "@angular-eslint/template-parser": "~21.0.0", "@angular/animations": "21.0.0", + "@angular/aria": "~21.1.0", "@angular/build": "~21.0.0", "@angular/cli": "~21.0.0", "@angular/common": "~21.0.0", @@ -80,23 +83,26 @@ "@nx/eslint": "~22.2.0", "@nx/eslint-plugin": "~22.2.0", "@nx/jest": "~22.2.0", - "@nx/js": "~22.2.0", + "@nx/js": "22.2.7", "@nx/plugin": "~22.2.0", + "@nx/vite": "22.2.7", + "@nx/vitest": "22.2.7", "@nx/web": "~22.2.0", "@nx/workspace": "~22.2.0", "@popperjs/core": "~2.11.0", "@schematics/angular": "~21.0.0", "@swc-node/register": "1.9.2", "@swc/cli": "0.6.0", - "@swc/core": "~1.5.0", - "@swc/helpers": "~0.5.0", + "@swc/core": "~1.5.7", + "@swc/helpers": "~0.5.11", "@swimlane/ngx-datatable": "~22.0.0", "@types/express": "~5.0.0", "@types/jest": "29.5.14", - "@types/node": "~20.11.0", + "@types/node": "20.19.9", "@typescript-eslint/eslint-plugin": "7.16.0", "@typescript-eslint/parser": "7.16.0", "@typescript-eslint/utils": "^7.16.0", + "@vitest/coverage-v8": "^4.0.0", "angular-oauth2-oidc": "~20.0.0", "autoprefixer": "^10.4.21", "bootstrap": "~5.0.0", @@ -114,6 +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", @@ -137,6 +144,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": { @@ -147,4 +156,4 @@ "dependencies": { "openid-client": "^6.6.4" } -} +} \ No newline at end of file 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 56b3340a19..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.1.0-rc.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.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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 41903ae20c..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.1.0-rc.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.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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/components/manage-profile/manage-profile.component.html b/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html index 6eff5e9ed6..91167b0ede 100644 --- a/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html +++ b/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html @@ -5,61 +5,47 @@ @if (profile$ | async) { -
- @if (selectedTab === 0) { -
-
-

- {{ 'AbpIdentity::ChangePassword' | abpLocalization }} -
-

- + @if (selectedTab === 0) { +
+
+

+ {{ 'AbpIdentity::ChangePassword' | abpLocalization }} +
+

+ -
-
- } - @if (selectedTab === 1) { -
-
-

- {{ 'AbpIdentity::PersonalSettings' | abpLocalization }} -
-

- +
+
+ } + @if (selectedTab === 1) { +
+
+

+ {{ 'AbpIdentity::PersonalSettings' | abpLocalization }} +
+

+ -
-
- } + }"> +
+ } +
} - + \ No newline at end of file diff --git a/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts b/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts index c9095f63da..5be91d34f7 100644 --- a/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts @@ -1,10 +1,9 @@ import { ProfileService } from '@abp/ng.account.core/proxy'; -import { fadeIn, LoadingDirective } from '@abp/ng.theme.shared'; -import { transition, trigger, useAnimation } from '@angular/animations'; +import { LoadingDirective } from '@abp/ng.theme.shared'; import { Component, inject, OnInit } from '@angular/core'; import { eAccountComponents } from '../../enums/components'; import { ManageProfileStateService } from '../../services/manage-profile.state.service'; -import { NgClass, AsyncPipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { LocalizationPipe, ReplaceableTemplateDirective } from '@abp/ng.core'; import { PersonalSettingsComponent } from '../personal-settings/personal-settings.component'; @@ -13,24 +12,34 @@ import { ChangePasswordComponent } from '../change-password/change-password.comp @Component({ selector: 'abp-manage-profile', templateUrl: './manage-profile.component.html', - animations: [trigger('fadeIn', [transition(':enter', useAnimation(fadeIn))])], styles: [ - //TODO: move static styles ` .min-h-400 { min-height: 400px; } + + .fade-in { + animation: fadeIn 350ms ease both; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } `, ], imports: [ - NgClass, AsyncPipe, ReactiveFormsModule, PersonalSettingsComponent, ChangePasswordComponent, LocalizationPipe, ReplaceableTemplateDirective, - LoadingDirective, + LoadingDirective ], }) export class ManageProfileComponent implements OnInit { 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/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html index 732a81c76b..2f9df1ae00 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.html @@ -1,177 +1,113 @@ @switch (getComponent(prop)) { - @case ('template') { - - } + @case ('template') { + + } } -
+
@switch (getComponent(prop)) { - @case ('input') { - - - } - @case ('hidden') { - - } - @case ('checkbox') { -
- - -
- } - @case ('select') { - - - } - @case ('multiselect') { - - - } - @case ('typeahead') { - -
- - -
- } - @case ('date') { - - - } - @case ('time') { - - - } - @case ('dateTime') { - - - } - @case ('textarea') { - - + @case ('input') { + + + } + @case ('hidden') { + + } + @case ('checkbox') { +
+ + +
+ } + @case ('select') { + + -
+ +
+ } } @if (prop.formText) { - {{ prop.formText | abpLocalization }} + {{ prop.formText | abpLocalization }} }
- + \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts index 696ce633a7..09a29a98a9 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts @@ -49,7 +49,7 @@ import { eExtensibleComponents } from '../../enums/components'; import { ExtensibleDateTimePickerComponent } from '../date-time-picker/extensible-date-time-picker.component'; import { NgxValidateCoreModule } from '@ngx-validate/core'; import { ExtensibleFormPropService } from '../../services/extensible-form-prop.service'; -import { AsyncPipe, NgClass, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; +import { AsyncPipe, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-form-multiselect.component'; @@ -70,11 +70,10 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f PermissionDirective, LocalizationPipe, AsyncPipe, - NgClass, NgComponentOutlet, NgTemplateOutlet, - FormsModule, - ], + FormsModule +], changeDetection: ChangeDetectionStrategy.OnPush, providers: [ExtensibleFormPropService], viewProviders: [ diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html index b88c2ecbfa..be937e21ca 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.html @@ -1,46 +1,35 @@ @if (form) { - @for (groupedProp of groupedPropList.items; track i; let i = $index; let first = $first) { - - @if (isAnyGroupMemberVisible(i, data) && groupedProp.group?.className) { -
- - -
- } @else { - - - } +@for (groupedProp of groupedPropList.items; track i; let i = $index; let first = $first) { + + @if (isAnyGroupMemberVisible(i, data) && groupedProp.group?.className) { +
+ +
+ } @else { + + } +
+} } @for (prop of groupedProp.formPropList; let index = $index; let first = $first; track prop.name) { - @if (prop.visible(data)) { - @if (extraProperties.controls[prop.name]) { - - - - } @else { - @if (form.get(prop.name)) { - - } - } - } + @if (prop.visible(data)) { + @if (extraProperties.controls[prop.name]) { + + + + } @else { + @if (form.get(prop.name)) { + + } + } + } } - + \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts index 9d81502291..8b5829754d 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts @@ -17,14 +17,14 @@ import { ExtensionsService } from '../../services/extensions.service'; import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; import { selfFactory } from '../../utils/factory.util'; import { ExtensibleFormPropComponent } from './extensible-form-prop.component'; -import { NgClass, NgTemplateOutlet } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { PropDataDirective } from '../../directives/prop-data.directive'; @Component({ exportAs: 'abpExtensibleForm', selector: 'abp-extensible-form', templateUrl: './extensible-form.component.html', - imports: [NgClass, NgTemplateOutlet, PropDataDirective, ReactiveFormsModule, ExtensibleFormPropComponent], + imports: [NgTemplateOutlet, PropDataDirective, ReactiveFormsModule, ExtensibleFormPropComponent], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts new file mode 100644 index 0000000000..aabbd2a255 --- /dev/null +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts @@ -0,0 +1,10 @@ +import { Component, contentChild, input, TemplateRef } from '@angular/core'; + +@Component({ + selector: 'abp-extensible-table-row-detail', + template: '', +}) +export class ExtensibleTableRowDetailComponent { + readonly rowHeight = input('100%'); + readonly template = contentChild(TemplateRef<{ row: R; expanded: boolean }>); +} diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts new file mode 100644 index 0000000000..e0666787b4 --- /dev/null +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts @@ -0,0 +1 @@ +export * from './extensible-table-row-detail.component'; diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html index 8581f6c4d4..8b0a5744b2 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html @@ -1,21 +1,30 @@ @if (isBrowser) { - -@if(selectable) { - + + @if (effectiveRowDetailTemplate) { + + + + + + + + + + + + } + @if(selectable) { + @@ -44,97 +53,67 @@ } @if (actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)) { - - - - - @if (isVisibleActions(row)) { - - } - + + + + + @if (isVisibleActions(row)) { + + } - + + } @for (prop of propList; track prop.name; let i = $index) { - - - @if (prop.tooltip) { - - {{ column.name }} - - } @else { - - {{ column.name }} - - } - - - - - @if (!row['_' + prop.name].component) { - @if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') { -
+ + @if (prop.tooltip) { + + {{ column.name }} + + } @else { + + {{ column.name }} + + } + + + + + @if (!row['_' + prop.name].component) { + @if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') { +
- } @else { -
+ } @else { +
- } - } @else { -
+ } + } @else { + - } -
+ ">
+ }
- - +
+ + } -} +} \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts index c70d2b39e7..75253b1f02 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts @@ -4,6 +4,7 @@ import { ChangeDetectorRef, Component, computed, + ContentChild, EventEmitter, inject, Injector, @@ -17,13 +18,14 @@ import { SimpleChanges, TemplateRef, TrackByFunction, + ViewChild, } from '@angular/core'; import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import { NgxDatatableModule, SelectionType } from '@swimlane/ngx-datatable'; +import { NgxDatatableModule, SelectionType, DatatableComponent } from '@swimlane/ngx-datatable'; import { ABP, @@ -53,6 +55,8 @@ import { ROW_RECORD, } from '../../tokens/extensions.token'; import { GridActionsComponent } from '../grid-actions/grid-actions.component'; +import { ExtensibleTableRowDetailComponent } from './extensible-table-row-detail'; +import { RowDetailContext } from '../../models/row-detail'; const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; @@ -75,6 +79,12 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; ], templateUrl: './extensible-table.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + styles: [` + :host ::ng-deep .ngx-datatable.material .datatable-body .datatable-row-detail { + background: none; + padding: 0; + } + `], }) export class ExtensibleTableComponent implements OnChanges, AfterViewInit, OnDestroy { readonly #injector = inject(Injector); @@ -127,6 +137,23 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn @Output() loadMore = new EventEmitter(); @Input() tableHeight: number; + @Input() rowDetailTemplate?: TemplateRef>; + @Input() rowDetailHeight: string | number = '100%'; + @Output() rowDetailToggle = new EventEmitter(); + + @ContentChild(ExtensibleTableRowDetailComponent) + rowDetailComponent?: ExtensibleTableRowDetailComponent; + + @ViewChild('table', { static: false }) table!: DatatableComponent; + + protected get effectiveRowDetailTemplate(): TemplateRef> | undefined { + return this.rowDetailComponent?.template() ?? this.rowDetailTemplate; + } + + protected get effectiveRowDetailHeight(): string | number { + return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight; + } + hasAtLeastOnePermittedAction: boolean; readonly propList: EntityPropList; @@ -290,6 +317,13 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn return this.tableHeight ? `${this.tableHeight}px` : 'auto'; } + toggleExpandRow(row: R): void { + if (this.table && this.table.rowDetail) { + this.table.rowDetail.toggleExpandRow(row); + } + this.rowDetailToggle.emit(row); + } + ngAfterViewInit(): void { if (!this.infiniteScroll) { this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.html index 606a6de12a..57a8b4a7fe 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.html @@ -1,88 +1,55 @@ @if (actionList.length > 1) { -
- -
- @for (action of actionList; track action.text) { - - - } -
+
+ +
+ @for (action of actionList; track action.text) { + + + }
+
} @if (actionList.length === 1) { - + } @if (action.visible(data)) { - + } - + @if (!action.showOnlyIcon) { - @if (action.icon) { - {{ action.text | abpLocalization }} - } @else { -
{{ action.text | abpLocalization }}
- } + @if (action.icon) { + {{ action.text | abpLocalization }} + } @else { +
{{ action.text | abpLocalization }}
+ } }
@if (action.visible(data)) { - @if (action.tooltip) { - - } @else { - - } + @if (action.tooltip) { + + } @else { + } - + } + \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts index 48bea482ca..9f7c28201e 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts @@ -1,8 +1,8 @@ -import { - ChangeDetectionStrategy, - Component, - Input, - TrackByFunction, +import { + ChangeDetectionStrategy, + Component, + Input, + TrackByFunction, } from '@angular/core'; import { EntityAction, EntityActionList } from '../../models/entity-actions'; import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; @@ -10,7 +10,7 @@ import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.c import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { LocalizationPipe, PermissionDirective } from '@abp/ng.core'; import { EllipsisDirective } from '@abp/ng.theme.shared'; -import { NgClass, NgTemplateOutlet } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; @Component({ exportAs: 'abpGridActions', @@ -18,7 +18,6 @@ import { NgClass, NgTemplateOutlet } from '@angular/common'; NgbDropdownModule, EllipsisDirective, PermissionDirective, - NgClass, LocalizationPipe, NgTemplateOutlet, NgbTooltipModule, diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts index 149043089b..ee501c875d 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/index.ts @@ -2,6 +2,7 @@ export * from './abstract-actions/abstract-actions.component'; export * from './extensible-form/extensible-form.component'; export * from './extensible-form/extensible-form-prop.component'; export * from './extensible-table/extensible-table.component'; +export * from './extensible-table/extensible-table-row-detail'; export * from './date-time-picker/extensible-date-time-picker.component'; export * from './grid-actions/grid-actions.component'; export * from './page-toolbar/page-toolbar.component'; diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.html index c39bf59dd1..747ee8963a 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.html @@ -4,26 +4,20 @@ @if (action.visible(data)) { @if (action.component; as component) { - + }@else { - @if (asToolbarAction(action).value; as toolbarAction ) { - - } + @if (asToolbarAction(action).value; as toolbarAction ) { + + } } }
} - - + \ No newline at end of file diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts index 9dadb7497a..ada52513f3 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts @@ -10,7 +10,7 @@ import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; import { CreateInjectorPipe } from '../../pipes/create-injector.pipe'; import { LocalizationPipe, PermissionDirective } from '@abp/ng.core'; -import { NgClass, NgComponentOutlet } from '@angular/common'; +import { NgComponentOutlet } from '@angular/common'; @Component({ exportAs: 'abpPageToolbar', @@ -19,7 +19,6 @@ import { NgClass, NgComponentOutlet } from '@angular/common'; CreateInjectorPipe, PermissionDirective, LocalizationPipe, - NgClass, NgComponentOutlet, ], templateUrl: './page-toolbar.component.html', @@ -33,8 +32,7 @@ import { NgClass, NgComponentOutlet } from '@angular/common'; }) export class PageToolbarComponent extends AbstractActionsComponent> - implements HasCreateInjectorPipe -{ + implements HasCreateInjectorPipe { readonly injector: Injector; defaultBtnClass = 'btn btn-sm btn-primary'; @@ -48,7 +46,7 @@ export class PageToolbarComponent const injector = inject(Injector); super(); - + this.injector = injector; } diff --git a/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts b/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts index 26741c2832..9f3ebd775b 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts @@ -13,6 +13,7 @@ import { ExtensibleFormComponent, ExtensibleFormPropComponent, ExtensibleTableComponent, + ExtensibleTableRowDetailComponent, GridActionsComponent, PageToolbarComponent, ExtensibleDateTimePickerComponent, @@ -32,6 +33,7 @@ const importWithExport = [ CreateInjectorPipe, ExtensibleFormComponent, ExtensibleTableComponent, + ExtensibleTableRowDetailComponent, ExtensibleFormMultiselectComponent, ]; @@ -50,4 +52,4 @@ const importWithExport = [ ], exports: [...importWithExport], }) -export class ExtensibleModule {} +export class ExtensibleModule { } diff --git a/npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts b/npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts new file mode 100644 index 0000000000..c344e9add7 --- /dev/null +++ b/npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts @@ -0,0 +1,4 @@ +export interface RowDetailContext { + row: R; + expanded: boolean; +} diff --git a/npm/ng-packs/packages/components/extensible/src/public-api.ts b/npm/ng-packs/packages/components/extensible/src/public-api.ts index e5c7883738..6d53f8ba9a 100644 --- a/npm/ng-packs/packages/components/extensible/src/public-api.ts +++ b/npm/ng-packs/packages/components/extensible/src/public-api.ts @@ -2,6 +2,7 @@ export * from './lib/components/date-time-picker/extensible-date-time-picker.com export * from './lib/components/extensible-form/extensible-form-prop.component'; export * from './lib/components/extensible-form/extensible-form.component'; export * from './lib/components/extensible-table/extensible-table.component'; +export * from './lib/components/extensible-table/extensible-table-row-detail'; export * from './lib/components/grid-actions/grid-actions.component'; export * from './lib/components/page-toolbar/page-toolbar.component'; export * from './lib/components/multi-select'; @@ -68,4 +69,5 @@ export * from './lib/utils/form-props.util'; export * from './lib/utils/props.util'; export * from './lib/utils/state.util'; export * from './lib/utils/model.utils'; +export * from './lib/models/row-detail'; export * from './lib/extensible.module'; 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.ts b/npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts index b78bfa19ce..6b4f475edd 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 @@ -12,7 +12,7 @@ import { inject, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { LocalizationPipe } from '@abp/ng.core'; import { Subject, Observable, debounceTime, distinctUntilChanged, of, finalize } from 'rxjs'; @@ -29,7 +29,7 @@ export type LookupSearchFn = (filter: string) => Observable selector: 'abp-lookup-search', templateUrl: './lookup-search.component.html', styleUrl: './lookup-search.component.scss', - imports: [CommonModule, FormsModule, LocalizationPipe], + imports: [FormsModule, LocalizationPipe, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, }) export class LookupSearchComponent implements OnInit { diff --git a/npm/ng-packs/packages/components/package.json b/npm/ng-packs/packages/components/package.json index 6fdbd3d622..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.1.0-rc.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.1.0-rc.1", - "@abp/ng.theme.shared": ">=10.1.0-rc.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 03ad0f5524..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.1.0-rc.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.1.0-rc.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/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 704fde958a..1506dc3a8b 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -16,6 +16,26 @@ export namespace ABP { othersGroup?: string; dynamicLayouts?: Map; disableProjectNameInTitle?: boolean; + uiLocalization?: UILocalizationOptions; + } + + export interface UILocalizationOptions { + /** + * Enable UI localization feature + * When enabled, localization files are automatically loaded based on selected language + * Files should be located at: {basePath}/{culture}.json + * Example: /assets/localization/en.json + * JSON format: { "ResourceName": { "Key": "Value" } } + * Merges with backend localizations (UI > Backend priority) + */ + enabled?: boolean; + /** + * Base path for localization JSON files + * Default: '/assets/localization' + * Files should be located at: {basePath}/{culture}.json + * Example: /assets/localization/en.json + */ + basePath?: string; } export interface Child { diff --git a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts index 6d46caedeb..08f1beb706 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts @@ -23,7 +23,12 @@ import { RoutesHandler } from '../handlers'; import { ABP, SortableItem } from '../models'; import { AuthErrorFilterService } from '../abstracts'; import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants'; -import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services'; +import { + LocalizationService, + LocalStorageListenerService, + AbpTitleStrategy, + UILocalizationService, +} from '../services'; import { DefaultQueueManager, getInitialData } from '../utils'; import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './'; import { timezoneInterceptor, transferStateInterceptor } from '../interceptors'; @@ -113,6 +118,11 @@ export function provideAbpCore(...features: CoreFeature[]) { inject(LocalizationService); inject(LocalStorageListenerService); inject(RoutesHandler); + // Initialize UILocalizationService if UI-only mode is enabled + const options = inject(CORE_OPTIONS); + if (options?.uiLocalization?.enabled) { + inject(UILocalizationService); + } await getInitialData(); }), LocaleProvider, diff --git a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts index 9dc35fa7b6..4e1b115b7c 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts @@ -6,8 +6,8 @@ import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) -export class AbpApplicationConfigurationService { - private restService = inject(RestService); +export class AbpApplicationConfigurationService { + private restService = inject(RestService); apiName = 'abp'; diff --git a/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts b/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts index 1243e8b2ba..a0d2c561e9 100644 --- a/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts @@ -10,6 +10,7 @@ import { } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models'; import { INCUDE_LOCALIZATION_RESOURCES_TOKEN } from '../tokens/include-localization-resources.token'; import { InternalStore } from '../utils/internal-store-utils'; +import { EnvironmentService } from './environment.service'; @Injectable({ providedIn: 'root', @@ -17,6 +18,7 @@ import { InternalStore } from '../utils/internal-store-utils'; export class ConfigStateService { private abpConfigService = inject(AbpApplicationConfigurationService); private abpApplicationLocalizationService = inject(AbpApplicationLocalizationService); + private environmentService = inject(EnvironmentService); private readonly includeLocalizationResources = inject(INCUDE_LOCALIZATION_RESOURCES_TOKEN, { optional: true }); private updateSubject = new Subject(); @@ -59,7 +61,18 @@ export class ConfigStateService { this.uiCultureFromAuthCodeFlow ?? appState.localization.currentCulture.cultureName; return this.getlocalizationResource(cultureName).pipe( - map(result => ({ ...appState, localization: { ...appState.localization, ...result } })), + map(result => { + const envLocalization = this.environmentService.getEnvironment()?.localization; + return { + ...appState, + localization: { + ...appState.localization, + ...result, + defaultResourceName: + appState.localization?.defaultResourceName ?? envLocalization?.defaultResourceName, + }, + }; + }), tap(() => (this.uiCultureFromAuthCodeFlow = undefined)), ); } @@ -83,9 +96,18 @@ export class ConfigStateService { return this.getlocalizationResource(lang) .pipe( - tap(result => - this.store.patch({ localization: { ...this.store.state.localization, ...result } }), - ), + tap(result => { + const envLocalization = this.environmentService.getEnvironment()?.localization; + this.store.patch({ + localization: { + ...this.store.state.localization, + ...result, + defaultResourceName: + this.store.state.localization?.defaultResourceName ?? + envLocalization?.defaultResourceName, + }, + }); + }), ) .pipe(map(() => null)); } diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index 61fd1ef480..205fd2f0dc 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -8,6 +8,7 @@ export * from './http-wait.service'; export * from './lazy-load.service'; export * from './list.service'; export * from './localization.service'; +export * from './ui-localization.service'; export * from './multi-tenancy.service'; export * from './permission.service'; export * from './replaceable-components.service'; 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/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index d91679effb..0e27973a90 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -60,6 +60,8 @@ export class LocalizationService { private initLocalizationValues() { localizations$.subscribe(val => this.addLocalization(val)); + // Backend-based localization loading (always enabled) + // UI localizations are merged via addLocalization() (UI > Backend priority) const legacyResources$ = this.configState.getDeep$('localization.values') as Observable< Record> >; @@ -90,7 +92,8 @@ export class LocalizationService { const resourceName = entry[0]; const remoteTexts = entry[1]; let resource = local?.get(resourceName) || {}; - resource = { ...resource, ...remoteTexts }; + // UI > Backend priority: local texts override remote texts + resource = { ...remoteTexts, ...resource }; local?.set(resourceName, resource); }); 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/services/ui-localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts new file mode 100644 index 0000000000..3bbbfa71aa --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts @@ -0,0 +1,119 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, distinctUntilChanged, switchMap, of } from 'rxjs'; +import { catchError, shareReplay, tap } from 'rxjs/operators'; +import { ABP } from '../models/common'; +import { LocalizationService } from './localization.service'; +import { SessionStateService } from './session-state.service'; +import { CORE_OPTIONS } from '../tokens/options.token'; + +export interface UILocalizationResource { + [resourceName: string]: Record; +} + +/** + * Service for managing UI localizations in ABP Angular applications. + * Automatically loads localization files based on selected language + * Merges with backend localizations (UI > Backend priority) + */ +@Injectable({ providedIn: 'root' }) +export class UILocalizationService { + private http = inject(HttpClient); + private localizationService = inject(LocalizationService); + private sessionState = inject(SessionStateService); + private options = inject(CORE_OPTIONS); + + private loadedLocalizations$ = new BehaviorSubject>({}); + + private currentLanguage$ = this.sessionState.getLanguage$(); + + constructor() { + const uiLocalization = this.options.uiLocalization; + if (uiLocalization?.enabled) { + this.subscribeToLanguageChanges(); + } + } + + private subscribeToLanguageChanges() { + this.currentLanguage$ + .pipe( + distinctUntilChanged(), + switchMap(culture => this.loadLocalizationFile(culture)) + ) + .subscribe(); + } + + private loadLocalizationFile(culture: string) { + const config = this.options.uiLocalization; + if (!config?.enabled) return of(null); + + const basePath = config.basePath || '/assets/localization'; + const url = `${basePath}/${culture}.json`; + + return this.http.get(url).pipe( + catchError(() => { + // If file not found or error occurs, return null + return of(null); + }), + tap(data => { + if (data) { + this.processLocalizationData(culture, data); + } + }), + ); + } + + private processLocalizationData(culture: string, data: UILocalizationResource) { + const abpFormat: ABP.Localization[] = [ + { + culture, + resources: Object.entries(data).map(([resourceName, texts]) => ({ + resourceName, + texts, + })), + }, + ]; + this.localizationService.addLocalization(abpFormat); + + const current = this.loadedLocalizations$.value; + current[culture] = data; + this.loadedLocalizations$.next(current); + } + + addAngularLocalizeLocalization( + culture: string, + resourceName: string, + translations: Record, + ): void { + const abpFormat: ABP.Localization[] = [ + { + culture, + resources: [ + { + resourceName, + texts: translations, + }, + ], + }, + ]; + this.localizationService.addLocalization(abpFormat); + + const current = this.loadedLocalizations$.value; + if (!current[culture]) { + current[culture] = {}; + } + if (!current[culture][resourceName]) { + current[culture][resourceName] = {}; + } + current[culture][resourceName] = { + ...current[culture][resourceName], + ...translations, + }; + this.loadedLocalizations$.next(current); + } + + getLoadedLocalizations(culture?: string): UILocalizationResource { + const lang = culture || this.sessionState.getLanguage(); + return this.loadedLocalizations$.value[lang] || {}; + } +} 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/lib/tests/ui-localization.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/ui-localization.service.spec.ts new file mode 100644 index 0000000000..abb15217c1 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/ui-localization.service.spec.ts @@ -0,0 +1,160 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { of, Subject, throwError } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { UILocalizationService } from '../services/ui-localization.service'; +import { LocalizationService } from '../services/localization.service'; +import { SessionStateService } from '../services/session-state.service'; +import { CORE_OPTIONS } from '../tokens/options.token'; + +describe('UILocalizationService', () => { + let spectator: SpectatorService; + let service: UILocalizationService; + let language$: Subject; + let httpGet: ReturnType; + let addLocalizationSpy: ReturnType; + + const createService = createServiceFactory({ + service: UILocalizationService, + mocks: [HttpClient, LocalizationService], + providers: [ + { + provide: SessionStateService, + useFactory: () => { + let currentLanguage = 'en'; + language$ = new Subject(); + language$.subscribe(lang => { + currentLanguage = lang; + }); + return { + getLanguage: vi.fn(() => currentLanguage), + getLanguage$: vi.fn(() => language$.asObservable()), + }; + }, + }, + { + provide: CORE_OPTIONS, + useValue: { + uiLocalization: { + enabled: true, + basePath: '/assets/localization', + }, + }, + }, + ], + }); + + beforeEach(() => { + spectator = createService(); + service = spectator.service; + const http = spectator.inject(HttpClient); + const localizationService = spectator.inject(LocalizationService); + httpGet = vi.fn(); + (http as any).get = httpGet; + addLocalizationSpy = vi.fn(); + (localizationService as any).addLocalization = addLocalizationSpy; + }); + + describe('when uiLocalization is enabled', () => { + it('should load localization file when language changes', () => { + const uiData = { MyApp: { Welcome: 'Welcome from UI' } }; + httpGet.mockReturnValue(of(uiData)); + + language$.next('en'); + + expect(httpGet).toHaveBeenCalledWith('/assets/localization/en.json'); + expect(addLocalizationSpy).toHaveBeenCalledWith([ + { + culture: 'en', + resources: [{ resourceName: 'MyApp', texts: { Welcome: 'Welcome from UI' } }], + }, + ]); + }); + + it('should use default basePath when not provided', () => { + expect(httpGet).not.toHaveBeenCalled(); + httpGet.mockReturnValue(of({})); + language$.next('en'); + expect(httpGet).toHaveBeenCalledWith('/assets/localization/en.json'); + }); + + it('should not call addLocalization when file is missing (HTTP error)', () => { + httpGet.mockReturnValue(throwError(() => new Error('404'))); + + language$.next('fr'); + + expect(httpGet).toHaveBeenCalledWith('/assets/localization/fr.json'); + expect(addLocalizationSpy).not.toHaveBeenCalled(); + }); + + it('should cache loaded data in getLoadedLocalizations', () => { + const uiData = { AbpAccount: { Login: 'Sign In (UI)' } }; + httpGet.mockReturnValue(of(uiData)); + + language$.next('en'); + + const loaded = service.getLoadedLocalizations('en'); + expect(loaded).toEqual(uiData); + }); + + it('should load again when language changes to another culture', () => { + httpGet.mockReturnValue(of({})); + language$.next('en'); + expect(httpGet).toHaveBeenCalledTimes(1); + + httpGet.mockClear(); + httpGet.mockReturnValue(of({ MyApp: { Title: 'Titre' } })); + language$.next('fr'); + + expect(httpGet).toHaveBeenCalledWith('/assets/localization/fr.json'); + expect(addLocalizationSpy).toHaveBeenCalledWith([ + { + culture: 'fr', + resources: [{ resourceName: 'MyApp', texts: { Title: 'Titre' } }], + }, + ]); + }); + }); + + describe('addAngularLocalizeLocalization', () => { + it('should add localization via LocalizationService (UI data in merge pipeline)', () => { + service.addAngularLocalizeLocalization('en', 'MyApp', { + CustomKey: 'UI Override', + }); + + expect(addLocalizationSpy).toHaveBeenCalledWith([ + { + culture: 'en', + resources: [ + { + resourceName: 'MyApp', + texts: { CustomKey: 'UI Override' }, + }, + ], + }, + ]); + }); + + it('should merge into getLoadedLocalizations cache', () => { + service.addAngularLocalizeLocalization('en', 'MyApp', { Key1: 'Val1' }); + service.addAngularLocalizeLocalization('en', 'MyApp', { Key2: 'Val2' }); + + const loaded = service.getLoadedLocalizations('en'); + expect(loaded.MyApp).toEqual({ Key1: 'Val1', Key2: 'Val2' }); + }); + }); + + describe('getLoadedLocalizations', () => { + it('should return empty object when no culture loaded', () => { + expect(service.getLoadedLocalizations('de')).toEqual({}); + }); + + it('should return current language when culture not passed', () => { + const uiData = { R: { K: 'V' } }; + httpGet.mockReturnValue(of(uiData)); + language$.next('tr'); + + const loaded = service.getLoadedLocalizations(); + expect(loaded).toEqual(uiData); + }); + }); +}); 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 7ad16aaec4..1b847655e7 100644 --- a/npm/ng-packs/packages/feature-management/package.json +++ b/npm/ng-packs/packages/feature-management/package.json @@ -1,15 +1,18 @@ { "name": "@abp/ng.feature-management", - "version": "10.1.0-rc.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.1.0-rc.1", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, + "peerDependencies": { + "@angular/aria": "~21.1.0" + }, "publishConfig": { "access": "public" } 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/src/lib/components/feature-management/feature-management.component.html b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html index 257221add9..f59a66b6a5 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html +++ b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html @@ -1,119 +1,106 @@ @if (visible) { - - -

- {{ 'AbpFeatureManagement::Features' | abpLocalization }} - @if (providerTitle) { - - {{ providerTitle }} - } -

-
+ + +

+ {{ 'AbpFeatureManagement::Features' | abpLocalization }} + @if (providerTitle) { + - {{ providerTitle }} + } +

+
@if (groups.length) { -
- +
- - - @if (description) { - {{ description }} - } - - -
} @if (!groups.length) { @@ -150,32 +129,23 @@
- - + + - @if (groups.length) { - - } + @if (groups.length) { + + } - @if (groups.length) { - - } - -
-} + @if (groups.length) { + + } + +
+} \ No newline at end of file diff --git a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts index 0c6c532473..5feac210e5 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts +++ b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts @@ -17,7 +17,7 @@ import { ModalComponent, ToasterService, } from '@abp/ng.theme.shared'; -import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { Tabs, TabList, Tab, TabPanel, TabContent } from '@angular/aria/tabs'; import { finalize } from 'rxjs/operators'; import { FreeTextInputDirective } from '../../directives'; import { FeatureManagement } from '../../models'; @@ -40,7 +40,11 @@ const DEFAULT_PROVIDER_NAME = 'D'; ModalComponent, LocalizationPipe, FormsModule, - NgbNavModule, + Tabs, + TabList, + Tab, + TabPanel, + TabContent, FreeTextInputDirective, ModalCloseDirective, ], 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 b70170a6bf..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.1.0-rc.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 e0de606028..140c739280 100644 --- a/npm/ng-packs/packages/identity/package.json +++ b/npm/ng-packs/packages/identity/package.json @@ -1,17 +1,20 @@ { "name": "@abp/ng.identity", - "version": "10.1.0-rc.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.1.0-rc.1", - "@abp/ng.permission-management": "~10.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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" }, + "peerDependencies": { + "@angular/aria": "~21.1.0" + }, "publishConfig": { "access": "public" } 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/src/lib/components/users/users.component.html b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html index 5085f1d120..07ac0ca8ec 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html +++ b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html @@ -28,39 +28,47 @@ @if (form) {
- -
+
+
+ + + +
+ +
+ + @for (roleGroup of roleGroups; track $index; let i = $index) { +
+ + +
+ } +
+
+
+
} @else {
diff --git a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts index ba7c5b451b..7ff04cf679 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts +++ b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts @@ -52,7 +52,8 @@ import { import { finalize, switchMap, tap } from 'rxjs/operators'; import { eIdentityComponents } from '../../enums/components'; import { PageComponent } from '@abp/ng.components/page'; -import { NgbDropdownModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { Tabs, TabList, Tab, TabPanel, TabContent } from '@angular/aria/tabs'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @Component({ @@ -70,7 +71,11 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; FormsModule, PermissionManagementComponent, PageComponent, - NgbNavModule, + Tabs, + TabList, + Tab, + TabPanel, + TabContent, NgbDropdownModule, NgxValidateCoreModule, LocalizationPipe, @@ -101,6 +106,8 @@ export class UsersComponent implements OnInit { selected?: IdentityUserDto; + selectedTab = 'user-info'; + selectedUserRoles?: IdentityRoleDto[]; roles?: IdentityRoleDto[]; @@ -159,6 +166,7 @@ export class UsersComponent implements OnInit { } openModal() { + this.selectedTab = 'user-info'; this.buildForm(); this.isModalVisible = true; } 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 171090e65f..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.1.0-rc.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.1.0-rc.1", - "@abp/utils": "~10.1.0-rc.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 7212ad60d8..3eb2efbfda 100644 --- a/npm/ng-packs/packages/permission-management/package.json +++ b/npm/ng-packs/packages/permission-management/package.json @@ -1,15 +1,18 @@ { "name": "@abp/ng.permission-management", - "version": "10.1.0-rc.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.1.0-rc.1", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, + "peerDependencies": { + "@angular/aria": "~21.1.0" + }, "publishConfig": { "access": "public" }, 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/permission-management.component.html b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html index 32fe44e488..0224f3d45a 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html +++ b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.html @@ -43,18 +43,27 @@ {{ 'AbpPermissionManagement::PermissionGroup' | abpLocalization }} -
+
-
- +
-
- @if (selectedGroupPermissions.length) { -
- - -
-
- @for (permission of selectedGroupPermissions; track $index; let i = $index) { -
- -
+ }
diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts index 2c7a4e57ec..b2612e9aaf 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts +++ b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts @@ -31,9 +31,11 @@ import { import { concat, of } from 'rxjs'; import { finalize, switchMap, take, tap } from 'rxjs/operators'; import { PermissionManagement } from '../models'; -import { NgStyle } from '@angular/common'; + import { FormsModule } from '@angular/forms'; +import { Tabs, TabList, Tab, TabPanel, TabContent } from '@angular/aria/tabs'; + type PermissionWithStyle = PermissionGrantInfoDto & { style: string; }; @@ -56,7 +58,7 @@ type PermissionWithGroupName = PermissionGrantInfoDto & { max-height: calc(100vh - 23.1rem); } - .lpx-scroll-pills-container ul { + .lpx-scroll-pills-container .nav-pills { display: block; overflow-y: auto; } @@ -66,7 +68,7 @@ type PermissionWithGroupName = PermissionGrantInfoDto & { .scroll-in-modal { max-height: calc(100vh - 15rem); } - .lpx-scroll-pills-container ul { + .lpx-scroll-pills-container .nav-pills { max-height: 500px; } } @@ -81,12 +83,12 @@ type PermissionWithGroupName = PermissionGrantInfoDto & { padding-bottom: 0 !important; } - .lpx-scroll-pills-container ul li { + .lpx-scroll-pills-container .nav-item { margin-bottom: 10px; border-radius: 10px; } - .lpx-scroll-pills-container ul li a.active { + .lpx-scroll-pills-container .nav-item .nav-link.active { color: #fff !important; border-color: #6c5dd3 !important; background-color: #6c5dd3 !important; @@ -95,11 +97,15 @@ type PermissionWithGroupName = PermissionGrantInfoDto & { ], imports: [ FormsModule, - NgStyle, ModalComponent, LocalizationPipe, ButtonComponent, ModalCloseDirective, + Tabs, + TabList, + Tab, + TabPanel, + TabContent, ], }) export class PermissionManagementComponent @@ -418,6 +424,13 @@ export class PermissionManagementComponent this.onChangeGroup(this.selectedGroup); } + onTabChange(groupName: string) { + const group = this.permissionGroups().find(g => g.name === groupName); + if (group) { + this.onChangeGroup(group); + } + } + onChangeGroup(group: PermissionGroupDto) { this.setDisabled(group.permissions); this.setSelectedGroup(group); 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 901db53a70..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.1.0-rc.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/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template deleted file mode 100644 index e181d4088d..0000000000 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template +++ /dev/null @@ -1,44 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'), - subdir: '.', - reporters: [ - { type: 'html' }, - { type: 'text-summary' } - ] - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true - }); -}; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test-setup.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test-setup.ts.template new file mode 100644 index 0000000000..699d1a1d5e --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test-setup.ts.template @@ -0,0 +1,7 @@ +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()); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template deleted file mode 100644 index 52e55168eb..0000000000 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template +++ /dev/null @@ -1,26 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js'; -import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; - -declare const require: { - context(path: string, deep?: boolean, filter?: RegExp): { - keys(): string[]; - (id: string): T; - }; -}; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template index 7b5ac72780..65831c6293 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template @@ -14,7 +14,7 @@ ] }, "exclude": [ - "src/test.ts", + "src/test-setup.ts", "**/*.spec.ts" ] } diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template index 715dd0a5d2..610066df66 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template @@ -3,15 +3,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["vitest/globals"] }, - "files": [ - "src/test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template deleted file mode 100644 index e181d4088d..0000000000 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template +++ /dev/null @@ -1,44 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'), - subdir: '.', - reporters: [ - { type: 'html' }, - { type: 'text-summary' } - ] - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true - }); -}; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test-setup.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test-setup.ts.template new file mode 100644 index 0000000000..699d1a1d5e --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test-setup.ts.template @@ -0,0 +1,7 @@ +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()); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test.ts.template deleted file mode 100644 index 52e55168eb..0000000000 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test.ts.template +++ /dev/null @@ -1,26 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js'; -import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; - -declare const require: { - context(path: string, deep?: boolean, filter?: RegExp): { - keys(): string[]; - (id: string): T; - }; -}; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template index 7b5ac72780..65831c6293 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template @@ -14,7 +14,7 @@ ] }, "exclude": [ - "src/test.ts", + "src/test-setup.ts", "**/*.spec.ts" ] } diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.spec.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.spec.json.template index 715dd0a5d2..610066df66 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.spec.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.spec.json.template @@ -3,15 +3,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["vitest/globals"] }, - "files": [ - "src/test.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "include": ["**/*.spec.ts", "**/*.d.ts"] } 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/angular-schematic/generate-lib.ts b/npm/ng-packs/packages/schematics/src/utils/angular-schematic/generate-lib.ts index f96b2b38ea..6980d08339 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular-schematic/generate-lib.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular-schematic/generate-lib.ts @@ -39,11 +39,11 @@ export function addLibToWorkspaceFile(projectRoot: string, projectName: string): }, }, test: { - builder: Builders.Karma, + builder: Builders.UnitTest, options: { - main: `${projectRoot}/src/test.ts`, tsConfig: `${projectRoot}/tsconfig.spec.json`, - karmaConfig: `${projectRoot}/karma.conf.js`, + buildTarget: `${projectName}:build`, + runner: 'vitest', }, }, }, diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts b/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts index 34c329b470..84a53e2bbc 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts @@ -36,6 +36,7 @@ export enum Builders { BuildExtractI18n = '@angular/build:extract-i18n', Protractor = '@angular-devkit/build-angular:private-protractor', BuildApplication = '@angular/build:application', + UnitTest = '@angular/build:unit-test', } export interface FileReplacements { 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/config/src/lib/components/email-setting-group/email-setting-group.component.ts b/npm/ng-packs/packages/setting-management/config/src/lib/components/email-setting-group/email-setting-group.component.ts index bb5f6fe6ee..d2e1dae003 100644 --- a/npm/ng-packs/packages/setting-management/config/src/lib/components/email-setting-group/email-setting-group.component.ts +++ b/npm/ng-packs/packages/setting-management/config/src/lib/components/email-setting-group/email-setting-group.component.ts @@ -25,7 +25,7 @@ import { PermissionDirective } from '@abp/ng.core'; import { NgxValidateCoreModule } from '@ngx-validate/core'; -import { CommonModule } from '@angular/common'; + const { required, email } = Validators; @@ -35,14 +35,13 @@ const { required, email } = Validators; animations: [collapse], imports: [ ReactiveFormsModule, - CommonModule, LocalizationPipe, ButtonComponent, ModalComponent, ModalCloseDirective, NgxValidateCoreModule, - PermissionDirective, - ], + PermissionDirective +], }) export class EmailSettingGroupComponent implements OnInit { private emailSettingsService = inject(EmailSettingsService); 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 579c744222..b182196ce9 100644 --- a/npm/ng-packs/packages/setting-management/package.json +++ b/npm/ng-packs/packages/setting-management/package.json @@ -1,16 +1,19 @@ { "name": "@abp/ng.setting-management", - "version": "10.1.0-rc.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.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.1", + "@abp/ng.components": "~10.1.0-rc.2", + "@abp/ng.theme.shared": "~10.1.0-rc.2", "tslib": "^2.0.0" }, + "peerDependencies": { + "@angular/aria": "~21.1.0" + }, "publishConfig": { "access": "public" }, @@ -28,4 +31,4 @@ "csharp", "webapp" ] -} +} \ No newline at end of file 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/src/lib/components/setting-management.component.html b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html index 0ffdd873c8..07830f2d1f 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html +++ b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.html @@ -2,41 +2,29 @@
-
+
- +
@if (settings.length) { -
-
- -
+ +
+
+
}
- + \ No newline at end of file diff --git a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts index 1cec644ed6..2bf5e684e4 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts +++ b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts @@ -4,11 +4,17 @@ import { Component, inject, OnDestroy, OnInit, TrackByFunction } from '@angular/ import { Subscription } from 'rxjs'; import { NgComponentOutlet } from '@angular/common'; import { PageComponent } from '@abp/ng.components/page'; +import { Tab, Tabs, TabList, TabPanel } from '@angular/aria/tabs'; @Component({ selector: 'abp-setting-management', templateUrl: './setting-management.component.html', - imports: [NgComponentOutlet, PageComponent, LocalizationPipe, PermissionDirective, ForDirective], + imports: [NgComponentOutlet, PageComponent, LocalizationPipe, PermissionDirective, ForDirective, Tabs, TabList, Tab, TabPanel], + styles: [` + :host [ngTabPanel][inert] { + display: none; + } + `], }) export class SettingManagementComponent implements OnDestroy, OnInit { private settingTabsService = inject(SettingTabsService); 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 e92e12af27..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.1.0-rc.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.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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/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 796531eb6a..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.1.0-rc.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.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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/page-alert-container/page-alert-container.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.html index 86e9d40177..daaa6c48ec 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.html @@ -1,26 +1,12 @@ @for (alert of service.alerts$ | async; track i; let i = $index) { - -} + +} \ No newline at end of file diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts index f40ba2cd91..3e5aa72d8a 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts @@ -1,13 +1,13 @@ import { Component, ViewEncapsulation, inject } from '@angular/core'; import { PageAlertService } from '@abp/ng.theme.shared'; -import { NgClass, AsyncPipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { LocalizationPipe, SafeHtmlPipe } from '@abp/ng.core'; @Component({ selector: 'abp-page-alert-container', templateUrl: './page-alert-container.component.html', encapsulation: ViewEncapsulation.None, - imports: [NgClass, AsyncPipe, LocalizationPipe, SafeHtmlPipe], + imports: [AsyncPipe, LocalizationPipe, SafeHtmlPipe], }) export class PageAlertContainerComponent { service = inject(PageAlertService); 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 28e408655d..a29fb3323e 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 @@ -1,16 +1,14 @@ + \ No newline at end of file diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts index 27463ea497..a6afd37b12 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts @@ -16,7 +16,7 @@ import { TrackByFunction, ViewChildren, } from '@angular/core'; -import { NgTemplateOutlet, NgClass, AsyncPipe } from '@angular/common'; +import { NgTemplateOutlet, AsyncPipe } from '@angular/common'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { RouterLink } from '@angular/router'; import { EllipsisDirective } from '@abp/ng.theme.shared'; @@ -26,7 +26,6 @@ import { EllipsisDirective } from '@abp/ng.theme.shared'; templateUrl: 'routes.component.html', imports: [ NgTemplateOutlet, - NgClass, AsyncPipe, RouterLink, NgbDropdownModule, diff --git a/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts b/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts index dd0591f9fd..847c3418a6 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts @@ -1,23 +1,59 @@ -import { LazyLoadService, LOADING_STRATEGY, LocalizationService } from '@abp/ng.core'; +import { DOCUMENT } from '@angular/common'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; +import { of, Subject } from 'rxjs'; +import { vi } from 'vitest'; + +import { LazyLoadService, LoadingStrategy, LocalizationService } from '@abp/ng.core'; import { DocumentDirHandlerService } from '@abp/ng.theme.shared'; -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; import { BOOTSTRAP, createLazyStyleHref, LazyStyleHandler } from '../handlers'; - -const currentLang$ = of({ payload: 'en' }); +import { LAZY_STYLES } from '../tokens/lazy-styles.token'; +import { setupComponentResources } from './utils'; describe('LazyStyleHandler', () => { 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 c2f64d4014..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.1.0-rc.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.1.0-rc.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/components/button/button.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts index 7388eb47ef..47272992fd 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts @@ -10,7 +10,6 @@ import { ViewChild, inject, } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { ABP, StopPropagationDirective } from '@abp/ng.core'; @Component({ @@ -21,16 +20,16 @@ import { ABP, StopPropagationDirective } from '@abp/ng.core'; [id]="buttonId" [attr.type]="buttonType" [attr.form]="formName" - [ngClass]="buttonClass" + [class]="buttonClass" [disabled]="loading || disabled" (click.stop)="click.next($event); abpClick.next($event)" (focus)="focus.next($event); abpFocus.next($event)" (blur)="blur.next($event); abpBlur.next($event)" > - + `, - imports: [CommonModule, StopPropagationDirective], + imports: [StopPropagationDirective], }) export class ButtonComponent implements OnInit { private renderer = inject(Renderer2); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts index 35790054b5..6f8eade8d2 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts @@ -1,12 +1,10 @@ import { Component, HostBinding, Input } from '@angular/core'; -import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card-body', - template: `
+ template: `
`, - imports: [NgClass, NgStyle], }) export class CardBodyComponent { @HostBinding('class') componentClass = 'card-body'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts index 38a798e146..3b96418fbd 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts @@ -1,15 +1,14 @@ import { Component, HostBinding, Input } from '@angular/core'; -import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card-footer', template: ` -
+
`, styles: [], - imports: [NgClass, NgStyle], + imports: [], }) export class CardFooterComponent { @HostBinding('class') componentClass = 'card-footer'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts index 4857913d77..3c31d16d74 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts @@ -1,15 +1,14 @@ import { Component, HostBinding, Input } from '@angular/core'; -import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card-header', template: ` -
+
`, styles: [], - imports: [NgClass, NgStyle], + imports: [], }) export class CardHeaderComponent { @HostBinding('class') componentClass = 'card-header'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts index 42973fdcb0..d65b8a12dc 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts @@ -1,12 +1,11 @@ import { Component, Input } from '@angular/core'; -import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card', - template: `
+ template: `
`, - imports: [NgClass, NgStyle], + imports: [], }) export class CardComponent { @Input() cardClass: string; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts index 8cadf828d1..8da578f9a7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts @@ -1,6 +1,5 @@ import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'; -import { NgClass, NgStyle } from '@angular/common'; import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core'; @Component({ @@ -12,13 +11,13 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core'; [(ngModel)]="value" [id]="checkboxId" [readonly]="checkboxReadonly" - [ngClass]="checkboxClass" - [ngStyle]="checkboxStyle" + [class]="checkboxClass" + [style]="checkboxStyle" (blur)="checkboxBlur.next()" (focus)="checkboxFocus.next()" /> @if (label) { -
- + \ No newline at end of file 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 1d1d350c2a..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.1.0-rc.1", - "@abp/aspnetcore.mvc.ui.theme.leptonxlite": "~5.1.0-rc.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 63723159e9..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.1.0-rc.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 e560238e6a..af7f564bc8 100644 --- a/templates/app/angular/package.json +++ b/templates/app/angular/package.json @@ -12,16 +12,17 @@ }, "private": true, "dependencies": { - "@abp/ng.account": "~10.1.0-rc.1", - "@abp/ng.components": "~10.1.0-rc.1", - "@abp/ng.core": "~10.1.0-rc.1", - "@abp/ng.identity": "~10.1.0-rc.1", - "@abp/ng.oauth": "~10.1.0-rc.1", - "@abp/ng.setting-management": "~10.1.0-rc.1", - "@abp/ng.tenant-management": "~10.1.0-rc.1", - "@abp/ng.theme.lepton-x": "~5.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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/aria": "~21.1.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", "@angular/core": "~21.0.0", @@ -36,7 +37,7 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@abp/ng.schematics": "~10.1.0-rc.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", @@ -59,4 +60,4 @@ "karma-jasmine-html-reporter": "^1.7.0", "typescript": "~5.9.3" } -} +} \ No newline at end of file diff --git a/templates/app/angular/src/app/home/home.component.html b/templates/app/angular/src/app/home/home.component.html index e3e73e48ae..83f1f9619e 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 @@
+
@@ -8,14 +9,12 @@

{{ '::LongWelcomeMessage' | abpLocalization }}

- {{ 'AbpAccount::Login' | abpLocalization }} + {{ + 'AbpAccount::Login' | abpLocalization }} }

Getting Started

- + ">

Web Application Development Tutorial

- + ">

Customize Lepton Theme

Learn how to customize LeptonX Theme as you wish.

- Customize Lepton + Customize Lepton
@@ -59,8 +51,7 @@
- - + + ">
@@ -107,17 +95,14 @@ Explore all ABP users' experiences with the ABP Framework, discover articles and videos on how to use ABP, and join raffles for a chance to win surprise gifts!

- Join ABP Community + Join ABP Community
- + ">
@@ -180,18 +164,12 @@ understanding of the framework and modern web application development techniques.

@@ -203,8 +181,7 @@
- + ">
@@ -278,10 +254,7 @@ @if(item.icon){ -
+
@if(item.iconType === 'image'){ }@else if(item.iconType === 'icon'){ @@ -306,4 +279,4 @@
- + \ No newline at end of file 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 3038ea90ab..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.1.0-rc.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 4b913408df..eb97d6f1cf 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 @@ -12,8 +12,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 d3ad404c7f..ee35cbea76 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 @@ -16,7 +16,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 63723159e9..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.1.0-rc.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.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 9e9e1ddf70..7425075726 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 63723159e9..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.1.0-rc.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.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 c0fd5ebff6..c866ed784c 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 @@ -13,8 +13,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 d909e41fc9..1a4abe213d 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 @@ -13,8 +13,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 56a0c5e62a..1a3e12ca59 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,10 +15,10 @@ - + - + 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 63723159e9..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.1.0-rc.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.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 72abeed549..c4f2b87496 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 63723159e9..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.1.0-rc.1", - "@abp/aspnetcore.components.server.leptonxlitetheme": "~5.1.0-rc.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 94bc077a11..61026e11ce 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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 b3eb9cb472..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.1.0-rc.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 aac3293e2a..2edba4edac 100644 --- a/templates/module/angular/package.json +++ b/templates/module/angular/package.json @@ -13,16 +13,17 @@ }, "private": true, "dependencies": { - "@abp/ng.account": "~10.1.0-rc.1", - "@abp/ng.components": "~10.1.0-rc.1", - "@abp/ng.core": "~10.1.0-rc.1", - "@abp/ng.identity": "~10.1.0-rc.1", - "@abp/ng.oauth": "~10.1.0-rc.1", - "@abp/ng.setting-management": "~10.1.0-rc.1", - "@abp/ng.tenant-management": "~10.1.0-rc.1", - "@abp/ng.theme.basic": "~10.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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/aria": "~21.1.0", "@angular/common": "~21.0.0", "@angular/compiler": "~21.0.0", "@angular/core": "~21.0.0", @@ -36,7 +37,7 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@abp/ng.schematics": "~10.1.0-rc.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", @@ -61,4 +62,4 @@ "symlink": "^2.0.0", "typescript": "~5.9.0" } -} +} \ No newline at end of file diff --git a/templates/module/angular/projects/my-project-name/package.json b/templates/module/angular/projects/my-project-name/package.json index aa4b0f95e3..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.1.0-rc.1", - "@abp/ng.theme.shared": "~10.1.0-rc.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 a437271d58..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.1.0-rc.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 c4d308a9c7..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.1.0-rc.1", - "@abp/aspnetcore.components.server.basictheme": "~10.1.0-rc.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 881359366e..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.1.0-rc.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 881359366e..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.1.0-rc.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 @@ - +