Compare commits

...
Author SHA1 Message Date
kjac 058798c420 Move ElementRepository to the right place 2025-11-20 15:36:34 +01:00
kjac a56652880d Rename all things ElementType to DocumentType 2025-11-20 15:27:29 +01:00
kjac 8bc3b8b032 Make the Element tree items look like Document tree items (with variations) 2025-11-19 17:10:21 +01:00
kjac 922fbc8c09 Add items endpoint incl. variation info at item level 2025-11-03 20:12:49 +01:00
kjac f0031a741e Element copying 2025-10-31 10:15:02 +01:00
kjac 6359d45130 Move element (incl. tests) 2025-10-24 19:20:32 +02:00
kjac 908b4de8b5 Add cache invalidation on update (like content and media) 2025-10-24 19:18:52 +02:00
kjac 8defac5b23 Merge branch 'main' into v16/temp/element-poc 2025-10-16 19:15:28 +02:00
kjac 5fd8f54f87 Merge branch 'main' into v16/temp/element-poc 2025-10-16 12:45:24 +02:00
kjac 99620721c3 Fix merge 2025-10-02 20:29:46 +02:00
kjac 0e9c45f334 Merge branch 'main' into v16/temp/element-poc
# Conflicts:
#	src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs
#	src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
#	src/Umbraco.Core/Services/ContentPublishingService.cs
2025-10-02 19:10:03 +02:00
kjac 1644b0e83e Fix merge from main 2025-09-10 12:43:12 +02:00
kjac 064c7bdb67 Merge branch 'main' into v16/temp/element-poc
# Conflicts:
#	src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs
#	src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantResponseModel.cs
#	src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
#	src/Umbraco.Core/Services/IContentService.cs
2025-09-10 11:40:58 +02:00
kjac 26ed5fb5bd Fix test build errors 2025-09-05 09:12:35 +02:00
kjac e7ae4ab48c Apply conditional appcache access to elements as well 2025-09-05 06:38:22 +02:00
kjac 264f4c1574 Merge branch 'main' into v16/temp/element-poc 2025-09-05 06:36:29 +02:00
kjac 0a2a518e5c Add request caching for published element creation (similar to published document creation) 2025-09-01 09:40:31 +02:00
kjac 28abcf334b Move scheduled content keys to base abstraction 2025-09-01 09:39:07 +02:00
kjac 2ce2b51d9c Merge branch 'main' into v16/temp/element-poc
# Conflicts:
#	src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
#	src/Umbraco.Core/Services/ContentService.cs
#	src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs
2025-09-01 08:30:27 +02:00
kjac d7b4fadb64 Added tests to prove published element status 2025-08-15 13:15:01 +02:00
kjac 3a53a9b6d0 Element picker property editor 2025-08-15 13:14:44 +02:00
kjac feb07a20c1 Fixing up a little tests 2025-08-15 12:24:14 +02:00
kjac ca9f0bd598 Fix delete at repo level 2025-08-15 09:36:19 +02:00
kjac 632b89d650 Published element cache (WIP) 2025-08-14 13:13:41 +02:00
kjac d6b5d57e03 API for publishing elements 2025-08-14 13:13:15 +02:00
kjac 523d96a0e1 Merge branch 'main' into v16/temp/element-poc 2025-08-14 09:57:20 +02:00
eadd6ecfad Update src/Umbraco.Core/Models/UmbracoObjectTypes.cs
Co-authored-by: Andy Butland <abutland73@gmail.com>
2025-08-13 08:27:52 +02:00
kjac fd59a8b1cf Added "foldersOnly" option to the siblings endpoint 2025-08-13 08:23:20 +02:00
kjac 251b8a4d94 Fix bad file name 2025-08-13 08:21:11 +02:00
kjac 70b5c52ce3 Publishing for Elements (incl. refactor) 2025-08-13 08:18:09 +02:00
kjac d2e0416ab6 Distributed cache handling for Elements 2025-08-13 08:14:57 +02:00
kjac eea3c57864 Fix infinite recursion 2025-08-11 15:08:09 +02:00
kjac b25ee766ed Merge branch 'main' into v16/temp/element-poc
# Conflicts:
#	src/Umbraco.Core/Persistence/Constants-Locks.cs
#	src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
#	src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs
2025-08-11 08:49:12 +02:00
kjac 148ac5167f CRUD + folders + API 2025-07-25 14:19:23 +02:00
195 changed files with 11675 additions and 3391 deletions
@@ -30,6 +30,7 @@ public abstract class DocumentControllerBase : ContentControllerBase
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,
@@ -0,0 +1,41 @@
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.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 IElementService _elementService;
private readonly IElementPresentationFactory _elementPresentationFactory;
public ByKeyElementController(IElementService elementService, IElementPresentationFactory elementPresentationFactory)
{
_elementService = elementService;
_elementPresentationFactory = elementPresentationFactory;
}
[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ElementResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
IElement? element = _elementService.GetById(id);
if (element is null)
{
return Task.FromResult(ContentEditingOperationStatusResult(ContentEditingOperationStatus.NotFound));
}
ContentScheduleCollection contentScheduleCollection = _elementService.GetContentScheduleByContentId(id);
ElementResponseModel model = _elementPresentationFactory.CreateResponseModel(element, contentScheduleCollection);
return Task.FromResult<IActionResult>(Ok(model));
}
}
@@ -0,0 +1,42 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Element;
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 CopyElementController : ElementControllerBase
{
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public CopyElementController(
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost("{id:guid}/copy")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Copy(CancellationToken cancellationToken, Guid id, CopyElementRequestModel copyElementRequestModel)
{
Attempt<IElement?, ContentEditingOperationStatus> result = await _elementEditingService.CopyAsync(
id,
copyElementRequestModel.Target?.Id,
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtId<ByKeyElementController>(controller => nameof(controller.ByKey), result.Result!.Key)
: ContentEditingOperationStatusResult(result.Status);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
{
}
@@ -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);
}
@@ -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));
}
@@ -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);
}
@@ -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)
{
}
}
@@ -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);
}
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Element.Item;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Element}")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))]
public class ElementItemControllerBase : ManagementApiControllerBase
{
}
@@ -0,0 +1,45 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Element.Item;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Element.Item;
[ApiVersion("1.0")]
public class ItemElementItemController : ElementItemControllerBase
{
private readonly IEntityService _entityService;
private readonly IElementPresentationFactory _elementPresentationFactory;
public ItemElementItemController(
IEntityService entityService,
IElementPresentationFactory elementPresentationFactory)
{
_entityService = entityService;
_elementPresentationFactory = elementPresentationFactory;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<ElementItemResponseModel>), StatusCodes.Status200OK)]
public Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Task.FromResult<IActionResult>(Ok(Enumerable.Empty<ElementItemResponseModel>()));
}
IEnumerable<IElementEntitySlim> elements = _entityService
.GetAll(UmbracoObjectTypes.Element, ids.ToArray())
.OfType<IElementEntitySlim>();
IEnumerable<ElementItemResponseModel> responseModels = elements.Select(_elementPresentationFactory.CreateItemResponseModel);
return Task.FromResult<IActionResult>(Ok(responseModels));
}
}
@@ -0,0 +1,42 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Element;
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 MoveElementController : ElementControllerBase
{
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public MoveElementController(
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPut("{id:guid}/move")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Move(CancellationToken cancellationToken, Guid id, MoveElementRequestModel moveElementRequestModel)
{
Attempt<ContentEditingOperationStatus> result = await _elementEditingService.MoveAsync(
id,
moveElementRequestModel.Target?.Id,
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Result);
}
}
@@ -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();
}
}
@@ -0,0 +1,24 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
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, IElementPresentationFactory elementPresentationFactory)
: base(entityService, umbracoMapper, elementPresentationFactory)
{
}
[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);
}
@@ -0,0 +1,28 @@
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.Mapping;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree;
[ApiVersion("1.0")]
public class ChildrenElementTreeController : ElementTreeControllerBase
{
public ChildrenElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory)
: base(entityService, umbracoMapper, elementPresentationFactory)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ElementTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ElementTreeItemResponseModel>>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentId, skip, take);
}
}
@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.Element}")]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))]
// TODO ELEMENTS: backoffice authorization policies
public class ElementTreeControllerBase : FolderTreeControllerBase<ElementTreeItemResponseModel>
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IElementPresentationFactory _elementPresentationFactory;
public ElementTreeControllerBase(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory)
: base(entityService)
{
_umbracoMapper = umbracoMapper;
_elementPresentationFactory = elementPresentationFactory;
}
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Element;
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.ElementContainer;
protected override Ordering ItemOrdering
{
get
{
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 ElementTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
=> entities.Select(entity =>
{
ElementTreeItemResponseModel responseModel = MapTreeItemViewModel(parentKey, entity);
if (entity is IElementEntitySlim elementEntitySlim)
{
responseModel.HasChildren = false;
responseModel.DocumentType = _umbracoMapper.Map<DocumentTypeReferenceResponseModel>(elementEntitySlim)!;
responseModel.Variants = _elementPresentationFactory.CreateVariantsItemResponseModels(elementEntitySlim);
}
return responseModel;
}).ToArray();
}
@@ -0,0 +1,32 @@
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.Mapping;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree;
[ApiVersion("1.0")]
public class RootElementTreeController : ElementTreeControllerBase
{
public RootElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory)
: base(entityService, umbracoMapper, elementPresentationFactory)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<ElementTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ElementTreeItemResponseModel>>> Root(
CancellationToken cancellationToken,
int skip = 0,
int take = 100,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}
@@ -0,0 +1,30 @@
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.Mapping;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree;
public class SiblingsElementTreeController : ElementTreeControllerBase
{
public SiblingsElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory)
: base(entityService, umbracoMapper, elementPresentationFactory)
{
}
[HttpGet("siblings")]
[ProducesResponseType(typeof(SubsetViewModel<ElementTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<SubsetViewModel<ElementTreeItemResponseModel>>> Siblings(
CancellationToken cancellationToken,
Guid target,
int before,
int after,
bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetSiblings(target, before, after);
}
}
@@ -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.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Element;
[ApiVersion("1.0")]
public class UnpublishElementController : ElementControllerBase
{
private readonly IElementPublishingService _elementPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
public UnpublishElementController(
IElementPublishingService elementPublishingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
{
_elementPublishingService = elementPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_documentPresentationFactory = documentPresentationFactory;
}
[HttpPut("{id:guid}/unpublish")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Unpublish(CancellationToken cancellationToken, Guid id, UnpublishElementRequestModel requestModel)
{
Attempt<ContentPublishingOperationStatus> attempt = await _elementPublishingService.UnpublishAsync(
id,
requestModel.Cultures,
CurrentUserKey(_backOfficeSecurityAccessor));
return attempt.Success
? Ok()
// TODO ELEMENTS: use refactored DocumentPublishingOperationStatusResult from DocumentControllerBase once it's ready
: BadRequest();
}
}
@@ -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 UpdateElementController : ElementControllerBase
{
private readonly IElementEditingPresentationFactory _elementEditingPresentationFactory;
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public UpdateElementController(
IElementEditingPresentationFactory elementEditingPresentationFactory,
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingPresentationFactory = elementEditingPresentationFactory;
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[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, UpdateElementRequestModel requestModel)
{
ElementUpdateModel model = _elementEditingPresentationFactory.MapUpdateModel(requestModel);
Attempt<ElementUpdateResult, ContentEditingOperationStatus> result =
await _elementEditingService.UpdateAsync(id, model, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}
@@ -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 ValidateCreateElementController : ElementControllerBase
{
private readonly IElementEditingPresentationFactory _elementEditingPresentationFactory;
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public ValidateCreateElementController(
IElementEditingPresentationFactory elementEditingPresentationFactory,
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingPresentationFactory = elementEditingPresentationFactory;
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost("validate")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Validate(CancellationToken cancellationToken, CreateElementRequestModel requestModel)
{
ElementCreateModel model = _elementEditingPresentationFactory.MapCreateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result =
await _elementEditingService.ValidateCreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}
@@ -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 ValidateUpdateElementController : ElementControllerBase
{
private readonly IElementEditingPresentationFactory _elementEditingPresentationFactory;
private readonly IElementEditingService _elementEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public ValidateUpdateElementController(
IElementEditingPresentationFactory elementEditingPresentationFactory,
IElementEditingService elementEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_elementEditingPresentationFactory = elementEditingPresentationFactory;
_elementEditingService = elementEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPut("{id:guid}/validate")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Validate(CancellationToken cancellationToken, Guid id, ValidateUpdateElementRequestModel requestModel)
{
ValidateElementUpdateModel model = _elementEditingPresentationFactory.MapValidateUpdateModel(requestModel);
Attempt<ContentValidationResult, ContentEditingOperationStatus> result =
await _elementEditingService.ValidateUpdateAsync(id, model, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}
@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Element;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class ElementBuilderExtensions
{
internal static IUmbracoBuilder AddElements(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IElementPresentationFactory, ElementPresentationFactory>();
builder.Services.AddTransient<IElementEditingPresentationFactory, ElementEditingPresentationFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
.Add<ElementMapDefinition>();
return builder;
}
}
@@ -36,6 +36,7 @@ public static partial class UmbracoBuilderExtensions
.AddConfigurationFactories()
.AddDocuments()
.AddDocumentTypes()
.AddElements()
.AddMedia()
.AddMediaTypes()
.AddMemberGroups()
@@ -0,0 +1,28 @@
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.Factories;
internal sealed class ElementEditingPresentationFactory : ContentEditingPresentationFactory<ElementValueModel, ElementVariantRequestModel>, IElementEditingPresentationFactory
{
public ElementCreateModel MapCreateModel(CreateElementRequestModel requestModel)
{
ElementCreateModel model = MapContentEditingModel<ElementCreateModel>(requestModel);
model.Key = requestModel.Id;
model.ContentTypeKey = requestModel.DocumentType.Id;
model.ParentKey = requestModel.Parent?.Id;
return model;
}
public ElementUpdateModel MapUpdateModel(UpdateElementRequestModel requestModel)
=> MapContentEditingModel<ElementUpdateModel>(requestModel);
public ValidateElementUpdateModel MapValidateUpdateModel(ValidateUpdateElementRequestModel requestModel)
{
ValidateElementUpdateModel model = MapContentEditingModel<ValidateElementUpdateModel>(requestModel);
model.Cultures = requestModel.Cultures;
return model;
}
}
@@ -0,0 +1,80 @@
using Umbraco.Cms.Api.Management.Mapping.Content;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Api.Management.ViewModels.Element.Item;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
// TODO ELEMENTS: lots of code here was duplicated from DocumentPresentationFactory - abstract and refactor
public class ElementPresentationFactory : IElementPresentationFactory
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IIdKeyMap _idKeyMap;
public ElementPresentationFactory(IUmbracoMapper umbracoMapper, IIdKeyMap idKeyMap)
{
_umbracoMapper = umbracoMapper;
_idKeyMap = idKeyMap;
}
public ElementResponseModel CreateResponseModel(IElement element, ContentScheduleCollection schedule)
{
ElementResponseModel responseModel = _umbracoMapper.Map<ElementResponseModel>(element)!;
_umbracoMapper.Map(schedule, responseModel);
return responseModel;
}
public ElementItemResponseModel CreateItemResponseModel(IElementEntitySlim entity)
{
Attempt<Guid> parentKeyAttempt = _idKeyMap.GetKeyForId(entity.ParentId, UmbracoObjectTypes.ElementContainer);
var responseModel = new ElementItemResponseModel
{
Id = entity.Key,
Parent = parentKeyAttempt.Success ? new ReferenceByIdModel { Id = parentKeyAttempt.Result } : null,
HasChildren = entity.HasChildren,
};
responseModel.DocumentType = _umbracoMapper.Map<DocumentTypeReferenceResponseModel>(entity)!;
responseModel.Variants = CreateVariantsItemResponseModels(entity);
return responseModel;
}
public IEnumerable<ElementVariantItemResponseModel> CreateVariantsItemResponseModels(IElementEntitySlim entity)
{
if (entity.Variations.VariesByCulture() is false)
{
var model = new ElementVariantItemResponseModel()
{
Name = entity.Name ?? string.Empty,
State = DocumentVariantStateHelper.GetState(entity, null),
Culture = null,
};
yield return model;
yield break;
}
foreach (KeyValuePair<string, string> cultureNamePair in entity.CultureNames)
{
var model = new ElementVariantItemResponseModel()
{
Name = cultureNamePair.Value,
Culture = cultureNamePair.Key,
State = DocumentVariantStateHelper.GetState(entity, cultureNamePair.Key)
};
yield return model;
}
}
}
@@ -0,0 +1,13 @@
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IElementEditingPresentationFactory
{
ElementCreateModel MapCreateModel(CreateElementRequestModel requestModel);
ElementUpdateModel MapUpdateModel(UpdateElementRequestModel requestModel);
ValidateElementUpdateModel MapValidateUpdateModel(ValidateUpdateElementRequestModel requestModel);
}
@@ -0,0 +1,15 @@
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Api.Management.ViewModels.Element.Item;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IElementPresentationFactory
{
ElementResponseModel CreateResponseModel(IElement element, ContentScheduleCollection schedule);
ElementItemResponseModel CreateItemResponseModel(IElementEntitySlim entity);
IEnumerable<ElementVariantItemResponseModel> CreateVariantsItemResponseModels(IElementEntitySlim entity);
}
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
@@ -108,4 +109,33 @@ public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantVi
}))
.ToArray();
}
protected void MapContentScheduleCollection<TContentResponseModel, TPublishableVariantResponseModelBase>(ContentScheduleCollection source, TContentResponseModel target, MapperContext context)
where TContentResponseModel : ContentResponseModelBase<TValueViewModel, TPublishableVariantResponseModelBase>
where TPublishableVariantResponseModelBase : PublishableVariantResponseModelBase, TVariantViewModel
{
foreach (ContentSchedule schedule in source.FullSchedule)
{
TPublishableVariantResponseModelBase? variant = target.Variants
.FirstOrDefault(v =>
v.Culture == schedule.Culture ||
(IsInvariant(v.Culture) && IsInvariant(schedule.Culture)));
if (variant is null)
{
continue;
}
switch (schedule.Action)
{
case ContentScheduleAction.Release:
variant.ScheduledPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero);
break;
case ContentScheduleAction.Expire:
variant.ScheduledUnpublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero);
break;
}
}
}
private static bool IsInvariant(string? culture) => culture.IsNullOrWhiteSpace() || culture == Core.Constants.System.InvariantCulture;
}
@@ -4,9 +4,10 @@ using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Api.Management.Mapping.Content;
// TODO ELEMENTS: rename this to VariantStateHelper or ContentVariantStateHelper (depending on the new name for DocumentVariantState)
internal static class DocumentVariantStateHelper
{
internal static DocumentVariantState GetState(IContent content, string? culture)
internal static DocumentVariantState GetState(IPublishableContentBase content, string? culture)
=> GetState(
content,
culture,
@@ -26,6 +27,16 @@ internal static class DocumentVariantStateHelper
content.EditedCultures,
content.PublishedCultures);
internal static DocumentVariantState GetState(IElementEntitySlim element, string? culture)
=> GetState(
element,
culture,
element.Edited,
element.Published,
element.CultureNames.Keys,
element.EditedCultures,
element.PublishedCultures);
private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, IEnumerable<string> availableCultures, IEnumerable<string> editedCultures, IEnumerable<string> publishedCultures)
{
if (entity.Id <= 0 || (culture is not null && availableCultures.Contains(culture) is false))
@@ -133,29 +133,5 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
}
private void Map(ContentScheduleCollection source, DocumentResponseModel target, MapperContext context)
{
foreach (ContentSchedule schedule in source.FullSchedule)
{
DocumentVariantResponseModel? variant = target.Variants
.FirstOrDefault(v =>
v.Culture == schedule.Culture ||
(IsInvariant(v.Culture) && IsInvariant(schedule.Culture)));
if (variant is null)
{
continue;
}
switch (schedule.Action)
{
case ContentScheduleAction.Release:
variant.ScheduledPublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero);
break;
case ContentScheduleAction.Expire:
variant.ScheduledUnpublishDate = new DateTimeOffset(schedule.Date, TimeSpan.Zero);
break;
}
}
}
private static bool IsInvariant(string? culture) => culture.IsNullOrWhiteSpace() || culture == Core.Constants.System.InvariantCulture;
=> MapContentScheduleCollection<DocumentResponseModel, DocumentVariantResponseModel>(source, target, context);
}
@@ -19,6 +19,7 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType,
mapper.Define<ISimpleContentType, DocumentTypeCollectionReferenceResponseModel>((_, _) => new DocumentTypeCollectionReferenceResponseModel(), Map);
mapper.Define<IContentEntitySlim, DocumentTypeReferenceResponseModel>((_, _) => new DocumentTypeReferenceResponseModel(), Map);
mapper.Define<IDocumentEntitySlim, DocumentTypeReferenceResponseModel>((_, _) => new DocumentTypeReferenceResponseModel(), Map);
mapper.Define<IElementEntitySlim, DocumentTypeReferenceResponseModel>((_, _) => new DocumentTypeReferenceResponseModel(), Map);
mapper.Define<IContent, DocumentTypeBlueprintItemResponseModel>((_, _) => new DocumentTypeBlueprintItemResponseModel(), Map);
}
@@ -0,0 +1,43 @@
using Umbraco.Cms.Api.Management.Mapping.Content;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.Element;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Api.Management.Mapping.Element;
public class ElementMapDefinition : ContentMapDefinition<IElement, ElementValueResponseModel, ElementVariantResponseModel>, IMapDefinition
{
public ElementMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
{
}
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<IElement, ElementResponseModel>((_, _) => new ElementResponseModel(), Map);
mapper.Define<ContentScheduleCollection, ElementResponseModel>(Map);
}
// Umbraco.Code.MapAll
private void Map(IElement source, ElementResponseModel target, MapperContext context)
{
target.Id = source.Key;
target.DocumentType = context.Map<DocumentTypeReferenceResponseModel>(source.ContentType)!;
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
{
documentVariantViewModel.State = DocumentVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
? source.PublishDate
: source.GetPublishDate(culture);
});
target.IsTrashed = source.Trashed;
}
private void Map(ContentScheduleCollection source, ElementResponseModel target, MapperContext context)
=> MapContentScheduleCollection<ElementResponseModel, ElementVariantResponseModel>(source, target, context);
}
@@ -0,0 +1,14 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class PublishableVariantResponseModelBase : VariantResponseModelBase
{
public DocumentVariantState State { get; set; }
public DateTimeOffset? PublishDate { get; set; }
public DateTimeOffset? ScheduledPublishDate { get; set; }
public DateTimeOffset? ScheduledUnpublishDate { get; set; }
}
@@ -2,16 +2,8 @@ using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentVariantResponseModel : VariantResponseModelBase, IHasFlags
public class DocumentVariantResponseModel : PublishableVariantResponseModelBase, IHasFlags
{
public DocumentVariantState State { get; set; }
public DateTimeOffset? PublishDate { get; set; }
public DateTimeOffset? ScheduledPublishDate { get; set; }
public DateTimeOffset? ScheduledUnpublishDate { get; set; }
private readonly List<FlagModel> _flags = [];
public Guid Id { get; }
@@ -3,6 +3,7 @@
/// <summary>
/// The saved state of a content item
/// </summary>
// TODO ELEMENTS: move this to ViewModels.Content and rename it to VariantState or ContentVariantState (shared between document and element variants)
public enum DocumentVariantState
{
/// <summary>
@@ -5,7 +5,7 @@ public class PublishDocumentRequestModel
public required IEnumerable<CultureAndScheduleRequestModel> PublishSchedules { get; set; }
}
// TODO ELEMENTS: move the following classes to ViewModels.Content
public class CultureAndScheduleRequestModel
{
/// <summary>
@@ -19,7 +19,6 @@ public class CultureAndScheduleRequestModel
public ScheduleRequestModel? Schedule { get; set; }
}
public class ScheduleRequestModel
{
public DateTimeOffset? PublishTime { get; set; }
@@ -0,0 +1,9 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class CopyElementRequestModel
{
public ReferenceByIdModel? Target { get; set; }
// TODO ELEMENTS: do we want a relate-to-original feature for elements?
// public bool RelateToOriginal { get; set; }
}
@@ -0,0 +1,8 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class CreateElementRequestModel : CreateContentWithParentRequestModelBase<ElementValueModel, ElementVariantRequestModel>
{
public required ReferenceByIdModel DocumentType { get; set; }
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ElementResponseModel : ElementResponseModelBase<ElementValueResponseModel, ElementVariantResponseModel>
{
public bool IsTrashed { get; set; }
}
@@ -0,0 +1,13 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public abstract class ElementResponseModelBase<TValueResponseModelBase, TVariantResponseModel>
: ContentResponseModelBase<TValueResponseModelBase, TVariantResponseModel>
where TValueResponseModelBase : ValueModelBase
where TVariantResponseModel : VariantResponseModelBase
{
public DocumentTypeReferenceResponseModel DocumentType { get; set; } = new();
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ElementValueModel : ValueModelBase
{
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ElementValueResponseModel : ValueResponseModelBase
{
}
@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Document;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ElementVariantItemResponseModel : VariantItemResponseModelBase
{
public required DocumentVariantState State { get; set; }
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ElementVariantRequestModel : VariantModelBase
{
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ElementVariantResponseModel : PublishableVariantResponseModelBase
{
}
@@ -0,0 +1,15 @@
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.Item;
namespace Umbraco.Cms.Api.Management.ViewModels.Element.Item;
public class ElementItemResponseModel : ItemResponseModelBase
{
public ReferenceByIdModel? Parent { get; set; }
public bool HasChildren { get; set; }
public DocumentTypeReferenceResponseModel DocumentType { get; set; } = new();
public IEnumerable<ElementVariantItemResponseModel> Variants { get; set; } = Enumerable.Empty<ElementVariantItemResponseModel>();
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class MoveElementRequestModel
{
public ReferenceByIdModel? Target { get; set; }
}
@@ -0,0 +1,8 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class PublishElementRequestModel
{
public required IEnumerable<CultureAndScheduleRequestModel> PublishSchedules { get; set; }
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class UnpublishElementRequestModel
{
public ISet<string>? Cultures { get; set; }
}
@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class UpdateElementRequestModel : UpdateContentRequestModelBase<ElementValueModel, ElementVariantRequestModel>
{
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Element;
public class ValidateUpdateElementRequestModel : UpdateElementRequestModel
{
public ISet<string>? Cultures { get; set; }
}
@@ -0,0 +1,11 @@
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Api.Management.ViewModels.Element;
namespace Umbraco.Cms.Api.Management.ViewModels.Tree;
public class ElementTreeItemResponseModel : FolderTreeItemResponseModel
{
public DocumentTypeReferenceResponseModel? DocumentType { get; set; }
public IEnumerable<ElementVariantItemResponseModel> Variants { get; set; } = [];
}
@@ -227,6 +227,18 @@ public static class DistributedCacheExtensions
#endregion
#region ElementCacheRefresher
public static void RefreshAllElementCache(this DistributedCache dc)
// note: refresh all element cache does refresh content types too
=> dc.RefreshByPayload(ElementCacheRefresher.UniqueId, new ElementCacheRefresher.JsonPayload(0, Guid.Empty, TreeChangeTypes.RefreshAll).Yield());
public static void RefreshElementCache(this DistributedCache dc, IEnumerable<TreeChange<IElement>> changes)
=> dc.RefreshByPayload(ElementCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new ElementCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)));
#endregion
#region Published Snapshot
public static void RefreshAllPublishedSnapshot(this DistributedCache dc)
@@ -234,6 +246,7 @@ public static class DistributedCacheExtensions
// note: refresh all content & media caches does refresh content types too
dc.RefreshAllContentCache();
dc.RefreshAllMediaCache();
dc.RefreshAllElementCache();
dc.RefreshAllDomainCache();
}
@@ -0,0 +1,28 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
/// <inheritdoc />
public class ElementTreeChangeDistributedCacheNotificationHandler : TreeChangeDistributedCacheNotificationHandlerBase<IElement, ElementTreeChangeNotification>
{
private readonly DistributedCache _distributedCache;
/// <summary>
/// Initializes a new instance of the <see cref="ElementTreeChangeDistributedCacheNotificationHandler" /> class.
/// </summary>
/// <param name="distributedCache">The distributed cache.</param>
public ElementTreeChangeDistributedCacheNotificationHandler(DistributedCache distributedCache)
=> _distributedCache = distributedCache;
/// <inheritdoc />
[Obsolete("Scheduled for removal in Umbraco 18.")]
protected override void Handle(IEnumerable<TreeChange<IElement>> entities)
=> Handle(entities, new Dictionary<string, object?>());
/// <inheritdoc />
protected override void Handle(IEnumerable<TreeChange<IElement>> entities, IDictionary<string, object?> state)
=> _distributedCache.RefreshElementCache(entities);
}
@@ -0,0 +1,137 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache;
public sealed class ElementCacheRefresher : PayloadCacheRefresherBase<ElementCacheRefresherNotification, ElementCacheRefresher.JsonPayload>
{
private readonly IIdKeyMap _idKeyMap;
private readonly IElementCacheService _elementCacheService;
private readonly ICacheManager _cacheManager;
public ElementCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory,
IElementCacheService elementCacheService,
ICacheManager cacheManager)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
_elementCacheService = elementCacheService;
// TODO: Use IElementsCache instead of ICacheManager, see ContentCacheRefresher for more information.
_cacheManager = cacheManager;
}
#region Json
public class JsonPayload
{
public JsonPayload(int id, Guid key, TreeChangeTypes changeTypes)
{
Id = id;
Key = key;
ChangeTypes = changeTypes;
}
public int Id { get; }
public Guid Key { get; }
public TreeChangeTypes ChangeTypes { get; }
// TODO ELEMENTS: should we support (un)published cultures in this payload? see ContentCacheRefresher.JsonPayload
}
#endregion
#region Define
public static readonly Guid UniqueId = Guid.Parse("EE5BB23A-A656-4F7E-A234-16F21AAABFD1");
public override Guid RefresherUniqueId => UniqueId;
public override string Name => "Element Cache Refresher";
#endregion
#region Refresher
public override void Refresh(JsonPayload[] payloads)
{
// TODO ELEMENTS: implement recycle bin
// AppCaches.RuntimeCache.ClearByKey(CacheKeys.ElementRecycleBinCacheKey);
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
_cacheManager.ElementsCache.Clear();
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate<IElement>();
foreach (JsonPayload payload in payloads)
{
// By INT Id
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IElement, int>(payload.Id));
// By GUID Key
isolatedCache.Clear(RepositoryCacheKeys.GetKey<IElement, Guid?>(payload.Key));
HandleMemoryCache(payload);
// TODO ELEMENTS: if we need published status caching for elements (e.g. for seeding purposes), make sure
// it is kept in sync here (see ContentCacheRefresher)
if (payload.ChangeTypes == TreeChangeTypes.Remove)
{
_idKeyMap.ClearCache(payload.Id);
}
}
AppCaches.ClearPartialViewCache();
base.Refresh(payloads);
}
private void HandleMemoryCache(JsonPayload payload)
{
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
{
_elementCacheService.ClearMemoryCacheAsync(CancellationToken.None).GetAwaiter().GetResult();
}
else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode) || payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
{
// NOTE: RefreshBranch might be triggered even though elements do not support branch publishing
_elementCacheService.RefreshMemoryCacheAsync(payload.Key).GetAwaiter().GetResult();
}
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
{
_elementCacheService.RemoveFromMemoryCacheAsync(payload.Key).GetAwaiter().GetResult();
}
}
// these events should never trigger
// everything should be JSON
public override void RefreshAll() => throw new NotSupportedException();
public override void Refresh(int id) => throw new NotSupportedException();
public override void Refresh(Guid id) => throw new NotSupportedException();
public override void Remove(int id) => throw new NotSupportedException();
#endregion
}
@@ -29,6 +29,10 @@ public static partial class Constants
public static readonly Guid DocumentType = new(Strings.DocumentType);
public static readonly Guid Element = new(Strings.Element);
public static readonly Guid ElementContainer = new(Strings.ElementContainer);
public static readonly Guid Media = new(Strings.Media);
public static readonly Guid MediaType = new(Strings.MediaType);
@@ -91,6 +95,10 @@ public static partial class Constants
public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB";
public const string Element = "3D7B623C-94B1-487D-8554-A46EC37568BE";
public const string ElementContainer = "2815B0CF-9706-499F-AA2A-8A4C7AEF005D";
public const string Media = "B796F64C-1F99-4FFB-B886-4BF4BC011A9C";
public const string MediaRecycleBin = "CF3D8E34-1C1C-41e9-AE56-878B57B32113";
@@ -234,6 +234,11 @@ public static partial class Constants
/// Configuration-less time.
/// </summary>
public const string PlainTime = "Umbraco.Plain.Time";
/// <summary>
/// Element Picker.
/// </summary>
public const string ElementPicker = "Umbraco.ElementPicker";
}
/// <summary>
@@ -28,6 +28,7 @@ public static partial class Constants
public const string DocumentType = "document-type";
public const string DocumentTypeContainer = "document-type-container";
public const string Element = "element";
public const string ElementContainer = "element-container";
public const string Media = "media";
public const string MediaType = "media-type";
public const string MediaTypeContainer = "media-type-container";
@@ -297,10 +297,15 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IContentPermissionService, ContentPermissionService>();
Services.AddUnique<IDictionaryPermissionService, DictionaryPermissionService>();
Services.AddUnique<IContentService, ContentService>();
Services.AddUnique<IElementService, ElementService>();
Services.AddUnique<IElementContainerService, ElementContainerService>();
Services.AddUnique<IContentBlueprintEditingService, ContentBlueprintEditingService>();
Services.AddUnique<IContentEditingService, ContentEditingService>();
Services.AddUnique<IElementEditingService, ElementEditingService>();
Services.AddUnique<IContentPublishingService, ContentPublishingService>();
Services.AddUnique<IElementPublishingService, ElementPublishingService>();
Services.AddUnique<IContentValidationService, ContentValidationService>();
Services.AddUnique<IElementValidationService, ElementValidationService>();
Services.AddUnique<IContentVersionCleanupPolicy, DefaultContentVersionCleanupPolicy>();
Services.AddUnique<IMemberService, MemberService>();
Services.AddUnique<IMemberValidationService, MemberValidationService>();
@@ -228,7 +228,7 @@ public static class ContentExtensions
/// <summary>
/// Gets the current status of the Content
/// </summary>
public static ContentStatus GetStatus(this IContent content, ContentScheduleCollection contentSchedule, string? culture = null)
public static ContentStatus GetStatus(this IPublishableContentBase content, ContentScheduleCollection contentSchedule, string? culture = null)
{
if (content.Trashed)
{
@@ -28,7 +28,7 @@ public static class PublishedContentExtensions
/// The specific culture to get the name for. If null is used the current culture is used (Default is
/// null).
/// </param>
public static string Name(this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null)
public static string Name(this IPublishedElement content, IVariationContextAccessor? variationContextAccessor, string? culture = null)
{
if (content == null)
{
@@ -76,6 +76,10 @@ public static class UdiGetterExtensions
{
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.Element)
{
entityType = Constants.UdiEntityType.ElementContainer;
}
else
{
throw new NotSupportedException($"Contained object type {entity.ContainedObjectType} is not supported.");
+1 -434
View File
@@ -1,6 +1,4 @@
using System.Collections.Specialized;
using System.Runtime.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models;
@@ -9,12 +7,8 @@ namespace Umbraco.Cms.Core.Models;
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public class Content : ContentBase, IContent
public class Content : PublishableContentBase, IContent
{
private HashSet<string>? _editedCultures;
private bool _published;
private PublishedState _publishedState;
private ContentCultureInfosCollection? _publishInfos;
private int? _templateId;
/// <summary>
@@ -55,13 +49,6 @@ public class Content : ContentBase, IContent
public Content(string name, IContent parent, IContentType contentType, PropertyCollection properties, string? culture = null)
: base(name, parent, contentType, properties, culture)
{
if (contentType == null)
{
throw new ArgumentNullException(nameof(contentType));
}
_publishedState = PublishedState.Unpublished;
PublishedVersionId = 0;
}
/// <summary>
@@ -102,13 +89,6 @@ public class Content : ContentBase, IContent
public Content(string? name, int parentId, IContentType? contentType, PropertyCollection properties, string? culture = null)
: base(name, parentId, contentType, properties, culture)
{
if (contentType == null)
{
throw new ArgumentNullException(nameof(contentType));
}
_publishedState = PublishedState.Unpublished;
PublishedVersionId = 0;
}
/// <summary>
@@ -126,278 +106,13 @@ public class Content : ContentBase, IContent
set => SetPropertyValueAndDetectChanges(value, ref _templateId, nameof(TemplateId));
}
/// <summary>
/// Gets or sets a value indicating whether this content item is published or not.
/// </summary>
/// <remarks>
/// the setter is should only be invoked from
/// - the ContentFactory when creating a content entity from a dto
/// - the ContentRepository when updating a content entity
/// </remarks>
[DataMember]
public bool Published
{
get => _published;
set
{
SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published));
_publishedState = _published ? PublishedState.Published : PublishedState.Unpublished;
}
}
/// <summary>
/// Gets the published state of the content item.
/// </summary>
/// <remarks>
/// The state should be Published or Unpublished, depending on whether Published
/// is true or false, but can also temporarily be Publishing or Unpublishing when the
/// content item is about to be saved.
/// </remarks>
[DataMember]
public PublishedState PublishedState
{
get => _publishedState;
set
{
if (value != PublishedState.Publishing && value != PublishedState.Unpublishing)
{
throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted.");
}
_publishedState = value;
}
}
[IgnoreDataMember]
public bool Edited { get; set; }
/// <inheritdoc />
[IgnoreDataMember]
public DateTime? PublishDate { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public int? PublisherId { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public int? PublishTemplateId { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public string? PublishName { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public IEnumerable<string>? EditedCultures
{
get => CultureInfos?.Keys.Where(IsCultureEdited);
set => _editedCultures = value == null ? null : new HashSet<string>(value, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
[IgnoreDataMember]
public IEnumerable<string> PublishedCultures => _publishInfos?.Keys ?? [];
/// <inheritdoc />
public bool IsCulturePublished(string culture)
// just check _publishInfos
// a non-available culture could not become published anyways
=> !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture);
/// <inheritdoc />
public bool IsCultureEdited(string culture)
=> IsCultureAvailable(culture) && // is available, and
(!IsCulturePublished(culture) || // is not published, or
(_editedCultures != null && _editedCultures.Contains(culture))); // is edited
/// <inheritdoc />
[IgnoreDataMember]
public ContentCultureInfosCollection? PublishCultureInfos
{
get
{
if (_publishInfos != null)
{
return _publishInfos;
}
_publishInfos = new ContentCultureInfosCollection();
_publishInfos.CollectionChanged += PublishNamesCollectionChanged;
return _publishInfos;
}
set
{
if (_publishInfos != null)
{
_publishInfos.ClearCollectionChangedEvents();
}
_publishInfos = value;
if (_publishInfos != null)
{
_publishInfos.CollectionChanged += PublishNamesCollectionChanged;
}
}
}
/// <inheritdoc />
public string? GetPublishName(string? culture)
{
if (culture.IsNullOrWhiteSpace())
{
return PublishName;
}
if (!ContentType.VariesByCulture())
{
return null;
}
if (_publishInfos == null)
{
return null;
}
return _publishInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null;
}
/// <inheritdoc />
public DateTime? GetPublishDate(string culture)
{
if (culture.IsNullOrWhiteSpace())
{
return PublishDate;
}
if (!ContentType.VariesByCulture())
{
return null;
}
if (_publishInfos == null)
{
return null;
}
return _publishInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null;
}
[IgnoreDataMember]
public int PublishedVersionId { get; set; }
[DataMember]
public bool Blueprint { get; set; }
public override void ResetWereDirtyProperties()
{
base.ResetWereDirtyProperties();
_previousPublishCultureChanges.updatedCultures = null;
_previousPublishCultureChanges.removedCultures = null;
_previousPublishCultureChanges.addedCultures = null;
}
public override void ResetDirtyProperties(bool rememberDirty)
{
base.ResetDirtyProperties(rememberDirty);
if (rememberDirty)
{
_previousPublishCultureChanges.addedCultures =
_currentPublishCultureChanges.addedCultures == null ||
_currentPublishCultureChanges.addedCultures.Count == 0
? null
: new HashSet<string>(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase);
_previousPublishCultureChanges.removedCultures =
_currentPublishCultureChanges.removedCultures == null ||
_currentPublishCultureChanges.removedCultures.Count == 0
? null
: new HashSet<string>(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase);
_previousPublishCultureChanges.updatedCultures =
_currentPublishCultureChanges.updatedCultures == null ||
_currentPublishCultureChanges.updatedCultures.Count == 0
? null
: new HashSet<string>(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase);
}
else
{
_previousPublishCultureChanges.addedCultures = null;
_previousPublishCultureChanges.removedCultures = null;
_previousPublishCultureChanges.updatedCultures = null;
}
_currentPublishCultureChanges.addedCultures?.Clear();
_currentPublishCultureChanges.removedCultures?.Clear();
_currentPublishCultureChanges.updatedCultures?.Clear();
// take care of the published state
_publishedState = _published ? PublishedState.Published : PublishedState.Unpublished;
if (_publishInfos == null)
{
return;
}
foreach (ContentCultureInfos infos in _publishInfos)
{
infos.ResetDirtyProperties(rememberDirty);
}
}
/// <inheritdoc />
/// <remarks>Overridden to check special keys.</remarks>
public override bool IsPropertyDirty(string propertyName)
{
// Special check here since we want to check if the request is for changed cultures
if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture);
return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture);
return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture);
return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false;
}
return base.IsPropertyDirty(propertyName);
}
/// <inheritdoc />
/// <remarks>Overridden to check special keys.</remarks>
public override bool WasPropertyDirty(string propertyName)
{
// Special check here since we want to check if the request is for changed cultures
if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture);
return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture);
return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture);
return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false;
}
return base.WasPropertyDirty(propertyName);
}
/// <summary>
/// Creates a deep clone of the current entity with its identity and it's property identities reset
/// </summary>
@@ -416,152 +131,4 @@ public class Content : ContentBase, IContent
return clone;
}
/// <summary>
/// Handles culture infos collection changes.
/// </summary>
private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(PublishCultureInfos));
// we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too
// which would allows us to continue doing WasCulturePublished, but don't think we need it anymore
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
ContentCultureInfos? cultureInfo = e.NewItems?.Cast<ContentCultureInfos>().First();
if (_currentPublishCultureChanges.addedCultures == null)
{
_currentPublishCultureChanges.addedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (_currentPublishCultureChanges.updatedCultures == null)
{
_currentPublishCultureChanges.updatedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (cultureInfo is not null)
{
_currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture);
_currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture);
_currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture);
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
// Remove listening for changes
ContentCultureInfos? cultureInfo = e.OldItems?.Cast<ContentCultureInfos>().First();
if (_currentPublishCultureChanges.removedCultures == null)
{
_currentPublishCultureChanges.removedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (cultureInfo is not null)
{
_currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture);
_currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture);
_currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture);
}
break;
}
case NotifyCollectionChangedAction.Replace:
{
// Replace occurs when an Update occurs
ContentCultureInfos? cultureInfo = e.NewItems?.Cast<ContentCultureInfos>().First();
if (_currentPublishCultureChanges.updatedCultures == null)
{
_currentPublishCultureChanges.updatedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (cultureInfo is not null)
{
_currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture);
}
break;
}
}
}
/// <summary>
/// Changes the <see cref="ContentType" /> for the current content object
/// </summary>
/// <param name="contentType">New ContentType for this content</param>
/// <remarks>Leaves PropertyTypes intact after change</remarks>
internal void ChangeContentType(IContentType contentType) => ChangeContentType(contentType, false);
/// <summary>
/// Changes the <see cref="ContentType" /> for the current content object and removes PropertyTypes,
/// which are not part of the new ContentType.
/// </summary>
/// <param name="contentType">New ContentType for this content</param>
/// <param name="clearProperties">Boolean indicating whether to clear PropertyTypes upon change</param>
internal void ChangeContentType(IContentType contentType, bool clearProperties)
{
ChangeContentType(new SimpleContentType(contentType));
if (clearProperties)
{
Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes);
}
else
{
Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes);
}
Properties.ClearCollectionChangedEvents(); // be sure not to double add
Properties.CollectionChanged += PropertiesChanged;
}
protected override void PerformDeepClone(object clone)
{
base.PerformDeepClone(clone);
var clonedContent = (Content)clone;
// TODO: need to reset change tracking bits
// if culture infos exist then deal with event bindings
if (clonedContent._publishInfos != null)
{
// Clear this event handler if any
clonedContent._publishInfos.ClearCollectionChangedEvents();
// Manually deep clone
clonedContent._publishInfos = (ContentCultureInfosCollection?)_publishInfos?.DeepClone();
if (clonedContent._publishInfos is not null)
{
// Re-assign correct event handler
clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged;
}
}
clonedContent._currentPublishCultureChanges.updatedCultures = null;
clonedContent._currentPublishCultureChanges.addedCultures = null;
clonedContent._currentPublishCultureChanges.removedCultures = null;
clonedContent._previousPublishCultureChanges.updatedCultures = null;
clonedContent._previousPublishCultureChanges.addedCultures = null;
clonedContent._previousPublishCultureChanges.removedCultures = null;
}
#region Used for change tracking
private (HashSet<string>? addedCultures, HashSet<string>? removedCultures, HashSet<string>? updatedCultures)
_currentPublishCultureChanges;
private (HashSet<string>? addedCultures, HashSet<string>? removedCultures, HashSet<string>? updatedCultures)
_previousPublishCultureChanges;
#endregion
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class ElementCreateModel : ContentCreationModelBase
{
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class ElementCreateResult : ContentCreateResultBase<IElement>
{
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class ElementUpdateModel : ContentEditingModelBase
{
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class ElementUpdateResult : ContentUpdateResultBase<IElement>
{
}
@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class ValidateElementUpdateModel : ElementUpdateModel
{
public ISet<string>? Cultures { get; set; }
}
@@ -2,7 +2,7 @@
public sealed class ContentPublishingResult
{
public IContent? Content { get; init; }
public IPublishableContentBase? Content { get; init; }
public IEnumerable<string> InvalidPropertyAliases { get; set; } = [];
}
@@ -67,7 +67,7 @@ public static class ContentRepositoryExtensions
/// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact
/// same time.
/// </remarks>
public static void AdjustDates(this IContent content, DateTime date, bool publishing)
public static void AdjustDates(this IPublishableContentBase content, DateTime date, bool publishing)
{
if (content.EditedCultures is not null)
{
@@ -129,7 +129,7 @@ public static class ContentRepositoryExtensions
/// Gets the cultures that have been flagged for unpublishing.
/// </summary>
/// <remarks>Gets cultures for which content.UnpublishCulture() has been invoked.</remarks>
public static IReadOnlyList<string>? GetCulturesUnpublishing(this IContent content)
public static IReadOnlyList<string>? GetCulturesUnpublishing(this IPublishableContentBase content)
{
if (!content.Published || !content.ContentType.VariesByCulture() ||
!content.IsPropertyDirty("PublishCultureInfos"))
@@ -147,7 +147,7 @@ public static class ContentRepositoryExtensions
/// <summary>
/// Copies values from another document.
/// </summary>
public static void CopyFrom(this IContent content, IContent other, string? culture = "*")
public static void CopyFrom(this IContent content, IPublishableContentBase other, string? culture = "*")
{
if (other.ContentTypeId != content.ContentTypeId)
{
@@ -243,7 +243,7 @@ public static class ContentRepositoryExtensions
}
}
public static void SetPublishInfo(this IContent content, string? culture, string? name, DateTime date)
public static void SetPublishInfo(this IPublishableContentBase content, string? culture, string? name, DateTime date)
{
if (name == null)
{
@@ -273,7 +273,7 @@ public static class ContentRepositoryExtensions
}
// sets the edited cultures on the content
public static void SetCultureEdited(this IContent content, IEnumerable<string?>? cultures)
public static void SetCultureEdited(this IPublishableContentBase content, IEnumerable<string?>? cultures)
{
if (cultures == null)
{
@@ -299,7 +299,7 @@ public static class ContentRepositoryExtensions
/// A value indicating whether it was possible to publish the names and values for the specified
/// culture(s). The method may fail if required names are not set, but it does NOT validate property data
/// </returns>
public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime, PropertyEditorCollection propertyEditorCollection)
public static bool PublishCulture(this IPublishableContentBase content, CultureImpact? impact, DateTime publishTime, PropertyEditorCollection propertyEditorCollection)
{
if (impact == null)
{
@@ -368,7 +368,7 @@ public static class ContentRepositoryExtensions
return true;
}
private static void PublishPropertyValues(IContent content, IProperty property, string? culture, PropertyEditorCollection propertyEditorCollection)
private static void PublishPropertyValues(IPublishableContentBase content, IProperty property, string? culture, PropertyEditorCollection propertyEditorCollection)
{
// if the content varies by culture, let data editor opt-in to perform partial property publishing (per culture)
if (content.ContentType.VariesByCulture()
@@ -390,7 +390,7 @@ public static class ContentRepositoryExtensions
/// <param name="content"></param>
/// <param name="culture"></param>
/// <returns></returns>
public static bool UnpublishCulture(this IContent content, string? culture = "*")
public static bool UnpublishCulture(this IPublishableContentBase content, string? culture = "*")
{
culture = culture?.NullOrWhiteSpaceAsNull();
@@ -428,7 +428,7 @@ public static class ContentRepositoryExtensions
return keepProcessing;
}
public static void ClearPublishInfos(this IContent content) => content.PublishCultureInfos = null;
public static void ClearPublishInfos(this IPublishableContentBase content) => content.PublishCultureInfos = null;
/// <summary>
/// Returns false if the culture is already unpublished
@@ -436,7 +436,7 @@ public static class ContentRepositoryExtensions
/// <param name="content"></param>
/// <param name="culture"></param>
/// <returns></returns>
public static bool ClearPublishInfo(this IContent content, string? culture)
public static bool ClearPublishInfo(this IPublishableContentBase content, string? culture)
{
if (culture == null)
{
+108
View File
@@ -0,0 +1,108 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models;
/// <summary>
/// Represents an Element object
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
public class Element : PublishableContentBase, IElement
{
/// <summary>
/// Constructor for creating an Element object
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="contentType">ContentType for the current Element object</param>
/// <param name="culture">An optional culture.</param>
public Element(string name, IContentType contentType, string? culture = null)
: this(name, contentType, new PropertyCollection(), culture)
{
}
/// <summary>
/// Constructor for creating an Element object
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="contentType">ContentType for the current Element object</param>
/// <param name="userId">The identifier of the user creating the Element object</param>
/// <param name="culture">An optional culture.</param>
public Element(string name, IContentType contentType, int userId, string? culture = null)
: this(name, contentType, new PropertyCollection(), culture)
{
CreatorId = userId;
WriterId = userId;
}
/// <summary>
/// Constructor for creating an Element object
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="contentType">ContentType for the current Element object</param>
/// <param name="properties">Collection of properties</param>
/// <param name="culture">An optional culture.</param>
public Element(string name, IContentType contentType, PropertyCollection properties, string? culture = null)
: base(name, Constants.System.Root, contentType, properties, culture)
{
}
/// <summary>
/// Constructor for creating an Element object
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="parentId">Id of the Parent folder</param>
/// <param name="contentType">ContentType for the current Element object</param>
/// <param name="culture">An optional culture.</param>
public Element(string? name, int parentId, IContentType? contentType, string? culture = null)
: this(name, parentId, contentType, new PropertyCollection(), culture)
{
}
/// <summary>
/// Constructor for creating an Element object
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="parentId">Id of the Parent folder</param>
/// <param name="contentType">ContentType for the current Element object</param>
/// <param name="userId">The identifier of the user creating the Element object</param>
/// <param name="culture">An optional culture.</param>
public Element(string name, int parentId, IContentType contentType, int userId, string? culture = null)
: this(name, parentId, contentType, new PropertyCollection(), culture)
{
CreatorId = userId;
WriterId = userId;
}
/// <summary>
/// Constructor for creating an Element object
/// </summary>
/// <param name="name">Name of the element</param>
/// <param name="parentId">Id of the Parent folder</param>
/// <param name="contentType">ContentType for the current Element object</param>
/// <param name="properties">Collection of properties</param>
/// <param name="culture">An optional culture.</param>
public Element(string? name, int parentId, IContentType? contentType, PropertyCollection properties, string? culture = null)
: base(name, parentId, contentType, properties, culture)
{
}
/// <summary>
/// Creates a deep clone of the current entity with its identity and it's property identities reset
/// </summary>
/// <returns></returns>
public IElement DeepCloneWithResetIdentities()
{
var clone = (Element)DeepClone();
clone.Key = Guid.Empty;
clone.VersionId = clone.PublishedVersionId = 0;
clone.ResetIdentity();
foreach (IProperty property in clone.Properties)
{
property.ResetIdentity();
}
return clone;
}
}
@@ -3,40 +3,6 @@ namespace Umbraco.Cms.Core.Models.Entities;
/// <summary>
/// Implements <see cref="IDocumentEntitySlim" />.
/// </summary>
public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim
public class DocumentEntitySlim : PublishableContentEntitySlim, IDocumentEntitySlim
{
private static readonly IReadOnlyDictionary<string, string> Empty = new Dictionary<string, string>();
private IReadOnlyDictionary<string, string>? _cultureNames;
private IEnumerable<string>? _editedCultures;
private IEnumerable<string>? _publishedCultures;
/// <inheritdoc />
public IReadOnlyDictionary<string, string> CultureNames
{
get => _cultureNames ?? Empty;
set => _cultureNames = value;
}
/// <inheritdoc />
public IEnumerable<string> PublishedCultures
{
get => _publishedCultures ?? Enumerable.Empty<string>();
set => _publishedCultures = value;
}
/// <inheritdoc />
public IEnumerable<string> EditedCultures
{
get => _editedCultures ?? Enumerable.Empty<string>();
set => _editedCultures = value;
}
public ContentVariation Variations { get; set; }
/// <inheritdoc />
public bool Published { get; set; }
/// <inheritdoc />
public bool Edited { get; set; }
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.Entities;
public class ElementEntitySlim : PublishableContentEntitySlim, IElementEntitySlim
{
}
@@ -3,35 +3,6 @@ namespace Umbraco.Cms.Core.Models.Entities;
/// <summary>
/// Represents a lightweight document entity, managed by the entity service.
/// </summary>
public interface IDocumentEntitySlim : IContentEntitySlim
public interface IDocumentEntitySlim : IPublishableContentEntitySlim
{
/// <summary>
/// Gets the variant name for each culture
/// </summary>
IReadOnlyDictionary<string, string> CultureNames { get; }
/// <summary>
/// Gets the published cultures.
/// </summary>
IEnumerable<string> PublishedCultures { get; }
/// <summary>
/// Gets the edited cultures.
/// </summary>
IEnumerable<string> EditedCultures { get; }
/// <summary>
/// Gets the content variation of the content type.
/// </summary>
ContentVariation Variations { get; }
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
bool Published { get; }
/// <summary>
/// Gets a value indicating whether the content has been edited.
/// </summary>
bool Edited { get; }
}
@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.Entities;
public interface IElementEntitySlim : IPublishableContentEntitySlim
{
}
@@ -0,0 +1,34 @@
namespace Umbraco.Cms.Core.Models.Entities;
public interface IPublishableContentEntitySlim : IContentEntitySlim
{
/// <summary>
/// Gets the variant name for each culture
/// </summary>
IReadOnlyDictionary<string, string> CultureNames { get; }
/// <summary>
/// Gets the published cultures.
/// </summary>
IEnumerable<string> PublishedCultures { get; }
/// <summary>
/// Gets the edited cultures.
/// </summary>
IEnumerable<string> EditedCultures { get; }
/// <summary>
/// Gets the content variation of the content type.
/// </summary>
ContentVariation Variations { get; }
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
bool Published { get; }
/// <summary>
/// Gets a value indicating whether the content has been edited.
/// </summary>
bool Edited { get; }
}
@@ -0,0 +1,39 @@
namespace Umbraco.Cms.Core.Models.Entities;
public abstract class PublishableContentEntitySlim : ContentEntitySlim
{
private static readonly IReadOnlyDictionary<string, string> Empty = new Dictionary<string, string>();
private IReadOnlyDictionary<string, string>? _cultureNames;
private IEnumerable<string>? _editedCultures;
private IEnumerable<string>? _publishedCultures;
/// <inheritdoc />
public IReadOnlyDictionary<string, string> CultureNames
{
get => _cultureNames ?? Empty;
set => _cultureNames = value;
}
/// <inheritdoc />
public IEnumerable<string> PublishedCultures
{
get => _publishedCultures ?? Enumerable.Empty<string>();
set => _publishedCultures = value;
}
/// <inheritdoc />
public IEnumerable<string> EditedCultures
{
get => _editedCultures ?? Enumerable.Empty<string>();
set => _editedCultures = value;
}
public ContentVariation Variations { get; set; }
/// <inheritdoc />
public bool Published { get; set; }
/// <inheritdoc />
public bool Edited { get; set; }
}
@@ -13,6 +13,7 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity
{ Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer },
{ Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer },
{ Constants.ObjectTypes.DocumentBlueprint, Constants.ObjectTypes.DocumentBlueprintContainer },
{ Constants.ObjectTypes.Element, Constants.ObjectTypes.ElementContainer },
};
/// <summary>
+1 -107
View File
@@ -6,35 +6,13 @@ namespace Umbraco.Cms.Core.Models;
/// <remarks>
/// <para>A document can be published, rendered by a template.</para>
/// </remarks>
public interface IContent : IContentBase
public interface IContent : IPublishableContentBase
{
/// <summary>
/// Gets or sets the template id used to render the content.
/// </summary>
int? TemplateId { get; set; }
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
/// <remarks>The <see cref="PublishedVersionId" /> property tells you which version of the content is currently published.</remarks>
bool Published { get; set; }
PublishedState PublishedState { get; set; }
/// <summary>
/// Gets a value indicating whether the content has been edited.
/// </summary>
/// <remarks>
/// Will return `true` once unpublished edits have been made after the version with
/// <see cref="PublishedVersionId" /> has been published.
/// </remarks>
bool Edited { get; set; }
/// <summary>
/// Gets the version identifier for the currently published version of the content.
/// </summary>
int PublishedVersionId { get; set; }
/// <summary>
/// Gets a value indicating whether the content item is a blueprint.
/// </summary>
@@ -46,90 +24,6 @@ public interface IContent : IContentBase
/// <remarks>When editing the content, the template can change, but this will not until the content is published.</remarks>
int? PublishTemplateId { get; set; }
/// <summary>
/// Gets the name of the published version of the content.
/// </summary>
/// <remarks>When editing the content, the name can change, but this will not until the content is published.</remarks>
string? PublishName { get; set; }
/// <summary>
/// Gets the identifier of the user who published the content.
/// </summary>
int? PublisherId { get; set; }
/// <summary>
/// Gets the date and time the content was published.
/// </summary>
DateTime? PublishDate { get; set; }
/// <summary>
/// Gets the published culture infos of the content.
/// </summary>
/// <remarks>
/// <para>
/// Because a dictionary key cannot be <c>null</c> this cannot get the invariant
/// name, which must be get via the <see cref="PublishName" /> property.
/// </para>
/// </remarks>
ContentCultureInfosCollection? PublishCultureInfos { get; set; }
/// <summary>
/// Gets the published cultures.
/// </summary>
IEnumerable<string> PublishedCultures { get; }
/// <summary>
/// Gets the edited cultures.
/// </summary>
IEnumerable<string>? EditedCultures { get; set; }
/// <summary>
/// Gets a value indicating whether a culture is published.
/// </summary>
/// <remarks>
/// <para>
/// A culture becomes published whenever values for this culture are published,
/// and the content published name for this culture is non-null. It becomes non-published
/// whenever values for this culture are unpublished.
/// </para>
/// <para>
/// A culture becomes published as soon as PublishCulture has been invoked,
/// even though the document might not have been saved yet (and can have no identity).
/// </para>
/// <para>Does not support the '*' wildcard (returns false).</para>
/// </remarks>
bool IsCulturePublished(string culture);
/// <summary>
/// Gets the date a culture was published.
/// </summary>
DateTime? GetPublishDate(string culture);
/// <summary>
/// Gets a value indicated whether a given culture is edited.
/// </summary>
/// <remarks>
/// <para>
/// A culture is edited when it is available, and not published or published but
/// with changes.
/// </para>
/// <para>A culture can be edited even though the document might now have been saved yet (and can have no identity).</para>
/// <para>Does not support the '*' wildcard (returns false).</para>
/// </remarks>
bool IsCultureEdited(string culture);
/// <summary>
/// Gets the name of the published version of the content for a given culture.
/// </summary>
/// <remarks>
/// <para>When editing the content, the name can change, but this will not until the content is published.</para>
/// <para>
/// When <paramref name="culture" /> is <c>null</c>, gets the invariant
/// language, which is the value of the <see cref="PublishName" /> property.
/// </para>
/// </remarks>
string? GetPublishName(string? culture);
/// <summary>
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
/// </summary>
+10
View File
@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Core.Models;
public interface IElement : IPublishableContentBase
{
/// <summary>
/// Creates a deep clone of the current entity with its identity/alias and it's property identities reset
/// </summary>
/// <returns></returns>
IElement DeepCloneWithResetIdentities();
}
@@ -0,0 +1,110 @@
namespace Umbraco.Cms.Core.Models;
public interface IPublishableContentBase : IContentBase
{
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
/// <remarks>The <see cref="PublishedVersionId" /> property tells you which version of the content is currently published.</remarks>
bool Published { get; set; }
PublishedState PublishedState { get; set; }
/// <summary>
/// Gets a value indicating whether the content has been edited.
/// </summary>
/// <remarks>
/// Will return `true` once unpublished edits have been made after the version with
/// <see cref="PublishedVersionId" /> has been published.
/// </remarks>
bool Edited { get; set; }
/// <summary>
/// Gets the version identifier for the currently published version of the content.
/// </summary>
int PublishedVersionId { get; set; }
/// <summary>
/// Gets the name of the published version of the content.
/// </summary>
/// <remarks>When editing the content, the name can change, but this will not until the content is published.</remarks>
string? PublishName { get; set; }
/// <summary>
/// Gets the identifier of the user who published the content.
/// </summary>
int? PublisherId { get; set; }
/// <summary>
/// Gets the date and time the content was published.
/// </summary>
DateTime? PublishDate { get; set; }
/// <summary>
/// Gets the published culture infos of the content.
/// </summary>
/// <remarks>
/// <para>
/// Because a dictionary key cannot be <c>null</c> this cannot get the invariant
/// name, which must be get via the <see cref="PublishName" /> property.
/// </para>
/// </remarks>
ContentCultureInfosCollection? PublishCultureInfos { get; set; }
/// <summary>
/// Gets the published cultures.
/// </summary>
IEnumerable<string> PublishedCultures { get; }
/// <summary>
/// Gets the edited cultures.
/// </summary>
IEnumerable<string>? EditedCultures { get; set; }
/// <summary>
/// Gets a value indicating whether a culture is published.
/// </summary>
/// <remarks>
/// <para>
/// A culture becomes published whenever values for this culture are published,
/// and the content published name for this culture is non-null. It becomes non-published
/// whenever values for this culture are unpublished.
/// </para>
/// <para>
/// A culture becomes published as soon as PublishCulture has been invoked,
/// even though the document might not have been saved yet (and can have no identity).
/// </para>
/// <para>Does not support the '*' wildcard (returns false).</para>
/// </remarks>
bool IsCulturePublished(string culture);
/// <summary>
/// Gets the date a culture was published.
/// </summary>
DateTime? GetPublishDate(string culture);
/// <summary>
/// Gets a value indicated whether a given culture is edited.
/// </summary>
/// <remarks>
/// <para>
/// A culture is edited when it is available, and not published or published but
/// with changes.
/// </para>
/// <para>A culture can be edited even though the document might now have been saved yet (and can have no identity).</para>
/// <para>Does not support the '*' wildcard (returns false).</para>
/// </remarks>
bool IsCultureEdited(string culture);
/// <summary>
/// Gets the name of the published version of the content for a given culture.
/// </summary>
/// <remarks>
/// <para>When editing the content, the name can change, but this will not until the content is published.</para>
/// <para>
/// When <paramref name="culture" /> is <c>null</c>, gets the invariant
/// language, which is the value of the <see cref="PublishName" /> property.
/// </para>
/// </remarks>
string? GetPublishName(string? culture);
}
@@ -0,0 +1,452 @@
namespace Umbraco.Cms.Core.Models;
using System.Collections.Specialized;
using System.Runtime.Serialization;
using Umbraco.Extensions;
// TODO ELEMENTS: ensure property annotations ect. are up to date from Content
public abstract class PublishableContentBase : ContentBase, IPublishableContentBase
{
private HashSet<string>? _editedCultures;
private bool _published;
private PublishedState _publishedState;
private ContentCultureInfosCollection? _publishInfos;
protected PublishableContentBase(string? name, int parentId, IContentTypeComposition? contentType, IPropertyCollection properties, string? culture = null)
: base(name, parentId, contentType, properties, culture)
{
if (contentType == null)
{
throw new ArgumentNullException(nameof(contentType));
}
_publishedState = PublishedState.Unpublished;
PublishedVersionId = 0;
}
protected PublishableContentBase(string? name, IContentBase? parent, IContentTypeComposition contentType, IPropertyCollection properties, string? culture = null)
: base(name, parent, contentType, properties, culture)
{
if (contentType == null)
{
throw new ArgumentNullException(nameof(contentType));
}
_publishedState = PublishedState.Unpublished;
PublishedVersionId = 0;
}
/// <summary>
/// Gets or sets a value indicating whether this content item is published or not.
/// </summary>
/// <remarks>
/// the setter is should only be invoked from
/// - the ContentFactory when creating a content entity from a dto
/// - the ContentRepository when updating a content entity
/// </remarks>
[DataMember]
public bool Published
{
get => _published;
set
{
SetPropertyValueAndDetectChanges(value, ref _published, nameof(Published));
_publishedState = _published ? PublishedState.Published : PublishedState.Unpublished;
}
}
/// <summary>
/// Gets the published state of the content item.
/// </summary>
/// <remarks>
/// The state should be Published or Unpublished, depending on whether Published
/// is true or false, but can also temporarily be Publishing or Unpublishing when the
/// content item is about to be saved.
/// </remarks>
[DataMember]
public PublishedState PublishedState
{
get => _publishedState;
set
{
if (value != PublishedState.Publishing && value != PublishedState.Unpublishing)
{
throw new ArgumentException("Invalid state, only Publishing and Unpublishing are accepted.");
}
_publishedState = value;
}
}
[IgnoreDataMember]
public bool Edited { get; set; }
/// <inheritdoc />
[IgnoreDataMember]
public DateTime? PublishDate { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public int? PublisherId { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public string? PublishName { get; set; } // set by persistence
/// <inheritdoc />
[IgnoreDataMember]
public IEnumerable<string>? EditedCultures
{
get => CultureInfos?.Keys.Where(IsCultureEdited);
set => _editedCultures = value == null ? null : new HashSet<string>(value, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
[IgnoreDataMember]
public IEnumerable<string> PublishedCultures => _publishInfos?.Keys ?? [];
/// <inheritdoc />
public bool IsCulturePublished(string culture)
// just check _publishInfos
// a non-available culture could not become published anyways
=> !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture);
/// <inheritdoc />
public bool IsCultureEdited(string culture)
=> IsCultureAvailable(culture) && // is available, and
(!IsCulturePublished(culture) || // is not published, or
(_editedCultures != null && _editedCultures.Contains(culture))); // is edited
/// <inheritdoc />
[IgnoreDataMember]
public ContentCultureInfosCollection? PublishCultureInfos
{
get
{
if (_publishInfos != null)
{
return _publishInfos;
}
_publishInfos = new ContentCultureInfosCollection();
_publishInfos.CollectionChanged += PublishNamesCollectionChanged;
return _publishInfos;
}
set
{
if (_publishInfos != null)
{
_publishInfos.ClearCollectionChangedEvents();
}
_publishInfos = value;
if (_publishInfos != null)
{
_publishInfos.CollectionChanged += PublishNamesCollectionChanged;
}
}
}
/// <inheritdoc />
public string? GetPublishName(string? culture)
{
if (culture.IsNullOrWhiteSpace())
{
return PublishName;
}
if (!ContentType.VariesByCulture())
{
return null;
}
if (_publishInfos == null)
{
return null;
}
return _publishInfos.TryGetValue(culture!, out ContentCultureInfos infos) ? infos.Name : null;
}
/// <inheritdoc />
public DateTime? GetPublishDate(string culture)
{
if (culture.IsNullOrWhiteSpace())
{
return PublishDate;
}
if (!ContentType.VariesByCulture())
{
return null;
}
if (_publishInfos == null)
{
return null;
}
return _publishInfos.TryGetValue(culture, out ContentCultureInfos infos) ? infos.Date : null;
}
[IgnoreDataMember]
public int PublishedVersionId { get; set; }
public override void ResetWereDirtyProperties()
{
base.ResetWereDirtyProperties();
_previousPublishCultureChanges.updatedCultures = null;
_previousPublishCultureChanges.removedCultures = null;
_previousPublishCultureChanges.addedCultures = null;
}
public override void ResetDirtyProperties(bool rememberDirty)
{
base.ResetDirtyProperties(rememberDirty);
if (rememberDirty)
{
_previousPublishCultureChanges.addedCultures =
_currentPublishCultureChanges.addedCultures == null ||
_currentPublishCultureChanges.addedCultures.Count == 0
? null
: new HashSet<string>(_currentPublishCultureChanges.addedCultures, StringComparer.InvariantCultureIgnoreCase);
_previousPublishCultureChanges.removedCultures =
_currentPublishCultureChanges.removedCultures == null ||
_currentPublishCultureChanges.removedCultures.Count == 0
? null
: new HashSet<string>(_currentPublishCultureChanges.removedCultures, StringComparer.InvariantCultureIgnoreCase);
_previousPublishCultureChanges.updatedCultures =
_currentPublishCultureChanges.updatedCultures == null ||
_currentPublishCultureChanges.updatedCultures.Count == 0
? null
: new HashSet<string>(_currentPublishCultureChanges.updatedCultures, StringComparer.InvariantCultureIgnoreCase);
}
else
{
_previousPublishCultureChanges.addedCultures = null;
_previousPublishCultureChanges.removedCultures = null;
_previousPublishCultureChanges.updatedCultures = null;
}
_currentPublishCultureChanges.addedCultures?.Clear();
_currentPublishCultureChanges.removedCultures?.Clear();
_currentPublishCultureChanges.updatedCultures?.Clear();
// take care of the published state
_publishedState = _published ? PublishedState.Published : PublishedState.Unpublished;
if (_publishInfos == null)
{
return;
}
foreach (ContentCultureInfos infos in _publishInfos)
{
infos.ResetDirtyProperties(rememberDirty);
}
}
/// <inheritdoc />
/// <remarks>Overridden to check special keys.</remarks>
public override bool IsPropertyDirty(string propertyName)
{
// Special check here since we want to check if the request is for changed cultures
if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture);
return _currentPublishCultureChanges.addedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture);
return _currentPublishCultureChanges.removedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture);
return _currentPublishCultureChanges.updatedCultures?.Contains(culture) ?? false;
}
return base.IsPropertyDirty(propertyName);
}
/// <inheritdoc />
/// <remarks>Overridden to check special keys.</remarks>
public override bool WasPropertyDirty(string propertyName)
{
// Special check here since we want to check if the request is for changed cultures
if (propertyName.StartsWith(ChangeTrackingPrefix.PublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.PublishedCulture);
return _previousPublishCultureChanges.addedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.UnpublishedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.UnpublishedCulture);
return _previousPublishCultureChanges.removedCultures?.Contains(culture) ?? false;
}
if (propertyName.StartsWith(ChangeTrackingPrefix.ChangedCulture))
{
var culture = propertyName.TrimStart(ChangeTrackingPrefix.ChangedCulture);
return _previousPublishCultureChanges.updatedCultures?.Contains(culture) ?? false;
}
return base.WasPropertyDirty(propertyName);
}
/// <summary>
/// Handles culture infos collection changes.
/// </summary>
private void PublishNamesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(PublishCultureInfos));
// we don't need to handle other actions, only add/remove, however we could implement Replace and track updated cultures in _updatedCultures too
// which would allows us to continue doing WasCulturePublished, but don't think we need it anymore
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
ContentCultureInfos? cultureInfo = e.NewItems?.Cast<ContentCultureInfos>().First();
if (_currentPublishCultureChanges.addedCultures == null)
{
_currentPublishCultureChanges.addedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (_currentPublishCultureChanges.updatedCultures == null)
{
_currentPublishCultureChanges.updatedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (cultureInfo is not null)
{
_currentPublishCultureChanges.addedCultures.Add(cultureInfo.Culture);
_currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture);
_currentPublishCultureChanges.removedCultures?.Remove(cultureInfo.Culture);
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
// Remove listening for changes
ContentCultureInfos? cultureInfo = e.OldItems?.Cast<ContentCultureInfos>().First();
if (_currentPublishCultureChanges.removedCultures == null)
{
_currentPublishCultureChanges.removedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (cultureInfo is not null)
{
_currentPublishCultureChanges.removedCultures.Add(cultureInfo.Culture);
_currentPublishCultureChanges.updatedCultures?.Remove(cultureInfo.Culture);
_currentPublishCultureChanges.addedCultures?.Remove(cultureInfo.Culture);
}
break;
}
case NotifyCollectionChangedAction.Replace:
{
// Replace occurs when an Update occurs
ContentCultureInfos? cultureInfo = e.NewItems?.Cast<ContentCultureInfos>().First();
if (_currentPublishCultureChanges.updatedCultures == null)
{
_currentPublishCultureChanges.updatedCultures =
new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
}
if (cultureInfo is not null)
{
_currentPublishCultureChanges.updatedCultures.Add(cultureInfo.Culture);
}
break;
}
}
}
/// <summary>
/// Changes the <see cref="ContentType" /> for the current content object
/// </summary>
/// <param name="contentType">New ContentType for this content</param>
/// <remarks>Leaves PropertyTypes intact after change</remarks>
internal void ChangeContentType(IContentType contentType) => ChangeContentType(contentType, false);
/// <summary>
/// Changes the <see cref="ContentType" /> for the current content object and removes PropertyTypes,
/// which are not part of the new ContentType.
/// </summary>
/// <param name="contentType">New ContentType for this content</param>
/// <param name="clearProperties">Boolean indicating whether to clear PropertyTypes upon change</param>
internal void ChangeContentType(IContentType contentType, bool clearProperties)
{
ChangeContentType(new SimpleContentType(contentType));
if (clearProperties)
{
Properties.EnsureCleanPropertyTypes(contentType.CompositionPropertyTypes);
}
else
{
Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes);
}
Properties.ClearCollectionChangedEvents(); // be sure not to double add
Properties.CollectionChanged += PropertiesChanged;
}
protected override void PerformDeepClone(object clone)
{
base.PerformDeepClone(clone);
var clonedContent = (PublishableContentBase)clone;
// TODO: need to reset change tracking bits
// if culture infos exist then deal with event bindings
if (clonedContent._publishInfos != null)
{
// Clear this event handler if any
clonedContent._publishInfos.ClearCollectionChangedEvents();
// Manually deep clone
clonedContent._publishInfos = (ContentCultureInfosCollection?)_publishInfos?.DeepClone();
if (clonedContent._publishInfos is not null)
{
// Re-assign correct event handler
clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged;
}
}
clonedContent._currentPublishCultureChanges.updatedCultures = null;
clonedContent._currentPublishCultureChanges.addedCultures = null;
clonedContent._currentPublishCultureChanges.removedCultures = null;
clonedContent._previousPublishCultureChanges.updatedCultures = null;
clonedContent._previousPublishCultureChanges.addedCultures = null;
clonedContent._previousPublishCultureChanges.removedCultures = null;
}
#region Used for change tracking
private (HashSet<string>? addedCultures, HashSet<string>? removedCultures, HashSet<string>? updatedCultures)
_currentPublishCultureChanges;
private (HashSet<string>? addedCultures, HashSet<string>? removedCultures, HashSet<string>? updatedCultures)
_previousPublishCultureChanges;
#endregion
}
@@ -9,31 +9,28 @@ namespace Umbraco.Cms.Core.Models.PublishedContent;
/// </remarks>
public interface IPublishedContent : IPublishedElement
{
// TODO: IPublishedContent properties colliding with models
// we need to find a way to remove as much clutter as possible from IPublishedContent,
// since this is preventing someone from creating a property named 'Path' and have it
// in a model, for instance. we could move them all under one unique property eg
// Infos, so we would do .Infos.SortOrder - just an idea - not going to do it in v8
/// <summary>
/// Gets the unique identifier of the content item.
/// </summary>
int Id { get; }
/// <summary>
/// Gets the name of the content item for the current culture.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the URL segment of the content item for the current culture.
/// </summary>
string? UrlSegment { get; }
/// <summary>
/// Gets the sort order of the content item.
/// Gets the identifier of the template to use to render the content item.
/// </summary>
int SortOrder { get; }
int? TemplateId { get; }
/// <summary>
/// Gets the parent of the content item.
/// </summary>
/// <remarks>The parent of root content is <c>null</c>.</remarks>
[Obsolete("Please use either the IPublishedContent.Parent<>() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in Umbraco 18.")]
IPublishedContent? Parent { get; }
/// <summary>
/// Gets the children of the content item that are available for the current culture.
/// </summary>
[Obsolete("Please use either the IPublishedContent.Children() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in Umbraco 18.")]
IEnumerable<IPublishedContent> Children { get; }
/// <summary>
/// Gets the tree level of the content item.
@@ -44,104 +41,4 @@ public interface IPublishedContent : IPublishedElement
/// Gets the tree path of the content item.
/// </summary>
string Path { get; }
/// <summary>
/// Gets the identifier of the template to use to render the content item.
/// </summary>
int? TemplateId { get; }
/// <summary>
/// Gets the identifier of the user who created the content item.
/// </summary>
int CreatorId { get; }
/// <summary>
/// Gets the date the content item was created.
/// </summary>
DateTime CreateDate { get; }
/// <summary>
/// Gets the identifier of the user who last updated the content item.
/// </summary>
int WriterId { get; }
/// <summary>
/// Gets the date the content item was last updated.
/// </summary>
/// <remarks>
/// <para>For published content items, this is also the date the item was published.</para>
/// <para>
/// This date is always global to the content item, see CultureDate() for the
/// date each culture was published.
/// </para>
/// </remarks>
DateTime UpdateDate { get; }
/// <summary>
/// Gets available culture infos.
/// </summary>
/// <remarks>
/// <para>
/// Contains only those culture that are available. For a published content, these are
/// the cultures that are published. For a draft content, those that are 'available' ie
/// have a non-empty content name.
/// </para>
/// <para>Does not contain the invariant culture.</para>
/// // TODO?
/// </remarks>
IReadOnlyDictionary<string, PublishedCultureInfo> Cultures { get; }
/// <summary>
/// Gets the type of the content item (document, media...).
/// </summary>
PublishedItemType ItemType { get; }
/// <summary>
/// Gets the parent of the content item.
/// </summary>
/// <remarks>The parent of root content is <c>null</c>.</remarks>
[Obsolete("Please use either the IPublishedContent.Parent<>() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in Umbraco 18.")]
IPublishedContent? Parent { get; }
/// <summary>
/// Gets a value indicating whether the content is draft.
/// </summary>
/// <remarks>
/// <para>
/// A content is draft when it is the unpublished version of a content, which may
/// have a published version, or not.
/// </para>
/// <para>
/// When retrieving documents from cache in non-preview mode, IsDraft is always false,
/// as only published documents are returned. When retrieving in preview mode, IsDraft can
/// either be true (document is not published, or has been edited, and what is returned
/// is the edited version) or false (document is published, and has not been edited, and
/// what is returned is the published version).
/// </para>
/// </remarks>
bool IsDraft(string? culture = null);
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
/// <remarks>
/// <para>A content is published when it has a published version.</para>
/// <para>
/// When retrieving documents from cache in non-preview mode, IsPublished is always
/// true, as only published documents are returned. When retrieving in draft mode, IsPublished
/// can either be true (document has a published version) or false (document has no
/// published version).
/// </para>
/// <para>
/// It is therefore possible for both IsDraft and IsPublished to be true at the same
/// time, meaning that the content is the draft version, and a published version exists.
/// </para>
/// </remarks>
bool IsPublished(string? culture = null);
/// <summary>
/// Gets the children of the content item that are available for the current culture.
/// </summary>
[Obsolete("Please use either the IPublishedContent.Children() extension method in the Umbraco.Extensions namespace, or IDocumentNavigationQueryService if you only need keys. Scheduled for removal in Umbraco 18.")]
IEnumerable<IPublishedContent> Children { get; }
}
@@ -47,4 +47,101 @@ public interface IPublishedElement
IPublishedProperty? GetProperty(string alias);
#endregion
/// <summary>
/// Gets the unique identifier of the content item.
/// </summary>
int Id { get; }
/// <summary>
/// Gets the name of the content item for the current culture.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the sort order of the content item.
/// </summary>
int SortOrder { get; }
/// <summary>
/// Gets the identifier of the user who created the content item.
/// </summary>
int CreatorId { get; }
/// <summary>
/// Gets the date the content item was created.
/// </summary>
DateTime CreateDate { get; }
/// <summary>
/// Gets the identifier of the user who last updated the content item.
/// </summary>
int WriterId { get; }
/// <summary>
/// Gets the date the content item was last updated.
/// </summary>
/// <remarks>
/// <para>For published content items, this is also the date the item was published.</para>
/// <para>
/// This date is always global to the content item, see CultureDate() for the
/// date each culture was published.
/// </para>
/// </remarks>
DateTime UpdateDate { get; }
/// <summary>
/// Gets available culture infos.
/// </summary>
/// <remarks>
/// <para>
/// Contains only those culture that are available. For a published content, these are
/// the cultures that are published. For a draft content, those that are 'available' ie
/// have a non-empty content name.
/// </para>
/// <para>Does not contain the invariant culture.</para>
/// // TODO?
/// </remarks>
IReadOnlyDictionary<string, PublishedCultureInfo> Cultures { get; }
/// <summary>
/// Gets the type of the content item (document, media...).
/// </summary>
PublishedItemType ItemType { get; }
/// <summary>
/// Gets a value indicating whether the content is draft.
/// </summary>
/// <remarks>
/// <para>
/// A content is draft when it is the unpublished version of a content, which may
/// have a published version, or not.
/// </para>
/// <para>
/// When retrieving documents from cache in non-preview mode, IsDraft is always false,
/// as only published documents are returned. When retrieving in preview mode, IsDraft can
/// either be true (document is not published, or has been edited, and what is returned
/// is the edited version) or false (document is published, and has not been edited, and
/// what is returned is the published version).
/// </para>
/// </remarks>
bool IsDraft(string? culture = null);
/// <summary>
/// Gets a value indicating whether the content is published.
/// </summary>
/// <remarks>
/// <para>A content is published when it has a published version.</para>
/// <para>
/// When retrieving documents from cache in non-preview mode, IsPublished is always
/// true, as only published documents are returned. When retrieving in draft mode, IsPublished
/// can either be true (document has a published version) or false (document has no
/// published version).
/// </para>
/// <para>
/// It is therefore possible for both IsDraft and IsPublished to be true at the same
/// time, meaning that the content is the draft version, and a published version exists.
/// </para>
/// </remarks>
bool IsPublished(string? culture = null);
}
@@ -0,0 +1,51 @@
using System.Diagnostics;
namespace Umbraco.Cms.Core.Models.PublishedContent;
/// <summary>
/// Provide an abstract base class for publishable content implementations (like <c>IPublishedContent</c> and <c>IPublishedElement</c> implementations).
/// </summary>
[DebuggerDisplay("Content Id: {Id}")]
public abstract class PublishableContentBase
{
public abstract IPublishedContentType ContentType { get; }
/// <inheritdoc />
public abstract Guid Key { get; }
/// <inheritdoc />
public abstract int Id { get; }
/// <inheritdoc />
public abstract int SortOrder { get; }
/// <inheritdoc />
public abstract int CreatorId { get; }
/// <inheritdoc />
public abstract DateTime CreateDate { get; }
/// <inheritdoc />
public abstract int WriterId { get; }
/// <inheritdoc />
public abstract DateTime UpdateDate { get; }
/// <inheritdoc />
public abstract IReadOnlyDictionary<string, PublishedCultureInfo> Cultures { get; }
/// <inheritdoc />
public abstract PublishedItemType ItemType { get; }
/// <inheritdoc />
public abstract bool IsDraft(string? culture = null);
/// <inheritdoc />
public abstract bool IsPublished(string? culture = null);
/// <inheritdoc cref="IPublishedElement.Properties"/>
public abstract IEnumerable<IPublishedProperty> Properties { get; }
/// <inheritdoc cref="IPublishedElement.GetProperty(string)"/>
public abstract IPublishedProperty? GetProperty(string alias);
}
@@ -1,7 +1,5 @@
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
@@ -12,21 +10,14 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// </summary>
/// <remarks>This base class does which (a) consistently resolves and caches the URL, (b) provides an implementation
/// for this[alias], and (c) provides basic content set management.</remarks>
[DebuggerDisplay("Content Id: {Id}")]
public abstract class PublishedContentBase : IPublishedContent
// TODO ELEMENTS: correct version for the obsolete message here
[Obsolete("Please implement PublishableContentBase instead. Scheduled for removal in VXX")]
public abstract class PublishedContentBase : PublishableContentBase, IPublishedContent
{
private readonly IVariationContextAccessor? _variationContextAccessor;
protected PublishedContentBase(IVariationContextAccessor? variationContextAccessor) => _variationContextAccessor = variationContextAccessor;
public abstract IPublishedContentType ContentType { get; }
/// <inheritdoc />
public abstract Guid Key { get; }
/// <inheritdoc />
public abstract int Id { get; }
/// <inheritdoc />
public virtual string Name => this.Name(_variationContextAccessor);
@@ -34,9 +25,6 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
[Obsolete("Please use GetUrlSegment() on IDocumentUrlService instead. Scheduled for removal in V16.")]
public virtual string? UrlSegment => this.UrlSegment(_variationContextAccessor);
/// <inheritdoc />
public abstract int SortOrder { get; }
/// <inheritdoc />
[Obsolete("Not supported for members, scheduled for removal in v17")]
public abstract int Level { get; }
@@ -48,30 +36,6 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// <inheritdoc />
public abstract int? TemplateId { get; }
/// <inheritdoc />
public abstract int CreatorId { get; }
/// <inheritdoc />
public abstract DateTime CreateDate { get; }
/// <inheritdoc />
public abstract int WriterId { get; }
/// <inheritdoc />
public abstract DateTime UpdateDate { get; }
/// <inheritdoc />
public abstract IReadOnlyDictionary<string, PublishedCultureInfo> Cultures { get; }
/// <inheritdoc />
public abstract PublishedItemType ItemType { get; }
/// <inheritdoc />
public abstract bool IsDraft(string? culture = null);
/// <inheritdoc />
public abstract bool IsPublished(string? culture = null);
/// <inheritdoc />
[Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")]
public abstract IPublishedContent? Parent { get; }
@@ -80,13 +44,6 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
[Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")]
public virtual IEnumerable<IPublishedContent> Children => GetChildren();
/// <inheritdoc cref="IPublishedElement.Properties"/>
public abstract IEnumerable<IPublishedProperty> Properties { get; }
/// <inheritdoc cref="IPublishedElement.GetProperty(string)"/>
public abstract IPublishedProperty? GetProperty(string alias);
private IEnumerable<IPublishedContent> GetChildren()
{
INavigationQueryService? navigationQueryService;
@@ -16,6 +16,23 @@ public static class PublishedContentExtensionsForModels
public static IPublishedContent? CreateModel(
this IPublishedContent? content,
IPublishedModelFactory? publishedModelFactory)
=> CreateModel<IPublishedContent>(content, publishedModelFactory);
/// <summary>
/// Creates a strongly typed published content model for an internal published element.
/// </summary>
/// <param name="element">The internal published element.</param>
/// <param name="publishedModelFactory">The published model factory</param>
/// <returns>The strongly typed published element model.</returns>
public static IPublishedElement? CreateModel(
this IPublishedElement? element,
IPublishedModelFactory? publishedModelFactory)
=> CreateModel<IPublishedElement>(element, publishedModelFactory);
private static T? CreateModel<T>(
IPublishedElement? content,
IPublishedModelFactory? publishedModelFactory)
where T : IPublishedElement
{
if (publishedModelFactory == null)
{
@@ -24,7 +41,7 @@ public static class PublishedContentExtensionsForModels
if (content == null)
{
return null;
return default;
}
// get model
@@ -36,10 +53,10 @@ public static class PublishedContentExtensionsForModels
}
// if factory returns a different type, throw
if (!(model is IPublishedContent publishedContent))
if (!(model is T publishedContent))
{
throw new InvalidOperationException(
$"Factory returned model of type {model.GetType().FullName} which does not implement IPublishedContent.");
$"Factory returned model of type {model.GetType().FullName} which does not implement {typeof(T).Name}.");
}
return publishedContent;
@@ -22,6 +22,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent;
/// wrap and extend another <c>IPublishedContent</c>.
/// </summary>
[DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")]
// TODO ELEMENTS: this should probably inherit PublishedElementWrapped, instead of all this code duplication
public abstract class PublishedContentWrapped : IPublishedContent
{
private readonly IPublishedContent _content;
@@ -33,6 +33,28 @@ public abstract class PublishedElementWrapped : IPublishedElement
/// <inheritdoc />
public IPublishedProperty? GetProperty(string alias) => _content.GetProperty(alias);
public int Id => _content.Id;
public string Name => _content.Name;
public int SortOrder => _content.SortOrder;
public int CreatorId => _content.CreatorId;
public DateTime CreateDate => _content.CreateDate;
public int WriterId => _content.WriterId;
public DateTime UpdateDate => _content.UpdateDate;
public IReadOnlyDictionary<string, PublishedCultureInfo> Cultures => _content.Cultures;
public PublishedItemType ItemType => _content.ItemType;
public bool IsDraft(string? culture = null) => _content.IsDraft(culture);
public bool IsPublished(string? culture = null) => _content.IsPublished(culture);
/// <summary>
/// Gets the wrapped content.
/// </summary>
@@ -180,4 +180,20 @@ public enum UmbracoObjectTypes
[UmbracoObjectType(Constants.ObjectTypes.Strings.IdReservation)]
[FriendlyName("Identifier Reservation")]
IdReservation,
/// <summary>
/// Element
/// </summary>
[UmbracoObjectType(Constants.ObjectTypes.Strings.Element, typeof(IElement))]
[FriendlyName("Element")]
[UmbracoUdiType(Constants.UdiEntityType.Element)]
Element,
/// <summary>
/// Element container.
/// </summary>
[UmbracoObjectType(Constants.ObjectTypes.Strings.ElementContainer)]
[FriendlyName("Element Container")]
[UmbracoUdiType(Constants.UdiEntityType.ElementContainer)]
ElementContainer,
}
@@ -0,0 +1,23 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the Element Cache Refresher.
/// </summary>
public class ElementCacheRefresherNotification : CacheRefresherNotification
{
/// <summary>
/// Initializes a new instance of the <see cref="ElementCacheRefresherNotification"/>
/// </summary>
/// <param name="messageObject">
/// The refresher payload.
/// </param>
/// <param name="messageType">
/// Type of the cache refresher message, <see cref="MessageType"/>
/// </param>
public ElementCacheRefresherNotification(object messageObject, MessageType messageType)
: base(messageObject, messageType)
{
}
}
@@ -0,0 +1,15 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// The notification is published after the element has been copied.
/// </summary>
public sealed class ElementCopiedNotification : CopiedNotification<IElement>
{
public ElementCopiedNotification(IElement original, IElement copy, int parentId, Guid? parentKey, bool relateToOriginal, EventMessages messages)
: base(original, copy, parentId, parentKey, relateToOriginal, messages)
{
}
}
@@ -0,0 +1,15 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// The notification is published after a copy object has been created and had its parentId updated.
/// </summary>
public sealed class ElementCopyingNotification : CopyingNotification<IElement>
{
public ElementCopyingNotification(IElement original, IElement copy, int parentId, Guid? parentKey, EventMessages messages)
: base(original, copy, parentId, parentKey, messages)
{
}
}
@@ -0,0 +1,15 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IElementService when the Delete and EmptyRecycleBin methods are called in the API.
/// </summary>
public sealed class ElementDeletedNotification : DeletedNotification<IElement>
{
public ElementDeletedNotification(IElement target, EventMessages messages)
: base(target, messages)
{
}
}
@@ -0,0 +1,38 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IElementService when the DeleteVersion and DeleteVersions methods are called in the API, and the version has been deleted.
/// </summary>
public sealed class ElementDeletedVersionsNotification : DeletedVersionsNotification<IElement>
{
/// <summary>
/// Initializes a new instance of the <see cref="ElementDeletedVersionsNotification"/>.
/// </summary>
/// <param name="id">
/// Gets the ID of the <see cref="IElement"/> object being deleted.
/// </param>
/// <param name="messages">
/// Initializes a new instance of the <see cref="EventMessages"/>.
/// </param>
/// <param name="specificVersion">
/// Gets the id of the IElement object version being deleted.
/// </param>
/// <param name="deletePriorVersions">
/// False by default.
/// </param>
/// <param name="dateToRetain">
/// Gets the latest version date.
/// </param>
public ElementDeletedVersionsNotification(
int id,
EventMessages messages,
int specificVersion = default,
bool deletePriorVersions = false,
DateTime dateToRetain = default)
: base(id, messages, specificVersion, deletePriorVersions, dateToRetain)
{
}
}
@@ -0,0 +1,20 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IElementService when the DeleteElementOfType, Delete and EmptyRecycleBin methods are called in the API.
/// </summary>
public sealed class ElementDeletingNotification : DeletingNotification<IElement>
{
public ElementDeletingNotification(IElement target, EventMessages messages)
: base(target, messages)
{
}
public ElementDeletingNotification(IEnumerable<IElement> target, EventMessages messages)
: base(target, messages)
{
}
}
@@ -0,0 +1,33 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Notifications;
/// <summary>
/// A notification that is used to trigger the IElementService when the DeleteVersion and DeleteVersions methods are called in the API.
/// </summary>
public sealed class ElementDeletingVersionsNotification : DeletingVersionsNotification<IElement>
{
/// <summary>
/// Initializes a new instance of the <see cref="ElementDeletingVersionsNotification"/>.
/// </summary>
/// <param name="id">
/// Gets the ID of the <see cref="IElement"/> object being deleted.
/// </param>
/// <param name="messages">
/// Initializes a new instance of the <see cref="EventMessages"/>.
/// </param>
/// <param name="specificVersion">
/// Gets the id of the IElement object version being deleted.
/// </param>
/// <param name="deletePriorVersions">
/// False by default.
/// </param>
/// <param name="dateToRetain">
/// Gets the latest version date.
/// </param>
public ElementDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default)
: base(id, messages, specificVersion, deletePriorVersions, dateToRetain)
{
}
}

Some files were not shown because too many files have changed in this diff Show More