Member types: Implement containers (#20706)

* Add MemberType/MemberTypeContainer to supported EntityContainer object types

* Implement MemberTypeContainerRepository

* Update and add member type container API endpoints

* Complete server and client-side implementation for member type container support.

* Fix FE linting errors.

* Export folder constants.

* Applied suggestions from code review.

* Updated management API authorization tests for member types.

* Resolved breaking change on copy member type controller.

* Allow content types to be moved to own folder without error.

* Use flag providers for member type siblings endpoint.

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Ronald Barendse
2025-11-20 11:28:03 +01:00
committed by GitHub
parent f70f6d4aba
commit 6a7360aded
107 changed files with 2897 additions and 210 deletions

View File

@@ -430,6 +430,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<ITemporaryFileToXmlImportService, TemporaryFileToXmlImportService>();
Services.AddUnique<IContentTypeImportService, ContentTypeImportService>();
Services.AddUnique<IMediaTypeImportService, MediaTypeImportService>();
Services.AddUnique<IMemberTypeImportService, MemberTypeImportService>();
// add validation services
Services.AddUnique<IElementSwitchValidator, ElementSwitchValidator>();

View File

@@ -1,6 +1,8 @@
namespace Umbraco.Cms.Core.Models.ContentTypeEditing;
namespace Umbraco.Cms.Core.Models.ContentTypeEditing;
public class MemberTypeCreateModel : MemberTypeModelBase
{
public Guid? Key { get; set; }
public Guid? ContainerKey { get; set; }
}

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.Services.OperationStatus;

View File

@@ -29,7 +29,7 @@ internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<I
public async Task<Attempt<IMemberType?, ContentTypeOperationStatus>> CreateAsync(MemberTypeCreateModel model, Guid userKey)
{
Attempt<IMemberType?, ContentTypeOperationStatus> result = await ValidateAndMapForCreationAsync(model, model.Key, containerKey: null);
Attempt<IMemberType?, ContentTypeOperationStatus> result = await ValidateAndMapForCreationAsync(model, model.Key, model.ContainerKey);
if (result.Success is false)
{
return result;
@@ -80,7 +80,7 @@ internal sealed class MemberTypeEditingService : ContentTypeEditingServiceBase<I
protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.MemberType;
protected override UmbracoObjectTypes ContainerObjectType => throw new NotSupportedException("Member type tree does not support containers");
protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.MemberTypeContainer;
protected override ISet<string> GetReservedFieldNames() => _reservedFieldNamesService.GetMemberReservedFieldNames();

View File

@@ -1053,9 +1053,9 @@ public abstract class ContentTypeServiceBase<TRepository, TItem> : ContentTypeSe
public Attempt<OperationResult<MoveOperationStatusType>?> Move(TItem moving, int containerId)
{
EventMessages eventMessages = EventMessagesFactory.Get();
if(moving.ParentId == containerId)
if (moving.ParentId == containerId)
{
return OperationResult.Attempt.Fail(MoveOperationStatusType.FailedNotAllowedByPath, eventMessages);
return OperationResult.Attempt.Succeed(MoveOperationStatusType.Success, eventMessages);
}
var moveInfo = new List<MoveEventInfo<TItem>>();

View File

@@ -374,7 +374,11 @@ internal sealed class EntityXmlSerializer : IEntityXmlSerializer
return xml;
}
public XElement Serialize(IMediaType mediaType)
public XElement Serialize(IMediaType mediaType) => SerializeMediaOrMemberType(mediaType, IEntityXmlSerializer.MediaTypeElementName);
public XElement Serialize(IMemberType memberType) => SerializeMediaOrMemberType(memberType, IEntityXmlSerializer.MemberTypeElementName);
private XElement SerializeMediaOrMemberType(IContentTypeComposition mediaType, string elementName)
{
var info = new XElement(
"Info",
@@ -410,7 +414,7 @@ internal sealed class EntityXmlSerializer : IEntityXmlSerializer
SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
var xml = new XElement(
IEntityXmlSerializer.MediaTypeElementName,
elementName,
info,
structure,
genericProperties,

View File

@@ -10,6 +10,7 @@ public interface IEntityXmlSerializer
{
internal const string DocumentTypeElementName = "DocumentType";
internal const string MediaTypeElementName = "MediaType";
internal const string MemberTypeElementName = "MemberType";
/// <summary>
/// Exports an IContent item as an XElement.
@@ -80,5 +81,7 @@ public interface IEntityXmlSerializer
XElement Serialize(IMediaType mediaType);
XElement Serialize(IMemberType memberType) => throw new NotImplementedException();
XElement Serialize(IContentType contentType);
}

View File

@@ -10,13 +10,21 @@ public interface IPackageDataInstallation
InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId);
/// <summary>
/// Imports and saves package xml as <see cref="IContentType"/>
/// Imports and saves package xml as <see cref="IMediaType"/>.
/// </summary>
/// <param name="docTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation. Default is zero (admin).</param>
/// <returns>An enumerable list of generated ContentTypes</returns>
/// <returns>An enumerable list of generated <see cref="IMediaType"/>s.</returns>
IReadOnlyList<IMediaType> ImportMediaTypes(IEnumerable<XElement> docTypeElements, int userId);
/// <summary>
/// Imports and saves package xml as <see cref="IMemberType"/>.
/// </summary>
/// <param name="docTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation. Default is zero (admin).</param>
/// <returns>An enumerable list of generated <see cref="IMemberType"/>s.</returns>
IReadOnlyList<IMemberType> ImportMemberTypes(IEnumerable<XElement> docTypeElements, int userId) => throw new NotImplementedException();
IReadOnlyList<TContentBase> ImportContentBase<TContentBase, TContentTypeComposition>(
IEnumerable<CompiledPackageContentBase> docs,
IDictionary<string, TContentTypeComposition> importedDocumentTypes,

View File

@@ -0,0 +1,12 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services.ImportExport;
public interface IMemberTypeImportService
{
Task<Attempt<IMemberType?, MemberTypeImportOperationStatus>> Import(
Guid temporaryFileId,
Guid userKey,
Guid? mediaTypeId = null);
}

View File

@@ -0,0 +1,88 @@
using System.Xml.Linq;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services.ImportExport;
public class MemberTypeImportService : IMemberTypeImportService
{
private readonly IPackageDataInstallation _packageDataInstallation;
private readonly IEntityService _entityService;
private readonly ITemporaryFileToXmlImportService _temporaryFileToXmlImportService;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly IUserIdKeyResolver _userIdKeyResolver;
public MemberTypeImportService(
IPackageDataInstallation packageDataInstallation,
IEntityService entityService,
ITemporaryFileToXmlImportService temporaryFileToXmlImportService,
ICoreScopeProvider coreScopeProvider,
IUserIdKeyResolver userIdKeyResolver)
{
_packageDataInstallation = packageDataInstallation;
_entityService = entityService;
_temporaryFileToXmlImportService = temporaryFileToXmlImportService;
_coreScopeProvider = coreScopeProvider;
_userIdKeyResolver = userIdKeyResolver;
}
public async Task<Attempt<IMemberType?, MemberTypeImportOperationStatus>> Import(
Guid temporaryFileId,
Guid userKey,
Guid? memberTypeId = null)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
_temporaryFileToXmlImportService.CleanupFileIfScopeCompletes(temporaryFileId);
Attempt<XElement?, TemporaryFileXmlImportOperationStatus> loadXmlAttempt =
await _temporaryFileToXmlImportService.LoadXElementFromTemporaryFileAsync(temporaryFileId);
if (loadXmlAttempt.Success is false)
{
return Attempt.FailWithStatus<IMemberType?, MemberTypeImportOperationStatus>(
loadXmlAttempt.Status is TemporaryFileXmlImportOperationStatus.TemporaryFileNotFound
? MemberTypeImportOperationStatus.TemporaryFileNotFound
: MemberTypeImportOperationStatus.TemporaryFileConversionFailure,
null);
}
Attempt<UmbracoEntityTypes> packageEntityTypeAttempt = _temporaryFileToXmlImportService.GetEntityType(loadXmlAttempt.Result!);
if (packageEntityTypeAttempt.Success is false ||
packageEntityTypeAttempt.Result is not UmbracoEntityTypes.MemberType)
{
return Attempt.FailWithStatus<IMemberType?, MemberTypeImportOperationStatus>(
MemberTypeImportOperationStatus.TypeMismatch,
null);
}
Guid packageEntityKey = _packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!);
if (memberTypeId is not null && memberTypeId.Equals(packageEntityKey) is false)
{
return Attempt.FailWithStatus<IMemberType?, MemberTypeImportOperationStatus>(
MemberTypeImportOperationStatus.IdMismatch,
null);
}
var entityExits = _entityService.Exists(
_packageDataInstallation.GetContentTypeKey(loadXmlAttempt.Result!),
UmbracoObjectTypes.MemberType);
if (entityExits && memberTypeId is null)
{
return Attempt.FailWithStatus<IMemberType?, MemberTypeImportOperationStatus>(
MemberTypeImportOperationStatus.MemberTypeExists,
null);
}
IReadOnlyList<IMemberType> importResult =
_packageDataInstallation.ImportMemberTypes(new[] { loadXmlAttempt.Result! }, await _userIdKeyResolver.GetAsync(userKey));
scope.Complete();
return Attempt.SucceedWithStatus<IMemberType?, MemberTypeImportOperationStatus>(
entityExits
? MemberTypeImportOperationStatus.SuccessUpdated
: MemberTypeImportOperationStatus.SuccessCreated,
importResult[0]);
}
}

View File

@@ -61,6 +61,8 @@ public class TemporaryFileToXmlImportService : ITemporaryFileToXmlImportService
=> Attempt<UmbracoEntityTypes>.Succeed(UmbracoEntityTypes.DocumentType),
IEntityXmlSerializer.MediaTypeElementName
=> Attempt<UmbracoEntityTypes>.Succeed(UmbracoEntityTypes.MediaType),
IEntityXmlSerializer.MemberTypeElementName
=> Attempt<UmbracoEntityTypes>.Succeed(UmbracoEntityTypes.MemberType),
_ => Attempt<UmbracoEntityTypes>.Fail()
};
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Core.Services.OperationStatus;
public enum MemberTypeImportOperationStatus
{
SuccessCreated,
SuccessUpdated,
TemporaryFileNotFound,
TemporaryFileConversionFailure,
MemberTypeExists,
TypeMismatch,
IdMismatch
}