diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/ByKeyDocumentBlueprintFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/ByKeyDocumentBlueprintFolderController.cs new file mode 100644 index 0000000000..45e5f46acf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/ByKeyDocumentBlueprintFolderController.cs @@ -0,0 +1,25 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Folder; + +[ApiVersion("1.0")] +public class ByKeyDocumentBlueprintFolderController : DocumentBlueprintFolderControllerBase +{ + public ByKeyDocumentBlueprintFolderController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentBlueprintContainerService contentBlueprintContainerService) + : base(backOfficeSecurityAccessor, contentBlueprintContainerService) + { + } + + [HttpGet("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task ByKey(CancellationToken cancellationToken, Guid id) => await GetFolderAsync(id); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/CreateDocumentBlueprintFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/CreateDocumentBlueprintFolderController.cs new file mode 100644 index 0000000000..9e69f807ff --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/CreateDocumentBlueprintFolderController.cs @@ -0,0 +1,29 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Folder; + +[ApiVersion("1.0")] +public class CreateDocumentBlueprintFolderController : DocumentBlueprintFolderControllerBase +{ + public CreateDocumentBlueprintFolderController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentBlueprintContainerService contentBlueprintContainerService) + : base(backOfficeSecurityAccessor, contentBlueprintContainerService) + { + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Create(CancellationToken cancellationToken, CreateFolderRequestModel createFolderRequestModel) + => await CreateFolderAsync( + createFolderRequestModel, + controller => nameof(controller.ByKey)); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/DeleteDocumentBlueprintFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/DeleteDocumentBlueprintFolderController.cs new file mode 100644 index 0000000000..a68f60cf98 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/DeleteDocumentBlueprintFolderController.cs @@ -0,0 +1,25 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Folder; + +[ApiVersion("1.0")] +public class DeleteDocumentBlueprintFolderController : DocumentBlueprintFolderControllerBase +{ + public DeleteDocumentBlueprintFolderController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentBlueprintContainerService contentBlueprintContainerService) + : base(backOfficeSecurityAccessor, contentBlueprintContainerService) + { + } + + [HttpDelete("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Delete(CancellationToken cancellationToken, Guid id) => await DeleteFolderAsync(id); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/DocumentBlueprintFolderControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/DocumentBlueprintFolderControllerBase.cs new file mode 100644 index 0000000000..91a12e509e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/DocumentBlueprintFolderControllerBase.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Folder; + +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentBlueprint}/folder")] +[ApiExplorerSettings(GroupName = "Document Blueprint")] +[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] +public abstract class DocumentBlueprintFolderControllerBase : FolderManagementControllerBase +{ + protected DocumentBlueprintFolderControllerBase( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentBlueprintContainerService contentBlueprintContainerService) + : base(backOfficeSecurityAccessor, contentBlueprintContainerService) + { + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/UpdateDocumentBlueprintFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/UpdateDocumentBlueprintFolderController.cs new file mode 100644 index 0000000000..94d1bf2ce1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Folder/UpdateDocumentBlueprintFolderController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Folder; + +[ApiVersion("1.0")] +public class UpdateDocumentBlueprintFolderController : DocumentBlueprintFolderControllerBase +{ + public UpdateDocumentBlueprintFolderController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentBlueprintContainerService contentBlueprintContainerService) + : base(backOfficeSecurityAccessor, contentBlueprintContainerService) + { + } + + [HttpPut("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Update(CancellationToken cancellationToken, Guid id, UpdateFolderResponseModel updateFolderResponseModel) + => await UpdateFolderAsync(id, updateFolderResponseModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/MoveDocumentBlueprintController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/MoveDocumentBlueprintController.cs new file mode 100644 index 0000000000..9adab9621f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/MoveDocumentBlueprintController.cs @@ -0,0 +1,38 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint; + +[ApiVersion("1.0")] +[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] +public class MoveDocumentBlueprintController : DocumentBlueprintControllerBase +{ + private readonly IContentBlueprintEditingService _contentBlueprintEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public MoveDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _contentBlueprintEditingService = contentBlueprintEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut("{id:guid}/move")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Move(CancellationToken cancellationToken, Guid id, MoveDocumentBlueprintRequestModel requestModel) + { + Attempt result = await _contentBlueprintEditingService.MoveAsync(id, requestModel.Target?.Id, CurrentUserKey(_backOfficeSecurityAccessor)); + return result.Success + ? Ok() + : ContentEditingOperationStatusResult(result.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs new file mode 100644 index 0000000000..92c1fe28b2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/ChildrenDocumentBlueprintTreeController.cs @@ -0,0 +1,27 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree; + +[ApiVersion("1.0")] +public class ChildrenDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase +{ + public ChildrenDocumentBlueprintTreeController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, documentPresentationFactory) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentId, skip, take); + } +} 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 17da28f06b..aeaf65b7ef 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs @@ -14,8 +14,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.DocumentBlueprint}")] [ApiExplorerSettings(GroupName = "Document Blueprint")] -[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] -public class DocumentBlueprintTreeControllerBase : NamedEntityTreeControllerBase +[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] +public class DocumentBlueprintTreeControllerBase : FolderTreeControllerBase { private readonly IDocumentPresentationFactory _documentPresentationFactory; @@ -25,18 +25,28 @@ public class DocumentBlueprintTreeControllerBase : NamedEntityTreeControllerBase protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentBlueprint; - protected override DocumentBlueprintTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities) - { - IDocumentEntitySlim[] documentEntities = entities - .OfType() - .ToArray(); + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DocumentBlueprintContainer; - return documentEntities.Select(entity => + protected override Ordering ItemOrdering + { + get { - DocumentBlueprintTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentId, entity); - responseModel.HasChildren = false; - responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(entity); + var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType), Direction.Descending); // We need to override to change direction + ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)); + + return ordering; + } + } + + protected override DocumentBlueprintTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities) + => entities.Select(entity => + { + DocumentBlueprintTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity); + if (entity is IDocumentEntitySlim documentEntitySlim) + { + responseModel.HasChildren = false; + responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim); + } return responseModel; }).ToArray(); - } } 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 efde9f47b7..3e626411f5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs @@ -19,6 +19,9 @@ public class RootDocumentBlueprintTreeController : DocumentBlueprintTreeControll [HttpGet("root")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] - public async Task>> Root(int skip = 0, int take = 100) - => await GetRoot(skip, take); + public async Task>> Root(CancellationToken cancellationToken, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index cbda8a5d6b..a7de16f19a 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -3833,6 +3833,580 @@ ] } }, + "/umbraco/management/api/v1/document-blueprint/{id}/move": { + "put": { + "tags": [ + "Document Blueprint" + ], + "operationId": "PutDocumentBlueprintByIdMove", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MoveDocumentBlueprintRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MoveDocumentBlueprintRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MoveDocumentBlueprintRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/document-blueprint/folder": { + "post": { + "tags": [ + "Document Blueprint" + ], + "operationId": "PostDocumentBlueprintFolder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateFolderRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateFolderRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateFolderRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Umb-Generated-Resource": { + "description": "Identifier of the newly created resource", + "schema": { + "type": "string", + "description": "Identifier of the newly created resource" + } + }, + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + }, + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1/document-blueprint/folder/{id}": { + "get": { + "tags": [ + "Document Blueprint" + ], + "operationId": "GetDocumentBlueprintFolderById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FolderResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FolderResponseModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FolderResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "delete": { + "tags": [ + "Document Blueprint" + ], + "operationId": "DeleteDocumentBlueprintFolderById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "put": { + "tags": [ + "Document Blueprint" + ], + "operationId": "PutDocumentBlueprintFolderById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateFolderResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateFolderResponseModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateFolderResponseModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/document-blueprint/from-document": { "post": { "tags": [ @@ -4032,6 +4606,80 @@ ] } }, + "/umbraco/management/api/v1/tree/document-blueprint/children": { + "get": { + "tags": [ + "Document Blueprint" + ], + "operationId": "GetTreeDocumentBlueprintChildren", + "parameters": [ + { + "name": "parentId", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedDocumentBlueprintTreeItemResponseModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PagedDocumentBlueprintTreeItemResponseModel" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PagedDocumentBlueprintTreeItemResponseModel" + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/tree/document-blueprint/root": { "get": { "tags": [ @@ -4056,6 +4704,14 @@ "format": "int32", "default": 100 } + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { @@ -36752,13 +37408,10 @@ "additionalProperties": false }, "DocumentBlueprintTreeItemResponseModel": { - "required": [ - "documentType" - ], "type": "object", "allOf": [ { - "$ref": "#/components/schemas/NamedEntityTreeItemResponseModel" + "$ref": "#/components/schemas/FolderTreeItemResponseModel" } ], "properties": { @@ -36767,7 +37420,8 @@ { "$ref": "#/components/schemas/DocumentTypeReferenceResponseModel" } - ] + ], + "nullable": true } }, "additionalProperties": false @@ -39461,6 +40115,20 @@ }, "additionalProperties": false }, + "MoveDocumentBlueprintRequestModel": { + "type": "object", + "properties": { + "target": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, "MoveDocumentRequestModel": { "type": "object", "properties": { diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/MoveDocumentBlueprintRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/MoveDocumentBlueprintRequestModel.cs new file mode 100644 index 0000000000..b8562eded7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/MoveDocumentBlueprintRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint; + +public class MoveDocumentBlueprintRequestModel +{ + public ReferenceByIdModel? Target { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentBlueprintTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentBlueprintTreeItemResponseModel.cs index 14dc79b94c..8020cde257 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentBlueprintTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentBlueprintTreeItemResponseModel.cs @@ -2,7 +2,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Tree; -public class DocumentBlueprintTreeItemResponseModel : NamedEntityTreeItemResponseModel +public class DocumentBlueprintTreeItemResponseModel : FolderTreeItemResponseModel { - public DocumentTypeReferenceResponseModel DocumentType { get; set; } = new(); + public DocumentTypeReferenceResponseModel? DocumentType { get; set; } } diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 049a536690..5f57548ffd 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -19,6 +19,8 @@ public static partial class Constants public static readonly Guid MediaTypeContainer = new(Strings.MediaTypeContainer); + public static readonly Guid DocumentBlueprintContainer = new(Strings.DocumentBlueprintContainer); + public static readonly Guid DataType = new(Strings.DataType); public static readonly Guid Document = new(Strings.Document); @@ -73,6 +75,8 @@ public static partial class Constants public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; + public const string DocumentBlueprintContainer = "A7EFF71B-FA69-4552-93FC-038F7DEEE453"; + public const string ContentItem = "10E2B09F-C28B-476D-B77A-AA686435E44A"; public const string ContentItemType = "7A333C54-6F43-40A4-86A2-18688DC7E532"; diff --git a/src/Umbraco.Core/Constants-UdiEntityType.cs b/src/Umbraco.Core/Constants-UdiEntityType.cs index 16b3ebe3b8..81e64b534c 100644 --- a/src/Umbraco.Core/Constants-UdiEntityType.cs +++ b/src/Umbraco.Core/Constants-UdiEntityType.cs @@ -34,8 +34,7 @@ public static partial class Constants public const string DocumentType = "document-type"; public const string DocumentTypeContainer = "document-type-container"; - // TODO: What is this? This alias is only used for the blue print tree to render the blueprint's document type, it's not a real udi type - public const string DocumentTypeBluePrints = "document-type-blueprints"; + public const string DocumentBlueprintContainer = "document-blueprint-container"; public const string MediaType = "media-type"; public const string MediaTypeContainer = "media-type-container"; public const string DataType = "data-type"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index f3fa6d029f..010714cc54 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -283,6 +283,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index 9821f7d6b7..e4b11ccb6c 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -142,6 +142,10 @@ public static class UdiGetterExtensions { entityType = Constants.UdiEntityType.MediaTypeContainer; } + else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint) + { + entityType = Constants.UdiEntityType.DocumentBlueprintContainer; + } else { throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 762297af07..c38c19159f 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -12,6 +12,7 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity { Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer }, { Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer }, { Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer }, + { Constants.ObjectTypes.DocumentBlueprint, Constants.ObjectTypes.DocumentBlueprintContainer }, }; /// diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index acc9888bf4..6c8e8d7466 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -122,6 +122,14 @@ public enum UmbracoObjectTypes [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] DataTypeContainer, + /// + /// Document blueprint container. + /// + [UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentBlueprintContainer)] + [FriendlyName("Document Blueprint Container")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentBlueprintContainer)] + DocumentBlueprintContainer, + /// /// Relation type /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintContainerRepository.cs new file mode 100644 index 0000000000..81eeba15e6 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintContainerRepository.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IDocumentBlueprintContainerRepository : IEntityContainerRepository +{ +} diff --git a/src/Umbraco.Core/Services/ContentBlueprintContainerService.cs b/src/Umbraco.Core/Services/ContentBlueprintContainerService.cs new file mode 100644 index 0000000000..e8d742b70e --- /dev/null +++ b/src/Umbraco.Core/Services/ContentBlueprintContainerService.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services; + +internal sealed class ContentBlueprintContainerService : EntityTypeContainerService, IContentBlueprintContainerService +{ + public ContentBlueprintContainerService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentBlueprintContainerRepository entityContainerRepository, + IAuditRepository auditRepository, + IEntityRepository entityRepository, + IUserIdKeyResolver userIdKeyResolver) + : base(provider, loggerFactory, eventMessagesFactory, entityContainerRepository, auditRepository, entityRepository, userIdKeyResolver) + { + } + + protected override Guid ContainedObjectType => Constants.ObjectTypes.DocumentBlueprint; + + protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.DocumentBlueprintContainer; + + protected override int[] ReadLockIds => new [] { Constants.Locks.ContentTree }; + + protected override int[] WriteLockIds => ReadLockIds; +} diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs index b17c1c01c5..22f1234297 100644 --- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs @@ -12,6 +12,8 @@ namespace Umbraco.Cms.Core.Services; internal sealed class ContentBlueprintEditingService : ContentEditingServiceBase, IContentBlueprintEditingService { + private readonly IContentBlueprintContainerService _containerService; + public ContentBlueprintEditingService( IContentService contentService, IContentTypeService contentTypeService, @@ -20,10 +22,10 @@ internal sealed class ContentBlueprintEditingService ILogger> logger, ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, - IContentValidationService validationService) + IContentValidationService validationService, + IContentBlueprintContainerService containerService) : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, validationService) - { - } + => _containerService = containerService; public async Task GetAsync(Guid key) { @@ -138,6 +140,19 @@ internal sealed class ContentBlueprintEditingService protected override IContent New(string? name, int parentId, IContentType contentType) => new Content(name, parentId, contentType); + protected override async Task<(int? ParentId, ContentEditingOperationStatus OperationStatus)> TryGetAndValidateParentIdAsync(Guid? parentKey, IContentType contentType) + { + if (parentKey.HasValue is false) + { + return (Constants.System.Root, ContentEditingOperationStatus.Success); + } + + EntityContainer? container = await _containerService.GetAsync(parentKey.Value); + return container is not null + ? (container.Id, ContentEditingOperationStatus.Success) + : (null, ContentEditingOperationStatus.ParentNotFound); + } + /// /// NB: Some methods from ContentEditingServiceBase are needed, so we need to inherit from it /// but there are others that are not required to be implemented in the case of blueprints, therefore they throw NotImplementedException as default. @@ -161,4 +176,42 @@ internal sealed class ContentBlueprintEditingService IEnumerable existing = ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); return existing.Any(c => c.Name == name && c.Id != content.Id) is false; } + + public async Task> MoveAsync(Guid key, Guid? containerKey, Guid userKey) + { + using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); + IContent? toMove = await GetAsync(key); + if (toMove is null) + { + return Attempt.Fail(ContentEditingOperationStatus.NotFound); + } + + var parentId = Constants.System.Root; + if (containerKey.HasValue && containerKey.Value != Guid.Empty) + { + EntityContainer? container = await _containerService.GetAsync(containerKey.Value); + if (container is null) + { + return Attempt.Fail(ContentEditingOperationStatus.ParentNotFound); + } + + parentId = container.Id; + } + + if (toMove.ParentId == parentId) + { + return Attempt.Succeed(ContentEditingOperationStatus.Success); + } + + // NOTE: as long as the parent ID is correct the document repo takes care of updating the rest of the + // structural node data like path, level, sort orders etc. + toMove.ParentId = parentId; + + // Save blueprint + await SaveAsync(toMove, userKey); + + scope.Complete(); + + return Attempt.Succeed(ContentEditingOperationStatus.Success); + } } diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 4d441ac569..290fc2e5d5 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -60,23 +60,23 @@ internal abstract class ContentEditingServiceBase> MapCreate(ContentCreationModelBase contentCreationModelBase) where TContentCreateResult : ContentCreateResultBase, new() { - TContentType? contentType = TryGetAndValidateContentType(contentCreationModelBase.ContentTypeKey, contentCreationModelBase, out ContentEditingOperationStatus operationStatus); + TContentType? contentType = TryGetAndValidateContentType(contentCreationModelBase.ContentTypeKey, contentCreationModelBase, out ContentEditingOperationStatus validationOperationStatus); if (contentType == null) { - return Attempt.FailWithStatus(operationStatus, new TContentCreateResult()); + return Attempt.FailWithStatus(validationOperationStatus, new TContentCreateResult()); } - TContent? parent = TryGetAndValidateParent(contentCreationModelBase.ParentKey, contentType, out operationStatus); - if (operationStatus != ContentEditingOperationStatus.Success) + (int? ParentId, ContentEditingOperationStatus OperationStatus) parent = await TryGetAndValidateParentIdAsync(contentCreationModelBase.ParentKey, contentType); + if (parent.OperationStatus != ContentEditingOperationStatus.Success) { - return Attempt.FailWithStatus(operationStatus, new TContentCreateResult()); + return Attempt.FailWithStatus(parent.OperationStatus, new TContentCreateResult()); } // NOTE: property level validation errors must NOT fail the update - it must be possible to save invalid properties. // instead, the error state and validation errors will be communicated in the return value. Attempt validationResult = await ValidatePropertiesAsync(contentCreationModelBase, contentType); - TContent content = New(null, parent?.Id ?? Constants.System.Root, contentType); + TContent content = New(null, parent.ParentId ?? Constants.System.Root, contentType); if (contentCreationModelBase.Key.HasValue) { content.Key = contentCreationModelBase.Key.Value; @@ -188,26 +188,32 @@ internal abstract class ContentEditingServiceBase(operationStatus, content); + return Attempt.FailWithStatus(parent.OperationStatus, content); } // special case for move: short circuit the operation if the content is already under the correct parent. - if ((parent == null && content.ParentId == Constants.System.Root) || (parent != null && parent.Id == content.ParentId)) + if ((parent.ParentId == null && content.ParentId == Constants.System.Root) || (parent.ParentId != null && parent.ParentId == content.ParentId)) { return Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, content); } // special case for move: do not allow moving an item beneath itself. - if (parent?.Path.Split(Constants.CharArrays.Comma).Select(int.Parse).Contains(content.Id) is true) + if (parentKey.HasValue) { - return Attempt.FailWithStatus(ContentEditingOperationStatus.ParentInvalid, content); + // at this point the parent MUST exist - unless someone starts using this move method + // e.g. for blueprints (which should be handled elsewhere). + TContent parentContent = ContentService.GetById(parentKey.Value) ?? throw new InvalidOperationException("The content parent ID was validated, but the parent was not found"); + if (parentContent.Path.Split(Constants.CharArrays.Comma).Select(int.Parse).Contains(content.Id) is true) + { + return Attempt.FailWithStatus(ContentEditingOperationStatus.ParentInvalid, content); + } } var userId = await GetUserIdAsync(userKey); - OperationResult? moveResult = Move(content, parent?.Id ?? Constants.System.Root, userId); + OperationResult? moveResult = Move(content, parent.ParentId ?? Constants.System.Root, userId); scope.Complete(); @@ -225,14 +231,14 @@ internal abstract class ContentEditingServiceBase(operationStatus, content); + return Attempt.FailWithStatus(parent.OperationStatus, content); } var userId = await GetUserIdAsync(userKey); - TContent? copy = Copy(content, parent?.Id ?? Constants.System.Root, relateToOriginal, includeDescendants, userId); + TContent? copy = Copy(content, parent.ParentId ?? Constants.System.Root, relateToOriginal, includeDescendants, userId); scope.Complete(); // we'll assume that we have performed all validations for unsuccessful scenarios above, so a null result here @@ -324,7 +330,7 @@ internal abstract class ContentEditingServiceBase TryGetAndValidateParentIdAsync(Guid? parentKey, TContentType contentType) { TContent? parent = parentKey.HasValue ? ContentService.GetById(parentKey.Value) @@ -332,22 +338,19 @@ internal abstract class ContentEditingServiceBase((null, ContentEditingOperationStatus.ParentNotFound)); } if (parent == null && contentType.AllowedAsRoot == false) { - operationStatus = ContentEditingOperationStatus.NotAllowed; - return null; + return (null, ContentEditingOperationStatus.NotAllowed); } if (parent != null) { if (parent.Trashed) { - operationStatus = ContentEditingOperationStatus.InTrash; - return null; + return (null, ContentEditingOperationStatus.InTrash); } TContentType? parentContentType = ContentTypeService.Get(parent.ContentType.Key); @@ -356,13 +359,11 @@ internal abstract class ContentEditingServiceBase().LogWarning( + LoggerFactory.CreateLogger(GetType()).LogWarning( $"Cannot use {nameof(UpdateAsync)} to change the container parent. Move the container instead."); return EntityContainerOperationStatus.ParentNotFound; } diff --git a/src/Umbraco.Core/Services/IContentBlueprintContainerService.cs b/src/Umbraco.Core/Services/IContentBlueprintContainerService.cs new file mode 100644 index 0000000000..445a855177 --- /dev/null +++ b/src/Umbraco.Core/Services/IContentBlueprintContainerService.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IContentBlueprintContainerService : IEntityTypeContainerService +{ +} diff --git a/src/Umbraco.Core/Services/IContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/IContentBlueprintEditingService.cs index ff139609e7..4c4fa6c066 100644 --- a/src/Umbraco.Core/Services/IContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/IContentBlueprintEditingService.cs @@ -15,4 +15,6 @@ public interface IContentBlueprintEditingService Task> UpdateAsync(Guid key, ContentBlueprintUpdateModel updateModel, Guid userKey); Task> DeleteAsync(Guid key, Guid userKey); + + Task> MoveAsync(Guid key, Guid? containerKey, Guid userKey); } diff --git a/src/Umbraco.Core/UdiEntityTypeHelper.cs b/src/Umbraco.Core/UdiEntityTypeHelper.cs index f0e8774cf8..40d5b8dd59 100644 --- a/src/Umbraco.Core/UdiEntityTypeHelper.cs +++ b/src/Umbraco.Core/UdiEntityTypeHelper.cs @@ -12,6 +12,8 @@ public static class UdiEntityTypeHelper return Constants.UdiEntityType.Document; case UmbracoObjectTypes.DocumentBlueprint: return Constants.UdiEntityType.DocumentBlueprint; + case UmbracoObjectTypes.DocumentBlueprintContainer: + return Constants.UdiEntityType.DocumentBlueprintContainer; case UmbracoObjectTypes.Media: return Constants.UdiEntityType.Media; case UmbracoObjectTypes.Member: diff --git a/src/Umbraco.Core/UdiParser.cs b/src/Umbraco.Core/UdiParser.cs index c91c12b22b..24ee238a69 100644 --- a/src/Umbraco.Core/UdiParser.cs +++ b/src/Umbraco.Core/UdiParser.cs @@ -204,13 +204,13 @@ public sealed class UdiParser { Constants.UdiEntityType.Element, UdiType.GuidUdi }, { Constants.UdiEntityType.Document, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, + { Constants.UdiEntityType.DocumentBlueprintContainer, UdiType.GuidUdi }, { Constants.UdiEntityType.Media, UdiType.GuidUdi }, { Constants.UdiEntityType.Member, UdiType.GuidUdi }, { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, { Constants.UdiEntityType.Template, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.DocumentTypeBluePrints, UdiType.GuidUdi }, { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 647e2c9f29..64e8af2290 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -28,6 +28,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index f6abdc9ba7..204a09a609 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -76,5 +76,6 @@ public class UmbracoPlan : MigrationPlan To("{1539A010-2EB5-4163-8518-4AE2AA98AFC6}"); To("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}"); To("{0D82C836-96DD-480D-A924-7964E458BD34}"); + To("{1A0FBC8A-6FC6-456C-805C-B94816B2E570}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs new file mode 100644 index 0000000000..d09783c061 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MoveDocumentBlueprintsToFolders.cs @@ -0,0 +1,69 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0; + +public class MoveDocumentBlueprintsToFolders : MigrationBase +{ + private readonly IEntityService _entityService; + private readonly IContentService _contentService; + private readonly IDocumentBlueprintContainerRepository _containerRepository; + + public MoveDocumentBlueprintsToFolders( + IMigrationContext context, + IEntityService entityService, + IContentService contentService, + IDocumentBlueprintContainerRepository containerRepository) + : base(context) + { + _entityService = entityService; + _contentService = contentService; + _containerRepository = containerRepository; + } + + protected override void Migrate() + { + Guid[] allDocumentBlueprintKeysAtRoot = _entityService + .GetAll(UmbracoObjectTypes.DocumentBlueprint) + .Where(e => e.ParentId == Constants.System.Root) + .Select(e => e.Key) + .ToArray(); + + if (allDocumentBlueprintKeysAtRoot.Any() is false) + { + return; + } + + var allContainersAtRoot = _containerRepository.GetMany( + _entityService + .GetRootEntities(UmbracoObjectTypes.DocumentBlueprint) + .Select(e => e.Id) + .ToArray()) + .ToList(); + + foreach (Guid key in allDocumentBlueprintKeysAtRoot) + { + IContent? blueprint = _contentService.GetBlueprintById(key); + if (blueprint is null) + { + continue; + } + + EntityContainer? container = allContainersAtRoot.FirstOrDefault(c => c.Name == blueprint.ContentType.Name); + if (container is null) + { + container = new EntityContainer(Constants.ObjectTypes.DocumentBlueprint) + { + Name = blueprint.ContentType.Name + }; + _containerRepository.Save(container); + allContainersAtRoot.Add(container); + } + + blueprint.ParentId = container.Id; + _contentService.SaveBlueprint(blueprint); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs new file mode 100644 index 0000000000..720f94d04e --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintContainerRepository.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Scoping; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentBlueprintContainerRepository : EntityContainerRepository, IDocumentBlueprintContainerRepository +{ + public DocumentBlueprintContainerRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentBlueprintContainer) + { + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs index c97199e76b..1f57f89420 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository { - public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentTypeContainer) { } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index 3d981d5946..11bd4b53e1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -23,7 +23,7 @@ internal class EntityContainerRepository : EntityRepositoryBase GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IDataValueEditorFactory DataValueEditorFactory => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + [Test] + public async Task Can_Create_Container_At_Root() + { + var result = await ContentBlueprintContainerService.CreateAsync(null, "Root Container", null, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + }); + + var created = await ContentBlueprintContainerService.GetAsync(result.Result.Key); + Assert.NotNull(created); + Assert.Multiple(() => + { + Assert.AreEqual("Root Container", created.Name); + Assert.AreEqual(Constants.System.Root, created.ParentId); + }); + } + + [Test] + public async Task Can_Create_Child_Container() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null, "Root Container", null, Constants.Security.SuperUserKey)).Result; + + var result = await ContentBlueprintContainerService.CreateAsync(null, "Child Container", root.Key, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + }); + + var created = await ContentBlueprintContainerService.GetAsync(result.Result.Key); + Assert.NotNull(created); + Assert.Multiple(() => + { + Assert.AreEqual("Child Container", created.Name); + Assert.AreEqual(root.Id, created.ParentId); + }); + } + + [Test] + public async Task Can_Create_Container_With_Explicit_Key() + { + var key = Guid.NewGuid(); + var result = await ContentBlueprintContainerService.CreateAsync(key, "Root Container", null, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + Assert.AreEqual(key, result.Result.Key); + }); + + var created = await ContentBlueprintContainerService.GetAsync(key); + Assert.NotNull(created); + Assert.Multiple(() => + { + Assert.AreEqual("Root Container", created.Name); + Assert.AreEqual(Constants.System.Root, created.ParentId); + }); + } + + [Test] + public async Task Can_Update_Container_At_Root() + { + var key = (await ContentBlueprintContainerService.CreateAsync(null, "Root Container", null, Constants.Security.SuperUserKey)).Result.Key; + + var result = await ContentBlueprintContainerService.UpdateAsync(key, "Root Container UPDATED", Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + }); + + var updated = await ContentBlueprintContainerService.GetAsync(key); + Assert.NotNull(updated); + Assert.Multiple(() => + { + Assert.AreEqual("Root Container UPDATED", updated.Name); + Assert.AreEqual(Constants.System.Root, updated.ParentId); + }); + } + + [Test] + public async Task Can_Update_Child_Container() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null, "Root Container", null, Constants.Security.SuperUserKey)).Result; + EntityContainer child = (await ContentBlueprintContainerService.CreateAsync(null, "Child Container", root.Key, Constants.Security.SuperUserKey)).Result; + + var result = await ContentBlueprintContainerService.UpdateAsync(child.Key, "Child Container UPDATED", Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + }); + + EntityContainer updated = await ContentBlueprintContainerService.GetAsync(child.Key); + Assert.NotNull(updated); + Assert.Multiple(() => + { + Assert.AreEqual("Child Container UPDATED", updated.Name); + Assert.AreEqual(root.Id, updated.ParentId); + }); + } + + [Test] + public async Task Can_Get_Container_At_Root() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null,"Root Container", null, Constants.Security.SuperUserKey)).Result; + + EntityContainer created = await ContentBlueprintContainerService.GetAsync(root.Key); + Assert.NotNull(created); + Assert.Multiple(() => + { + Assert.AreEqual("Root Container", created.Name); + Assert.AreEqual(Constants.System.Root, created.ParentId); + }); + } + + [Test] + public async Task Can_Get_Child_Container() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null,"Root Container", null, Constants.Security.SuperUserKey)).Result; + EntityContainer child = (await ContentBlueprintContainerService.CreateAsync(null, "Child Container", root.Key, Constants.Security.SuperUserKey)).Result; + + EntityContainer created = await ContentBlueprintContainerService.GetAsync(child.Key); + Assert.IsNotNull(created); + Assert.Multiple(() => + { + Assert.AreEqual("Child Container", created.Name); + Assert.AreEqual(root.Id, child.ParentId); + }); + } + + [Test] + public async Task Can_Delete_Container_At_Root() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null,"Root Container", null, Constants.Security.SuperUserKey)).Result; + + var result = await ContentBlueprintContainerService.DeleteAsync(root.Key, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + }); + + var current = await ContentBlueprintContainerService.GetAsync(root.Key); + Assert.IsNull(current); + } + + [Test] + public async Task Can_Delete_Child_Container() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null,"Root Container", null, Constants.Security.SuperUserKey)).Result; + EntityContainer child = (await ContentBlueprintContainerService.CreateAsync(null, "Child Container", root.Key, Constants.Security.SuperUserKey)).Result; + + var result = await ContentBlueprintContainerService.DeleteAsync(child.Key, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.Success, result.Status); + }); + + child = await ContentBlueprintContainerService.GetAsync(child.Key); + Assert.IsNull(child); + + root = await ContentBlueprintContainerService.GetAsync(root.Key); + Assert.IsNotNull(root); + } + + [Test] + public async Task Cannot_Create_Child_Container_Below_Invalid_Parent() + { + var key = Guid.NewGuid(); + var result = await ContentBlueprintContainerService.CreateAsync(key, "Child Container", Guid.NewGuid(), Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.ParentNotFound, result.Status); + }); + + var created = await ContentBlueprintContainerService.GetAsync(key); + Assert.IsNull(created); + } + + [Test] + public async Task Cannot_Delete_Container_With_Child_Container() + { + EntityContainer root = (await ContentBlueprintContainerService.CreateAsync(null,"Root Container", null, Constants.Security.SuperUserKey)).Result; + EntityContainer child = (await ContentBlueprintContainerService.CreateAsync(null, "Child Container", root.Key, Constants.Security.SuperUserKey)).Result; + + var result = await ContentBlueprintContainerService.DeleteAsync(root.Key, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.NotEmpty, result.Status); + }); + + var current = await ContentBlueprintContainerService.GetAsync(root.Key); + Assert.IsNotNull(current); + } + + [Test] + public async Task Cannot_Delete_Container_With_Child_DataType() + { + EntityContainer container = (await ContentBlueprintContainerService.CreateAsync(null,"Root Container", null, Constants.Security.SuperUserKey)).Result; + + IDataType dataType = + new DataType(new TextboxPropertyEditor(DataValueEditorFactory, IOHelper), ConfigurationEditorJsonSerializer) + { + Name = Guid.NewGuid().ToString(), + DatabaseType = ValueStorageType.Nvarchar, + ParentId = container.Id + }; + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + + var result = await ContentBlueprintContainerService.DeleteAsync(container.Key, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.NotEmpty, result.Status); + }); + + var currentContainer = await ContentBlueprintContainerService.GetAsync(container.Key); + Assert.IsNotNull(currentContainer); + + var currentDataType = await DataTypeService.GetAsync(dataType.Key); + Assert.IsNotNull(currentDataType); + } + + [Test] + public async Task Cannot_Delete_Non_Existing_Container() + { + var result = await ContentBlueprintContainerService.DeleteAsync(Guid.NewGuid(), Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(EntityContainerOperationStatus.NotFound, result.Status); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Create.cs index 144c01948e..6f92b14e16 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Create.cs @@ -284,4 +284,29 @@ public partial class ContentBlueprintEditingServiceTests }); Assert.IsNull(result.Result.Content); } + + [Test] + public async Task Can_Create_Blueprint_In_A_Folder() + { + var containerKey = Guid.NewGuid(); + var container = (await ContentBlueprintContainerService.CreateAsync(containerKey, "Root Container", null, Constants.Security.SuperUserKey)).Result; + + var blueprintKey = Guid.NewGuid(); + await ContentBlueprintEditingService.CreateAsync(SimpleContentBlueprintCreateModel(blueprintKey, containerKey), Constants.Security.SuperUserKey); + + var blueprint = await ContentBlueprintEditingService.GetAsync(blueprintKey); + Assert.NotNull(blueprint); + Assert.Multiple(() => + { + Assert.AreEqual(container.Id, blueprint.ParentId); + Assert.AreEqual($"{container.Path},{blueprint.Id}", blueprint.Path); + }); + + var result = GetBlueprintChildren(containerKey); + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Length); + Assert.AreEqual(blueprintKey, result.First().Key); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Move.cs new file mode 100644 index 0000000000..240f280ad8 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Move.cs @@ -0,0 +1,93 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +public partial class ContentBlueprintEditingServiceTests +{ + [Test] + public async Task Can_Move_Blueprint_From_Root_To_A_Folder() + { + var containerKey = Guid.NewGuid(); + var container = (await ContentBlueprintContainerService.CreateAsync(containerKey, "Root Container", null, Constants.Security.SuperUserKey)).Result; + Assert.AreEqual(0, GetBlueprintChildren(containerKey).Length); + + var blueprintKey = Guid.NewGuid(); + await ContentBlueprintEditingService.CreateAsync(SimpleContentBlueprintCreateModel(blueprintKey, null), Constants.Security.SuperUserKey); + + await ContentBlueprintEditingService.MoveAsync(blueprintKey, containerKey, Constants.Security.SuperUserKey); + + var blueprint = await ContentBlueprintEditingService.GetAsync(blueprintKey); + Assert.NotNull(blueprint); + Assert.Multiple(() => + { + Assert.AreEqual(container.Id, blueprint.ParentId); + Assert.AreEqual($"{container.Path},{blueprint.Id}", blueprint.Path); + }); + + var result = GetBlueprintChildren(containerKey); + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Length); + Assert.AreEqual(blueprintKey, result.First().Key); + }); + } + + [Test] + public async Task Can_Move_Blueprint_From_A_Folder_To_Root() + { + var containerKey = Guid.NewGuid(); + await ContentBlueprintContainerService.CreateAsync(containerKey, "Root Container", null, Constants.Security.SuperUserKey); + + var blueprintKey = Guid.NewGuid(); + await ContentBlueprintEditingService.CreateAsync(SimpleContentBlueprintCreateModel(blueprintKey, containerKey), Constants.Security.SuperUserKey); + Assert.AreEqual(1, GetBlueprintChildren(containerKey).Length); + + await ContentBlueprintEditingService.MoveAsync(blueprintKey, null, Constants.Security.SuperUserKey); + Assert.AreEqual(0, GetBlueprintChildren(containerKey).Length); + + var blueprint = await ContentBlueprintEditingService.GetAsync(blueprintKey); + Assert.NotNull(blueprint); + Assert.Multiple(() => + { + Assert.AreEqual(Constants.System.Root, blueprint.ParentId); + Assert.AreEqual($"{Constants.System.Root},{blueprint.Id}", blueprint.Path); + }); + + var result = GetBlueprintChildren(null); + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Length); + Assert.AreEqual(blueprintKey, result.First().Key); + }); + } + + [Test] + public async Task Can_Move_Blueprint_Between_Folders() + { + var containerKey1 = Guid.NewGuid(); + await ContentBlueprintContainerService.CreateAsync(containerKey1, "Container #1", null, Constants.Security.SuperUserKey); + var containerKey2 = Guid.NewGuid(); + var container2 = (await ContentBlueprintContainerService.CreateAsync(containerKey2, "Container #2", null, Constants.Security.SuperUserKey)).Result; + + var blueprintKey = Guid.NewGuid(); + await ContentBlueprintEditingService.CreateAsync(SimpleContentBlueprintCreateModel(blueprintKey, containerKey1), Constants.Security.SuperUserKey); + + await ContentBlueprintEditingService.MoveAsync(blueprintKey, containerKey2, Constants.Security.SuperUserKey); + + var blueprint = await ContentBlueprintEditingService.GetAsync(blueprintKey); + Assert.NotNull(blueprint); + Assert.Multiple(() => + { + Assert.AreEqual(container2.Id, blueprint.ParentId); + Assert.AreEqual($"{container2.Path},{blueprint.Id}", blueprint.Path); + }); + + var result = GetBlueprintChildren(containerKey2); + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Length); + Assert.AreEqual(blueprintKey, result.First().Key); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Update.cs index 5f340e8e6a..41692e0ea9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.Update.cs @@ -149,4 +149,40 @@ public partial class ContentBlueprintEditingServiceTests }); Assert.IsNull(updateResult.Result.Content); } + + [Test] + public async Task Can_Update_Blueprint_In_A_Folder() + { + var containerKey = Guid.NewGuid(); + var container = (await ContentBlueprintContainerService.CreateAsync(containerKey, "Root Container", null, Constants.Security.SuperUserKey)).Result; + + var blueprintKey = Guid.NewGuid(); + await ContentBlueprintEditingService.CreateAsync(SimpleContentBlueprintCreateModel(blueprintKey, containerKey), Constants.Security.SuperUserKey); + + await ContentBlueprintEditingService.UpdateAsync(blueprintKey, SimpleContentBlueprintUpdateModel(), Constants.Security.SuperUserKey); + + var blueprint = await ContentBlueprintEditingService.GetAsync(blueprintKey); + Assert.NotNull(blueprint); + Assert.Multiple(() => + { + Assert.AreEqual(container.Id, blueprint.ParentId); + Assert.AreEqual($"{container.Path},{blueprint.Id}", blueprint.Path); + }); + + var result = GetBlueprintChildren(containerKey); + Assert.Multiple(() => + { + Assert.AreEqual(1, result.Length); + Assert.AreEqual(blueprintKey, result.First().Key); + }); + + blueprint = await ContentBlueprintEditingService.GetAsync(blueprintKey); + Assert.IsNotNull(blueprint); + Assert.Multiple(() => + { + Assert.AreEqual("Blueprint #1 updated", blueprint.Name); + Assert.AreEqual("The title value updated", blueprint.GetValue("title")); + Assert.AreEqual("The author value updated", blueprint.GetValue("author")); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs index 088799e271..ef559773cd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs @@ -2,6 +2,8 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -11,6 +13,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; /// public partial class ContentBlueprintEditingServiceTests : ContentEditingServiceTestsBase { + private IContentBlueprintContainerService ContentBlueprintContainerService => GetRequiredService(); + + private IEntityService EntityService => GetRequiredService(); + private async Task CreateInvariantContentBlueprint() { var contentType = CreateInvariantContentType(); @@ -71,4 +77,38 @@ public partial class ContentBlueprintEditingServiceTests : ContentEditingService Assert.IsTrue(result.Success); return result.Result.Content!; } + + private ContentBlueprintCreateModel SimpleContentBlueprintCreateModel(Guid blueprintKey, Guid? containerKey) + { + var createModel = new ContentBlueprintCreateModel + { + Key = blueprintKey, + ContentTypeKey = ContentType.Key, + ParentKey = containerKey, + InvariantName = "Blueprint #1", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "The title value" }, + new PropertyValueModel { Alias = "author", Value = "The author value" } + } + }; + return createModel; + } + + private ContentBlueprintUpdateModel SimpleContentBlueprintUpdateModel() + { + var createModel = new ContentBlueprintUpdateModel + { + InvariantName = "Blueprint #1 updated", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "The title value updated" }, + new PropertyValueModel { Alias = "author", Value = "The author value updated" } + } + }; + return createModel; + } + + private IEntitySlim[] GetBlueprintChildren(Guid? containerKey) + => EntityService.GetPagedChildren(containerKey, new[] { UmbracoObjectTypes.DocumentBlueprintContainer }, UmbracoObjectTypes.DocumentBlueprint, 0, 100, out _).ToArray(); }