Skip to content

WIP: POC for global elements (CRUD + folders + API) #19877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ protected IActionResult DocumentEditingOperationStatusResult<TContentModelBase>(
where TContentModelBase : ContentModelBase<DocumentValueModel, DocumentVariantRequestModel>
=> ContentEditingOperationStatusResult<TContentModelBase, DocumentValueModel, DocumentVariantRequestModel>(status, requestModel, validationResult);

// TODO ELEMENTS: move this to ContentControllerBase
protected IActionResult DocumentPublishingOperationStatusResult(
ContentPublishingOperationStatus status,
IEnumerable<string>? invalidPropertyAliases = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Element;

[ApiVersion("1.0")]
public class ByKeyElementController : ElementControllerBase
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IElementService _elementService;

public ByKeyElementController(IUmbracoMapper umbracoMapper, IElementService elementService)
{
_umbracoMapper = umbracoMapper;
_elementService = elementService;
}

[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
// TODO ELEMENTS: move logic to a presentation factory
IElement? element = _elementService.GetById(id);
if (element is null)
{
return Task.FromResult(ContentEditingOperationStatusResult(ContentEditingOperationStatus.NotFound));
}

ContentScheduleCollection contentScheduleCollection = _elementService.GetContentScheduleByContentId(id);

var model = new ElementResponseModel();
_umbracoMapper.Map(element, model);
_umbracoMapper.Map(contentScheduleCollection, model);

return Task.FromResult<IActionResult>(Ok(model));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Element;

[ApiVersion("1.0")]
public class CreateElementController : ElementControllerBase
{
private readonly IElementEditingPresentationFactory _elementEditingPresentationFactory;
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public CreateElementController(
IElementEditingPresentationFactory elementEditingPresentationFactory,
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingPresentationFactory = elementEditingPresentationFactory;
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateElementRequestModel requestModel)
{
ElementCreateModel model = _elementEditingPresentationFactory.MapCreateModel(requestModel);
Attempt<ElementCreateResult, ContentEditingOperationStatus> result =
await _elementEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? CreatedAtId<ByKeyElementController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
: ContentEditingOperationStatusResult(result.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Element;

[ApiVersion("1.0")]
public class DeleteElementController : ElementControllerBase
{
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public DeleteElementController(
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
{
Attempt<IElement?, ContentEditingOperationStatus> result = await _elementEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Content;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.Controllers.Element;

[VersionedApiBackOfficeRoute(Constants.UdiEntityType.Element)]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))]
// TODO ELEMENTS: backoffice authorization policies
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public class ElementControllerBase : ContentControllerBase
{
}
Original file line number Diff line number Diff line change
@@ -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.Element.Folder;

[ApiVersion("1.0")]
public class ByKeyElementFolderController : ElementFolderControllerBase
{
public ByKeyElementFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IElementContainerService elementContainerService)
: base(backOfficeSecurityAccessor, elementContainerService)
{
}

[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id) => await GetFolderAsync(id);
}
Original file line number Diff line number Diff line change
@@ -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.Element.Folder;

[ApiVersion("1.0")]
public class CreateElementFolderController : ElementFolderControllerBase
{
public CreateElementFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IElementContainerService elementContainerService)
: base(backOfficeSecurityAccessor, elementContainerService)
{
}

[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateFolderRequestModel createFolderRequestModel)
=> await CreateFolderAsync<ByKeyElementFolderController>(
createFolderRequestModel,
controller => nameof(controller.ByKey));
}
Original file line number Diff line number Diff line change
@@ -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.Element.Folder;

[ApiVersion("1.0")]
public class DeleteElementFolderController : ElementFolderControllerBase
{
public DeleteElementFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IElementContainerService elementContainerService)
: base(backOfficeSecurityAccessor, elementContainerService)
{
}

[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id) => await DeleteFolderAsync(id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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;

namespace Umbraco.Cms.Api.Management.Controllers.Element.Folder;

[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Element}/folder")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))]
// TODO ELEMENTS: backoffice authorization policies
public abstract class ElementFolderControllerBase : FolderManagementControllerBase<IElement>
{
protected ElementFolderControllerBase(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IElementContainerService elementContainerService)
: base(backOfficeSecurityAccessor, elementContainerService)
{
}
}
Original file line number Diff line number Diff line change
@@ -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.Element.Folder;

[ApiVersion("1.0")]
public class UpdateElementFolderController : ElementFolderControllerBase
{
public UpdateElementFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IElementContainerService elementContainerService)
: base(backOfficeSecurityAccessor, elementContainerService)
{
}

[HttpPut("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(CancellationToken cancellationToken, Guid id, UpdateFolderResponseModel updateFolderResponseModel)
=> await UpdateFolderAsync(id, updateFolderResponseModel);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.Element;

[ApiVersion("1.0")]
public class PublishElementController : ElementControllerBase
{
private readonly IElementPublishingService _elementPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IDocumentPresentationFactory _documentPresentationFactory;

public PublishElementController(
IElementPublishingService elementPublishingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
{
_elementPublishingService = elementPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_documentPresentationFactory = documentPresentationFactory;
}

[HttpPut("{id:guid}/publish")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Publish(CancellationToken cancellationToken, Guid id, PublishElementRequestModel requestModel)
{
// TODO ELEMENTS: IDocumentPresentationFactory carries the implementation of this mapping - it should probably be renamed
var tempModel = new PublishDocumentRequestModel { PublishSchedules = requestModel.PublishSchedules };
Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCulturePublishScheduleModels(tempModel);

if (modelResult.Success is false)
{
// TODO ELEMENTS: use refactored DocumentPublishingOperationStatusResult from DocumentControllerBase once it's ready
return BadRequest();
}

Attempt<ContentPublishingResult, ContentPublishingOperationStatus> attempt = await _elementPublishingService.PublishAsync(
id,
modelResult.Result,
CurrentUserKey(_backOfficeSecurityAccessor));
return attempt.Success
? Ok()
// TODO ELEMENTS: use refactored DocumentPublishingOperationStatusResult from DocumentControllerBase once it's ready
: BadRequest();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree;

[ApiVersion("1.0")]
public class AncestorsElementTreeController : ElementTreeControllerBase
{
public AncestorsElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper)
: base(entityService, umbracoMapper)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<ElementTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ElementTreeItemResponseModel>>> Ancestors(CancellationToken cancellationToken, Guid descendantId)
=> await GetAncestors(descendantId);
}
Loading
Loading