diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..b438a0027b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,183 @@ +# Umbraco CMS Development Guide + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Working Effectively + +Bootstrap, build, and test the repository: + +- Install .NET SDK (version specified in global.json): + - `curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version $(jq -r '.sdk.version' global.json)` + - `export PATH="/home/runner/.dotnet:$PATH"` +- Install Node.js (version specified in src/Umbraco.Web.UI.Client/.nvmrc): + - `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash` + - `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"` + - `nvm install $(cat src/Umbraco.Web.UI.Client/.nvmrc) && nvm use $(cat src/Umbraco.Web.UI.Client/.nvmrc)` +- Fix shallow clone issue (required for GitVersioning): + - `git fetch --unshallow` +- Restore packages: + - `dotnet restore` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds. +- Build the solution: + - `dotnet build` -- takes 4.5 minutes. NEVER CANCEL. Set timeout to 10+ minutes. +- Install and build frontend: + - `cd src/Umbraco.Web.UI.Client` + - `npm ci --no-fund --no-audit --prefer-offline` -- takes 11 seconds. + - `npm run build:for:cms` -- takes 1.25 minutes. NEVER CANCEL. Set timeout to 5+ minutes. +- Install and build Login + - `cd src/Umbraco.Web.UI.Login` + - `npm ci --no-fund --no-audit --prefer-offline` + - `npm run build` +- Run the application: + - `cd src/Umbraco.Web.UI` + - `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000 + +## Validation + +- ALWAYS run through at least one complete end-to-end scenario after making changes. +- Build and unit tests must pass before committing changes. +- Frontend build produces output in src/Umbraco.Web.UI.Client/dist-cms/ which gets copied to src/Umbraco.Web.UI/wwwroot/umbraco/backoffice/ +- Always run `dotnet build` and `npm run build:for:cms` before running the application to see your changes. +- For login-only changes, you can run `npm run build` from src/Umbraco.Web.UI.Login and then `dotnet run --no-build` from src/Umbraco.Web.UI. +- For frontend-only changes, you can run `npm run dev:server` from src/Umbraco.Web.UI.Client for hot reloading. +- Frontend changes should be linted using `npm run lint:fix` which uses Eslint. + +## Testing + +### Unit Tests (.NET) +- Location: tests/Umbraco.Tests.UnitTests/ +- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal` +- Duration: ~1 minute with 3,343 tests +- NEVER CANCEL: Set timeout to 5+ minutes + +### Integration Tests (.NET) +- Location: tests/Umbraco.Tests.Integration/ +- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal` +- NEVER CANCEL: Set timeout to 10+ minutes + +### Frontend Tests +- Location: src/Umbraco.Web.UI.Client/ +- Run: `npm test` (requires `npx playwright install` first) +- Frontend tests use Web Test Runner with Playwright + +### Acceptance Tests (E2E) +- Location: tests/Umbraco.Tests.AcceptanceTest/ +- Requires running Umbraco application and configuration +- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first) + +## Project Structure + +The solution contains 30 C# projects organized as follows: + +### Main Application Projects +- **Umbraco.Web.UI**: Main web application project (startup project) +- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice) +- **Umbraco.Web.UI.Login**: Separate login screen frontend +- **Umbraco.Core**: Core domain models and interfaces +- **Umbraco.Infrastructure**: Data access and infrastructure +- **Umbraco.Cms**: Main CMS package + +### API Projects +- **Umbraco.Cms.Api.Management**: Management API +- **Umbraco.Cms.Api.Delivery**: Content Delivery API +- **Umbraco.Cms.Api.Common**: Shared API components + +### Persistence Projects +- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support +- **Umbraco.Cms.Persistence.Sqlite**: SQLite support +- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions + +### Test Projects +- **Umbraco.Tests.UnitTests**: Unit tests +- **Umbraco.Tests.Integration**: Integration tests +- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright +- **Umbraco.Tests.Common**: Shared test utilities + +## Common Tasks + +### Frontend Development +For frontend-only changes: +1. Configure backend for frontend development: + ```json + + ```json + "BackOfficeHost": "http://localhost:5173", + "AuthorizeCallbackPathName": "/oauth_complete", + "AuthorizeCallbackLogoutPathName": "/logout", + "AuthorizeCallbackErrorPathName": "/error" + ``` +2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` +3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server` + +### Backend-Only Development +For backend-only changes, disable frontend builds: +- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj: + ``` + + ``` +- Remember to uncomment before committing + +### Building NuGet Packages +To build custom NuGet packages for testing: +```bash +dotnet pack -c Release -o Build.Out +dotnet nuget add source [Path to Build.Out folder] -n MyLocalFeed +``` + +### Regenerating Frontend API Types +When changing Management API: +```bash +cd src/Umbraco.Web.UI.Client +npm run generate:server-api-dev +``` +Also update OpenApi.json from /umbraco/swagger/management/swagger.json + +## Database Setup + +Default configuration supports SQLite for development. For production-like testing: +- Use SQL Server/LocalDb for better performance +- Configure connection string in src/Umbraco.Web.UI/appsettings.json + +## Clean Up / Reset + +To reset development environment: +```bash +# Remove configuration and database +rm src/Umbraco.Web.UI/appsettings.json +rm -rf src/Umbraco.Web.UI/umbraco/Data + +# Full clean (removes all untracked files) +git clean -xdf . +``` + +## Version Information + +- Target Framework: .NET (version specified in global.json) +- Current Version: (specified in version.json) +- Node.js Requirement: (specified in src/Umbraco.Web.UI.Client/.nvmrc) +- npm Requirement: Latest compatible version + +## Known Issues + +- Build requires full git history (not shallow clone) due to GitVersioning +- Some NuGet package security warnings are expected (SixLabors.ImageSharp vulnerabilities) +- Frontend tests require Playwright browser installation: `npx playwright install` +- Older Node.js versions may show engine compatibility warnings (check .nvmrc for current requirement) + +## Timing Expectations + +**NEVER CANCEL** these operations - they are expected to take time: + +| Operation | Expected Time | Timeout Setting | +|-----------|--------------|-----------------| +| `dotnet restore` | 50 seconds | 90+ seconds | +| `dotnet build` | 4.5 minutes | 10+ minutes | +| `npm ci` | 11 seconds | 30+ seconds | +| `npm run build:for:cms` | 1.25 minutes | 5+ minutes | +| `npm test` | 2 minutes | 5+ minutes | +| `npm run lint` | 1 minute | 5+ minutes | +| Unit tests | 1 minute | 5+ minutes | +| Integration tests | Variable | 10+ minutes | + +Always wait for commands to complete rather than canceling and retrying. \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs index 2adfef9664..a38ade6060 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -18,8 +21,19 @@ public abstract class ContentCollectionControllerBase _mapper = mapper; + protected ContentCollectionControllerBase(IUmbracoMapper mapper, SignProviderCollection signProvider) + { + _mapper = mapper; + _signProviders = signProvider; + } + + [Obsolete("Use the constructer with all parameters. To be removed in Umbraco 18")] + protected ContentCollectionControllerBase(IUmbracoMapper mapper) + : this(mapper, StaticServiceProvider.Instance.GetRequiredService()) + { + } [Obsolete("This method is no longer used and will be removed in Umbraco 17.")] protected IActionResult CollectionResult(ListViewPagedModel result) @@ -48,6 +62,9 @@ public abstract class ContentCollectionControllerBase + /// Creates a collection result from the provided collection response models and total number of items. + /// protected IActionResult CollectionResult(List collectionResponseModels, long totalNumberOfItems) { var pageViewModel = new PagedViewModel @@ -104,4 +121,15 @@ public abstract class ContentCollectionControllerBase + /// Populates the signs for the collection response models. + /// + protected async Task PopulateSigns(IEnumerable itemViewModels) + { + foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) + { + await signProvider.PopulateSignsAsync(itemViewModels); + } + } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs index d93b76718d..b3a103a01a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs @@ -1,6 +1,8 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -9,11 +11,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree; [ApiVersion("1.0")] public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) : base(entityService, dataTypeService) { } + [ActivatorUtilitiesConstructor] + public AncestorsDataTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IDataTypeService dataTypeService) + : base(entityService, signProviders, dataTypeService) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs index cd9f4171cd..7c801530ac 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree; [ApiVersion("1.0")] public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) : base(entityService, dataTypeService) { } + [ActivatorUtilitiesConstructor] + public ChildrenDataTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IDataTypeService dataTypeService) + : base(entityService, signProviders, dataTypeService) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs index 52a7319b1a..f0dbb8a8ec 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs @@ -1,9 +1,12 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -19,8 +22,17 @@ public class DataTypeTreeControllerBase : FolderTreeControllerBase + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + dataTypeService) + { + } + + public DataTypeTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders, IDataTypeService dataTypeService) + : base(entityService, signProviders) => _dataTypeService = dataTypeService; protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/RootDataTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/RootDataTypeTreeController.cs index 0554bff8e5..fa1b5c7bfb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/RootDataTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/RootDataTypeTreeController.cs @@ -1,21 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Common.ViewModels.Pagination; -using Umbraco.Cms.Api.Management.ViewModels.DataType.Item; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree; [ApiVersion("1.0")] public class RootDataTypeTreeController : DataTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) : base(entityService, dataTypeService) { } + [ActivatorUtilitiesConstructor] + public RootDataTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IDataTypeService dataTypeService) + : base(entityService, signProviders, dataTypeService) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs index d487df48ff..fcc496e71d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/SiblingsDataTypeTreeController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -8,11 +10,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree; public class SiblingsDataTypeTreeController : DataTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) : base(entityService, dataTypeService) { } + [ActivatorUtilitiesConstructor] + public SiblingsDataTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IDataTypeService dataTypeService) + : base(entityService, signProviders, dataTypeService) + { + } + [HttpGet("siblings")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, bool foldersOnly = false) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs index 2c17199602..f7c94de4db 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs @@ -1,6 +1,8 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -9,11 +11,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree; [ApiVersion("1.0")] public class AncestorsDictionaryTreeController : DictionaryTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService) : base(entityService, dictionaryItemService) { } + [ActivatorUtilitiesConstructor] + public AncestorsDictionaryTreeController(IEntityService entityService, SignProviderCollection signProviders, IDictionaryItemService dictionaryItemService) + : base(entityService, signProviders, dictionaryItemService) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs index 59b23ff801..39c135be2b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs @@ -1,21 +1,30 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Api.Common.ViewModels.Pagination; -using Umbraco.Cms.Api.Management.ViewModels.Tree; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree; [ApiVersion("1.0")] public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService) : base(entityService, dictionaryItemService) { } + [ActivatorUtilitiesConstructor] + public ChildrenDictionaryTreeController(IEntityService entityService, SignProviderCollection signProviders, IDictionaryItemService dictionaryItemService) + : base(entityService, signProviders, dictionaryItemService) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs index 2b02ff541a..6533370245 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs @@ -1,10 +1,13 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Authorization; @@ -18,8 +21,17 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree; // tree controller base. We'll keep it though, in the hope that we can mend EntityService. public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public DictionaryTreeControllerBase(IEntityService entityService, IDictionaryItemService dictionaryItemService) - : base(entityService) => + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + dictionaryItemService) + { + } + + public DictionaryTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders, IDictionaryItemService dictionaryItemService) + : base(entityService, signProviders) => DictionaryItemService = dictionaryItemService; // dictionary items do not currently have a known UmbracoObjectType, so we'll settle with Unknown for now diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs index a6d65ed764..7bac1e66a8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs @@ -1,10 +1,12 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Api.Common.ViewModels.Pagination; -using Umbraco.Cms.Api.Management.ViewModels.Tree; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree; @@ -16,6 +18,12 @@ public class RootDictionaryTreeController : DictionaryTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public RootDictionaryTreeController(IEntityService entityService, SignProviderCollection signProviders, IDictionaryItemService dictionaryItemService) + : base(entityService, signProviders, dictionaryItemService) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs index f55c666cfc..5ab89ba274 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/ByKeyDocumentCollectionController.cs @@ -1,10 +1,13 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; @@ -20,16 +23,33 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IDocumentCollectionPresentationFactory _documentCollectionPresentationFactory; + [ActivatorUtilitiesConstructor] + public ByKeyDocumentCollectionController( + IContentListViewService contentListViewService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUmbracoMapper mapper, + IDocumentCollectionPresentationFactory documentCollectionPresentationFactory, + SignProviderCollection signProviders) + : base(mapper, signProviders) + { + _contentListViewService = contentListViewService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _documentCollectionPresentationFactory = documentCollectionPresentationFactory; + } + + [Obsolete("Please use the constructor with all parameters. Scheduled to be removed in V18")] public ByKeyDocumentCollectionController( IContentListViewService contentListViewService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IUmbracoMapper mapper, IDocumentCollectionPresentationFactory documentCollectionPresentationFactory) - : base(mapper) + : this( + contentListViewService, + backOfficeSecurityAccessor, + mapper, + documentCollectionPresentationFactory, + StaticServiceProvider.Instance.GetRequiredService()) { - _contentListViewService = contentListViewService; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _documentCollectionPresentationFactory = documentCollectionPresentationFactory; } [HttpGet("{id:guid}")] @@ -65,6 +85,7 @@ public class ByKeyDocumentCollectionController : DocumentCollectionControllerBas } List collectionResponseModels = await _documentCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!); + await PopulateSigns(collectionResponseModels); return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs index b4ac5a86da..8de4daa401 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Controllers.Content; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Core; @@ -17,6 +18,12 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Collection; [Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase { + protected DocumentCollectionControllerBase(IUmbracoMapper mapper, SignProviderCollection signProviders) + : base(mapper, signProviders) + { + } + + [Obsolete("Please use the constructor with all parameters. Scheduled to be removed in V18")] protected DocumentCollectionControllerBase(IUmbracoMapper mapper) : base(mapper) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs index ebefb5bfb2..f67f880e57 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/ItemDocumentItemController.cs @@ -1,8 +1,11 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -14,30 +17,53 @@ public class ItemDocumentItemController : DocumentItemControllerBase { private readonly IEntityService _entityService; private readonly IDocumentPresentationFactory _documentPresentationFactory; + private readonly SignProviderCollection _signProviders; - public ItemDocumentItemController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) + [ActivatorUtilitiesConstructor] + public ItemDocumentItemController( + IEntityService entityService, + IDocumentPresentationFactory documentPresentationFactory, + SignProviderCollection signProvider) { _entityService = entityService; _documentPresentationFactory = documentPresentationFactory; + _signProviders = signProvider; + } + + [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18")] + public ItemDocumentItemController( + IEntityService entityService, + IDocumentPresentationFactory documentPresentationFactory) + : this(entityService, documentPresentationFactory, StaticServiceProvider.Instance.GetRequiredService()) + { } [HttpGet] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public Task Item( + public async Task Item( CancellationToken cancellationToken, [FromQuery(Name = "id")] HashSet ids) { if (ids.Count is 0) { - return Task.FromResult(Ok(Enumerable.Empty())); + return Ok(Enumerable.Empty()); } IEnumerable documents = _entityService .GetAll(UmbracoObjectTypes.Document, ids.ToArray()) .OfType(); - IEnumerable documentItemResponseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel); - return Task.FromResult(Ok(documentItemResponseModels)); + IEnumerable responseModels = documents.Select(_documentPresentationFactory.CreateItemResponseModel); + await PopulateSigns(responseModels); + return Ok(responseModels); + } + + private async Task PopulateSigns(IEnumerable itemViewModels) + { + foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) + { + await signProvider.PopulateSignsAsync(itemViewModels); + } } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs index eef178ea70..5e3c347d00 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs @@ -1,8 +1,10 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; @@ -13,6 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree; [ApiVersion("1.0")] public class AncestorsDocumentTreeController : DocumentTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsDocumentTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -32,6 +35,28 @@ public class AncestorsDocumentTreeController : DocumentTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public AncestorsDocumentTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IDocumentPresentationFactory documentPresentationFactory) + : base( + entityService, + signProviders, + userStartNodeEntitiesService, + dataTypeService, + publicAccessService, + appCaches, + backofficeSecurityAccessor, + documentPresentationFactory) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/ChildrenDocumentTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/ChildrenDocumentTreeController.cs index e0eb3df87e..76bcb70deb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/ChildrenDocumentTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/ChildrenDocumentTreeController.cs @@ -1,19 +1,22 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Api.Management.Services.Entities; -using Umbraco.Cms.Api.Common.ViewModels.Pagination; -using Umbraco.Cms.Api.Management.Factories; -using Umbraco.Cms.Api.Management.ViewModels.Tree; namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree; [ApiVersion("1.0")] public class ChildrenDocumentTreeController : DocumentTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenDocumentTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -33,6 +36,28 @@ public class ChildrenDocumentTreeController : DocumentTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public ChildrenDocumentTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IDocumentPresentationFactory documentPresentationFactory) + : base( + entityService, + signProviders, + userStartNodeEntitiesService, + dataTypeService, + publicAccessService, + appCaches, + backofficeSecurityAccessor, + documentPresentationFactory) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs index cacf862b57..a2015d7c76 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs @@ -1,13 +1,16 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Security; @@ -26,6 +29,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IDocumentPresentationFactory _documentPresentationFactory; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] protected DocumentTreeControllerBase( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -34,7 +38,29 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa AppCaches appCaches, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IDocumentPresentationFactory documentPresentationFactory) - : base(entityService, userStartNodeEntitiesService, dataTypeService) + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + userStartNodeEntitiesService, + dataTypeService, + publicAccessService, + appCaches, + backofficeSecurityAccessor, + documentPresentationFactory) + { + } + + [ActivatorUtilitiesConstructor] + protected DocumentTreeControllerBase( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, signProviders, userStartNodeEntitiesService, dataTypeService) { _publicAccessService = publicAccessService; _appCaches = appCaches; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/RootDocumentTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/RootDocumentTreeController.cs index a9d63dc4b0..822679e581 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/RootDocumentTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/RootDocumentTreeController.cs @@ -1,19 +1,22 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Api.Management.Services.Entities; -using Umbraco.Cms.Api.Common.ViewModels.Pagination; -using Umbraco.Cms.Api.Management.Factories; -using Umbraco.Cms.Api.Management.ViewModels.Tree; namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree; [ApiVersion("1.0")] public class RootDocumentTreeController : DocumentTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootDocumentTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -33,6 +36,28 @@ public class RootDocumentTreeController : DocumentTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public RootDocumentTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IDocumentPresentationFactory documentPresentationFactory) + : base( + entityService, + signProviders, + userStartNodeEntitiesService, + dataTypeService, + publicAccessService, + appCaches, + backofficeSecurityAccessor, + documentPresentationFactory) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs index 850eb9f856..d9e591b664 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs @@ -1,9 +1,11 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; @@ -14,6 +16,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree; [ApiVersion("1.0")] public class SiblingsDocumentTreeController : DocumentTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsDocumentTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -33,6 +36,28 @@ public class SiblingsDocumentTreeController : DocumentTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public SiblingsDocumentTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IDocumentPresentationFactory documentPresentationFactory) + : base( + entityService, + signProviders, + userStartNodeEntitiesService, + dataTypeService, + publicAccessService, + appCaches, + backofficeSecurityAccessor, + documentPresentationFactory) + { + } + [HttpGet("siblings")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/AncestorsDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/AncestorsDocumentBlueprintTreeController.cs index 9124b1ae2a..c3d155e54c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/AncestorsDocumentBlueprintTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/AncestorsDocumentBlueprintTreeController.cs @@ -1,7 +1,9 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -10,11 +12,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree; [ApiVersion("1.0")] public class AncestorsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) : base(entityService, documentPresentationFactory) { } + [ActivatorUtilitiesConstructor] + public AncestorsDocumentBlueprintTreeController(IEntityService entityService, SignProviderCollection signProviders, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, signProviders, documentPresentationFactory) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs index 92c1fe28b2..7f2f3ea226 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs @@ -1,8 +1,10 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -11,11 +13,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree; [ApiVersion("1.0")] public class ChildrenDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) : base(entityService, documentPresentationFactory) { } + [ActivatorUtilitiesConstructor] + public ChildrenDocumentBlueprintTreeController(IEntityService entityService, SignProviderCollection signProviders, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, signProviders, documentPresentationFactory) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs index aeaf65b7ef..3a02abb6f0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs @@ -1,10 +1,13 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -19,8 +22,17 @@ public class DocumentBlueprintTreeControllerBase : FolderTreeControllerBase(), + documentPresentationFactory) + { + } + + public DocumentBlueprintTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, signProviders) => _documentPresentationFactory = documentPresentationFactory; protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentBlueprint; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs index 79e20385c1..b2a8bfa9cf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs @@ -1,8 +1,10 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -11,11 +13,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree; [ApiVersion("1.0")] public class RootDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) : base(entityService, documentPresentationFactory) { } + [ActivatorUtilitiesConstructor] + public RootDocumentBlueprintTreeController(IEntityService entityService, SignProviderCollection signProviders, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, signProviders, documentPresentationFactory) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs index 6c5e5314b6..c6592c5dda 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/SiblingsDocumentBlueprintTreeController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -9,11 +11,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree; public class SiblingsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) : base(entityService, documentPresentationFactory) { } + [ActivatorUtilitiesConstructor] + public SiblingsDocumentBlueprintTreeController(IEntityService entityService, SignProviderCollection signProviders, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, signProviders, documentPresentationFactory) + { + } + [HttpGet("siblings")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs index c49f7b0d3c..02bb34403f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs @@ -1,6 +1,8 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -9,11 +11,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree; [ApiVersion("1.0")] public class AncestorsDocumentTypeTreeController : DocumentTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) : base(entityService, contentTypeService) { } + [ActivatorUtilitiesConstructor] + public AncestorsDocumentTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IContentTypeService contentTypeService) + : base(entityService, signProviders, contentTypeService) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs index de25c4c1be..9127a359d3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Services; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree; [ApiVersion("1.0")] public class ChildrenDocumentTypeTreeController : DocumentTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) : base(entityService, contentTypeService) { } + [ActivatorUtilitiesConstructor] + public ChildrenDocumentTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IContentTypeService contentTypeService) + : base(entityService, signProviders, contentTypeService) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs index 0a54417200..a76fd49ee8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs @@ -1,9 +1,12 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -18,8 +21,17 @@ public class DocumentTypeTreeControllerBase : FolderTreeControllerBase + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + contentTypeService) + { + } + + public DocumentTypeTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders, IContentTypeService contentTypeService) + : base(entityService, signProviders) => _contentTypeService = contentTypeService; protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentType; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs index 4824dc0495..b581b7bbb4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Services; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree; [ApiVersion("1.0")] public class RootDocumentTypeTreeController : DocumentTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) : base(entityService, contentTypeService) { } + [ActivatorUtilitiesConstructor] + public RootDocumentTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IContentTypeService contentTypeService) + : base(entityService, signProviders, contentTypeService) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs index c0c36ab9d8..3ecce3e383 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/SiblingsDocumentTypeTreeController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -8,11 +10,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree; public class SiblingsDocumentTypeTreeController : DocumentTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) : base(entityService, contentTypeService) { } + [ActivatorUtilitiesConstructor] + public SiblingsDocumentTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IContentTypeService contentTypeService) + : base(entityService, signProviders, contentTypeService) + { + } + [HttpGet("siblings")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs index ba6eb5c8b3..184cfe4de0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/ByKeyMediaCollectionController.cs @@ -1,10 +1,13 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Media.Collection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; @@ -20,16 +23,33 @@ public class ByKeyMediaCollectionController : MediaCollectionControllerBase private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IMediaCollectionPresentationFactory _mediaCollectionPresentationFactory; + [ActivatorUtilitiesConstructor] + public ByKeyMediaCollectionController( + IMediaListViewService mediaListViewService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUmbracoMapper mapper, + IMediaCollectionPresentationFactory mediaCollectionPresentationFactory, + SignProviderCollection signProviders) + : base(mapper, signProviders) + { + _mediaListViewService = mediaListViewService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mediaCollectionPresentationFactory = mediaCollectionPresentationFactory; + } + + [Obsolete("Please use the constructor with all parameters. Scheduled to be removed in Umbraco 18")] public ByKeyMediaCollectionController( IMediaListViewService mediaListViewService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IUmbracoMapper mapper, IMediaCollectionPresentationFactory mediaCollectionPresentationFactory) - : base(mapper) + : this( + mediaListViewService, + backOfficeSecurityAccessor, + mapper, + mediaCollectionPresentationFactory, + StaticServiceProvider.Instance.GetRequiredService()) { - _mediaListViewService = mediaListViewService; - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _mediaCollectionPresentationFactory = mediaCollectionPresentationFactory; } [HttpGet] @@ -64,6 +84,7 @@ public class ByKeyMediaCollectionController : MediaCollectionControllerBase } List collectionResponseModels = await _mediaCollectionPresentationFactory.CreateCollectionModelAsync(collectionAttempt.Result!); + await PopulateSigns(collectionResponseModels); return CollectionResult(collectionResponseModels, collectionAttempt.Result!.Items.Total); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs index cf6b05d8b9..875c65c7ac 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs @@ -1,10 +1,14 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Content; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Api.Management.ViewModels.Media.Collection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.OperationStatus; @@ -17,6 +21,12 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Collection; [Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)] public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase { + protected MediaCollectionControllerBase(IUmbracoMapper mapper, SignProviderCollection signProviders) + : base(mapper, signProviders) + { + } + + [Obsolete("Please use the constructor with all parameters. Scheduled to be removed in Umbraco 18")] protected MediaCollectionControllerBase(IUmbracoMapper mapper) : base(mapper) { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/ItemMediaItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/ItemMediaItemController.cs index 7bab65c3ca..bd8684e7a0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/ItemMediaItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/ItemMediaItemController.cs @@ -1,8 +1,12 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; using Umbraco.Cms.Api.Management.ViewModels.Media.Item; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -15,23 +19,35 @@ public class ItemMediaItemController : MediaItemControllerBase { private readonly IEntityService _entityService; private readonly IMediaPresentationFactory _mediaPresentationFactory; + private readonly SignProviderCollection _signProviders; - public ItemMediaItemController(IEntityService entityService, IMediaPresentationFactory mediaPresentationFactory) + [ActivatorUtilitiesConstructor] + public ItemMediaItemController( + IEntityService entityService, + IMediaPresentationFactory mediaPresentationFactory, + SignProviderCollection signProvider) { _entityService = entityService; _mediaPresentationFactory = mediaPresentationFactory; + _signProviders = signProvider; + } + + [Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18")] + public ItemMediaItemController(IEntityService entityService, IMediaPresentationFactory mediaPresentationFactory) + : this(entityService, mediaPresentationFactory, StaticServiceProvider.Instance.GetRequiredService()) + { } [HttpGet] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public Task Item( + public async Task Item( CancellationToken cancellationToken, [FromQuery(Name = "id")] HashSet ids) { if (ids.Count is 0) { - return Task.FromResult(Ok(Enumerable.Empty())); + return Ok(Enumerable.Empty()); } IEnumerable media = _entityService @@ -39,6 +55,16 @@ public class ItemMediaItemController : MediaItemControllerBase .OfType(); IEnumerable responseModels = media.Select(_mediaPresentationFactory.CreateItemResponseModel); - return Task.FromResult(Ok(responseModels)); + await PopulateSigns(responseModels); + + return Ok(responseModels); + } + + private async Task PopulateSigns(IEnumerable itemViewModels) + { + foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) + { + await signProvider.PopulateSignsAsync(itemViewModels); + } } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs index 9fde8d2a8e..bf5bade404 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs @@ -1,8 +1,10 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; @@ -13,6 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree; [ApiVersion("1.0")] public class AncestorsMediaTreeController : MediaTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsMediaTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -24,6 +27,19 @@ public class AncestorsMediaTreeController : MediaTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public AncestorsMediaTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IMediaPresentationFactory mediaPresentationFactory) + : base(entityService, signProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/ChildrenMediaTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/ChildrenMediaTreeController.cs index 6aee4b0206..c61de0a65b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/ChildrenMediaTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/ChildrenMediaTreeController.cs @@ -1,9 +1,11 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; @@ -14,6 +16,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree; [ApiVersion("1.0")] public class ChildrenMediaTreeController : MediaTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenMediaTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -25,6 +28,19 @@ public class ChildrenMediaTreeController : MediaTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public ChildrenMediaTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IMediaPresentationFactory mediaPresentationFactory) + : base(entityService, signProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs index 7b2f7facd4..f3283466c4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/MediaTreeControllerBase.cs @@ -1,12 +1,15 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Security; @@ -24,6 +27,7 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase(), + userStartNodeEntitiesService, + dataTypeService, + appCaches, + backofficeSecurityAccessor, + mediaPresentationFactory) + { + } + + [ActivatorUtilitiesConstructor] + public MediaTreeControllerBase( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IMediaPresentationFactory mediaPresentationFactory) + : base(entityService, signProviders, userStartNodeEntitiesService, dataTypeService) { _appCaches = appCaches; _backofficeSecurityAccessor = backofficeSecurityAccessor; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/RootMediaTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/RootMediaTreeController.cs index d2ffa8a4bb..2cadb4c4fc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/RootMediaTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/RootMediaTreeController.cs @@ -1,9 +1,11 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; @@ -14,6 +16,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree; [ApiVersion("1.0")] public class RootMediaTreeController : MediaTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootMediaTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -25,6 +28,19 @@ public class RootMediaTreeController : MediaTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public RootMediaTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IMediaPresentationFactory mediaPresentationFactory) + : base(entityService, signProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs index 012816dbfc..910eeb0bfb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Security; @@ -12,6 +14,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree; public class SiblingsMediaTreeController : MediaTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsMediaTreeController( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, @@ -23,6 +26,19 @@ public class SiblingsMediaTreeController : MediaTreeControllerBase { } + [ActivatorUtilitiesConstructor] + public SiblingsMediaTreeController( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IMediaPresentationFactory mediaPresentationFactory) + : base(entityService, signProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory) + { + } + [HttpGet("siblings")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs index ff90cea529..eeb3c73c5e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs @@ -1,6 +1,8 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -9,11 +11,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree; [ApiVersion("1.0")] public class AncestorsMediaTypeTreeController : MediaTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) : base(entityService, mediaTypeService) { } + [ActivatorUtilitiesConstructor] + public AncestorsMediaTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IMediaTypeService mediaTypeService) + : base(entityService, signProviders, mediaTypeService) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs index 3662360c89..545c5178e3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs @@ -1,7 +1,9 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -10,11 +12,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree; [ApiVersion("1.0")] public class ChildrenMediaTypeTreeController : MediaTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) : base(entityService, mediaTypeService) { } + [ActivatorUtilitiesConstructor] + public ChildrenMediaTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IMediaTypeService mediaTypeService) + : base(entityService, signProviders, mediaTypeService) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs index a3cc202b28..9731a7fb4a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs @@ -1,9 +1,12 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -19,8 +22,17 @@ public class MediaTypeTreeControllerBase : FolderTreeControllerBase + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + mediaTypeService) + { + } + + public MediaTypeTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders, IMediaTypeService mediaTypeService) + : base(entityService, signProviders) => _mediaTypeService = mediaTypeService; protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MediaType; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs index 7c0aca4791..3def7719cf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs @@ -1,7 +1,9 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -10,11 +12,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree; [ApiVersion("1.0")] public class RootMediaTypeTreeController : MediaTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) : base(entityService, mediaTypeService) { } + [ActivatorUtilitiesConstructor] + public RootMediaTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IMediaTypeService mediaTypeService) + : base(entityService, signProviders, mediaTypeService) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs index f4dd06d632..5e873fb9cc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/SiblingsMediaTypeTreeController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -8,11 +10,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree; public class SiblingsMediaTypeTreeController : MediaTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) : base(entityService, mediaTypeService) { } + [ActivatorUtilitiesConstructor] + public SiblingsMediaTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IMediaTypeService mediaTypeService) + : base(entityService, signProviders, mediaTypeService) + { + } + [HttpGet("siblings")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs index 2066e79768..90ef8d12d9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs @@ -1,7 +1,8 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -15,10 +16,16 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessMemberGroups)] public class MemberGroupTreeControllerBase : NamedEntityTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public MemberGroupTreeControllerBase(IEntityService entityService) : base(entityService) { } + public MemberGroupTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberGroup; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs index 3328df5514..08f8319205 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Api.Management.Services.Signs; +using Microsoft.Extensions.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Tree; [ApiVersion("1.0")] public class RootMemberGroupTreeController : MemberGroupTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootMemberGroupTreeController(IEntityService entityService) : base(entityService) { } + [ActivatorUtilitiesConstructor] + public RootMemberGroupTreeController(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs index 9fc8111b6c..a612b32d87 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs @@ -1,9 +1,12 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -18,10 +21,20 @@ public class MemberTypeTreeControllerBase : NamedEntityTreeControllerBase + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + memberTypeService) + { + } + + public MemberTypeTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders, IMemberTypeService memberTypeService) + : base(entityService, signProviders) => _memberTypeService = memberTypeService; + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberType; protected override MemberTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs index eeae87da51..4ce9248ecc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Services; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree; [ApiVersion("1.0")] public class RootMemberTypeTreeController : MemberTypeTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootMemberTypeTreeController(IEntityService entityService, IMemberTypeService memberTypeService) : base(entityService, memberTypeService) { } + [ActivatorUtilitiesConstructor] + public RootMemberTypeTreeController(IEntityService entityService, SignProviderCollection signProviders, IMemberTypeService memberTypeService) + : base(entityService, signProviders, memberTypeService) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs index 3d5f0d3ff5..943aeba37f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs @@ -1,6 +1,8 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -9,11 +11,18 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree; [ApiVersion("1.0")] public class AncestorsTemplateTreeController : TemplateTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public AncestorsTemplateTreeController(IEntityService entityService) : base(entityService) { } + [ActivatorUtilitiesConstructor] + public AncestorsTemplateTreeController(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + [HttpGet("ancestors")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/ChildrenTemplateTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/ChildrenTemplateTreeController.cs index b5d487b0ed..030be7d062 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/ChildrenTemplateTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/ChildrenTemplateTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Services; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree; [ApiVersion("1.0")] public class ChildrenTemplateTreeController : TemplateTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public ChildrenTemplateTreeController(IEntityService entityService) : base(entityService) { } + [ActivatorUtilitiesConstructor] + public ChildrenTemplateTreeController(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/RootTemplateTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/RootTemplateTreeController.cs index 4de5731c40..871fdcf368 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/RootTemplateTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/RootTemplateTreeController.cs @@ -1,20 +1,29 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Services; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree; [ApiVersion("1.0")] public class RootTemplateTreeController : TemplateTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public RootTemplateTreeController(IEntityService entityService) : base(entityService) { } + [ActivatorUtilitiesConstructor] + public RootTemplateTreeController(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs index f566dd52f6..542014c042 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/SiblingsTemplateTreeController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Services; @@ -8,11 +10,19 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree; public class SiblingsTemplateTreeController : TemplateTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public SiblingsTemplateTreeController(IEntityService entityService) : base(entityService) { } + [ActivatorUtilitiesConstructor] + public SiblingsTemplateTreeController(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + + [HttpGet("siblings")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs index 2ab19a1ee5..aa65c4f7a5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/TemplateTreeControllerBase.cs @@ -1,7 +1,8 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -15,10 +16,16 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessTemplates)] public class TemplateTreeControllerBase : NamedEntityTreeControllerBase { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public TemplateTreeControllerBase(IEntityService entityService) : base(entityService) { } + public TemplateTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Template; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs index 2f5a62b9bb..8a5cc27ece 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -13,8 +16,21 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree; public abstract class EntityTreeControllerBase : ManagementApiControllerBase where TItem : EntityTreeItemResponseModel, new() { + private readonly SignProviderCollection _signProviders; + + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] protected EntityTreeControllerBase(IEntityService entityService) - => EntityService = entityService; + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + protected EntityTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders) + { + EntityService = entityService; + _signProviders = signProviders; + } protected IEntityService EntityService { get; } @@ -22,34 +38,38 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB protected virtual Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)); - protected Task>> GetRoot(int skip, int take) + protected async Task>> GetRoot(int skip, int take) { IEntitySlim[] rootEntities = GetPagedRootEntities(skip, take, out var totalItems); TItem[] treeItemViewModels = MapTreeItemViewModels(null, rootEntities); + await PopulateSigns(treeItemViewModels); + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); - return Task.FromResult>>(Ok(result)); + return Ok(result); } - protected Task>> GetChildren(Guid parentId, int skip, int take) + protected async Task>> GetChildren(Guid parentId, int skip, int take) { IEntitySlim[] children = GetPagedChildEntities(parentId, skip, take, out var totalItems); TItem[] treeItemViewModels = MapTreeItemViewModels(parentId, children); + await PopulateSigns(treeItemViewModels); + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); - return Task.FromResult>>(Ok(result)); + return Ok(result); } - protected Task>> GetSiblings(Guid target, int before, int after) + protected async Task>> GetSiblings(Guid target, int before, int after) { IEntitySlim[] siblings = GetSiblingEntities(target, before, after, out var totalBefore, out var totalAfter); if (siblings.Length == 0) { - return Task.FromResult>>(NotFound()); + return NotFound(); } IEntitySlim? entity = siblings.FirstOrDefault(); @@ -59,16 +79,18 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, siblings); + await PopulateSigns(treeItemViewModels); + SubsetViewModel result = SubsetViewModel(treeItemViewModels, totalBefore, totalAfter); - return Task.FromResult>>(Ok(result)); + return Ok(result); } protected virtual async Task>> GetAncestors(Guid descendantKey, bool includeSelf = true) { IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf); - TItem[] result = ancestorEntities + TItem[] treeItemViewModels = ancestorEntities .Select(ancestor => { IEntitySlim? parent = ancestor.ParentId > 0 @@ -79,7 +101,9 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB }) .ToArray(); - return Ok(result); + await PopulateSigns(treeItemViewModels); + + return Ok(treeItemViewModels); } protected virtual Task GetAncestorEntitiesAsync(Guid descendantKey, bool includeSelf) @@ -138,6 +162,14 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) => entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray(); + protected virtual async Task PopulateSigns(TItem[] treeItemViewModels) + { + foreach (ISignProvider signProvider in _signProviders.Where(x => x.CanProvideSigns())) + { + await signProvider.PopulateSignsAsync(treeItemViewModels); + } + } + protected virtual TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) { var viewModel = new TItem @@ -147,9 +179,9 @@ public abstract class EntityTreeControllerBase : ManagementApiControllerB Parent = parentKey.HasValue ? new ReferenceByIdModel { - Id = parentKey.Value + Id = parentKey.Value, } - : null + : null, }; return viewModel; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs index 4e52bbe4e8..d54e537637 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs @@ -1,9 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Extensions; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.Tree; @@ -28,9 +31,16 @@ public abstract class FolderTreeControllerBase : NamedEntityTreeControlle } } + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] protected FolderTreeControllerBase(IEntityService entityService) - : base(entityService) => - // ReSharper disable once VirtualMemberCallInConstructor + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + protected FolderTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) => _folderObjectTypeId = FolderObjectType.GetGuid(); protected abstract UmbracoObjectTypes FolderObjectType { get; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs index a50c6b1cbe..be1fc3c6ce 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/NamedEntityTreeControllerBase.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -7,11 +8,17 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree; public abstract class NamedEntityTreeControllerBase : EntityTreeControllerBase where TItem : NamedEntityTreeItemResponseModel, new() { + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] protected NamedEntityTreeControllerBase(IEntityService entityService) : base(entityService) { } + protected NamedEntityTreeControllerBase(IEntityService entityService, SignProviderCollection signProviders) + : base(entityService, signProviders) + { + } + protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) { TItem item = base.MapTreeItemViewModel(parentKey, entity); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs index 6f95e6210e..a6d3ba5ff5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -1,7 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Models.Entities; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Signs; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -19,11 +22,25 @@ public abstract class UserStartNodeTreeControllerBase : EntityTreeControl private Dictionary _accessMap = new(); private Guid? _dataTypeKey; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] protected UserStartNodeTreeControllerBase( IEntityService entityService, IUserStartNodeEntitiesService userStartNodeEntitiesService, IDataTypeService dataTypeService) - : base(entityService) + : this( + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + userStartNodeEntitiesService, + dataTypeService) + { + } + + protected UserStartNodeTreeControllerBase( + IEntityService entityService, + SignProviderCollection signProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService) + : base(entityService, signProviders) { _userStartNodeEntitiesService = userStartNodeEntitiesService; _dataTypeService = dataTypeService; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs new file mode 100644 index 0000000000..e93fb98dfc --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.Collections.cs @@ -0,0 +1,27 @@ +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Extensions +{ + /// + /// Extension methods for for the Umbraco back office + /// + public static partial class UmbracoBuilderExtensions + { + internal static void AddCollectionBuilders(this IUmbracoBuilder builder) + { + builder.SignProviders() + .Append() + .Append() + .Append() + .Append(); + } + + /// + /// Gets the sign providers collection builder. + /// + /// The builder. + public static SignProviderCollectionBuilder SignProviders(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index 48e53c97e0..a788b1fad2 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.Configuration; using Umbraco.Cms.Api.Common.DependencyInjection; using Umbraco.Cms.Api.Management.Configuration; @@ -95,6 +95,8 @@ public static partial class UmbracoBuilderExtensions }); } + builder.AddCollectionBuilders(); + return builder; } } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs index 0e9aeba492..7aeb4f1b15 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs @@ -115,6 +115,7 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition +/// Implements a that provides signs for entities that have a collection. +/// +public class HasCollectionSignProvider : ISignProvider +{ + private const string Alias = Constants.Conventions.Signs.Prefix + "HasCollection"; + + /// + public bool CanProvideSigns() + where TItem : IHasSigns => + typeof(TItem) == typeof(DocumentTreeItemResponseModel) || + typeof(TItem) == typeof(DocumentCollectionResponseModel) || + typeof(TItem) == typeof(DocumentItemResponseModel) || + typeof(TItem) == typeof(MediaTreeItemResponseModel) || + typeof(TItem) == typeof(MediaCollectionResponseModel) || + typeof(TItem) == typeof(MediaItemResponseModel); + + /// + public Task PopulateSignsAsync(IEnumerable itemViewModels) + where TItem : IHasSigns + { + foreach (TItem item in itemViewModels) + { + if (HasCollection(item)) + { + item.AddSign(Alias); + } + } + + return Task.CompletedTask; + } + + /// + /// Determines if the given view model contains a collection. + /// + private static bool HasCollection(object item) => item switch + { + DocumentTreeItemResponseModel { DocumentType.Collection: not null } => true, + DocumentCollectionResponseModel { DocumentType.Collection: not null } => true, + DocumentItemResponseModel { DocumentType.Collection: not null } => true, + MediaTreeItemResponseModel { MediaType.Collection: not null } => true, + MediaCollectionResponseModel { MediaType.Collection: not null } => true, + MediaItemResponseModel { MediaType.Collection: not null } => true, + _ => false, + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs new file mode 100644 index 0000000000..50c421a792 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProvider.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Api.Management.Services.Signs; + +/// +/// Implements a that provides signs for documents that have pending changes. +/// +public class HasPendingChangesSignProvider : ISignProvider +{ + private const string Alias = Constants.Conventions.Signs.Prefix + "PendingChanges"; + + /// + public bool CanProvideSigns() + where TItem : IHasSigns => + typeof(TItem) == typeof(DocumentTreeItemResponseModel) || + typeof(TItem) == typeof(DocumentCollectionResponseModel) || + typeof(TItem) == typeof(DocumentItemResponseModel); + + /// + public Task PopulateSignsAsync(IEnumerable itemViewModels) + where TItem : IHasSigns + { + foreach (TItem item in itemViewModels) + { + if (HasPendingChanges(item)) + { + item.AddSign(Alias); + } + } + + return Task.CompletedTask; + } + + /// + /// Determines if the given item has any variant that has pending changes. + /// + private bool HasPendingChanges(object item) => item switch + { + DocumentTreeItemResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, + DocumentCollectionResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, + DocumentItemResponseModel { Variants: var v } when v.Any(x => x.State == DocumentVariantState.PublishedPendingChanges) => true, + _ => false, + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs new file mode 100644 index 0000000000..599d10ae67 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProvider.cs @@ -0,0 +1,43 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Api.Management.Services.Signs; + +/// +/// Implements a that provides signs for documents that are scheduled for publication. +/// +internal class HasScheduleSignProvider : ISignProvider +{ + private const string Alias = Constants.Conventions.Signs.Prefix + "ScheduledForPublish"; + + private readonly IContentService _contentService; + + /// + /// Initializes a new instance of the class. + /// + public HasScheduleSignProvider(IContentService contentService) => _contentService = contentService; + + /// + public bool CanProvideSigns() + where TItem : IHasSigns => + typeof(TItem) == typeof(DocumentTreeItemResponseModel) || + typeof(TItem) == typeof(DocumentCollectionResponseModel) || + typeof(TItem) == typeof(DocumentItemResponseModel); + + /// + public Task PopulateSignsAsync(IEnumerable itemViewModels) + where TItem : IHasSigns + { + IEnumerable contentKeysScheduledForPublishing = _contentService.GetScheduledContentKeys(itemViewModels.Select(x => x.Id)); + foreach (Guid key in contentKeysScheduledForPublishing) + { + itemViewModels.First(x => x.Id == key).AddSign(Alias); + } + + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs new file mode 100644 index 0000000000..e0324f05c8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/ISignProvider.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Api.Management.ViewModels; + +namespace Umbraco.Cms.Api.Management.Services.Signs; + +/// +/// Defines operation for the provision of presentation signs for item, tree and collection nodes. +/// +public interface ISignProvider +{ + /// + /// Gets a value indicating whether this provider can provide signs for the specified item type. + /// + /// Type of view model supporting signs. + bool CanProvideSigns() + where TItem : IHasSigns; + + /// + /// Populates the provided item view models with signs. + /// + /// Type of item view model supporting signs. + /// The collection of item view models to be populated with signs. + Task PopulateSignsAsync(IEnumerable itemViewModels) + where TItem : IHasSigns; +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/IsProtectedSignProvider.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/IsProtectedSignProvider.cs new file mode 100644 index 0000000000..acd375ca9e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/IsProtectedSignProvider.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Api.Management.Services.Signs; + +/// +/// Implements a that provides signs for documents that are protected. +/// +internal class IsProtectedSignProvider : ISignProvider +{ + private const string Alias = Constants.Conventions.Signs.Prefix + "IsProtected"; + + /// > + public bool CanProvideSigns() + where TItem : IHasSigns => + typeof(IIsProtected).IsAssignableFrom(typeof(TItem)); + + /// > + public Task PopulateSignsAsync(IEnumerable itemViewModels) + where TItem : IHasSigns + { + foreach (TItem item in itemViewModels) + { + if (item is IIsProtected { IsProtected: true }) + { + item.AddSign(Alias); + } + } + + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/SignProviderCollection.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/SignProviderCollection.cs new file mode 100644 index 0000000000..81c50aafd5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/SignProviderCollection.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Api.Management.Services.Signs; + +/// +/// Defines an ordered collection of . +/// +public class SignProviderCollection : BuilderCollectionBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The collection items. + public SignProviderCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.Cms.Api.Management/Services/Signs/SignProviderCollectionBuilder.cs b/src/Umbraco.Cms.Api.Management/Services/Signs/SignProviderCollectionBuilder.cs new file mode 100644 index 0000000000..a07d67634f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/Signs/SignProviderCollectionBuilder.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Api.Management.Services.Signs; + +/// +/// Builds an ordered collection of . +/// +public class SignProviderCollectionBuilder : OrderedCollectionBuilderBase +{ + /// + protected override SignProviderCollectionBuilder This => this; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentResponseModelBase.cs index 1eddf7af46..8425079f52 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentResponseModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentResponseModelBase.cs @@ -1,11 +1,19 @@ -using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentEditing; namespace Umbraco.Cms.Api.Management.ViewModels.Content; public abstract class ContentResponseModelBase - : ContentModelBase + : ContentModelBase, IHasSigns where TValueResponseModelBase : ValueModelBase where TVariantResponseModel : VariantResponseModelBase { + private readonly List _signs = []; + public Guid Id { get; set; } + + public IEnumerable Signs => _signs.AsEnumerable(); + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCollectionReferenceResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCollectionReferenceResponseModelBase.cs index 70d3b992ad..1760b8d364 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCollectionReferenceResponseModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCollectionReferenceResponseModelBase.cs @@ -7,4 +7,6 @@ public abstract class ContentTypeCollectionReferenceResponseModelBase public string Alias { get; set; } = string.Empty; public string Icon { get; set; } = string.Empty; + + public ReferenceByIdModel? Collection { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs index 391714346a..107ec0c891 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.DocumentType; namespace Umbraco.Cms.Api.Management.ViewModels.Document.Collection; -public class DocumentCollectionResponseModel : ContentCollectionResponseModelBase +public class DocumentCollectionResponseModel : ContentCollectionResponseModelBase, IHasSigns, IIsProtected { public DocumentTypeCollectionReferenceResponseModel DocumentType { get; set; } = new(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Item/DocumentItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Item/DocumentItemResponseModel.cs index 25f4975b9f..4301b8096d 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Item/DocumentItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Item/DocumentItemResponseModel.cs @@ -1,10 +1,9 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Api.Management.ViewModels.Item; namespace Umbraco.Cms.Api.Management.ViewModels.Document.Item; -public class DocumentItemResponseModel : ItemResponseModelBase +public class DocumentItemResponseModel : ItemResponseModelBase, IIsProtected { public bool IsTrashed { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/IHasSigns.cs b/src/Umbraco.Cms.Api.Management/ViewModels/IHasSigns.cs new file mode 100644 index 0000000000..23aafd1e2b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/IHasSigns.cs @@ -0,0 +1,30 @@ +namespace Umbraco.Cms.Api.Management.ViewModels; + +/// +/// Marker interface that indicates the type has support for backoffice signs (presented as icons +/// overlaying the main icon for the entity). +/// +public interface IHasSigns +{ + /// + /// Gets the unique identifier for the entity. + /// + Guid Id { get; } + + /// + /// Gets the collection of signs for the entity. + /// + IEnumerable Signs { get; } + + /// + /// Adds a sign to the entity with the specified alias. + /// + /// + void AddSign(string alias); + + /// + /// Removes a sign from the entity with the specified alias. + /// + /// + void RemoveSign(string alias); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/IIsProtected.cs b/src/Umbraco.Cms.Api.Management/ViewModels/IIsProtected.cs new file mode 100644 index 0000000000..3babb19b82 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/IIsProtected.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels; + +/// +/// Marker interface that indicates the type can represent the state of protected content. +/// +public interface IIsProtected +{ + /// + /// Gets or sets a value indicating whether the model represents content that is protected. + /// + bool IsProtected { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Item/ItemResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Item/ItemResponseModelBase.cs index f39ee8c578..01622557fd 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Item/ItemResponseModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Item/ItemResponseModelBase.cs @@ -1,6 +1,14 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Item; -public abstract class ItemResponseModelBase +public abstract class ItemResponseModelBase : IHasSigns { + private readonly List _signs = []; + public Guid Id { get; set; } + + public IEnumerable Signs => _signs.AsEnumerable(); + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/SignModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/SignModel.cs new file mode 100644 index 0000000000..0cfec8e36f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/SignModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels; + +public class SignModel +{ + public required string Alias { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/ContentTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/ContentTreeItemResponseModel.cs index a18f1c8ea8..aab2af128d 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/ContentTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/ContentTreeItemResponseModel.cs @@ -6,7 +6,5 @@ public abstract class ContentTreeItemResponseModel : EntityTreeItemResponseModel public bool IsTrashed { get; set; } - public Guid Id { get; set; } - public DateTimeOffset CreateDate { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs index 1bde763102..f784665657 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.DocumentType; namespace Umbraco.Cms.Api.Management.ViewModels.Tree; -public class DocumentTreeItemResponseModel : ContentTreeItemResponseModel +public class DocumentTreeItemResponseModel : ContentTreeItemResponseModel, IIsProtected { public bool IsProtected { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemResponseModel.cs index 3373daf20d..ac1057e29e 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/EntityTreeItemResponseModel.cs @@ -1,8 +1,16 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.Tree; +namespace Umbraco.Cms.Api.Management.ViewModels.Tree; -public class EntityTreeItemResponseModel : TreeItemPresentationModel +public class EntityTreeItemResponseModel : TreeItemPresentationModel, IHasSigns { + private readonly List _signs = []; + public Guid Id { get; set; } public ReferenceByIdModel? Parent { get; set; } + + public IEnumerable Signs => _signs.AsEnumerable(); + + public void AddSign(string alias) => _signs.Add(new SignModel { Alias = alias }); + + public void RemoveSign(string alias) => _signs.RemoveAll(x => x.Alias == alias); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTreeItemResponseModel.cs index 8e6aa960cd..a952269e12 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTreeItemResponseModel.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.MediaType; namespace Umbraco.Cms.Api.Management.ViewModels.Tree; diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 989b947adc..fd57fef49a 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -303,5 +303,16 @@ public static partial class Constants { public const string Prefix = "umb://"; } + + /// + /// Constants for relating to view model signs. + /// + public static class Signs + { + /// + /// Prefix for all sign aliases. + /// + public const string Prefix = "Umb."; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index f6ee0b6926..9ff75c2b3d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -50,6 +50,15 @@ public interface IDocumentRepository : IContentRepository, IReadR /// IEnumerable GetContentForRelease(DateTime date); + /// + /// Gets the content keys from the provided collection of keys that are scheduled for publishing. + /// + /// The content keys. + /// + /// The provided collection of content keys filtered for those that are scheduled for publishing. + /// + IEnumerable GetScheduledContentKeys(Guid[] keys) => []; + /// /// Get the count of published items /// diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 246682ec14..066f97cfbf 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1015,6 +1015,23 @@ public class ContentService : RepositoryService, IContentService /// True if the content has any children otherwise False public bool HasChildren(int id) => CountChildren(id) > 0; + + /// + public IEnumerable GetScheduledContentKeys(IEnumerable keys) + { + Guid[] idsA = keys.ToArray(); + if (idsA.Length == 0) + { + return Enumerable.Empty(); + } + + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.GetScheduledContentKeys(idsA); + } + } + /// /// Checks if the passed in can be published based on the ancestors publish state. /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index a7bde2dc46..228d7fa6db 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -275,6 +275,15 @@ public interface IContentService : IContentServiceBase /// bool HasChildren(int id); + /// + /// Gets the content keys from the provided collection of keys that are scheduled for publishing. + /// + /// The content keys. + /// + /// The provided collection of content keys filtered for those that are scheduled for publishing. + /// + IEnumerable GetScheduledContentKeys(IEnumerable keys) => []; + #endregion #region Save, Delete Document diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 687a878c83..1279d62186 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1671,6 +1671,27 @@ public class DocumentRepository : ContentRepositoryBase(sql)); } + /// + public IEnumerable GetScheduledContentKeys(Guid[] keys) + { + var action = ContentScheduleAction.Release.ToString(); + DateTime now = DateTime.UtcNow; + + Sql sql = SqlContext.Sql(); + sql + .Select(x => x.UniqueId) + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .WhereIn(x => x.UniqueId, keys) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date >= now)); + + return Database.Fetch(sql); + } + /// public IEnumerable GetContentForExpiration(DateTime date) { diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs index 6e1df8b5d0..6923110a2b 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs @@ -21,6 +21,7 @@ using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence; +/// internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRepository { private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; @@ -58,9 +59,11 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe _nucacheSettings = nucacheSettings; } + /// public async Task DeleteContentItemAsync(int id) - => await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = id }); + => await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId = @id", new { id }); + /// public async Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState) { IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); @@ -69,7 +72,8 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe if (contentCacheNode.IsDraft) { await OnRepositoryRefreshed(serializer, contentCacheNode, true); - // if it's a draft node we don't need to worry about the published state + + // If it's a draft node we don't need to worry about the published state. return; } @@ -79,11 +83,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe await OnRepositoryRefreshed(serializer, contentCacheNode, false); break; case PublishedState.Unpublishing: - await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = contentCacheNode.Id }); + await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId = @id AND published = 1", new { id = contentCacheNode.Id }); break; } } + /// public async Task RefreshMediaAsync(ContentCacheNode contentCacheNode) { IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); @@ -107,15 +112,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe mediaTypeIds is not null && mediaTypeIds.Count == 0 && memberTypeIds is not null && memberTypeIds.Count == 0) { - if (Database.DatabaseType == DatabaseType.SqlServer2012) - { - Database.Execute($"TRUNCATE TABLE cmsContentNu"); - } - - if (Database.DatabaseType == DatabaseType.SQLite) - { - Database.Execute($"DELETE FROM cmsContentNu"); - } + TruncateContent(); } RebuildContentDbCache(serializer, _nucacheSettings.Value.SqlPageSize, contentTypeIds); @@ -123,6 +120,20 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe RebuildMemberDbCache(serializer, _nucacheSettings.Value.SqlPageSize, memberTypeIds); } + private void TruncateContent() + { + if (Database.DatabaseType == DatabaseType.SqlServer2012) + { + Database.Execute($"TRUNCATE TABLE cmsContentNu"); + } + + if (Database.DatabaseType == DatabaseType.SQLite) + { + Database.Execute($"DELETE FROM cmsContentNu"); + } + } + + /// public async Task GetContentSourceAsync(Guid key, bool preview = false) { Sql? sql = SqlContentSourcesSelect() @@ -147,6 +158,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe return CreateContentNodeKit(dto, serializer, preview); } + /// public async Task> GetContentSourcesAsync(IEnumerable keys, bool preview = false) { Sql? sql = SqlContentSourcesSelect() @@ -170,7 +182,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe private IEnumerable GetContentSourceByDocumentTypeKey(IEnumerable documentTypeKeys, Guid objectType) { Guid[] keys = documentTypeKeys.ToArray(); - if (keys.Any() is false) + if (keys.Length == 0) { return []; } @@ -182,14 +194,15 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe : throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null); sql.InnerJoin("n") - .On((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent") - .Append(SqlObjectTypeNotTrashed(SqlContext, objectType)) - .WhereIn(x => x.UniqueId, keys,"n") - .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + .On((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent") + .Append(SqlObjectTypeNotTrashed(SqlContext, objectType)) + .WhereIn(x => x.UniqueId, keys,"n") + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); return GetContentNodeDtos(sql); } + /// public IEnumerable GetContentByContentTypeKey(IEnumerable keys, ContentCacheDataSerializerEntityType entityType) { Guid objectType = entityType switch @@ -222,6 +235,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe public IEnumerable GetDocumentKeysByContentTypeKeys(IEnumerable keys, bool published = false) => GetContentSourceByDocumentTypeKey(keys, Constants.ObjectTypes.Document).Where(x => x.Published == published).Select(x => x.Key); + /// public async Task GetMediaSourceAsync(Guid key) { Sql? sql = SqlMediaSourcesSelect() @@ -241,6 +255,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe return CreateMediaNodeKit(dto, serializer); } + /// public async Task> GetMediaSourcesAsync(IEnumerable keys) { Sql? sql = SqlMediaSourcesSelect() @@ -262,13 +277,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview) { - // use a custom SQL to update row version on each update - // db.InsertOrUpdate(dto); + ContentNuDto dto = GetDtoFromCacheNode(content, !preview, serializer); await Database.InsertOrUpdateAsync( dto, - "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", + "SET data = @data, dataRaw = @dataRaw, rv = rv + 1 WHERE nodeId = @id AND published = @published", new { dataRaw = dto.RawData ?? Array.Empty(), @@ -278,7 +292,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe }); } - // assumes content tree lock + /// + /// Rebuilds the content database cache for documents. + /// + /// + /// Assumes content tree lock. + /// private void RebuildContentDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) { if (contentTypeIds is null) @@ -288,55 +307,35 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe Guid contentObjectType = Constants.ObjectTypes.Document; - // remove all - if anything fails the transaction will rollback + // Remove all - if anything fails the transaction will rollback. if (contentTypeIds.Count == 0) { - // must support SQL-CE - Database.Execute( - @"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = contentObjectType }); + DeleteForObjectType(contentObjectType); } else { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - Database.Execute( - $@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = contentObjectType, ctypes = contentTypeIds }); + DeleteForObjectTypeAndContentTypes(contentObjectType, contentTypeIds); } - // insert back - if anything fails the transaction will rollback - IQuery query = SqlContext.Query(); - if (contentTypeIds.Count > 0) - { - query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) - } + // Insert back - if anything fails the transaction will rollback. + IQuery query = GetInsertQuery(contentTypeIds); long pageIndex = 0; long processed = 0; long total; do { - // the tree is locked, counting and comparing to total is safe + // The tree is locked, counting and comparing to total is safe. IEnumerable descendants = _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); var items = new List(); var count = 0; foreach (IContent c in descendants) { - // always the edited version + // Always include the edited version. items.Add(GetDtoFromContent(c, false, serializer)); - // and also the published version if it makes any sense + // And also the published version if the document is published. if (c.Published) { items.Add(GetDtoFromContent(c, true, serializer)); @@ -347,10 +346,16 @@ WHERE cmsContentNu.nodeId IN ( Database.BulkInsertRecords(items); processed += count; - } while (processed < total); + } + while (processed < total); } - // assumes media tree lock + /// + /// Rebuilds the content database cache for media. + /// + /// + /// Assumes media tree lock. + /// private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) { if (contentTypeIds is null) @@ -360,54 +365,40 @@ WHERE cmsContentNu.nodeId IN ( Guid mediaObjectType = Constants.ObjectTypes.Media; - // remove all - if anything fails the transaction will rollback + // Remove all - if anything fails the transaction will rollback. if (contentTypeIds.Count == 0) { - // must support SQL-CE - Database.Execute( - @"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = mediaObjectType }); + DeleteForObjectType(mediaObjectType); } else { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - Database.Execute( - $@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = mediaObjectType, ctypes = contentTypeIds }); + DeleteForObjectTypeAndContentTypes(mediaObjectType, contentTypeIds); } - // insert back - if anything fails the transaction will rollback - IQuery query = SqlContext.Query(); - if (contentTypeIds.Count > 0) - { - query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) - } + // Insert back - if anything fails the transaction will rollback. + IQuery query = GetInsertQuery(contentTypeIds); long pageIndex = 0; long processed = 0; long total; do { - // the tree is locked, counting and comparing to total is safe + // The tree is locked, counting and comparing to total is safe. IEnumerable descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray(); + ContentNuDto[] items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray(); Database.BulkInsertRecords(items); processed += items.Length; - } while (processed < total); + } + while (processed < total); } - // assumes member tree lock + /// + /// Rebuilds the content database cache for members. + /// + /// + /// Assumes member tree lock. + /// private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) { if (contentTypeIds is null) @@ -417,38 +408,18 @@ WHERE cmsContentNu.nodeId IN ( Guid memberObjectType = Constants.ObjectTypes.Member; - // remove all - if anything fails the transaction will rollback + // Remove all - if anything fails the transaction will rollback. if (contentTypeIds.Count == 0) { - // must support SQL-CE - Database.Execute( - @"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType -)", - new { objType = memberObjectType }); + DeleteForObjectType(memberObjectType); } else { - // assume number of ctypes won't blow IN(...) - // must support SQL-CE - Database.Execute( - $@"DELETE FROM cmsContentNu -WHERE cmsContentNu.nodeId IN ( - SELECT id FROM umbracoNode - JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id - WHERE umbracoNode.nodeObjectType=@objType - AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) -)", - new { objType = memberObjectType, ctypes = contentTypeIds }); + DeleteForObjectTypeAndContentTypes(memberObjectType, contentTypeIds); } - // insert back - if anything fails the transaction will rollback - IQuery query = SqlContext.Query(); - if (contentTypeIds.Count > 0) - { - query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) - } + // Insert back - if anything fails the transaction will rollback. + IQuery query = GetInsertQuery(contentTypeIds); long pageIndex = 0; long processed = 0; @@ -460,12 +431,46 @@ WHERE cmsContentNu.nodeId IN ( ContentNuDto[] items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray(); Database.BulkInsertRecords(items); processed += items.Length; - } while (processed < total); + } + while (processed < total); + } + + private void DeleteForObjectType(Guid nodeObjectType) => + Database.Execute( + @" + DELETE FROM cmsContentNu + WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType = @objType + )", + new { objType = nodeObjectType }); + + private void DeleteForObjectTypeAndContentTypes(Guid nodeObjectType, IReadOnlyCollection contentTypeIds) => + Database.Execute( + $@" + DELETE FROM cmsContentNu + WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType = @objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) + )", + new { objType = nodeObjectType, ctypes = contentTypeIds }); + + private IQuery GetInsertQuery(IReadOnlyCollection contentTypeIds) + where TContent : IContentBase + { + IQuery query = SqlContext.Query(); + if (contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + return query; } private ContentNuDto GetDtoFromCacheNode(ContentCacheNode cacheNode, bool published, IContentCacheDataSerializer serializer) { - // the dictionary that will be serialized + // Prepare the data structure that will be serialized. var contentCacheData = new ContentCacheDataModel { PropertyData = cacheNode.Data?.Properties, @@ -487,23 +492,19 @@ WHERE cmsContentNu.nodeId IN ( private ContentNuDto GetDtoFromContent(IContentBase content, bool published, IContentCacheDataSerializer serializer) { - // should inject these in ctor - // BUT for the time being we decide not to support ConvertDbToXml/String - // var propertyEditorResolver = PropertyEditorResolver.Current; - // var dataTypeService = ApplicationContext.Current.Services.DataTypeService; var propertyData = new Dictionary(); foreach (IProperty prop in content.Properties) { var pdatas = new List(); foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture)) { - // sanitize - properties should be ok but ... never knows + // Sanitize - properties should be ok but ... never knows. if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) { continue; } - // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' + // Note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (value != null) { @@ -521,7 +522,7 @@ WHERE cmsContentNu.nodeId IN ( var cultureData = new Dictionary(); - // sanitize - names should be ok but ... never knows + // Sanitize - names should be ok but ... never knows. if (content.ContentType.VariesByCulture()) { ContentCultureInfosCollection? infos = content is IContent document @@ -530,7 +531,6 @@ WHERE cmsContentNu.nodeId IN ( : document.CultureInfos : content.CultureInfos; - // ReSharper disable once UseDeconstruction if (infos is not null) { foreach (ContentCultureInfos cultureInfo in infos) @@ -547,7 +547,7 @@ WHERE cmsContentNu.nodeId IN ( } } - // the dictionary that will be serialized + // Prepare the data structure that will be serialized. var contentCacheData = new ContentCacheDataModel { PropertyData = propertyData, @@ -566,7 +566,6 @@ WHERE cmsContentNu.nodeId IN ( return dto; } - // we want arrays, we want them all loaded, not an enumerable private Sql SqlContentSourcesSelect(Func>? joins = null) { SqlTemplate sqlTemplate = SqlContext.Templates.Get( @@ -632,36 +631,6 @@ WHERE cmsContentNu.nodeId IN ( return sql; } - private Sql SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder => - builder.InnerJoin("x") - .On( - (left, right) => left.NodeId == right.NodeId || - SqlText(left.Path, right.Path, - (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), - aliasRight: "x")); - - Sql sql = sqlTemplate.Sql(); - return sql; - } - - private Sql SqlWhereNodeId(ISqlContext sqlContext, int id) - { - ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; - - SqlTemplate sqlTemplate = sqlContext.Templates.Get( - Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, - builder => - builder.Where(x => x.NodeId == SqlTemplate.Arg("id"))); - - Sql sql = sqlTemplate.Sql(id); - return sql; - } - private Sql SqlWhereNodeKey(ISqlContext sqlContext, Guid key) { ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; @@ -702,10 +671,8 @@ WHERE cmsContentNu.nodeId IN ( } /// - /// Returns a slightly more optimized query to use for the document counting when paging over the content sources + /// Returns a slightly more optimized query to use for the document counting when paging over the content sources. /// - /// - /// private Sql SqlContentSourcesCount(Func>? joins = null) { SqlTemplate sqlTemplate = SqlContext.Templates.Get( diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs index a10a616fdc..ede7c426d1 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs @@ -3,6 +3,9 @@ using Umbraco.Cms.Infrastructure.HybridCache.Serialization; namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence; +/// +/// Defines a repository for persisting content cache data. +/// internal interface IDatabaseCacheRepository { /// diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entry-point.ts new file mode 100644 index 0000000000..4b6cc2608e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entry-point.ts @@ -0,0 +1,12 @@ +import { UmbManagementApiDataTypeDetailDataCacheInvalidationManager } from './repository/detail/server-data-source/data-type-detail.server.cache-invalidation.manager.js'; +import type { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api'; + +let detailDataCacheInvalidationManager: UmbManagementApiDataTypeDetailDataCacheInvalidationManager | undefined; + +export const onInit: UmbEntryPointOnInit = (host) => { + detailDataCacheInvalidationManager = new UmbManagementApiDataTypeDetailDataCacheInvalidationManager(host); +}; + +export const onUnload: UmbEntryPointOnUnload = () => { + detailDataCacheInvalidationManager?.destroy(); +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts index e2085b25ad..fc4b1cd4f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts @@ -21,4 +21,10 @@ export const manifests: Array = ...searchProviderManifests, ...treeManifests, ...workspaceManifests, + { + name: 'Data Type Backoffice Entry Point', + alias: 'Umb.EntryPoint.DataType', + type: 'backofficeEntryPoint', + js: () => import('./entry-point.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.cache-invalidation.manager.ts new file mode 100644 index 0000000000..b05d96ed50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.cache-invalidation.manager.ts @@ -0,0 +1,13 @@ +import { dataTypeDetailCache } from './data-type-detail.server.cache.js'; +import { UmbManagementApiDetailDataCacheInvalidationManager } from '@umbraco-cms/backoffice/management-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { DataTypeResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export class UmbManagementApiDataTypeDetailDataCacheInvalidationManager extends UmbManagementApiDetailDataCacheInvalidationManager { + constructor(host: UmbControllerHost) { + super(host, { + dataCache: dataTypeDetailCache, + eventSources: ['Umbraco:CMS:DataType'], + }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts index 46c49519dd..dbe94f739c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts @@ -21,7 +21,6 @@ export class UmbManagementApiDataTypeDetailDataRequestManager extends UmbManagem update: (id: string, body: UpdateDataTypeRequestModel) => DataTypeService.putDataTypeById({ path: { id }, body }), delete: (id: string) => DataTypeService.deleteDataTypeById({ path: { id } }), dataCache: dataTypeDetailCache, - serverEventSource: 'Umbraco:CMS:DataType', }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entry-point.ts new file mode 100644 index 0000000000..a622c66c01 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entry-point.ts @@ -0,0 +1,12 @@ +import { UmbManagementApiDocumentTypeDetailDataCacheInvalidationManager } from './repository/detail/server-data-source/document-type-detail.server.cache-invalidation.manager.js'; +import type { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api'; + +let detailDataCacheInvalidationManager: UmbManagementApiDocumentTypeDetailDataCacheInvalidationManager | undefined; + +export const onInit: UmbEntryPointOnInit = (host) => { + detailDataCacheInvalidationManager = new UmbManagementApiDocumentTypeDetailDataCacheInvalidationManager(host); +}; + +export const onUnload: UmbEntryPointOnUnload = () => { + detailDataCacheInvalidationManager?.destroy(); +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts index 7069bcbfe6..7334c63e55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/manifests.ts @@ -19,4 +19,10 @@ export const manifests: Array = ...searchManifests, ...treeManifests, ...workspaceManifests, + { + name: 'Document Type Backoffice Entry Point', + alias: 'Umb.EntryPoint.DocumentType', + type: 'backofficeEntryPoint', + js: () => import('./entry-point.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.cache-invalidation.manager.ts new file mode 100644 index 0000000000..2e362f76f4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.cache-invalidation.manager.ts @@ -0,0 +1,13 @@ +import { documentTypeDetailCache } from './document-type-detail.server.cache.js'; +import { UmbManagementApiDetailDataCacheInvalidationManager } from '@umbraco-cms/backoffice/management-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export class UmbManagementApiDocumentTypeDetailDataCacheInvalidationManager extends UmbManagementApiDetailDataCacheInvalidationManager { + constructor(host: UmbControllerHost) { + super(host, { + dataCache: documentTypeDetailCache, + eventSources: ['Umbraco:CMS:DocumentType'], + }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts index 84e76a3b81..0d78bce8f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts @@ -22,7 +22,6 @@ export class UmbManagementApiDocumentTypeDetailDataRequestManager extends UmbMan DocumentTypeService.putDocumentTypeById({ path: { id }, body }), delete: (id: string) => DocumentTypeService.deleteDocumentTypeById({ path: { id } }), dataCache: documentTypeDetailCache, - serverEventSource: 'Umbraco:CMS:DocumentType', }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entry-point.ts new file mode 100644 index 0000000000..c90fdac470 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entry-point.ts @@ -0,0 +1,12 @@ +import { UmbManagementApiDocumentItemDataCacheInvalidationManager } from './item/repository/document-item.server.cache-invalidation.manager.js'; +import type { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api'; + +let itemDataCacheInvalidationManager: UmbManagementApiDocumentItemDataCacheInvalidationManager | undefined; + +export const onInit: UmbEntryPointOnInit = (host) => { + itemDataCacheInvalidationManager = new UmbManagementApiDocumentItemDataCacheInvalidationManager(host); +}; + +export const onUnload: UmbEntryPointOnUnload = () => { + itemDataCacheInvalidationManager?.destroy(); +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.cache-invalidation.manager.ts new file mode 100644 index 0000000000..7b4f06e0fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.cache-invalidation.manager.ts @@ -0,0 +1,43 @@ +import { documentItemCache } from './document-item.server.cache.js'; +import { + UmbManagementApiItemDataCacheInvalidationManager, + type UmbManagementApiServerEventModel, +} from '@umbraco-cms/backoffice/management-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export class UmbManagementApiDocumentItemDataCacheInvalidationManager extends UmbManagementApiItemDataCacheInvalidationManager { + constructor(host: UmbControllerHost) { + super(host, { + dataCache: documentItemCache, + /* The Document item model includes info about the Document Type. + We need to invalidate the cache for both Document and DocumentType events. */ + eventSources: ['Umbraco:CMS:Document', 'Umbraco:CMS:DocumentType'], + }); + } + + protected override _onServerEvent(event: UmbManagementApiServerEventModel) { + if (event.eventSource === 'Umbraco:CMS:DocumentType') { + this.#onDocumentTypeChange(event); + } else { + this.#onDocumentChange(event); + } + } + + #onDocumentChange(event: UmbManagementApiServerEventModel) { + // Invalidate the specific document + const documentId = event.key; + this._dataCache.delete(documentId); + } + + #onDocumentTypeChange(event: UmbManagementApiServerEventModel) { + // Invalidate all documents of the specified Document Type + const documentTypeId = event.key; + const documentIds = this._dataCache + .getAll() + .filter((cachedItem) => cachedItem.documentType.id === documentTypeId) + .map((item) => item.id); + + documentIds.forEach((id) => this._dataCache.delete(id)); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.cache.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.cache.ts new file mode 100644 index 0000000000..3fe1688c29 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.cache.ts @@ -0,0 +1,6 @@ +import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbManagementApiItemDataCache } from '@umbraco-cms/backoffice/management-api'; + +const documentItemCache = new UmbManagementApiItemDataCache(); + +export { documentItemCache }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts index 5c6453b35e..7035a4b926 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.data-source.ts @@ -1,10 +1,9 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import type { UmbDocumentItemModel } from './types.js'; +import { UmbManagementApiDocumentItemDataRequestManager } from './document-item.server.request-manager.js'; import type { DocumentItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; -import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; /** * A data source for Document items that fetches data from the server @@ -15,6 +14,8 @@ export class UmbDocumentItemServerDataSource extends UmbItemServerDataSourceBase DocumentItemResponseModel, UmbDocumentItemModel > { + #itemRequestManager = new UmbManagementApiDocumentItemDataRequestManager(this); + /** * Creates an instance of UmbDocumentItemServerDataSource. * @param {UmbControllerHost} host - The controller host for this controller to be appended to @@ -29,13 +30,7 @@ export class UmbDocumentItemServerDataSource extends UmbItemServerDataSourceBase override async getItems(uniques: Array) { if (!uniques) throw new Error('Uniques are missing'); - const itemRequestManager = new UmbItemDataApiGetRequestController(this, { - // eslint-disable-next-line local-rules/no-direct-api-import - api: (args) => DocumentService.getItemDocument({ query: { id: args.uniques } }), - uniques, - }); - - const { data, error } = await itemRequestManager.request(); + const { data, error } = await this.#itemRequestManager.getItems(uniques); return { data: this._getMappedItems(data), error }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts new file mode 100644 index 0000000000..3c05698cff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts @@ -0,0 +1,15 @@ +/* eslint-disable local-rules/no-direct-api-import */ +import { documentItemCache } from './document-item.server.cache.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { DocumentService, type DocumentItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; + +export class UmbManagementApiDocumentItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + constructor(host: UmbControllerHost) { + super(host, { + getItems: (ids: Array) => DocumentService.getItemDocument({ query: { id: ids } }), + dataCache: documentItemCache, + getUniqueMethod: (item: DocumentItemResponseModel) => item.id, + }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts index c7910bc1b5..29ec4cf581 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts @@ -42,4 +42,10 @@ export const manifests: Array = ...urlManifests, ...userPermissionManifests, ...workspaceManifests, + { + name: 'Document Backoffice Entry Point', + alias: 'Umb.BackofficeEntryPoint.Document', + type: 'backofficeEntryPoint', + js: () => import('./entry-point.js'), + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache-invalidation.manager.ts new file mode 100644 index 0000000000..3a5767b755 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache-invalidation.manager.ts @@ -0,0 +1,57 @@ +import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from '../server-event/constants.js'; +import type { UmbManagementApiServerEventModel } from '../server-event/types.js'; +import type { UmbManagementApiDetailDataCache } from './cache.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +export interface UmbManagementApiDetailDataInvalidationManagerArgs { + dataCache: UmbManagementApiDetailDataCache; + eventSources: Array; +} + +export class UmbManagementApiDetailDataCacheInvalidationManager extends UmbControllerBase { + protected _dataCache: UmbManagementApiDetailDataCache; + #eventSources: Array; + #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; + + constructor( + host: UmbControllerHost, + args: UmbManagementApiDetailDataInvalidationManagerArgs, + ) { + super(host); + { + this._dataCache = args.dataCache; + this.#eventSources = args.eventSources; + + this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => { + this.#serverEventContext = context; + this.#observeServerEvents(); + }); + } + } + + /** + * Handles server events + * @protected + * @param {UmbManagementApiServerEventModel} event - The server event to handle + * @memberof UmbManagementApiDetailDataCacheInvalidationManager + */ + protected _onServerEvent(event: UmbManagementApiServerEventModel) { + this._dataCache.delete(event.key); + } + + #observeServerEvents() { + this.observe( + this.#serverEventContext?.byEventSourcesAndEventTypes(this.#eventSources, ['Updated', 'Deleted']), + (event) => { + if (!event) return; + this._onServerEvent(event); + }, + 'umbObserveServerEvents', + ); + } + + override destroy(): void { + this._dataCache.clear(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts index 5e5a0577a9..e5f4d81721 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.test.ts @@ -24,6 +24,10 @@ describe('UmbManagementApiDetailDataCache', () => { expect(cache).to.have.property('get').that.is.a('function'); }); + it('has a getAll method', () => { + expect(cache).to.have.property('getAll').that.is.a('function'); + }); + it('has a delete method', () => { expect(cache).to.have.property('delete').that.is.a('function'); }); @@ -69,6 +73,14 @@ describe('UmbManagementApiDetailDataCache', () => { }); }); + describe('GetAll', () => { + it('returns all items from the cache', () => { + cache.set('item1', { id: 'item1' }); + cache.set('item2', { id: 'item2' }); + expect(cache.getAll()).to.deep.equal([{ id: 'item1' }, { id: 'item2' }]); + }); + }); + describe('Delete', () => { it('removes an item from the cache', () => { cache.set('item1', { id: 'item1' }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts index a8b51f3c9e..7d7cec8866 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/cache.ts @@ -1,16 +1,16 @@ // Keep internal -interface UmbCacheEntryModel { +interface UmbCacheEntryModel { id: string; - data: DataModelType; + data: DetailDataModelType; } /** * A runtime cache for storing entity detail data from the Management Api * @class UmbManagementApiDetailDataCache - * @template DataModelType + * @template DetailDataModelType */ -export class UmbManagementApiDetailDataCache { - #entries: Map> = new Map(); +export class UmbManagementApiDetailDataCache { + #entries: Map> = new Map(); /** * Checks if an entry exists in the cache @@ -25,11 +25,11 @@ export class UmbManagementApiDetailDataCache { /** * Adds an entry to the cache * @param {string} id - The ID of the entry to add - * @param {DataModelType} data - The data to cache + * @param {DetailDataModelType} data - The data to cache * @memberof UmbManagementApiDetailDataCache */ - set(id: string, data: DataModelType): void { - const cacheEntry: UmbCacheEntryModel = { + set(id: string, data: DetailDataModelType): void { + const cacheEntry: UmbCacheEntryModel = { id: id, data, }; @@ -40,14 +40,23 @@ export class UmbManagementApiDetailDataCache { /** * Retrieves an entry from the cache * @param {string} id - The ID of the entry to retrieve - * @returns {DataModelType | undefined} - The cached entry or undefined if not found + * @returns {DetailDataModelType | undefined} - The cached entry or undefined if not found * @memberof UmbManagementApiDetailDataCache */ - get(id: string): DataModelType | undefined { + get(id: string): DetailDataModelType | undefined { const entry = this.#entries.get(id); return entry ? entry.data : undefined; } + /** + * Retrieves all entries from the cache + * @returns {Array} - An array of all cached entries + * @memberof UmbManagementApiItemDataCache + */ + getAll(): Array { + return Array.from(this.#entries.values()).map((entry) => entry.data); + } + /** * Deletes an entry from the cache * @param {string} id - The ID of the entry to delete diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts index db2228dd86..0ec8095434 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts @@ -20,7 +20,6 @@ export interface UmbManagementApiDetailDataRequestManagerArgs< update: (id: string, data: UpdateRequestModelType) => Promise>; delete: (id: string) => Promise>; dataCache: UmbManagementApiDetailDataCache; - serverEventSource: string; } export class UmbManagementApiDetailDataRequestManager< @@ -29,13 +28,12 @@ export class UmbManagementApiDetailDataRequestManager< UpdateRequestModelType, > extends UmbControllerBase { #dataCache: UmbManagementApiDetailDataCache; - #serverEventSource: string; - #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; #create; #read; #update; #delete; + #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; #isConnectedToServerEvents = false; constructor( @@ -54,7 +52,6 @@ export class UmbManagementApiDetailDataRequestManager< this.#delete = args.delete; this.#dataCache = args.dataCache; - this.#serverEventSource = args.serverEventSource; this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => { this.#serverEventContext = context; @@ -130,15 +127,5 @@ export class UmbManagementApiDetailDataRequestManager< }, 'umbObserveServerEventsConnection', ); - - // Invalidate cache entries when entities are updated or deleted - this.observe( - this.#serverEventContext?.byEventSourceAndTypes(this.#serverEventSource, ['Updated', 'Deleted']), - (event) => { - if (!event) return; - this.#dataCache.delete(event.key); - }, - 'umbObserveServerEvents', - ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts index 890386ff47..69a6dc3af6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts @@ -1,2 +1,3 @@ export * from './detail-data.request-manager.js'; export * from './cache.js'; +export * from './cache-invalidation.manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts index 5b2e199c84..772b286b1e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts @@ -1,2 +1,4 @@ export * from './detail/index.js'; +export * from './item/index.js'; export * from './server-event/constants.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts new file mode 100644 index 0000000000..ee6a275f78 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache-invalidation.manager.ts @@ -0,0 +1,55 @@ +import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from '../server-event/constants.js'; +import type { UmbManagementApiServerEventModel } from '../server-event/types.js'; +import type { UmbManagementApiItemDataCache } from './cache.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +export interface UmbManagementApiItemDataInvalidationManagerArgs { + dataCache: UmbManagementApiItemDataCache; + eventSources: Array; +} + +export class UmbManagementApiItemDataCacheInvalidationManager extends UmbControllerBase { + protected _dataCache: UmbManagementApiItemDataCache; + #eventSources: Array; + #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; + + constructor(host: UmbControllerHost, args: UmbManagementApiItemDataInvalidationManagerArgs) { + super(host); + { + this._dataCache = args.dataCache; + this.#eventSources = args.eventSources; + + this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => { + this.#serverEventContext = context; + this.#observeServerEvents(); + }); + } + } + + /** + * Handles server events + * @protected + * @param {UmbManagementApiServerEventModel} event - The server event to handle + * @memberof UmbManagementApiItemDataCacheInvalidationManager + */ + protected _onServerEvent(event: UmbManagementApiServerEventModel) { + this._dataCache.delete(event.key); + } + + #observeServerEvents() { + // Invalidate cache entries when entities are updated or deleted + this.observe( + this.#serverEventContext?.byEventSourcesAndEventTypes(this.#eventSources, ['Updated', 'Deleted']), + (event) => { + if (!event) return; + this._onServerEvent(event); + }, + 'umbObserveServerEvents', + ); + } + + override destroy(): void { + this._dataCache.clear(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache.test.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache.test.ts new file mode 100644 index 0000000000..8b33078ad2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache.test.ts @@ -0,0 +1,101 @@ +import { UmbManagementApiItemDataCache } from './cache.js'; +import { expect } from '@open-wc/testing'; + +describe('UmbManagementApiItemDataCache', () => { + let cache: UmbManagementApiItemDataCache<{ id: string }>; + + beforeEach(() => { + cache = new UmbManagementApiItemDataCache(); + }); + + describe('Public API', () => { + describe('properties', () => {}); + + describe('methods', () => { + it('has a has method', () => { + expect(cache).to.have.property('has').that.is.a('function'); + }); + + it('has a set method', () => { + expect(cache).to.have.property('set').that.is.a('function'); + }); + + it('has a get method', () => { + expect(cache).to.have.property('get').that.is.a('function'); + }); + + it('has a getAll method', () => { + expect(cache).to.have.property('getAll').that.is.a('function'); + }); + + it('has a delete method', () => { + expect(cache).to.have.property('delete').that.is.a('function'); + }); + + it('has a clear method', () => { + expect(cache).to.have.property('clear').that.is.a('function'); + }); + }); + }); + + describe('Has', () => { + it('returns true if the item exists in the cache', () => { + cache.set('item1', { id: 'item1' }); + expect(cache.has('item1')).to.be.true; + }); + + it('returns false if the item does not exist in the cache', () => { + expect(cache.has('item2')).to.be.false; + }); + }); + + describe('Set', () => { + it('adds an item to the cache', () => { + cache.set('item1', { id: 'item1' }); + expect(cache.has('item1')).to.be.true; + }); + + it('updates an existing item in the cache', () => { + cache.set('item1', { id: 'item1' }); + cache.set('item1', { id: 'item1-updated' }); + expect(cache.get('item1')).to.deep.equal({ id: 'item1-updated' }); + }); + }); + + describe('Get', () => { + it('returns an item from the cache', () => { + cache.set('item1', { id: 'item1' }); + expect(cache.get('item1')).to.deep.equal({ id: 'item1' }); + }); + + it('returns undefined if the item does not exist in the cache', () => { + expect(cache.get('item2')).to.be.undefined; + }); + }); + + describe('GetAll', () => { + it('returns all items from the cache', () => { + cache.set('item1', { id: 'item1' }); + cache.set('item2', { id: 'item2' }); + expect(cache.getAll()).to.deep.equal([{ id: 'item1' }, { id: 'item2' }]); + }); + }); + + describe('Delete', () => { + it('removes an item from the cache', () => { + cache.set('item1', { id: 'item1' }); + cache.delete('item1'); + expect(cache.has('item1')).to.be.false; + }); + }); + + describe('Clear', () => { + it('removes all items from the cache', () => { + cache.set('item1', { id: 'item1' }); + cache.set('item2', { id: 'item2' }); + cache.clear(); + expect(cache.has('item1')).to.be.false; + expect(cache.has('item2')).to.be.false; + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache.ts new file mode 100644 index 0000000000..171af2c5ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/cache.ts @@ -0,0 +1,76 @@ +// Keep internal +interface UmbCacheEntryModel { + id: string; + data: ItemDataModelType; +} + +/** + * A runtime cache for storing entity item data from the Management Api + * @class UmbManagementApiItemDataCache + * @template ItemDataModelType + */ +export class UmbManagementApiItemDataCache { + #entries: Map> = new Map(); + + /** + * Checks if an entry exists in the cache + * @param {string} id - The ID of the entry to check + * @returns {boolean} - True if the entry exists, false otherwise + * @memberof UmbManagementApiItemDataCache + */ + has(id: string): boolean { + return this.#entries.has(id); + } + + /** + * Adds an entry to the cache + * @param {string} id - The ID of the entry to add + * @param {ItemDataModelType} data - The data to cache + * @memberof UmbManagementApiItemDataCache + */ + set(id: string, data: ItemDataModelType): void { + const cacheEntry: UmbCacheEntryModel = { + id: id, + data, + }; + + this.#entries.set(id, cacheEntry); + } + + /** + * Retrieves an entry from the cache + * @param {string} id - The ID of the entry to retrieve + * @returns {ItemDataModelType | undefined} - The cached entry or undefined if not found + * @memberof UmbManagementApiItemDataCache + */ + get(id: string): ItemDataModelType | undefined { + const entry = this.#entries.get(id); + return entry ? entry.data : undefined; + } + + /** + * Retrieves all entries from the cache + * @returns {Array} - An array of all cached entries + * @memberof UmbManagementApiItemDataCache + */ + getAll(): Array { + return Array.from(this.#entries.values()).map((entry) => entry.data); + } + + /** + * Deletes an entry from the cache + * @param {string} id - The ID of the entry to delete + * @memberof UmbManagementApiItemDataCache + */ + delete(id: string): void { + this.#entries.delete(id); + } + + /** + * Clears all entries from the cache + * @memberof UmbManagementApiItemDataCache + */ + clear(): void { + this.#entries.clear(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/index.ts new file mode 100644 index 0000000000..2840c7a25e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/index.ts @@ -0,0 +1,3 @@ +export * from './item-data.request-manager.js'; +export * from './cache-invalidation.manager.js'; +export * from './cache.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.ts new file mode 100644 index 0000000000..8e071a1b96 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.ts @@ -0,0 +1,89 @@ +import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from '../server-event/constants.js'; +import type { UmbManagementApiItemDataCache } from './cache.js'; +import type { UmbApiError, UmbCancelError, UmbApiResponse } from '@umbraco-cms/backoffice/resources'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/entity-item'; + +export interface UmbManagementApiItemDataRequestManagerArgs { + getItems: (unique: Array) => Promise }>>; + dataCache: UmbManagementApiItemDataCache; + getUniqueMethod: (item: ItemResponseModelType) => string; +} + +export class UmbManagementApiItemDataRequestManager extends UmbControllerBase { + #dataCache: UmbManagementApiItemDataCache; + #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; + getUniqueMethod: (item: ItemResponseModelType) => string; + + #getItems; + #isConnectedToServerEvents = false; + + constructor(host: UmbControllerHost, args: UmbManagementApiItemDataRequestManagerArgs) { + super(host); + + this.#getItems = args.getItems; + this.#dataCache = args.dataCache; + this.getUniqueMethod = args.getUniqueMethod; + + this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => { + this.#serverEventContext = context; + this.#observeServerEventsConnection(); + }); + } + + async getItems(ids: Array): Promise }>> { + let error: UmbApiError | UmbCancelError | undefined; + let idsToRequest: Array = [...ids]; + let cacheItems: Array = []; + let serverItems: Array | undefined; + + // Only read from the cache when we are connected to the server events + if (this.#isConnectedToServerEvents) { + const cachedIds = ids.filter((id) => this.#dataCache.has(id)); + cacheItems = cachedIds + .map((id) => this.#dataCache.get(id)) + .filter((x) => x !== undefined) as Array; + idsToRequest = ids.filter((id) => !this.#dataCache.has(id)); + } + + if (idsToRequest.length > 0) { + const getItemsController = new UmbItemDataApiGetRequestController(this, { + api: (args) => this.#getItems(args.uniques), + uniques: idsToRequest, + }); + + const { data: serverData, error: serverError } = await getItemsController.request(); + + serverItems = serverData ?? []; + error = serverError; + + if (this.#isConnectedToServerEvents) { + // If we are connected to server events, we can cache the server data + serverItems?.forEach((item) => this.#dataCache.set(this.getUniqueMethod(item), item)); + } + } + + const data: Array = [...cacheItems, ...(serverItems ?? [])]; + + return { data, error }; + } + + #observeServerEventsConnection() { + this.observe( + this.#serverEventContext?.isConnected, + (isConnected) => { + /* We purposefully ignore the initial value of isConnected. + We only care about whether the connection is established or not (true/false) */ + if (isConnected === undefined) return; + this.#isConnectedToServerEvents = isConnected; + + // Clear the cache if we lose connection to the server events + if (this.#isConnectedToServerEvents === false) { + this.#dataCache.clear(); + } + }, + 'umbObserveServerEventsConnection', + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts index 34f025ebff..1e08e907b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/global-context/server-event.context.ts @@ -21,26 +21,29 @@ export class UmbManagementApiServerEventContext extends UmbContextBase { public readonly isConnected = this.#isConnected.asObservable(); /** - * Filters events by the given event source - * @param {string} eventSource + * Filters events by the given event types + * @param {string} eventTypes - The event types to filter by * @returns {Observable} - The filtered events * @memberof UmbManagementApiServerEventContext */ - byEventSource(eventSource: string): Observable { - return this.#events.asObservable().pipe(filter((event) => event.eventSource === eventSource)); + byEventSource(eventTypes: string): Observable { + return this.#events.asObservable().pipe(filter((event) => event.eventType === eventTypes)); } /** - * Filters events by the given event source and event types - * @param {string} eventSource - * @param {Array} eventTypes + * Filters events by the given event sources and event types + * @param {Array} eventSources - The event sources to filter by + * @param {Array} eventTypes - The event types to filter by * @returns {Observable} - The filtered events * @memberof UmbManagementApiServerEventContext */ - byEventSourceAndTypes(eventSource: string, eventTypes: Array): Observable { + byEventSourcesAndEventTypes( + eventSources: Array, + eventTypes: Array, + ): Observable { return this.#events .asObservable() - .pipe(filter((event) => event.eventSource === eventSource && eventTypes.includes(event.eventType))); + .pipe(filter((event) => eventSources.includes(event.eventSource) && eventTypes.includes(event.eventType))); } constructor(host: UmbControllerHost) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/types.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/types.ts new file mode 100644 index 0000000000..c091343fa0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/server-event/types.ts @@ -0,0 +1 @@ +export type * from './global-context/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/types.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/types.ts new file mode 100644 index 0000000000..d32dcc5030 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/types.ts @@ -0,0 +1 @@ +export type * from './server-event/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts index 6ff0bbcde8..1a2bb5cce5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts @@ -3,7 +3,7 @@ import { css } from '@umbraco-cms/backoffice/external/lit'; import { CharacterCount } from '@umbraco-cms/backoffice/external/tiptap'; export default class UmbTiptapWordCountExtensionApi extends UmbTiptapExtensionApiBase { - getTiptapExtensions = () => [CharacterCount]; + getTiptapExtensions = () => [CharacterCount.configure()]; override getStyles = () => css``; } diff --git a/templates/UmbracoExtension/Client/package.json b/templates/UmbracoExtension/Client/package.json index 87514b0f7d..72f8ab4120 100644 --- a/templates/UmbracoExtension/Client/package.json +++ b/templates/UmbracoExtension/Client/package.json @@ -9,13 +9,12 @@ "generate-client": "node scripts/generate-openapi.js https://localhost:44339/umbraco/swagger/umbracoextension/swagger.json" }, "devDependencies": { - "@hey-api/client-fetch": "^0.10.0", - "@hey-api/openapi-ts": "^0.66.7", + "@hey-api/openapi-ts": "^0.80.14", "@umbraco-cms/backoffice": "^UMBRACO_VERSION_FROM_TEMPLATE", - "chalk": "^5.4.1", - "cross-env": "^7.0.3", + "chalk": "^5.6.0", + "cross-env": "^10.0.0", "node-fetch": "^3.3.2", - "typescript": "^5.8.3", - "vite": "^6.3.4" + "typescript": "^5.9.2", + "vite": "^7.1.3" } } diff --git a/templates/UmbracoExtension/Client/scripts/generate-openapi.js b/templates/UmbracoExtension/Client/scripts/generate-openapi.js index b320781bc7..c54afa56f9 100644 --- a/templates/UmbracoExtension/Client/scripts/generate-openapi.js +++ b/templates/UmbracoExtension/Client/scripts/generate-openapi.js @@ -32,18 +32,15 @@ fetch(swaggerUrl).then(async (response) => { console.log(`Calling ${chalk.yellow('hey-api')} to generate TypeScript client`); await createClient({ + client: '@hey-api/client-fetch', input: swaggerUrl, output: 'src/api', plugins: [ ...defaultPlugins, - '@hey-api/client-fetch', - { - name: '@hey-api/typescript', - enums: 'typescript' - }, { name: '@hey-api/sdk', - asClass: true + asClass: true, + classNameBuilder: '{{name}}Service', } ], }); diff --git a/templates/UmbracoExtension/Client/src/api/client.gen.ts b/templates/UmbracoExtension/Client/src/api/client.gen.ts index d7bb5e8272..20b48eb4b3 100644 --- a/templates/UmbracoExtension/Client/src/api/client.gen.ts +++ b/templates/UmbracoExtension/Client/src/api/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { ClientOptions } from './types.gen'; -import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch'; +import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client'; /** * The `createClientConfig()` function will be called on client initialization diff --git a/templates/UmbracoExtension/Client/src/api/client/client.gen.ts b/templates/UmbracoExtension/Client/src/api/client/client.gen.ts new file mode 100644 index 0000000000..0c606b81c6 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/client/client.gen.ts @@ -0,0 +1,199 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const request: Client['request'] = async (options) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.serializedBody === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: opts.serializedBody, + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request._fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response._fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + return opts.responseStyle === 'data' + ? {} + : { + data: {}, + ...result, + }; + } + + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error._fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + return { + buildUrl, + connect: (options) => request({ ...options, method: 'CONNECT' }), + delete: (options) => request({ ...options, method: 'DELETE' }), + get: (options) => request({ ...options, method: 'GET' }), + getConfig, + head: (options) => request({ ...options, method: 'HEAD' }), + interceptors, + options: (options) => request({ ...options, method: 'OPTIONS' }), + patch: (options) => request({ ...options, method: 'PATCH' }), + post: (options) => request({ ...options, method: 'POST' }), + put: (options) => request({ ...options, method: 'PUT' }), + request, + setConfig, + trace: (options) => request({ ...options, method: 'TRACE' }), + }; +}; diff --git a/templates/UmbracoExtension/Client/src/api/client/index.ts b/templates/UmbracoExtension/Client/src/api/client/index.ts new file mode 100644 index 0000000000..318a84b6a8 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/templates/UmbracoExtension/Client/src/api/client/types.gen.ts b/templates/UmbracoExtension/Client/src/api/client/types.gen.ts new file mode 100644 index 0000000000..2a123be9a1 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/client/types.gen.ts @@ -0,0 +1,232 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: (request: Request) => ReturnType; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }> { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'headers' | 'url' + > & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/templates/UmbracoExtension/Client/src/api/client/utils.gen.ts b/templates/UmbracoExtension/Client/src/api/client/utils.gen.ts new file mode 100644 index 0000000000..6d82364ef8 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/client/utils.gen.ts @@ -0,0 +1,419 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { + QuerySerializer, + QuerySerializerOptions, +} from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +interface PathSerializer { + path: Record; + url: string; +} + +const PATH_PARAM_RE = /\{[^{}]+\}/g; + +type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +type ArraySeparatorStyle = ArrayStyle | MatrixStyle; + +const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + + return; + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => { + const url = getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = + header instanceof Headers ? header.entries() : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + _fns: (Interceptor | null)[]; + + constructor() { + this._fns = []; + } + + clear() { + this._fns = []; + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this._fns[id] ? id : -1; + } else { + return this._fns.indexOf(id); + } + } + exists(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + return !!this._fns[index]; + } + + eject(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = null; + } + } + + update(id: number | Interceptor, fn: Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = fn; + return id; + } else { + return false; + } + } + + use(fn: Interceptor) { + this._fns = [...this._fns, fn]; + return this._fns.length - 1; + } +} + +// `createInterceptors()` response, meant for external use as it does not +// expose internals +export interface Middleware { + error: Pick< + Interceptors>, + 'eject' | 'use' + >; + request: Pick>, 'eject' | 'use'>; + response: Pick< + Interceptors>, + 'eject' | 'use' + >; +} + +// do not add `Middleware` as return type so we can use _fns internally +export const createInterceptors = () => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/templates/UmbracoExtension/Client/src/api/core/auth.gen.ts b/templates/UmbracoExtension/Client/src/api/core/auth.gen.ts new file mode 100644 index 0000000000..f8a73266f9 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/templates/UmbracoExtension/Client/src/api/core/bodySerializer.gen.ts b/templates/UmbracoExtension/Client/src/api/core/bodySerializer.gen.ts new file mode 100644 index 0000000000..49cd8925e3 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/templates/UmbracoExtension/Client/src/api/core/params.gen.ts b/templates/UmbracoExtension/Client/src/api/core/params.gen.ts new file mode 100644 index 0000000000..71c88e852b --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/templates/UmbracoExtension/Client/src/api/core/pathSerializer.gen.ts b/templates/UmbracoExtension/Client/src/api/core/pathSerializer.gen.ts new file mode 100644 index 0000000000..8d99931047 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/templates/UmbracoExtension/Client/src/api/core/types.gen.ts b/templates/UmbracoExtension/Client/src/api/core/types.gen.ts new file mode 100644 index 0000000000..5bfae35c0a --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/core/types.gen.ts @@ -0,0 +1,120 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export interface Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, +> { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + connect: MethodFn; + delete: MethodFn; + get: MethodFn; + getConfig: () => Config; + head: MethodFn; + options: MethodFn; + patch: MethodFn; + post: MethodFn; + put: MethodFn; + request: RequestFn; + setConfig: (config: Config) => Config; + trace: MethodFn; +} + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: + | 'CONNECT' + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT' + | 'TRACE'; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/templates/UmbracoExtension/Client/src/api/sdk.gen.ts b/templates/UmbracoExtension/Client/src/api/sdk.gen.ts index 0465ad97b5..438dc6d075 100644 --- a/templates/UmbracoExtension/Client/src/api/sdk.gen.ts +++ b/templates/UmbracoExtension/Client/src/api/sdk.gen.ts @@ -1,10 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; +import type { Options as ClientOptions, TDataShape, Client } from './client'; //#if(IncludeExample) -import type { PingData, PingResponse, WhatsMyNameData, WhatsMyNameResponse, WhatsTheTimeMrWolfData, WhatsTheTimeMrWolfResponse, WhoAmIData, WhoAmIResponse } from './types.gen'; +import type { PingData, PingResponses, PingErrors, WhatsMyNameData, WhatsMyNameResponses, WhatsMyNameErrors, WhatsTheTimeMrWolfData, WhatsTheTimeMrWolfResponses, WhatsTheTimeMrWolfErrors, WhoAmIData, WhoAmIResponses, WhoAmIErrors } from './types.gen'; //#else -import type { PingData, PingResponse } from './types.gen'; +import type { PingData, PingResponses, PingErrors } from './types.gen'; //#endif import { client as _heyApiClient } from './client.gen'; @@ -24,7 +24,7 @@ export type Options(options?: Options) { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ security: [ { scheme: 'bearer', @@ -37,7 +37,7 @@ export class UmbracoExtensionService { } //#if(IncludeExample) public static whatsMyName(options?: Options) { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ security: [ { scheme: 'bearer', @@ -48,9 +48,9 @@ export class UmbracoExtensionService { ...options }); } - + public static whatsTheTimeMrWolf(options?: Options) { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ security: [ { scheme: 'bearer', @@ -61,9 +61,9 @@ export class UmbracoExtensionService { ...options }); } - + public static whoAmI(options?: Options) { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ security: [ { scheme: 'bearer', @@ -75,4 +75,4 @@ export class UmbracoExtensionService { }); } //#endif -} \ No newline at end of file +} diff --git a/templates/UmbracoExtension/Client/src/api/types.gen.ts b/templates/UmbracoExtension/Client/src/api/types.gen.ts index 9da3ce6897..72e310f94a 100644 --- a/templates/UmbracoExtension/Client/src/api/types.gen.ts +++ b/templates/UmbracoExtension/Client/src/api/types.gen.ts @@ -6,6 +6,12 @@ export type DocumentGranularPermissionModel = { permission: string; }; +export type DocumentPropertyValueGranularPermissionModel = { + key: string; + readonly context: string; + permission: string; +}; + export type ReadOnlyUserGroupModel = { id: number; key: string; @@ -17,7 +23,7 @@ export type ReadOnlyUserGroupModel = { hasAccessToAllLanguages: boolean; allowedLanguages: Array; permissions: Array; - granularPermissions: Array; + granularPermissions: Array; allowedSections: Array; }; @@ -40,16 +46,13 @@ export type UserGroupModel = { name?: string | null; hasAccessToAllLanguages: boolean; permissions: Array; - granularPermissions: Array; + granularPermissions: Array; readonly allowedSections: Array; readonly userCount: number; readonly allowedLanguages: Array; }; -export enum UserKindModel { - DEFAULT = 'Default', - API = 'Api' -} +export type UserKindModel = 'Default' | 'Api'; export type UserModel = { id: number; @@ -90,20 +93,13 @@ export type UserProfileModel = { name?: string | null; }; -export enum UserStateModel { - ACTIVE = 'Active', - DISABLED = 'Disabled', - LOCKED_OUT = 'LockedOut', - INVITED = 'Invited', - INACTIVE = 'Inactive', - ALL = 'All' -} +export type UserStateModel = 'Active' | 'Disabled' | 'LockedOut' | 'Invited' | 'Inactive' | 'All'; //#endif export type PingData = { body?: never; path?: never; query?: never; - url: '/umbraco/hackclient/api/v1/ping'; + url: '/umbraco/umbracoextension/api/v1/ping'; }; export type PingErrors = { @@ -126,7 +122,7 @@ export type WhatsMyNameData = { body?: never; path?: never; query?: never; - url: '/umbraco/hackclient/api/v1/whatsMyName'; + url: '/umbraco/umbracoextension/api/v1/whatsMyName'; }; export type WhatsMyNameErrors = { @@ -149,7 +145,7 @@ export type WhatsTheTimeMrWolfData = { body?: never; path?: never; query?: never; - url: '/umbraco/hackclient/api/v1/whatsTheTimeMrWolf'; + url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf'; }; export type WhatsTheTimeMrWolfErrors = { @@ -172,7 +168,7 @@ export type WhoAmIData = { body?: never; path?: never; query?: never; - url: '/umbraco/hackclient/api/v1/whoAmI'; + url: '/umbraco/umbracoextension/api/v1/whoAmI'; }; export type WhoAmIErrors = { diff --git a/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts b/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts index 4d5f99562b..474a6eebc5 100644 --- a/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts +++ b/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts @@ -61,7 +61,7 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) { } if (data !== undefined) { - this._serverUserData = data; + this._serverUserData = data as UserModel; buttonElement.state = "success"; } diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 412b6c2806..25408ef5f1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.37", - "@umbraco/playwright-testhelpers": "^16.0.36", + "@umbraco/playwright-testhelpers": "^16.0.37", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "16.0.36", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.36.tgz", - "integrity": "sha512-SjPrVgWI18ErfyCUEuIwt1V7HjCGXFLae0S8u3NO74QBbOO9z79+JM0/U4Xwqwq9KdV2XMiVPkzDm/5xThSvMg==", + "version": "16.0.37", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.37.tgz", + "integrity": "sha512-hpYrQJRxB8yK/1B2THsh2NDBaXM4DdM4npnjjFSwujBxnjOFjpJj8VfDU/D4eR8pyzZUu9qV4UkzrGxDuf9uvQ==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.37", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 6df0b2ca45..a19b8ff520 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.37", - "@umbraco/playwright-testhelpers": "^16.0.36", + "@umbraco/playwright-testhelpers": "^16.0.37", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 6741ea26e6..b7c8e1be4d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -687,6 +687,25 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.That(contents.Count(), Is.EqualTo(1)); } + [Test] + public void Can_Get_Scheduled_Content_Keys() + { + // Arrange + var root = ContentService.GetById(Textpage.Id); + ContentService.Publish(root!, root!.AvailableCultures.ToArray()); + var content = ContentService.GetById(Subpage.Id); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddDays(1), null); + ContentService.PersistContentSchedule(content!, contentSchedule); + ContentService.Publish(content, content.AvailableCultures.ToArray()); + + // Act + var keys = ContentService.GetScheduledContentKeys([Textpage.Key, Subpage.Key, Subpage2.Key]).ToList(); + + // Assert + Assert.AreEqual(1, keys.Count); + Assert.AreEqual(Subpage.Key, keys.First()); + } + [Test] public void Can_Get_Content_For_Release() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasCollectionSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasCollectionSignProviderTests.cs new file mode 100644 index 0000000000..8c9a4cb3e3 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasCollectionSignProviderTests.cs @@ -0,0 +1,196 @@ +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Api.Management.ViewModels.Media.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Media.Item; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Api.Management.ViewModels.Tree; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs; + +[TestFixture] +internal class HasCollectionSignProviderTests +{ + [Test] + public void HasCollectionSignProvider_Can_Provide_Document_Tree_Signs() + { + var sut = new HasCollectionSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasCollectionSignProvider_Can_Provide_Document_Collection_Signs() + { + var sut = new HasCollectionSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasCollectionSignProvider_Can_Provide_Document_Item_Signs() + { + var sut = new HasCollectionSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasCollectionSignProvider_Can_Provide_Media_Tree_Signs() + { + var sut = new HasCollectionSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasCollectionSignProvider_Can_Provide_Media_Collection_Signs() + { + var sut = new HasCollectionSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasCollectionSignProvider_Can_Provide_Media_Item_Signs() + { + var sut = new HasCollectionSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public async Task HasCollectionSignProvider_Should_Populate_Document_Tree_Signs() + { + var sut = new HasCollectionSignProvider(); + + var viewModels = new List + { + new() + { + Id = Guid.NewGuid(), DocumentType = new DocumentTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) }, + }, + new() { Id = Guid.NewGuid() }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Signs.Count(), 0); + + var signModel = viewModels[0].Signs.First(); + Assert.AreEqual("Umb.HasCollection", signModel.Alias); + } + + [Test] + public async Task HasCollectionSignProvider_Should_Populate_Document_Collection_Signs() + { + var sut = new HasCollectionSignProvider(); + + var viewModels = new List + { + new() + { + Id = Guid.NewGuid(), DocumentType = new DocumentTypeCollectionReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) }, + }, + new() { Id = Guid.NewGuid() }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Signs.Count(), 0); + + var signModel = viewModels[0].Signs.First(); + Assert.AreEqual("Umb.HasCollection", signModel.Alias); + } + + [Test] + public async Task HasCollectionSignProvider_Should_Populate_Document_Item_Signs() + { + var sut = new HasCollectionSignProvider(); + + var viewModels = new List + { + new() + { + Id = Guid.NewGuid(), DocumentType = new DocumentTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) }, + }, + new() { Id = Guid.NewGuid() }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Signs.Count(), 0); + + var signModel = viewModels[0].Signs.First(); + Assert.AreEqual("Umb.HasCollection", signModel.Alias); + } + + [Test] + public async Task HasCollectionSignProvider_Should_Populate_Media_Tree_Signs() + { + var sut = new HasCollectionSignProvider(); + + var viewModels = new List + { + new() + { + Id = Guid.NewGuid(), MediaType = new MediaTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) }, + }, + new() { Id = Guid.NewGuid() }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Signs.Count(), 0); + + var signModel = viewModels[0].Signs.First(); + Assert.AreEqual("Umb.HasCollection", signModel.Alias); + } + + [Test] + public async Task HasCollectionSignProvider_Should_Populate_Media_Collection_Signs() + { + var sut = new HasCollectionSignProvider(); + + var viewModels = new List + { + new() + { + Id = Guid.NewGuid(), MediaType = new MediaTypeCollectionReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) }, + }, + new() { Id = Guid.NewGuid() }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Signs.Count(), 0); + + var signModel = viewModels[0].Signs.First(); + Assert.AreEqual("Umb.HasCollection", signModel.Alias); + } + + [Test] + public async Task HasCollectionSignProvider_Should_Populate_Media_Item_Signs() + { + var sut = new HasCollectionSignProvider(); + + var viewModels = new List + { + new() + { + Id = Guid.NewGuid(), MediaType = new MediaTypeReferenceResponseModel() { Collection = new ReferenceByIdModel(Guid.NewGuid()) }, + }, + new() { Id = Guid.NewGuid() }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 1); + Assert.AreEqual(viewModels[1].Signs.Count(), 0); + + var signModel = viewModels[0].Signs.First(); + Assert.AreEqual("Umb.HasCollection", signModel.Alias); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs new file mode 100644 index 0000000000..7bc7435919 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasPendingChangesSignProviderTests.cs @@ -0,0 +1,126 @@ +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs; + +[TestFixture] +internal class HasPendingChangesSignProviderTests +{ + [Test] + public void HasPendingChangesSignProvider_Can_Provide_Document_Tree_Signs() + { + var sut = new HasPendingChangesSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasPendingChangesSignProvider_Can_Provide_Document_Collection_Signs() + { + var sut = new HasPendingChangesSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasPendingChangesSignProvider_Can_Provide_Document_Item_Signs() + { + var sut = new HasPendingChangesSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public async Task HasPendingChangesSignProvider_Should_Populate_Document_Tree_Signs() + { + var sut = new HasPendingChangesSignProvider(); + + var viewModels = new List + { + new() { Id = Guid.NewGuid() }, + new() + { + Id = Guid.NewGuid(), Variants = + [ + new() + { + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + ], + }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.PendingChanges", signModel.Alias); + } + + [Test] + public async Task HasPendingChangesSignProvider_Should_Populate_Document_Collection_Signs() + { + var sut = new HasPendingChangesSignProvider(); + + var viewModels = new List + { + new() { Id = Guid.NewGuid() }, + new() + { + Id = Guid.NewGuid(), Variants = + [ + new() + { + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + ], + }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.PendingChanges", signModel.Alias); + } + + [Test] + public async Task HasPendingChangesSignProvider_Should_Populate_Document_Item_Signs() + { + var sut = new HasPendingChangesSignProvider(); + + var viewModels = new List + { + new() { Id = Guid.NewGuid() }, + new() + { + Id = Guid.NewGuid(), Variants = + [ + new() + { + State = DocumentVariantState.PublishedPendingChanges, + Culture = null, + Name = "Test", + }, + ], + }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.PendingChanges", signModel.Alias); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs new file mode 100644 index 0000000000..48b97ff30b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/HasScheduleSignProviderTests.cs @@ -0,0 +1,125 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs; + +[TestFixture] +internal class HasScheduleSignProviderTests +{ + [Test] + public void HasScheduleSignProvider_Can_Provide_Document_Tree_Signs() + { + var contentServiceMock = new Mock(); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasScheduleSignProvider_Can_Provide_Document_Collection_Signs() + { + var contentServiceMock = new Mock(); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void HasScheduleSignProvider_Can_Provide_Document_Item_Signs() + { + var contentServiceMock = new Mock(); + + var sut = new HasScheduleSignProvider(contentServiceMock.Object); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public async Task HasScheduleSignProvider_Should_Populate_Document_Tree_Signs() + { + var entities = new List + { + new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, + }; + + var contentServiceMock = new Mock(); + contentServiceMock + .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) + .Returns([entities[1].Key]); + var sut = new HasScheduleSignProvider(contentServiceMock.Object); + + var viewModels = new List + { + new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); + } + + [Test] + public async Task HasScheduleSignProvider_Should_Populate_Document_Collection_Signs() + { + var entities = new List + { + new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, + }; + + var contentServiceMock = new Mock(); + contentServiceMock + .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) + .Returns([entities[1].Key]); + var sut = new HasScheduleSignProvider(contentServiceMock.Object); + + var viewModels = new List + { + new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); + } + + [Test] + public async Task HasScheduleSignProvider_Should_Populate_Document_Item_Signs() + { + var entities = new List + { + new() { Key = Guid.NewGuid(), Name = "Item 1" }, new() { Key = Guid.NewGuid(), Name = "Item 2" }, + }; + + var contentServiceMock = new Mock(); + contentServiceMock + .Setup(x => x.GetScheduledContentKeys(It.IsAny>())) + .Returns([entities[1].Key]); + var sut = new HasScheduleSignProvider(contentServiceMock.Object); + + var viewModels = new List + { + new() { Id = entities[0].Key }, new() { Id = entities[1].Key }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.ScheduledForPublish", signModel.Alias); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/IsProtectedSignProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/IsProtectedSignProviderTests.cs new file mode 100644 index 0000000000..c15fec787c --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Services/Signs/IsProtectedSignProviderTests.cs @@ -0,0 +1,92 @@ +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Signs; +using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Api.Management.ViewModels.Document.Item; +using Umbraco.Cms.Api.Management.ViewModels.Tree; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Services.Signs; + +[TestFixture] +internal class IsProtectedSignProviderTests +{ + [Test] + public void IsProtectedSignProvider_Can_Provide_Tree_Signs() + { + var sut = new IsProtectedSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void IsProtectedSignProvider_Can_Provide_Collection_Signs() + { + var sut = new IsProtectedSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public void IsProtectedSignProvider_Can_Provide_Plain_Signs() + { + var sut = new IsProtectedSignProvider(); + Assert.IsTrue(sut.CanProvideSigns()); + } + + [Test] + public async Task IsProtectedSignProvider_Should_Populate_Tree_Signs() + { + var sut = new IsProtectedSignProvider(); + + var viewModels = new List + { + new(), + new() { IsProtected = true }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.IsProtected", signModel.Alias); + } + + [Test] + public async Task IsProtectedSignProvider_Should_Populate_Collection_Signs() + { + var sut = new IsProtectedSignProvider(); + + var viewModels = new List + { + new(), + new() { IsProtected = true }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.IsProtected", signModel.Alias); + } + + [Test] + public async Task IsProtectedSignProvider_Should_Populate_Plain_Signs() + { + var sut = new IsProtectedSignProvider(); + + var viewModels = new List + { + new(), + new() { IsProtected = true }, + }; + + await sut.PopulateSignsAsync(viewModels); + + Assert.AreEqual(viewModels[0].Signs.Count(), 0); + Assert.AreEqual(viewModels[1].Signs.Count(), 1); + + var signModel = viewModels[1].Signs.First(); + Assert.AreEqual("Umb.IsProtected", signModel.Alias); + } +}