From 0096addcb9369aec4d9dbc4369da051f2229d976 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 17 Aug 2023 12:28:16 +0200 Subject: [PATCH] Content and media type CRUD controllers and services (#14665) * Add GetAsync method * Fix up delete document type controller * Add scope to delete async * Add some scaffolding * Add create model * Start working on validation * Move validation to its own service * Use GetAllAsync instead of GetAsync * Add initial composition support Still need to figure out some kinks * Validate compositions when creating * Add initial folder support * Initial handling of generic properties * Add operation status responses * Move create operation into service * Add first test * Fix issued shown by test * Ensure a specific key can be specified when creating * Rename container id to container key Let's try and be consistent * Create basic composition test * Ensure new property groups are created with the correct key * Add test showing property type issue * Fix property types not using the expected key. * Validate against model fetched from content type service Just to make sure nothing explodes on the round trip * Make helper for creating create models * Add helper for creating container * Make helper methods simpler to use * Add test for compositions using compositions * Add more composition tests * Fix bug allowing element types to be composed by non element types * Remove validators This can just be a part of the editing service * Minor cleanup * Ensure that multiple levels of inheritance is possible * Ensure doctype cannot be used as both composition and inheritance on the same doctype * Ensure no duplicate aliases from composition and that compositions exists * Minor cleanup * Address todos * Add SaveAsync method * Renamed some models * Rename from DocumentType to ContentType * Clarify ParentKey as being container only + untangle things a tiny bit * Clean out another TODO (less duplicate code) + more tests * Refactor for reuse across different content types + add media type editing service + unit tests * Refactor in preparation for update handling * More tests + fixed bugs found while testing * Simplify things a bit * Content type update + a lot of unit tests + some refactor + fix bugs found while testing * Begin building presentation factories for mapping view models to editing models * Use async save * Mapping factories and some clean-up * Rename Key to Id (ParentKey to ParentId) * Fix slight typo * Use editing service in document type controllers and introduce media type controllers * Validate containers and align container aliases with the current backoffice * Remove ParentId from response * Fix scope handling in DeleteAsync * Refactor ContentTypeSort * A little renaming for clarity + safeguard against changes to inheritance * Persist allowed content types * Fix bad merge + update controller response annotations * Update OpenAPI JSON * Update src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs Co-authored-by: Mole * Fix review comments * Update usage of MapCreateAsync to ValidateAndMapForCreationAsync --------- Co-authored-by: Nikolaj --- .../ByKeyDocumentTypeController.cs | 10 +- .../CreateDocumentTypeController.cs | 45 +- .../CreateUpdateDocumentTypeControllerBase.cs | 222 ---- .../DeleteDocumentTypeController.cs | 26 +- .../DocumentTypeControllerBase.cs | 63 +- .../UpdateDocumentTypeController.cs | 43 +- .../MediaType/ByKeyMediaTypeController.cs | 6 +- .../MediaType/CreateMediaTypeController.cs | 46 + .../MediaType/DeleteMediaTypeController.cs | 32 + .../MediaType/MediaTypeControllerBase.cs | 10 +- .../MediaType/UpdateMediaTypeController.cs | 56 + .../DocumentTypeBuilderExtensions.cs | 6 +- .../MediaTypeBuilderExtensions.cs | 6 +- .../ContentTypeEditingPresentationFactory.cs | 113 ++ .../DocumentTypeEditingPresentationFactory.cs | 60 + ...IDocumentTypeEditingPresentationFactory.cs | 11 + .../IMediaTypeEditingPresentationFactory.cs | 11 + .../MediaTypeEditingPresentationFactory.cs | 38 + .../DocumentType/DocumentTypeMapDefinition.cs | 3 +- .../MediaType/MediaTypeMapDefinition.cs | 3 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 545 ++++++++- .../CreateContentTypeRequestModelBase.cs | 3 + .../CreateDocumentTypeRequestModel.cs | 2 +- .../DocumentType/IDocumentTypeRequestModel.cs | 12 - .../UpdateDocumentTypeRequestModel.cs | 2 +- ...iaTypePropertyTypeContainerRequestModel.cs | 7 + ...CreateMediaTypePropertyTypeRequestModel.cs | 7 + .../MediaType/CreateMediaTypeRequestModel.cs | 8 + ...iaTypePropertyTypeContainerRequestModel.cs | 7 + ...UpdateMediaTypePropertyTypeRequestModel.cs | 7 + .../MediaType/UpdateMediaTypeRequestModel.cs | 8 + .../DependencyInjection/UmbracoBuilder.cs | 3 + src/Umbraco.Core/Extensions/TypeExtensions.cs | 159 ++- .../Models/ContentTypeCompositionBase.cs | 21 + .../Models/ContentTypeEditing/Composition.cs | 8 + .../ContentTypeEditing/CompositionType.cs | 7 + .../ContentTypeEditing/ContentTypeCleanup.cs | 10 + .../ContentTypeCreateModel.cs | 8 + .../ContentTypeEditingModelBase.cs | 40 + .../ContentTypeModelBase.cs | 10 + .../ContentTypePropertyContainerModel.cs | 5 + .../ContentTypePropertyTypeModel.cs | 5 + .../ContentTypeUpdateModel.cs | 5 + .../MediaTypeCreateModel.cs | 8 + .../ContentTypeEditing/MediaTypeModelBase.cs | 5 + .../MediaTypePropertyContainerModel.cs | 5 + .../MediaTypePropertyTypeModel.cs | 5 + .../MediaTypeUpdateModel.cs | 5 + .../PropertyTypeAppearance.cs | 6 + .../PropertyTypeContainerModelBase.cs | 15 + .../PropertyTypeModelBase.cs | 26 + .../PropertyTypeValidation.cs | 12 + src/Umbraco.Core/Models/ContentTypeSort.cs | 22 +- .../Models/IContentTypeComposition.cs | 13 + .../Mapping/ContentTypeMapDefinition.cs | 25 +- .../ContentTypeEditingService.cs | 107 ++ .../ContentTypeEditingServiceBase.cs | 665 +++++++++++ .../IContentTypeEditingService.cs | 12 + .../IMediaTypeEditingService.cs | 12 + .../MediaTypeEditingService.cs | 56 + .../Services/ContentTypeService.cs | 40 +- ...peServiceBaseOfTRepositoryTItemTService.cs | 63 +- src/Umbraco.Core/Services/DataTypeService.cs | 16 + .../Services/IContentTypeServiceBase.cs | 22 + src/Umbraco.Core/Services/IDataTypeService.cs | 7 + src/Umbraco.Core/Services/MediaTypeService.cs | 40 +- .../Services/MemberTypeService.cs | 14 +- .../ContentTypeOperationStatus.cs | 11 +- .../Models/Mapping/EntityMapDefinition.cs | 1 - .../Packaging/PackageDataInstallation.cs | 4 +- .../Implement/ContentTypeCommonRepository.cs | 1 - .../Implement/ContentTypeRepositoryBase.cs | 35 +- .../Controllers/ContentController.cs | 4 +- .../Controllers/ContentTypeController.cs | 12 +- .../Controllers/ContentTypeControllerBase.cs | 4 +- .../Controllers/MediaController.cs | 4 +- .../Controllers/MediaTypeController.cs | 6 +- tests/Umbraco.TestData/LoadTestController.cs | 2 +- .../UmbracoTestDataController.cs | 2 +- .../Builders/ContentTypeSortBuilder.cs | 15 +- .../ContentTypeBuilderExtensions.cs | 4 +- .../Mapping/ContentTypeModelMappingTests.cs | 38 - .../ContentTypeEditingServiceTests.Create.cs | 1060 +++++++++++++++++ .../ContentTypeEditingServiceTests.Update.cs | 805 +++++++++++++ .../ContentTypeEditingServiceTests.cs | 92 ++ .../MediaTypeEditingServiceTests.Create.cs | 117 ++ .../MediaTypeEditingServiceTests.Update.cs | 116 ++ .../Services/MediaTypeEditingServiceTests.cs | 92 ++ .../Repositories/ContentTypeRepositoryTest.cs | 4 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../ContentEditingServiceTests.Create.cs | 4 +- .../Services/ContentEditingServiceTests.cs | 2 +- .../Services/ContentServicePerformanceTest.cs | 12 +- .../Services/ContentServiceTagsTests.cs | 2 +- .../Services/ContentServiceTests.cs | 4 +- .../Umbraco.Tests.Integration.csproj | 12 + .../Extensions/TypeExtensionsTests.cs | 251 ++++ .../Umbraco.Core/Models/ContentTypeTests.cs | 4 +- .../Builders/AllowedContentTypeDetail.cs | 2 +- .../Builders/ContentTypeBuilderTests.cs | 10 +- 100 files changed, 5074 insertions(+), 560 deletions(-) delete mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/CreateMediaTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/DeleteMediaTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/UpdateMediaTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IDocumentTypeEditingPresentationFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IMediaTypeEditingPresentationFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs delete mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/IDocumentTypeRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeContainerRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeContainerRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypeRequestModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/Composition.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/CompositionType.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCleanup.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCreateModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeEditingModelBase.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeModelBase.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyContainerModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyTypeModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeUpdateModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeCreateModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeModelBase.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyContainerModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyTypeModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeUpdateModel.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeAppearance.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeContainerModelBase.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeModelBase.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeValidation.cs create mode 100644 src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs create mode 100644 src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs create mode 100644 src/Umbraco.Core/Services/ContentTypeEditing/IContentTypeEditingService.cs create mode 100644 src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs create mode 100644 src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Create.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TypeExtensionsTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs index e1d8a7fd2a..da1c9eb143 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Api.Management.ViewModels.DocumentType; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; @@ -26,14 +27,13 @@ public class ByKeyDocumentTypeController : DocumentTypeControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ByKey(Guid id) { - // FIXME: create and use an async get method here. - IContentType? contentType = _contentTypeService.Get(id); - if (contentType == null) + IContentType? contentType = await _contentTypeService.GetAsync(id); + if (contentType is null) { - return DocumentTypeNotFound(); + return OperationStatusResult(ContentTypeOperationStatus.NotFound); } DocumentTypeResponseModel model = _umbracoMapper.Map(contentType)!; - return await Task.FromResult(Ok(model)); + return Ok(model); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateDocumentTypeController.cs index f5d9709499..2126776e4f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateDocumentTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateDocumentTypeController.cs @@ -1,43 +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.DocumentType; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; [ApiVersion("1.0")] -public class CreateDocumentTypeController : CreateUpdateDocumentTypeControllerBase +public class CreateDocumentTypeController : DocumentTypeControllerBase { - private readonly IShortStringHelper _shortStringHelper; + private readonly IDocumentTypeEditingPresentationFactory _documentTypeEditingPresentationFactory; + private readonly IContentTypeEditingService _contentTypeEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public CreateDocumentTypeController(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ITemplateService templateService) - : base(contentTypeService, dataTypeService, shortStringHelper, templateService) - => _shortStringHelper = shortStringHelper; + public CreateDocumentTypeController( + IDocumentTypeEditingPresentationFactory documentTypeEditingPresentationFactory, + IContentTypeEditingService contentTypeEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _documentTypeEditingPresentationFactory = documentTypeEditingPresentationFactory; + _contentTypeEditingService = contentTypeEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } [HttpPost] [MapToApiVersion("1.0")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Create(CreateDocumentTypeRequestModel requestModel) { - // FIXME: support document type folders (and creation within folders) - const int parentId = Constants.System.Root; + ContentTypeCreateModel model = _documentTypeEditingPresentationFactory.MapCreateModel(requestModel); + Attempt result = await _contentTypeEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor)); - if (requestModel.Compositions.Any()) - { - return await Task.FromResult(BadRequest("Compositions and inheritance is not yet supported by this endpoint")); - } - - IContentType contentType = new ContentType(_shortStringHelper, parentId); - ContentTypeOperationStatus result = HandleRequest(contentType, requestModel); - - return result == ContentTypeOperationStatus.Success - ? CreatedAtAction(controller => nameof(controller.ByKey), contentType.Key) - : BadRequest(result); + return result.Success + ? CreatedAtAction(controller => nameof(controller.ByKey), result.Result!.Key) + : OperationStatusResult(result.Status); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs deleted file mode 100644 index 352f0eeb8a..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs +++ /dev/null @@ -1,222 +0,0 @@ -using Umbraco.Cms.Api.Management.ViewModels.ContentType; -using Umbraco.Cms.Api.Management.ViewModels.DocumentType; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Core.Strings; -using Umbraco.Extensions; -using ContentTypeSort = Umbraco.Cms.Core.Models.ContentTypeSort; - -namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; - -// FIXME: pretty much everything here should be moved to mappers and possibly new services for content type editing - like the ContentEditingService for content -public abstract class CreateUpdateDocumentTypeControllerBase : DocumentTypeControllerBase -{ - private readonly IContentTypeService _contentTypeService; - private readonly IDataTypeService _dataTypeService; - private readonly IShortStringHelper _shortStringHelper; - private readonly ITemplateService _templateService; - - protected CreateUpdateDocumentTypeControllerBase(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ITemplateService templateService) - { - _contentTypeService = contentTypeService; - _dataTypeService = dataTypeService; - _shortStringHelper = shortStringHelper; - _templateService = templateService; - } - - protected ContentTypeOperationStatus HandleRequest(IContentType contentType, TRequestModel requestModel) - where TRequestModel : ContentTypeModelBase, IDocumentTypeRequestModel - where TPropertyType : PropertyTypeModelBase - where TPropertyTypeContainer : PropertyTypeContainerModelBase - { - // validate content type alias - if (contentType.Alias.Equals(requestModel.Alias) is false) - { - if (_contentTypeService.GetAllContentTypeAliases().Contains(requestModel.Alias)) - { - return ContentTypeOperationStatus.DuplicateAlias; - } - - var reservedModelAliases = new[] { "system" }; - if (reservedModelAliases.InvariantContains(requestModel.Alias)) - { - return ContentTypeOperationStatus.InvalidAlias; - } - } - - // validate properties - var reservedPropertyTypeNames = typeof(IPublishedContent).GetProperties().Select(x => x.Name) - .Union(typeof(IPublishedContent).GetMethods().Select(x => x.Name)) - .ToArray(); - foreach (TPropertyType propertyType in requestModel.Properties) - { - if (propertyType.Alias.Equals(requestModel.Alias, StringComparison.OrdinalIgnoreCase)) - { - return ContentTypeOperationStatus.InvalidPropertyTypeAlias; - } - - if (reservedPropertyTypeNames.InvariantContains(propertyType.Alias)) - { - return ContentTypeOperationStatus.InvalidPropertyTypeAlias; - } - } - - // validate property data types - Guid[] dataTypeKeys = requestModel.Properties.Select(property => property.DataTypeId).ToArray(); - var dataTypesByKey = dataTypeKeys - // FIXME: create GetAllAsync(params Guid[] keys) method on IDataTypeService - .Select(async key => await _dataTypeService.GetAsync(key)) - .Select(t => t.Result) - .WhereNotNull() - .ToDictionary(dataType => dataType.Key); - if (dataTypeKeys.Length != dataTypesByKey.Count()) - { - return ContentTypeOperationStatus.InvalidDataType; - } - - // filter out properties and containers with no name/alias - requestModel.Properties = requestModel.Properties.Where(propertyType => propertyType.Alias.IsNullOrWhiteSpace() is false).ToArray(); - requestModel.Containers = requestModel.Containers.Where(container => container.Name.IsNullOrWhiteSpace() is false).ToArray(); - - // update basic content type settings - contentType.Alias = requestModel.Alias; - contentType.Description = requestModel.Description; - contentType.Icon = requestModel.Icon; - contentType.IsElement = requestModel.IsElement; - contentType.Name = requestModel.Name; - contentType.AllowedAsRoot = requestModel.AllowedAsRoot; - contentType.SetVariesBy(ContentVariation.Culture, requestModel.VariesByCulture); - contentType.SetVariesBy(ContentVariation.Segment, requestModel.VariesBySegment); - - // update allowed content types - var allowedContentTypesUnchanged = contentType.AllowedContentTypes? - .OrderBy(contentTypeSort => contentTypeSort.SortOrder) - .Select(contentTypeSort => contentTypeSort.Key) - .SequenceEqual(requestModel.AllowedContentTypes - .OrderBy(contentTypeSort => contentTypeSort.SortOrder) - .Select(contentTypeSort => contentTypeSort.Id)) ?? false; - if (allowedContentTypesUnchanged is false) - { - // need the content type IDs here - yet, anyway - see FIXME in Umbraco.Cms.Core.Models.ContentTypeSort - var allContentTypesByKey = _contentTypeService.GetAll().ToDictionary(c => c.Key); - contentType.AllowedContentTypes = requestModel - .AllowedContentTypes - .Select((contentTypeSort, index) => allContentTypesByKey.TryGetValue(contentTypeSort.Id, out IContentType? ct) - ? new ContentTypeSort(new Lazy(() => ct.Id), contentTypeSort.Id, index, ct.Alias) - : null) - .WhereNotNull() - .ToArray(); - } - - // build a dictionary of parent container IDs and their names (we need it when mapping property groups) - var parentContainerNamesById = requestModel - .Containers - .Where(container => container.ParentId is not null) - .DistinctBy(container => container.ParentId) - .ToDictionary( - container => container.ParentId!.Value, - container => requestModel.Containers.First(c => c.Id == container.ParentId).Name!); - - // FIXME: when refactoring for media and member types, this needs to be some kind of abstract implementation - media and member types do not support publishing - const bool supportsPublishing = true; - - // update properties and groups - PropertyGroup[] propertyGroups = requestModel.Containers.Select(container => - { - PropertyGroup propertyGroup = contentType.PropertyGroups.FirstOrDefault(group => group.Key == container.Id) ?? - new PropertyGroup(supportsPublishing); - // NOTE: eventually group.Type should be a string to make the client more flexible; for now we'll have to parse the string value back to its expected enum - propertyGroup.Type = Enum.Parse(container.Type); - propertyGroup.Name = container.Name; - // this is not pretty, but this is how the data structure is at the moment; we just have to live with it for the time being. - var alias = container.Name!; - if (container.ParentId is not null) - { - alias = $"{parentContainerNamesById[container.ParentId.Value]}/{alias}"; - } - propertyGroup.Alias = alias; - propertyGroup.SortOrder = container.SortOrder; - - IPropertyType[] properties = requestModel - .Properties - .Where(property => property.ContainerId == container.Id) - .Select(property => - { - // get the selected data type - // NOTE: this only works because we already ensured that the data type is present in the dataTypesByKey dictionary - IDataType dataType = dataTypesByKey[property.DataTypeId]; - - // get the current property type (if it exists) - IPropertyType propertyType = contentType.PropertyTypes.FirstOrDefault(pt => pt.Key == property.Id) - ?? new Core.Models.PropertyType(_shortStringHelper, dataType); - - propertyType.Name = property.Name; - propertyType.DataTypeId = dataType.Id; - propertyType.DataTypeKey = dataType.Key; - propertyType.Mandatory = property.Validation.Mandatory; - propertyType.MandatoryMessage = property.Validation.MandatoryMessage; - propertyType.ValidationRegExp = property.Validation.RegEx; - propertyType.ValidationRegExpMessage = property.Validation.RegExMessage; - propertyType.SetVariesBy(ContentVariation.Culture, property.VariesByCulture); - propertyType.SetVariesBy(ContentVariation.Segment, property.VariesBySegment); - propertyType.PropertyGroupId = new Lazy(() => propertyGroup.Id, false); - propertyType.Alias = property.Alias; - propertyType.Description = property.Description; - propertyType.SortOrder = property.SortOrder; - propertyType.LabelOnTop = property.Appearance.LabelOnTop; - - return propertyType; - }) - .ToArray(); - - if (properties.Any() is false && parentContainerNamesById.ContainsKey(container.Id) is false) - { - // FIXME: if at all possible, retain empty containers (bad DX to remove stuff that's been attempted saved) - return null; - } - - if (propertyGroup.PropertyTypes == null || propertyGroup.PropertyTypes.SequenceEqual(properties) is false) - { - propertyGroup.PropertyTypes = new PropertyTypeCollection(supportsPublishing, properties); - } - - return propertyGroup; - }) - .WhereNotNull() - .ToArray(); - - if (contentType.PropertyGroups.SequenceEqual(propertyGroups) is false) - { - contentType.PropertyGroups = new PropertyGroupCollection(propertyGroups); - } - - // FIXME: handle properties outside containers ("generic properties") if they still exist - // FIXME: handle compositions (yeah, that) - - // update content type history clean-up - contentType.HistoryCleanup ??= new HistoryCleanup(); - contentType.HistoryCleanup.PreventCleanup = requestModel.Cleanup.PreventCleanup; - contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = requestModel.Cleanup.KeepAllVersionsNewerThanDays; - contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = requestModel.Cleanup.KeepLatestVersionPerDayForDays; - - // update allowed templates and assign default template - ITemplate[] allowedTemplates = requestModel.AllowedTemplateIds - .Select(async templateId => await _templateService.GetAsync(templateId)) - .Select(t => t.Result) - .WhereNotNull() - .ToArray(); - contentType.AllowedTemplates = allowedTemplates; - // NOTE: incidentally this also covers removing the default template; when requestModel.DefaultTemplateId is null, - // contentType.SetDefaultTemplate() will be called with a null value, which will reset the default template. - contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == requestModel.DefaultTemplateId)); - - // save content type - // FIXME: create and use an async get method here. - _contentTypeService.Save(contentType); - - return ContentTypeOperationStatus.Success; - } -} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DeleteDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DeleteDocumentTypeController.cs index bcd13fb526..a4ae3b2691 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DeleteDocumentTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DeleteDocumentTypeController.cs @@ -2,9 +2,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; -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.DocumentType; @@ -12,25 +12,21 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; public class DeleteDocumentTypeController : DocumentTypeControllerBase { private readonly IContentTypeService _contentTypeService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public DeleteDocumentTypeController(IContentTypeService contentTypeService) - => _contentTypeService = contentTypeService; + public DeleteDocumentTypeController(IContentTypeService contentTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _contentTypeService = contentTypeService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } [HttpDelete("{id:guid}")] [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(DocumentTypeResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Delete(Guid id) { - // FIXME: create and use an async get method here. - IContentType? contentType = _contentTypeService.Get(id); - if (contentType == null) - { - return DocumentTypeNotFound(); - } - - // FIXME: create overload that accepts user key - _contentTypeService.Delete(contentType, Constants.Security.SuperUserId); - return await Task.FromResult(Ok()); + ContentTypeOperationStatus status = await _contentTypeService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); + return OperationStatusResult(status); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs index 1c2fbbc9a4..3a2e954e8b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentTypeControllerBase.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; @@ -13,7 +15,62 @@ namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; [Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessDocumentTypes)] public abstract class DocumentTypeControllerBase : ManagementApiControllerBase { - protected IActionResult DocumentTypeNotFound() => NotFound(new ProblemDetailsBuilder() - .WithTitle("The document type could not be found") - .Build()); + protected IActionResult OperationStatusResult(ContentTypeOperationStatus status) + => ContentTypeOperationStatusResult(status, "document"); + + internal static IActionResult ContentTypeOperationStatusResult(ContentTypeOperationStatus status, string type) => + status switch + { + ContentTypeOperationStatus.Success => new OkResult(), + ContentTypeOperationStatus.NotFound => new NotFoundObjectResult(new ProblemDetailsBuilder() + .WithTitle("Not Found") + .WithDetail($"The specified {type} type was not found") + .Build()), + ContentTypeOperationStatus.DuplicateAlias => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Duplicate alias") + .WithDetail($"The specified {type} type alias is already in use") + .Build()), + ContentTypeOperationStatus.InvalidAlias => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Invalid alias") + .WithDetail($"The specified {type} type alias is invalid") + .Build()), + ContentTypeOperationStatus.InvalidPropertyTypeAlias => new BadRequestObjectResult( + new ProblemDetailsBuilder() + .WithTitle("Invalid property type alias") + .WithDetail("One or more property type aliases are invalid") + .Build()), + ContentTypeOperationStatus.InvalidContainerName => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Invalid container name") + .WithDetail("One or more container names are invalid") + .Build()), + ContentTypeOperationStatus.MissingContainer => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Missing container") + .WithDetail("One or more containers or properties are listed as parents to containers that are not defined.") + .Build()), + ContentTypeOperationStatus.DuplicateContainer => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Duplicate container") + .WithDetail("One or more containers (or container keys) are defined multiple times.") + .Build()), + ContentTypeOperationStatus.DataTypeNotFound => new NotFoundObjectResult(new ProblemDetailsBuilder() + .WithTitle("Data Type not found") + .WithDetail("One or more of the specified data types were not found") + .Build()), + ContentTypeOperationStatus.InvalidInheritance => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Invalid inheritance") + .WithDetail($"The specified {type} type inheritance is invalid") + .Build()), + ContentTypeOperationStatus.InvalidComposition => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Invalid composition") + .WithDetail($"The specified {type} type composition is invalid")), + ContentTypeOperationStatus.InvalidParent => new BadRequestObjectResult(new ProblemDetailsBuilder() + .WithTitle("Invalid parent") + .WithDetail( + "The specified parent is invalid, or cannot be used in combination with the specified composition/inheritance")), + ContentTypeOperationStatus.DuplicatePropertyTypeAlias => new BadRequestObjectResult( + new ProblemDetailsBuilder() + .WithTitle("Duplicate property type alias") + .WithDetail("One or more property type aliases are already in use, all property type aliases must be unique.") + .Build()), + _ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError }, + }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/UpdateDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/UpdateDocumentTypeController.cs index 2886dfd615..b6cc54bd12 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/UpdateDocumentTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/UpdateDocumentTypeController.cs @@ -1,45 +1,56 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; [ApiVersion("1.0")] -public class UpdateDocumentTypeController : CreateUpdateDocumentTypeControllerBase +public class UpdateDocumentTypeController : DocumentTypeControllerBase { + private readonly IDocumentTypeEditingPresentationFactory _documentTypeEditingPresentationFactory; + private readonly IContentTypeEditingService _contentTypeEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IContentTypeService _contentTypeService; - public UpdateDocumentTypeController(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ITemplateService templateService) - : base(contentTypeService, dataTypeService, shortStringHelper, templateService) - => _contentTypeService = contentTypeService; + public UpdateDocumentTypeController( + IDocumentTypeEditingPresentationFactory documentTypeEditingPresentationFactory, + IContentTypeEditingService contentTypeEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentTypeService contentTypeService) + { + _documentTypeEditingPresentationFactory = documentTypeEditingPresentationFactory; + _contentTypeEditingService = contentTypeEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _contentTypeService = contentTypeService; + } [HttpPut("{id:guid}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Update(Guid id, UpdateDocumentTypeRequestModel requestModel) { - if (requestModel.Compositions.Any()) - { - return await Task.FromResult(BadRequest("Compositions and inheritance is not yet supported by this endpoint")); - } - - IContentType? contentType = _contentTypeService.Get(id); + IContentType? contentType = await _contentTypeService.GetAsync(id); if (contentType is null) { - return DocumentTypeNotFound(); + return OperationStatusResult(ContentTypeOperationStatus.NotFound); } - ContentTypeOperationStatus result = HandleRequest(contentType, requestModel); + ContentTypeUpdateModel model = _documentTypeEditingPresentationFactory.MapUpdateModel(requestModel); + Attempt result = await _contentTypeEditingService.UpdateAsync(contentType, model, CurrentUserKey(_backOfficeSecurityAccessor)); - return result == ContentTypeOperationStatus.Success + return result.Success ? Ok() - : BadRequest(result); + : OperationStatusResult(result.Status); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ByKeyMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ByKeyMediaTypeController.cs index 96821c9012..cbfe93e3cb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ByKeyMediaTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/ByKeyMediaTypeController.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Api.Management.ViewModels.MediaType; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.MediaType; @@ -26,11 +27,10 @@ public class ByKeyMediaTypeController : MediaTypeControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task ByKey(Guid id) { - // FIXME: create and use an async get method here. - IMediaType? mediaType = _mediaTypeService.Get(id); + IMediaType? mediaType = await _mediaTypeService.GetAsync(id); if (mediaType == null) { - return MediaTypeNotFound(); + return OperationStatusResult(ContentTypeOperationStatus.NotFound); } MediaTypeResponseModel model = _umbracoMapper.Map(mediaType)!; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/CreateMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/CreateMediaTypeController.cs new file mode 100644 index 0000000000..a19f8a51da --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/CreateMediaTypeController.cs @@ -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.MediaType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +public class CreateMediaTypeController : MediaTypeControllerBase +{ + private readonly IMediaTypeEditingPresentationFactory _mediaTypeEditingPresentationFactory; + private readonly IMediaTypeEditingService _mediaTypeEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateMediaTypeController( + IMediaTypeEditingPresentationFactory mediaTypeEditingPresentationFactory, + IMediaTypeEditingService mediaTypeEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _mediaTypeEditingPresentationFactory = mediaTypeEditingPresentationFactory; + _mediaTypeEditingService = mediaTypeEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Create(CreateMediaTypeRequestModel requestModel) + { + MediaTypeCreateModel model = _mediaTypeEditingPresentationFactory.MapCreateModel(requestModel); + Attempt result = await _mediaTypeEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? CreatedAtAction(controller => nameof(controller.ByKey), result.Result!.Key) + : OperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/DeleteMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/DeleteMediaTypeController.cs new file mode 100644 index 0000000000..59eb282e3e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/DeleteMediaTypeController.cs @@ -0,0 +1,32 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +public class DeleteMediaTypeController : MediaTypeControllerBase +{ + private readonly IMediaTypeService _mediaTypeService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteMediaTypeController(IMediaTypeService mediaTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _mediaTypeService = mediaTypeService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Delete(Guid id) + { + ContentTypeOperationStatus status = await _mediaTypeService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); + return OperationStatusResult(status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs index 1484f7cde4..74910e82a5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/MediaTypeControllerBase.cs @@ -1,8 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Controllers.DocumentType; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.MediaType; @@ -13,9 +14,6 @@ namespace Umbraco.Cms.Api.Management.Controllers.MediaType; [Authorize(Policy = "New" + AuthorizationPolicies.TreeAccessMediaTypes)] public abstract class MediaTypeControllerBase : ManagementApiControllerBase { - protected IActionResult MediaTypeNotFound() => NotFound(new ProblemDetailsBuilder() - .WithTitle("The media type could not be found") - .Build()); - - + protected IActionResult OperationStatusResult(ContentTypeOperationStatus status) + => DocumentTypeControllerBase.ContentTypeOperationStatusResult(status, "media"); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/UpdateMediaTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/UpdateMediaTypeController.cs new file mode 100644 index 0000000000..8e19473fc6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/UpdateMediaTypeController.cs @@ -0,0 +1,56 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType; + +[ApiVersion("1.0")] +public class UpdateMediaTypeController : MediaTypeControllerBase +{ + private readonly IMediaTypeEditingPresentationFactory _mediaTypeEditingPresentationFactory; + private readonly IMediaTypeEditingService _mediaTypeEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IMediaTypeService _mediaTypeService; + + public UpdateMediaTypeController( + IMediaTypeEditingPresentationFactory mediaTypeEditingPresentationFactory, + IMediaTypeEditingService mediaTypeEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IMediaTypeService mediaTypeService) + { + _mediaTypeEditingPresentationFactory = mediaTypeEditingPresentationFactory; + _mediaTypeEditingService = mediaTypeEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _mediaTypeService = mediaTypeService; + } + + [HttpPut("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Update(Guid id, UpdateMediaTypeRequestModel requestModel) + { + IMediaType? mediaType = await _mediaTypeService.GetAsync(id); + if (mediaType is null) + { + return OperationStatusResult(ContentTypeOperationStatus.NotFound); + } + + MediaTypeUpdateModel model = _mediaTypeEditingPresentationFactory.MapUpdateModel(requestModel); + Attempt result = await _mediaTypeEditingService.UpdateAsync(mediaType, model, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : OperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs index f0bbb227c1..8acab3feb2 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Api.Management.Mapping.DocumentType; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.DocumentType; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; @@ -8,6 +10,8 @@ internal static class DocumentTypeBuilderExtensions { internal static IUmbracoBuilder AddDocumentTypes(this IUmbracoBuilder builder) { + builder.Services.AddTransient(); + builder.WithCollectionBuilder().Add(); return builder; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MediaTypeBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MediaTypeBuilderExtensions.cs index bebf5d8118..f38e385858 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/MediaTypeBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MediaTypeBuilderExtensions.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Api.Management.Mapping.MediaType; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.MediaType; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; @@ -8,6 +10,8 @@ internal static class MediaTypeBuilderExtensions { internal static IUmbracoBuilder AddMediaTypes(this IUmbracoBuilder builder) { + builder.Services.AddTransient(); + builder.WithCollectionBuilder().Add(); return builder; diff --git a/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs new file mode 100644 index 0000000000..621643d27f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs @@ -0,0 +1,113 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; +using ContentTypeEditingModels = Umbraco.Cms.Core.Models.ContentTypeEditing; +using ContentTypeViewModels = Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal abstract class ContentTypeEditingPresentationFactory +{ + private readonly IContentTypeService _contentTypeService; + + protected ContentTypeEditingPresentationFactory(IContentTypeService contentTypeService) + => _contentTypeService = contentTypeService; + + protected TContentTypeEditingModel MapContentTypeEditingModel< + TContentTypeEditingModel, + TPropertyTypeEditingModel, + TPropertyTypeContainerEditingModel, + TPropertyTypeViewModel, + TPropertyTypeContainerViewModel + >(ContentTypeViewModels.ContentTypeModelBase viewModel) + where TContentTypeEditingModel : ContentTypeEditingModels.ContentTypeEditingModelBase, new() + where TPropertyTypeEditingModel : ContentTypeEditingModels.PropertyTypeModelBase, new() + where TPropertyTypeContainerEditingModel : ContentTypeEditingModels.PropertyTypeContainerModelBase, new() + where TPropertyTypeViewModel : ContentTypeViewModels.PropertyTypeModelBase + where TPropertyTypeContainerViewModel : ContentTypeViewModels.PropertyTypeContainerModelBase + { + TContentTypeEditingModel editingModel = new() + { + Alias = viewModel.Alias, + Description = viewModel.Description, + Icon = viewModel.Icon, + Name = viewModel.Name, + IsElement = viewModel.IsElement, + AllowedAsRoot = viewModel.AllowedAsRoot, + VariesByCulture = viewModel.VariesByCulture, + VariesBySegment = viewModel.VariesBySegment, + Compositions = MapCompositions(viewModel.Compositions), + Containers = MapContainers(viewModel.Containers), + Properties = MapProperties(viewModel.Properties), + AllowedContentTypes = MapAllowedContentTypes(viewModel.AllowedContentTypes), + }; + + return editingModel; + } + + private ContentTypeSort[] MapAllowedContentTypes(IEnumerable allowedContentTypes) + { + // need to fetch the content type aliases to construct the corresponding ContentTypeSort entities + ContentTypeViewModels.ContentTypeSort[] allowedContentTypesArray = allowedContentTypes as ContentTypeViewModels.ContentTypeSort[] + ?? allowedContentTypes.ToArray(); + Guid[] contentTypeKeys = allowedContentTypesArray.Select(a => a.Id).ToArray(); + IDictionary contentTypeAliasesByKey = _contentTypeService + .GetAll() + .Where(c => contentTypeKeys.Contains(c.Key)) + .ToDictionary(c => c.Key, c => c.Alias); + + return allowedContentTypesArray + .Select(a => + contentTypeAliasesByKey.TryGetValue(a.Id, out var alias) + ? new ContentTypeSort(a.Id, a.SortOrder, alias) + : null) + .WhereNotNull() + .ToArray(); + } + + private TPropertyTypeEditingModel[] MapProperties( + IEnumerable properties) + where TPropertyTypeEditingModel : ContentTypeEditingModels.PropertyTypeModelBase, new() + => properties.Select(property => new TPropertyTypeEditingModel + { + Alias = property.Alias, + Appearance = + new ContentTypeEditingModels.PropertyTypeAppearance { LabelOnTop = property.Appearance.LabelOnTop }, + Name = property.Name, + Validation = new ContentTypeEditingModels.PropertyTypeValidation + { + Mandatory = property.Validation.Mandatory, + MandatoryMessage = property.Validation.MandatoryMessage, + RegularExpression = property.Validation.RegEx, + RegularExpressionMessage = property.Validation.RegExMessage + }, + Description = property.Description, + VariesBySegment = property.VariesBySegment, + VariesByCulture = property.VariesByCulture, + Key = property.Id, + ContainerKey = property.ContainerId, + SortOrder = property.SortOrder, + DataTypeKey = property.DataTypeId, + }).ToArray(); + + private TPropertyTypeContainerEditingModel[] MapContainers( + IEnumerable containers) + where TPropertyTypeContainerEditingModel : ContentTypeEditingModels.PropertyTypeContainerModelBase, new() + => containers.Select(container => new TPropertyTypeContainerEditingModel + { + Type = container.Type, + Key = container.Id, + SortOrder = container.SortOrder, + Name = container.Name, + ParentKey = container.ParentId, + }).ToArray(); + + private ContentTypeEditingModels.Composition[] MapCompositions(IEnumerable compositions) + => compositions.Select(composition => new ContentTypeEditingModels.Composition + { + Key = composition.Id, + CompositionType = composition.CompositionType == ContentTypeViewModels.ContentTypeCompositionType.Inheritance + ? ContentTypeEditingModels.CompositionType.Inheritance + : ContentTypeEditingModels.CompositionType.Composition + }).ToArray(); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs new file mode 100644 index 0000000000..246da2f5a7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs @@ -0,0 +1,60 @@ +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services; +using ContentTypeCleanupViewModel = Umbraco.Cms.Api.Management.ViewModels.ContentType.ContentTypeCleanup; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditingPresentationFactory, IDocumentTypeEditingPresentationFactory +{ + public DocumentTypeEditingPresentationFactory(IContentTypeService contentTypeService) + : base(contentTypeService) + { + } + + public ContentTypeCreateModel MapCreateModel(CreateDocumentTypeRequestModel requestModel) + { + ContentTypeCreateModel createModel = MapContentTypeEditingModel< + ContentTypeCreateModel, + ContentTypePropertyTypeModel, + ContentTypePropertyContainerModel, + CreateDocumentTypePropertyTypeRequestModel, + CreateDocumentTypePropertyTypeContainerRequestModel + >(requestModel); + + MapCleanup(createModel, requestModel.Cleanup); + + createModel.Key = requestModel.Id; + createModel.ContainerKey = requestModel.ContainerId; + createModel.AllowedTemplateKeys = requestModel.AllowedTemplateIds; + createModel.DefaultTemplateKey = requestModel.DefaultTemplateId; + + return createModel; + } + + public ContentTypeUpdateModel MapUpdateModel(UpdateDocumentTypeRequestModel requestModel) + { + ContentTypeUpdateModel updateModel = MapContentTypeEditingModel< + ContentTypeUpdateModel, + ContentTypePropertyTypeModel, + ContentTypePropertyContainerModel, + UpdateDocumentTypePropertyTypeRequestModel, + UpdateDocumentTypePropertyTypeContainerRequestModel + >(requestModel); + + MapCleanup(updateModel, requestModel.Cleanup); + + updateModel.AllowedTemplateKeys = requestModel.AllowedTemplateIds; + updateModel.DefaultTemplateKey = requestModel.DefaultTemplateId; + + return updateModel; + } + + private void MapCleanup(ContentTypeModelBase model, ContentTypeCleanupViewModel cleanup) + => model.Cleanup = new ContentTypeCleanup + { + PreventCleanup = cleanup.PreventCleanup, + KeepAllVersionsNewerThanDays = cleanup.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = cleanup.KeepLatestVersionPerDayForDays + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentTypeEditingPresentationFactory.cs new file mode 100644 index 0000000000..1a45056aea --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentTypeEditingPresentationFactory.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.Models.ContentTypeEditing; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IDocumentTypeEditingPresentationFactory +{ + ContentTypeCreateModel MapCreateModel(CreateDocumentTypeRequestModel requestModel); + + ContentTypeUpdateModel MapUpdateModel(UpdateDocumentTypeRequestModel requestModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IMediaTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IMediaTypeEditingPresentationFactory.cs new file mode 100644 index 0000000000..3e817fa448 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IMediaTypeEditingPresentationFactory.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core.Models.ContentTypeEditing; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IMediaTypeEditingPresentationFactory +{ + MediaTypeCreateModel MapCreateModel(CreateMediaTypeRequestModel requestModel); + + MediaTypeUpdateModel MapUpdateModel(UpdateMediaTypeRequestModel requestModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs new file mode 100644 index 0000000000..93d0629a73 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs @@ -0,0 +1,38 @@ +using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPresentationFactory, IMediaTypeEditingPresentationFactory +{ + public MediaTypeEditingPresentationFactory(IContentTypeService contentTypeService) + : base(contentTypeService) + { + } + + public MediaTypeCreateModel MapCreateModel(CreateMediaTypeRequestModel requestModel) + { + MediaTypeCreateModel createModel = MapContentTypeEditingModel< + MediaTypeCreateModel, + MediaTypePropertyTypeModel, + MediaTypePropertyContainerModel, + CreateMediaTypePropertyTypeRequestModel, + CreateMediaTypePropertyTypeContainerRequestModel + >(requestModel); + + createModel.Key = requestModel.Id; + createModel.ParentKey = requestModel.ContainerId; + + return createModel; + } + + public MediaTypeUpdateModel MapUpdateModel(UpdateMediaTypeRequestModel requestModel) + => MapContentTypeEditingModel< + MediaTypeUpdateModel, + MediaTypePropertyTypeModel, + MediaTypePropertyContainerModel, + UpdateMediaTypePropertyTypeRequestModel, + UpdateMediaTypePropertyTypeContainerRequestModel + >(requestModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs index 1eb0b5b911..0888d1bef5 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs @@ -12,7 +12,8 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition mapper.Define((_, _) => new DocumentTypeResponseModel(), Map); - // Umbraco.Code.MapAll + // TODO: ParentId + // Umbraco.Code.MapAll -ParentId private void Map(IContentType source, DocumentTypeResponseModel target, MapperContext context) { target.Id = source.Key; diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs index 186521083f..3c2cf95db2 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs @@ -11,7 +11,8 @@ public class MediaTypeMapDefinition : ContentTypeMapDefinition mapper.Define((_, _) => new MediaTypeResponseModel(), Map); - // Umbraco.Code.MapAll + // Todo: ParentId + // Umbraco.Code.MapAll -ParentId private void Map(IMediaType source, MediaTypeResponseModel target, MapperContext context) { target.Id = source.Key; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index c423eb025a..9ccd312a9d 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -2511,8 +2511,38 @@ } }, "responses": { - "200": { - "description": "Success" + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } }, "404": { "description": "Not Found", @@ -2637,36 +2667,7 @@ ], "responses": { "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/DocumentTypeResponseModel" - } - ] - } - }, - "text/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/DocumentTypeResponseModel" - } - ] - } - }, - "text/plain": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/DocumentTypeResponseModel" - } - ] - } - } - } + "description": "Success" }, "404": { "description": "Not Found", @@ -2746,6 +2747,26 @@ "200": { "description": "Success" }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "404": { "description": "Not Found", "content": { @@ -6475,6 +6496,105 @@ ] } }, + "/umbraco/management/api/v1/media-type": { + "post": { + "tags": [ + "Media Type" + ], + "operationId": "PostMediaType", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMediaTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMediaTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMediaTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/media-type/{id}": { "get": { "tags": [ @@ -6551,6 +6671,151 @@ "Backoffice User": [ ] } ] + }, + "delete": { + "tags": [ + "Media Type" + ], + "operationId": "DeleteMediaTypeById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success" + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + }, + "put": { + "tags": [ + "Media Type" + ], + "operationId": "PutMediaTypeById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMediaTypeRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMediaTypeRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMediaTypeRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] } }, "/umbraco/management/api/v1/media-type/item": { @@ -16333,6 +16598,97 @@ } ] } + }, + "id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "containerId": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "CreateContentTypeRequestModelBaseCreateMediaTypePropertyTypeRequestModelCreateMediaTypePropertyTypeContainerRequestModel": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string" + }, + "allowedAsRoot": { + "type": "boolean" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "isElement": { + "type": "boolean" + }, + "properties": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMediaTypePropertyTypeRequestModel" + } + ] + } + }, + "containers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMediaTypePropertyTypeContainerRequestModel" + } + ] + } + }, + "allowedContentTypes": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeSortModel" + } + ] + } + }, + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeCompositionModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "containerId": { + "type": "string", + "format": "uuid", + "nullable": true } }, "additionalProperties": false @@ -16506,6 +16862,33 @@ }, "additionalProperties": false }, + "CreateMediaTypePropertyTypeContainerRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeContainerModelBaseModel" + } + ], + "additionalProperties": false + }, + "CreateMediaTypePropertyTypeRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeModelBaseModel" + } + ], + "additionalProperties": false + }, + "CreateMediaTypeRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/CreateContentTypeRequestModelBaseCreateMediaTypePropertyTypeRequestModelCreateMediaTypePropertyTypeContainerRequestModel" + } + ], + "additionalProperties": false + }, "CreatePackageRequestModel": { "type": "object", "allOf": [ @@ -20602,6 +20985,77 @@ }, "additionalProperties": false }, + "UpdateContentTypeRequestModelBaseUpdateMediaTypePropertyTypeRequestModelUpdateMediaTypePropertyTypeContainerRequestModel": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string" + }, + "allowedAsRoot": { + "type": "boolean" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "isElement": { + "type": "boolean" + }, + "properties": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMediaTypePropertyTypeRequestModel" + } + ] + } + }, + "containers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UpdateMediaTypePropertyTypeContainerRequestModel" + } + ] + } + }, + "allowedContentTypes": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeSortModel" + } + ] + } + }, + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeCompositionModel" + } + ] + } + } + }, + "additionalProperties": false + }, "UpdateDataTypeRequestModel": { "type": "object", "allOf": [ @@ -20732,6 +21186,33 @@ ], "additionalProperties": false }, + "UpdateMediaTypePropertyTypeContainerRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeContainerModelBaseModel" + } + ], + "additionalProperties": false + }, + "UpdateMediaTypePropertyTypeRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeModelBaseModel" + } + ], + "additionalProperties": false + }, + "UpdateMediaTypeRequestModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateContentTypeRequestModelBaseUpdateMediaTypePropertyTypeRequestModelUpdateMediaTypePropertyTypeContainerRequestModel" + } + ], + "additionalProperties": false + }, "UpdatePackageRequestModel": { "type": "object", "allOf": [ diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs index b0b0a87734..90ca19cb29 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/CreateContentTypeRequestModelBase.cs @@ -5,4 +5,7 @@ public abstract class CreateContentTypeRequestModelBase, IDocumentTypeRequestModel + : CreateContentTypeRequestModelBase { public IEnumerable AllowedTemplateIds { get; set; } = Array.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/IDocumentTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/IDocumentTypeRequestModel.cs deleted file mode 100644 index d61f6ffafd..0000000000 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/IDocumentTypeRequestModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Umbraco.Cms.Api.Management.ViewModels.ContentType; - -namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; - -public interface IDocumentTypeRequestModel -{ - IEnumerable AllowedTemplateIds { get; set; } - - Guid? DefaultTemplateId { get; set; } - - ContentTypeCleanup Cleanup { get; set; } -} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/UpdateDocumentTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/UpdateDocumentTypeRequestModel.cs index 44f65c97f8..a33b3988bf 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/UpdateDocumentTypeRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/UpdateDocumentTypeRequestModel.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.ContentType; namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; public class UpdateDocumentTypeRequestModel - : UpdateContentTypeRequestModelBase, IDocumentTypeRequestModel + : UpdateContentTypeRequestModelBase { public IEnumerable AllowedTemplateIds { get; set; } = Array.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeContainerRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeContainerRequestModel.cs new file mode 100644 index 0000000000..3349f4a96a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeContainerRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class CreateMediaTypePropertyTypeContainerRequestModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeRequestModel.cs new file mode 100644 index 0000000000..3019f3cdde --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypePropertyTypeRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class CreateMediaTypePropertyTypeRequestModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs new file mode 100644 index 0000000000..c32c239102 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/CreateMediaTypeRequestModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class CreateMediaTypeRequestModel + : CreateContentTypeRequestModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeContainerRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeContainerRequestModel.cs new file mode 100644 index 0000000000..0ec7333c1b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeContainerRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class UpdateMediaTypePropertyTypeContainerRequestModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeRequestModel.cs new file mode 100644 index 0000000000..3eab9874a5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypePropertyTypeRequestModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class UpdateMediaTypePropertyTypeRequestModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypeRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypeRequestModel.cs new file mode 100644 index 0000000000..acbbf66b0c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/UpdateMediaTypeRequestModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.MediaType; + +public class UpdateMediaTypeRequestModel + : UpdateContentTypeRequestModelBase +{ +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index c22681e82f..02811d1a2d 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -35,6 +35,7 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; @@ -306,6 +307,8 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Extensions/TypeExtensions.cs b/src/Umbraco.Core/Extensions/TypeExtensions.cs index e3da8d9ee1..1416888e73 100644 --- a/src/Umbraco.Core/Extensions/TypeExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeExtensions.cs @@ -219,96 +219,52 @@ public static class TypeExtensions /// public static PropertyInfo[] GetAllProperties(this Type type) { - if (type.IsInterface) - { - var propertyInfos = new List(); - - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) - { - Type subType = queue.Dequeue(); - foreach (Type subInterface in subType.GetInterfaces()) - { - if (considered.Contains(subInterface)) - { - continue; - } - - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - PropertyInfo[] typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.Instance); - - IEnumerable newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos.ToArray(); - } - - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance; + return type.GetAllMemberInfos(t => t.GetProperties(bindingFlags)); } /// - /// Returns all public properties including inherited properties even for interfaces + /// Returns public properties including inherited properties even for interfaces /// /// /// - /// - /// taken from - /// http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy - /// public static PropertyInfo[] GetPublicProperties(this Type type) { - if (type.IsInterface) - { - var propertyInfos = new List(); + const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.Instance; + return type.GetAllMemberInfos(t => t.GetProperties(bindingFlags)); + } - var considered = new List(); - var queue = new Queue(); - considered.Add(type); - queue.Enqueue(type); - while (queue.Count > 0) - { - Type subType = queue.Dequeue(); - foreach (Type subInterface in subType.GetInterfaces()) - { - if (considered.Contains(subInterface)) - { - continue; - } + /// + /// Returns public methods including inherited methods even for interfaces + /// + /// + /// + public static MethodInfo[] GetPublicMethods(this Type type) + { + const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.Instance; + return type.GetAllMemberInfos(t => t.GetMethods(bindingFlags)); + } - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - PropertyInfo[] typeProperties = subType.GetProperties( - BindingFlags.FlattenHierarchy - | BindingFlags.Public - | BindingFlags.Instance); - - IEnumerable newPropertyInfos = typeProperties - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos.ToArray(); - } - - return type.GetProperties(BindingFlags.FlattenHierarchy - | BindingFlags.Public | BindingFlags.Instance); + /// + /// Returns all methods including inherited methods even for interfaces + /// + /// Includes both Public and Non-Public methods + /// + /// + public static MethodInfo[] GetAllMethods(this Type type) + { + const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance; + return type.GetAllMemberInfos(t => t.GetMethods(bindingFlags)); } /// @@ -512,4 +468,47 @@ public static class TypeExtensions return attempt; } + + /// + /// taken from + /// http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy + /// + private static T[] GetAllMemberInfos(this Type type, Func getMemberInfos) + where T : MemberInfo + { + if (type.IsInterface is false) + { + return getMemberInfos(type); + } + + var memberInfos = new List(); + + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + while (queue.Count > 0) + { + Type subType = queue.Dequeue(); + foreach (Type subInterface in subType.GetInterfaces()) + { + if (considered.Contains(subInterface)) + { + continue; + } + + considered.Add(subInterface); + queue.Enqueue(subInterface); + } + + T[] typeMethodInfos = getMemberInfos(subType); + + IEnumerable newMethodInfos = typeMethodInfos + .Where(x => !memberInfos.Contains(x)); + + memberInfos.InsertRange(0, newMethodInfos); + } + + return memberInfos.ToArray(); + } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index b7b9af6231..dcf17b7c20 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -104,6 +104,7 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType } } + /// public IEnumerable GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes(); @@ -153,6 +154,19 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType return false; } + /// + public bool RemoveContentType(Guid key) + { + // Kinda hacky, but no reason to dupe all the code + var aliasToRemove = ContentTypeComposition.FirstOrDefault(x => x.Key == key)?.Alias; + if (aliasToRemove is null) + { + return false; + } + + return RemoveContentType(aliasToRemove); + } + /// /// Removes a content type with a specified alias from the composition. /// @@ -274,6 +288,13 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType .Select(x => x.Id) .Union(ContentTypeComposition.SelectMany(x => x.CompositionIds())); + /// + public IEnumerable CompositionKeys() + => ContentTypeComposition + .Select(x => x.Key) + .Union(ContentTypeComposition.SelectMany(x => x.CompositionKeys())); + + protected override void PerformDeepClone(object clone) { base.PerformDeepClone(clone); diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/Composition.cs b/src/Umbraco.Core/Models/ContentTypeEditing/Composition.cs new file mode 100644 index 0000000000..5d688e47fa --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/Composition.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class Composition +{ + public required Guid Key { get; init; } + + public required CompositionType CompositionType { get; init; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/CompositionType.cs b/src/Umbraco.Core/Models/ContentTypeEditing/CompositionType.cs new file mode 100644 index 0000000000..2f49a65a76 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/CompositionType.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public enum CompositionType +{ + Composition, + Inheritance +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCleanup.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCleanup.cs new file mode 100644 index 0000000000..818a769288 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCleanup.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class ContentTypeCleanup +{ + public bool PreventCleanup { get; init; } + + public int? KeepAllVersionsNewerThanDays { get; init; } + + public int? KeepLatestVersionPerDayForDays { get; init; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCreateModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCreateModel.cs new file mode 100644 index 0000000000..aa77a804c4 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeCreateModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class ContentTypeCreateModel : ContentTypeModelBase +{ + public Guid? Key { get; set; } + + public Guid? ContainerKey { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeEditingModelBase.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeEditingModelBase.cs new file mode 100644 index 0000000000..2f6f205931 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeEditingModelBase.cs @@ -0,0 +1,40 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +/// +/// +/// This is the common model for all content types, Documents, Media, Members. +/// All the properties are common across all content types. Additionally most properties of the property types are shared as well. +/// +/// To allow for common validation and such, the property types are required to inherit from the same base. +/// The same goes for the property type containers. (I.E Tabs and Groups) +/// +/// The type of the property types, I.E . +/// The type of the content type containers, I.E . +public abstract class ContentTypeEditingModelBase + where TPropertyType : PropertyTypeModelBase + where TPropertyTypeContainer : PropertyTypeContainerModelBase +{ + public string Alias { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public string Icon { get; set; } = string.Empty; + + public bool AllowedAsRoot { get; set; } + + public bool VariesByCulture { get; set; } + + public bool VariesBySegment { get; set; } + + public bool IsElement { get; set; } + + public IEnumerable Properties { get; set; } = Array.Empty(); + + public IEnumerable Containers { get; set; } = Array.Empty(); + + public IEnumerable AllowedContentTypes { get; set; } = Array.Empty(); + + public IEnumerable Compositions { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeModelBase.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeModelBase.cs new file mode 100644 index 0000000000..1e228e0c21 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeModelBase.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public abstract class ContentTypeModelBase : ContentTypeEditingModelBase +{ + public ContentTypeCleanup Cleanup { get; set; } = new(); + + public IEnumerable AllowedTemplateKeys { get; set; } = Array.Empty(); + + public Guid? DefaultTemplateKey { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyContainerModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyContainerModel.cs new file mode 100644 index 0000000000..cba40c473d --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyContainerModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class ContentTypePropertyContainerModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyTypeModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyTypeModel.cs new file mode 100644 index 0000000000..f0220b9a26 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypePropertyTypeModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class ContentTypePropertyTypeModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeUpdateModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeUpdateModel.cs new file mode 100644 index 0000000000..be9d25f2be --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/ContentTypeUpdateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class ContentTypeUpdateModel : ContentTypeModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeCreateModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeCreateModel.cs new file mode 100644 index 0000000000..0d8583e6b3 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeCreateModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MediaTypeCreateModel : MediaTypeModelBase +{ + public Guid? Key { get; set; } + + public Guid? ParentKey { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeModelBase.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeModelBase.cs new file mode 100644 index 0000000000..89b837012d --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeModelBase.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MediaTypeModelBase : ContentTypeEditingModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyContainerModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyContainerModel.cs new file mode 100644 index 0000000000..e69cb074aa --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyContainerModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MediaTypePropertyContainerModel : PropertyTypeContainerModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyTypeModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyTypeModel.cs new file mode 100644 index 0000000000..5019c09b57 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypePropertyTypeModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MediaTypePropertyTypeModel : PropertyTypeModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeUpdateModel.cs b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeUpdateModel.cs new file mode 100644 index 0000000000..00ece8a2a0 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/MediaTypeUpdateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class MediaTypeUpdateModel : MediaTypeModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeAppearance.cs b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeAppearance.cs new file mode 100644 index 0000000000..b7134f3094 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeAppearance.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class PropertyTypeAppearance +{ + public bool LabelOnTop { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeContainerModelBase.cs b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeContainerModelBase.cs new file mode 100644 index 0000000000..04ed7c350a --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeContainerModelBase.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public abstract class PropertyTypeContainerModelBase +{ + public Guid Key { get; set; } + + public Guid? ParentKey { get; set; } + + public string? Name { get; set; } + + // NOTE: This needs to be a string because it can be anything in the future (= not necessarily limited to "tab" or "group") + public string Type { get; set; } = string.Empty; + + public int SortOrder { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeModelBase.cs b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeModelBase.cs new file mode 100644 index 0000000000..93966f6068 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeModelBase.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public abstract class PropertyTypeModelBase +{ + public Guid Key { get; set; } + + public Guid? ContainerKey { get; set; } + + public int SortOrder { get; set; } + + public string Alias { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public Guid DataTypeKey { get; set; } + + public bool VariesByCulture { get; set; } + + public bool VariesBySegment { get; set; } + + public PropertyTypeValidation Validation { get; set; } = new(); + + public PropertyTypeAppearance Appearance { get; set; } = new(); +} diff --git a/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeValidation.cs b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeValidation.cs new file mode 100644 index 0000000000..0b0b782282 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeEditing/PropertyTypeValidation.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Models.ContentTypeEditing; + +public class PropertyTypeValidation +{ + public bool Mandatory { get; set; } + + public string? MandatoryMessage { get; set; } + + public string? RegularExpression { get; set; } + + public string? RegularExpressionMessage { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index 802eab6ff4..e3ad70cb36 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -15,27 +15,13 @@ public class ContentTypeSort : IValueObject, IDeepCloneable /// /// Initializes a new instance of the class. /// - public ContentTypeSort(int id, int sortOrder) + public ContentTypeSort(Guid key, int sortOrder, string alias) { - Id = new Lazy(() => id); - SortOrder = sortOrder; - } - - // FIXME: remove integer ID in constructor - public ContentTypeSort(Lazy id, Guid key, int sortOrder, string alias) - { - Id = id; SortOrder = sortOrder; Alias = alias; Key = key; } - /// - /// Gets or sets the Id of the ContentType - /// - // FIXME: remove this in favor of Key (Id should only be used at repository level) - public Lazy Id { get; set; } = new(() => 0); - /// /// Gets or sets the Sort Order of the ContentType /// @@ -54,8 +40,6 @@ public class ContentTypeSort : IValueObject, IDeepCloneable public object DeepClone() { var clone = (ContentTypeSort)MemberwiseClone(); - var id = Id.Value; - clone.Id = new Lazy(() => id); return clone; } @@ -80,7 +64,7 @@ public class ContentTypeSort : IValueObject, IDeepCloneable } protected bool Equals(ContentTypeSort other) => - Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); + Key.Equals(other.Key) && string.Equals(Alias, other.Alias); public override int GetHashCode() { @@ -88,7 +72,7 @@ public class ContentTypeSort : IValueObject, IDeepCloneable { // The hash code will just be the alias if one is assigned, otherwise it will be the hash code of the Id. // In some cases the alias can be null of the non lazy ctor is used, in that case, the lazy Id will already have a value created. - return Alias != null ? Alias.GetHashCode() : Id.Value.GetHashCode() * 397; + return Alias != null ? Alias.GetHashCode() : Key.GetHashCode() * 397; } } } diff --git a/src/Umbraco.Core/Models/IContentTypeComposition.cs b/src/Umbraco.Core/Models/IContentTypeComposition.cs index 650328548e..7fcfd8abe3 100644 --- a/src/Umbraco.Core/Models/IContentTypeComposition.cs +++ b/src/Umbraco.Core/Models/IContentTypeComposition.cs @@ -40,6 +40,13 @@ public interface IContentTypeComposition : IContentTypeBase /// True if ContentType was removed, otherwise returns False bool RemoveContentType(string alias); + /// + /// Removes a content type with a specified key from the composition. + /// + /// The key of the content type to remove. + /// True if the content type was removed, otherwise false. + bool RemoveContentType(Guid key); + /// /// Checks if a ContentType with the supplied alias exists in the list of composite ContentTypes /// @@ -59,6 +66,12 @@ public interface IContentTypeComposition : IContentTypeBase /// An enumerable list of integer ids IEnumerable CompositionIds(); + /// + /// Gets a list of ContentType keys from the current composition + /// + /// An enumerable list of integer ids. + IEnumerable CompositionKeys(); + /// /// Gets the property types obtained via composition. /// diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index 5d35e3a32a..5b10a9e5e7 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -553,7 +553,7 @@ public class ContentTypeMapDefinition : IMapDefinition // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) - private static void MapSaveToTypeBase( + private void MapSaveToTypeBase( TSource source, IContentTypeComposition target, MapperContext context) @@ -582,13 +582,12 @@ public class ContentTypeMapDefinition : IMapDefinition target.AllowedAsRoot = source.AllowAsRoot; - var allowedContentTypesUnchanged = target.AllowedContentTypes?.Select(x => x.Id.Value) - .SequenceEqual(source.AllowedContentTypes) ?? false; - - if (allowedContentTypesUnchanged is false) - { - target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); - } + // NOTE: we're now always overwriting the AllowedContentTypes instead of checking for changes. it is + // OK because this mapping method will be gone for V14 anyway (along with the old view models). + IContentType[] allowedContentTypes = target.AllowedContentTypes?.Any() is true + ? _contentTypeService.GetAll(target.AllowedContentTypes.Select(c => c.Key)).ToArray() + : Array.Empty(); + target.AllowedContentTypes = allowedContentTypes.Select((c, i) => new ContentTypeSort(c.Key, i, c.Alias)); if (!(target is IMemberType)) { @@ -710,7 +709,15 @@ public class ContentTypeMapDefinition : IMapDefinition target.Udi = MapContentTypeUdi(source); target.UpdateDate = source.UpdateDate; - target.AllowedContentTypes = source.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); + // NOTE: this mapping is somewhat cumbersome at this point. it is OK because it will + // be gone for V14 anyway (along with the old view models). + IContentType[] allowedContentTypes = source.AllowedContentTypes?.Any() is true + ? _contentTypeService.GetAll(source.AllowedContentTypes.Select(c => c.Key)).ToArray() + : Array.Empty(); + Guid[] allowedContentTypesSortOrder = source.AllowedContentTypes?.Any() is true + ? source.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(c => c.Key).ToArray() + : Array.Empty(); + target.AllowedContentTypes = allowedContentTypes.OrderBy(c => allowedContentTypesSortOrder.IndexOf(c.Key)).Select(c => c.Id).ToArray(); target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); target.LockedCompositeContentTypes = MapLockedCompositions(source); target.Variations = source.Variations; diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs new file mode 100644 index 0000000000..d368f58285 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingService.cs @@ -0,0 +1,107 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +// NOTE: this is the implementation for document types. in the code we refer to document types as content types +// at core level, so it has to be named ContentTypeEditingService instead of DocumentTypeEditingService. +internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase, IContentTypeEditingService +{ + private readonly ITemplateService _templateService; + private readonly IContentTypeService _contentTypeService; + + public ContentTypeEditingService( + IContentTypeService contentTypeService, + ITemplateService templateService, + IDataTypeService dataTypeService, + IEntityService entityService, + IShortStringHelper shortStringHelper) + : base(contentTypeService, contentTypeService, dataTypeService, entityService, shortStringHelper) + { + _contentTypeService = contentTypeService; + _templateService = templateService; + } + + public async Task> CreateAsync(ContentTypeCreateModel model, Guid userKey) + { + Attempt result = await ValidateAndMapForCreationAsync(model, model.Key, model.ContainerKey); + if (result.Success is false) + { + return result; + } + + IContentType contentType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForCreationAsync)} succeeded but did not yield any result"); + + UpdateHistoryCleanup(contentType, model); + UpdateTemplates(contentType, model); + + // save content type + await SaveAsync(contentType, userKey); + + return Attempt.SucceedWithStatus(ContentTypeOperationStatus.Success, contentType); + } + + public async Task> UpdateAsync(IContentType contentType, ContentTypeUpdateModel model, Guid userKey) + { + Attempt result = await ValidateAndMapForUpdateAsync(contentType, model); + if (result.Success is false) + { + return result; + } + + contentType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result"); + + UpdateHistoryCleanup(contentType, model); + UpdateTemplates(contentType, model); + + await SaveAsync(contentType, userKey); + + return Attempt.SucceedWithStatus(ContentTypeOperationStatus.Success, contentType); + } + + // update content type history clean-up + private void UpdateHistoryCleanup(IContentType contentType, ContentTypeModelBase model) + { + contentType.HistoryCleanup ??= new HistoryCleanup(); + contentType.HistoryCleanup.PreventCleanup = model.Cleanup.PreventCleanup; + contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = model.Cleanup.KeepAllVersionsNewerThanDays; + contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = model.Cleanup.KeepLatestVersionPerDayForDays; + } + + // update allowed templates and assign default template + private void UpdateTemplates(IContentType contentType, ContentTypeModelBase model) + { + ITemplate[] allowedTemplates = model.AllowedTemplateKeys + .Select(async templateId => await _templateService.GetAsync(templateId)) + .Select(t => t.Result) + .WhereNotNull() + .ToArray(); + contentType.AllowedTemplates = allowedTemplates; + // NOTE: incidentally this also covers removing the default template; when model.DefaultTemplateId is null, + // contentType.SetDefaultTemplate() will be called with a null value, which will reset the default template. + contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == model.DefaultTemplateKey)); + } + + private async Task SaveAsync(IContentType contentType, Guid userKey) + => await _contentTypeService.SaveAsync(contentType, userKey); + + protected override Guid[] GetAvailableCompositionKeys(IContentTypeComposition? source, IContentTypeComposition[] allContentTypes, bool isElement) + => _contentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, isElement: isElement) + .Results + .Where(x => x.Allowed) + .Select(x => x.Composition.Key) + .ToArray(); + + protected override IContentType CreateContentType(IShortStringHelper shortStringHelper, int parentId) + => new ContentType(shortStringHelper, parentId); + + protected override bool SupportsPublishing => true; + + protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.DocumentType; + + protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.DocumentTypeContainer; +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs new file mode 100644 index 0000000000..ee20af2256 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs @@ -0,0 +1,665 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +internal abstract class ContentTypeEditingServiceBase + where TContentType : class, IContentTypeComposition + where TContentTypeService : IContentTypeBaseService + where TPropertyTypeModel : PropertyTypeModelBase + where TPropertyTypeContainer : PropertyTypeContainerModelBase +{ + private readonly IContentTypeService _contentTypeService; + private readonly TContentTypeService _concreteContentTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly IEntityService _entityService; + private readonly IShortStringHelper _shortStringHelper; + + protected ContentTypeEditingServiceBase( + IContentTypeService contentTypeService, + TContentTypeService concreteContentTypeService, + IDataTypeService dataTypeService, + IEntityService entityService, + IShortStringHelper shortStringHelper) + { + _contentTypeService = contentTypeService; + _concreteContentTypeService = concreteContentTypeService; + _dataTypeService = dataTypeService; + _entityService = entityService; + _shortStringHelper = shortStringHelper; + } + + protected abstract Guid[] GetAvailableCompositionKeys(IContentTypeComposition? source, IContentTypeComposition[] allContentTypes, bool isElement); + + protected abstract TContentType CreateContentType(IShortStringHelper shortStringHelper, int parentId); + + protected abstract bool SupportsPublishing { get; } + + protected abstract UmbracoObjectTypes ContentTypeObjectType { get; } + + protected abstract UmbracoObjectTypes ContainerObjectType { get; } + + protected async Task> ValidateAndMapForCreationAsync(ContentTypeEditingModelBase model, Guid? key, Guid? containerKey) + { + SanitizeModelAliases(model); + + // validate that this is a new content type alias + if (ContentTypeAliasIsInUse(model.Alias)) + { + return Attempt.FailWithStatus(ContentTypeOperationStatus.DuplicateAlias, null); + } + + // get all existing content type compositions + IContentTypeComposition[] allContentTypeCompositions = GetAllContentTypeCompositions(); + + // validate inheritance or parent container - a content type can be created either under another content type (inheritance) or inside a container (folder) + ContentTypeOperationStatus operationStatus = ValidateInheritanceAndParent(model, containerKey); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return Attempt.FailWithStatus(operationStatus, null); + } + + // validate the rest of the model + operationStatus = await ValidateAsync(model, null, allContentTypeCompositions); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return Attempt.FailWithStatus(operationStatus, null); + } + + // get the ID of the parent to create the content type under (we already validated that it exists) + var parentId = GetParentId(model, containerKey) ?? throw new ArgumentException("Parent ID could not be found", nameof(model)); + TContentType contentType = CreateContentType(_shortStringHelper, parentId); + + // if the key is specified explicitly, set it (create only) + if (key is not null) + { + contentType.Key = key.Value; + } + + // map the model to the content type + contentType = await UpdateAsync(contentType, model, allContentTypeCompositions); + return Attempt.SucceedWithStatus(ContentTypeOperationStatus.Success, contentType); + } + + protected async Task> ValidateAndMapForUpdateAsync(TContentType contentType, ContentTypeEditingModelBase model) + { + SanitizeModelAliases(model); + + if (ContentTypeAliasCanBeUsedFor(model.Alias, contentType.Key) is false) + { + return Attempt.FailWithStatus(ContentTypeOperationStatus.InvalidAlias, null); + } + + // get all existing content type compositions + IContentTypeComposition[] allContentTypeCompositions = GetAllContentTypeCompositions(); + + // validate that inheritance or parent relationship hasn't changed + ContentTypeOperationStatus operationStatus = ValidateInheritanceAndParent(contentType, model); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return Attempt.FailWithStatus(operationStatus, null); + } + + // validate the rest of the model + operationStatus = await ValidateAsync(model, contentType, allContentTypeCompositions); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return Attempt.FailWithStatus(operationStatus, null); + } + + // map the model to the content type + contentType = await UpdateAsync(contentType, model, allContentTypeCompositions); + return Attempt.SucceedWithStatus(ContentTypeOperationStatus.Success, contentType); + } + + #region Sanitization + + private void SanitizeModelAliases(ContentTypeEditingModelBase model) + { + model.Alias = model.Alias.ToSafeAlias(_shortStringHelper); + foreach (TPropertyTypeModel property in model.Properties) + { + property.Alias = property.Alias.ToSafeAlias(_shortStringHelper); + } + } + + #endregion + + #region Model validation + + private async Task ValidateAsync(ContentTypeEditingModelBase model, TContentType? contentType, IContentTypeComposition[] allContentTypeCompositions) + { + // validate all model aliases (content type alias, property type aliases) + ContentTypeOperationStatus operationStatus = ValidateModelAliases(model); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return operationStatus; + } + + // validate property data types exists. + operationStatus = await ValidateDataTypesAsync(model); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return operationStatus; + } + + // verify that all compositions are valid + operationStatus = ValidateCompositions(contentType, model, allContentTypeCompositions); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return operationStatus; + } + + // verify that all properties are valid + operationStatus = ValidateProperties(model, allContentTypeCompositions); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return operationStatus; + } + + // verify that all property/container relationships (groups/tabs) are valid + operationStatus = ValidateContainers(model, allContentTypeCompositions); + if (operationStatus is not ContentTypeOperationStatus.Success) + { + return operationStatus; + } + + return ContentTypeOperationStatus.Success; + } + + private ContentTypeOperationStatus ValidateModelAliases(ContentTypeEditingModelBase model) + { + // Validate model alias is not reserved. + if (IsReservedContentTypeAlias(model.Alias) || IsUnsafeAlias(model.Alias)) + { + return ContentTypeOperationStatus.InvalidAlias; + } + + // Validate properties for reserved aliases. + if (ContainsReservedPropertyTypeAlias(model)) + { + return ContentTypeOperationStatus.InvalidPropertyTypeAlias; + } + + // properties must have aliases + if (model.Properties.Any(p => IsUnsafeAlias(p.Alias))) + { + return ContentTypeOperationStatus.InvalidPropertyTypeAlias; + } + + // containers must names + if (model.Containers.Any(p => p.Name.IsNullOrWhiteSpace())) + { + return ContentTypeOperationStatus.InvalidContainerName; + } + + return ContentTypeOperationStatus.Success; + } + + private async Task ValidateDataTypesAsync(ContentTypeEditingModelBase model) + { + Guid[] dataTypeKeys = GetDataTypeKeys(model); + IDataType[] dataTypes = await GetDataTypesAsync(model); + + if (dataTypeKeys.Length != dataTypes.Length) + { + return ContentTypeOperationStatus.DataTypeNotFound; + } + + return ContentTypeOperationStatus.Success; + } + + private ContentTypeOperationStatus ValidateInheritanceAndParent(ContentTypeEditingModelBase model, Guid? containerKey) + { + Guid[] inheritedKeys = KeysForCompositionTypes(model, CompositionType.Inheritance); + Guid[] compositionKeys = KeysForCompositionTypes(model, CompositionType.Composition); + + // Only one composition can be of type inheritance, and composed items cannot also be inherited. + if (inheritedKeys.Length > 1 || compositionKeys.Intersect(inheritedKeys).Any()) + { + return ContentTypeOperationStatus.InvalidInheritance; + } + + // a content type cannot be created/saved in an entity container (a folder) if has an inheritance type composition + if (inheritedKeys.Any() && containerKey.HasValue) + { + return ContentTypeOperationStatus.InvalidParent; + } + + var parentId = GetParentId(model, containerKey); + if (parentId.HasValue) + { + return ContentTypeOperationStatus.Success; + } + + // no parent ID => must be either an invalid inheritance (if attempted) or an invalid container + return inheritedKeys.Any() + ? ContentTypeOperationStatus.InvalidInheritance + : ContentTypeOperationStatus.InvalidParent; + } + + private ContentTypeOperationStatus ValidateInheritanceAndParent(TContentType contentType, ContentTypeEditingModelBase model) + { + Guid[] inheritedKeys = KeysForCompositionTypes(model, CompositionType.Inheritance); + if (inheritedKeys.Length > 1) + { + return ContentTypeOperationStatus.InvalidInheritance; + } + + if (contentType.ParentId == Constants.System.Root) + { + // the content type does not inherit from another content type, nor does it reside in a container + return inheritedKeys.Any() + ? ContentTypeOperationStatus.InvalidInheritance + : ContentTypeOperationStatus.Success; + } + + Attempt parentContentTypeKeyAttempt = _entityService.GetKey(contentType.ParentId, ContentTypeObjectType); + if (parentContentTypeKeyAttempt.Success) + { + // the content type inherits from another content type - the model must specify that content type as inheritance + return inheritedKeys.Any() is false || inheritedKeys.First() != parentContentTypeKeyAttempt.Result + ? ContentTypeOperationStatus.InvalidInheritance + : ContentTypeOperationStatus.Success; + } + + Attempt parentContainerKeyAttempt = _entityService.GetKey(contentType.ParentId, ContainerObjectType); + if (parentContainerKeyAttempt.Success) + { + // the content resides within a container (folder) - the model must not specify any inheritance + return inheritedKeys.Any() + ? ContentTypeOperationStatus.InvalidInheritance + : ContentTypeOperationStatus.Success; + } + + // something went terribly wrong here; the existing content type parent ID does not match the root, another + // content type or a container. this should not be possible. + throw new ArgumentException("The content type parent ID does not match another content type, nor a container", nameof(contentType)); + } + + private ContentTypeOperationStatus ValidateCompositions(TContentType? contentType, ContentTypeEditingModelBase model, IContentTypeComposition[] allContentTypeCompositions) + { + // get the content type keys we want to use for compositions + Guid[] compositionKeys = KeysForCompositionTypes(model, CompositionType.Composition); + + // verify that all compositions keys are allowed + Guid[] allowedCompositionKeys = GetAvailableCompositionKeys(contentType, allContentTypeCompositions, model.IsElement); + if (allowedCompositionKeys.ContainsAll(compositionKeys) is false) + { + return ContentTypeOperationStatus.InvalidComposition; + } + + return ContentTypeOperationStatus.Success; + } + + private ContentTypeOperationStatus ValidateProperties(ContentTypeEditingModelBase model, IContentTypeComposition[] allContentTypeCompositions) + { + // grab all content types used for composition and/or inheritance + Guid[] allCompositionKeys = KeysForCompositionTypes(model, CompositionType.Composition, CompositionType.Inheritance); + IContentTypeComposition[] allCompositionTypes = allContentTypeCompositions.Where(c => allCompositionKeys.Contains(c.Key)).ToArray(); + + // get the aliases of all properties across these content types + var allPropertyTypeAliases = allCompositionTypes.SelectMany(x => x.CompositionPropertyTypes).Select(x => x.Alias).ToList(); + + // add all the aliases we're going to try to add as well + allPropertyTypeAliases.AddRange(model.Properties.Select(x => x.Alias)); + if (allPropertyTypeAliases.Select(a => a.ToLowerInvariant()).HasDuplicates(true)) + { + return ContentTypeOperationStatus.DuplicatePropertyTypeAlias; + } + + return ContentTypeOperationStatus.Success; + } + + private ContentTypeOperationStatus ValidateContainers(ContentTypeEditingModelBase model, IContentTypeComposition[] allContentTypeCompositions) + { + // all property container keys must be present in the model + Guid[] modelContainerKeys = model.Containers.Select(c => c.Key).ToArray(); + if (model.Properties.Any(p => p.ContainerKey is not null && modelContainerKeys.Contains(p.ContainerKey.Value) is false)) + { + return ContentTypeOperationStatus.MissingContainer; + } + + // duplicate container keys are not allowed + if (modelContainerKeys.Distinct().Count() != modelContainerKeys.Length) + { + return ContentTypeOperationStatus.DuplicateContainer; + } + + // all container parent keys must also be present in the model + if (model.Containers.Any(c => c.ParentKey.HasValue && modelContainerKeys.Contains(c.ParentKey.Value) is false)) + { + return ContentTypeOperationStatus.MissingContainer; + } + + // make sure no container keys in the model originate from compositions + Guid[] allCompositionKeys = KeysForCompositionTypes(model, CompositionType.Composition, CompositionType.Inheritance); + Guid[] compositionContainerKeys = allContentTypeCompositions + .Where(c => allCompositionKeys.Contains(c.Key)) + .SelectMany(c => c.CompositionPropertyGroups.Select(g => g.Key)) + .Distinct() + .ToArray(); + if (model.Containers.Any(c => compositionContainerKeys.Contains(c.Key))) + { + return ContentTypeOperationStatus.DuplicateContainer; + } + + return ContentTypeOperationStatus.Success; + } + + // This this method gets aliases across documents, members, and media, so it covers it all + private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().Contains(alias); + + private bool ContentTypeAliasCanBeUsedFor(string alias, Guid key) + { + IContentType? existingContentType = _contentTypeService.Get(alias); + if (existingContentType is null || existingContentType.Key == key) + { + return true; + } + + return ContentTypeAliasIsInUse(alias) is false; + } + + private bool IsReservedContentTypeAlias(string alias) + { + var reservedAliases = new[] { "system" }; + return reservedAliases.InvariantContains(alias); + } + + private bool ContainsReservedPropertyTypeAlias(ContentTypeEditingModelBase model) + { + // Because of models builder you cannot have an alias that already exists in IPublishedContent, for instance Path. + // Since MyModel.Path would conflict with IPublishedContent.Path. + var reservedPropertyTypeNames = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name) + .Union(typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name)) + .ToArray(); + + return model.Properties.Any(propertyType => propertyType.Alias.Equals(model.Alias, StringComparison.OrdinalIgnoreCase) + || reservedPropertyTypeNames.InvariantContains(propertyType.Alias)); + } + + private bool IsUnsafeAlias(string alias) => alias.IsNullOrWhiteSpace() + || alias.Length != alias.ToSafeAlias(_shortStringHelper).Length; + + #endregion + + #region Model update + + private async Task UpdateAsync( + TContentType contentType, + ContentTypeEditingModelBase model, + IContentTypeComposition[] allContentTypeCompositions) + { + contentType.Alias = model.Alias; + contentType.Description = model.Description; + contentType.Icon = model.Icon; + contentType.Name = model.Name; + contentType.AllowedAsRoot = model.AllowedAsRoot; + contentType.IsElement = model.IsElement; + contentType.SetVariesBy(ContentVariation.Culture, model.VariesByCulture); + contentType.SetVariesBy(ContentVariation.Segment, model.VariesBySegment); + + // update the allowed content types + UpdateAllowedContentTypes(contentType, model, allContentTypeCompositions); + + // update all compositions + UpdateCompositions(contentType, model, allContentTypeCompositions); + + // ensure parent content type assignment (inheritance) if any + UpdateParentContentType(contentType, model, allContentTypeCompositions); + + // update/map all properties + await UpdatePropertiesAsync(contentType, model); + + return contentType; + } + + private void UpdateAllowedContentTypes( + TContentType contentType, + ContentTypeEditingModelBase model, + IContentTypeComposition[] allContentTypeCompositions) + { + var allowedContentTypesUnchanged = contentType.AllowedContentTypes? + .OrderBy(contentTypeSort => contentTypeSort.SortOrder) + .Select(contentTypeSort => contentTypeSort.Key) + .SequenceEqual(model.AllowedContentTypes + .OrderBy(contentTypeSort => contentTypeSort.SortOrder) + .Select(contentTypeSort => contentTypeSort.Key)) ?? false; + + if (allowedContentTypesUnchanged) + { + return; + } + + var allContentTypesByKey = allContentTypeCompositions.ToDictionary(c => c.Key); + contentType.AllowedContentTypes = model + .AllowedContentTypes + .OrderBy(contentTypeSort => contentTypeSort.SortOrder) + .Select((contentTypeSort, index) => allContentTypesByKey.TryGetValue(contentTypeSort.Key, out IContentTypeComposition? ct) + ? new ContentTypeSort(contentTypeSort.Key, index, ct.Alias) + : null) + .WhereNotNull() + .ToArray(); + } + + private async Task UpdatePropertiesAsync( + TContentType contentType, + ContentTypeEditingModelBase model) + { + // build a dictionary of all data types within the model by their keys (we need it when mapping properties) + var dataTypesByKey = (await GetDataTypesAsync(model)).ToDictionary(d => d.Key); + + // build a dictionary of parent container IDs and their names (we need it when mapping property groups) + var parentContainerNamesById = model + .Containers + .Where(container => container.ParentKey is not null) + .DistinctBy(container => container.ParentKey) + .ToDictionary( + container => container.ParentKey!.Value, + // NOTE: this look-up appears to be a little dangerous, but at this point we should have validated + // the containers and their parent relationships in the model, so it's ok + container => model.Containers.First(c => c.Key == container.ParentKey).Name); + + // handle properties in groups + PropertyGroup[] propertyGroups = model.Containers.Select(container => + { + PropertyGroup propertyGroup = contentType.PropertyGroups.FirstOrDefault(group => group.Key == container.Key) ?? + new PropertyGroup(SupportsPublishing) { Key = container.Key }; + // NOTE: eventually group.Type should be a string to make the client more flexible; for now we'll have to parse the string value back to its expected enum + propertyGroup.Type = Enum.Parse(container.Type); + propertyGroup.Name = container.Name; + // this is not pretty, but this is how the data structure is at the moment; we just have to live with it for the time being. + var alias = PropertyGroupAlias(container.Name); + if (container.ParentKey is not null) + { + alias = $"{PropertyGroupAlias(parentContainerNamesById[container.ParentKey.Value])}/{alias}"; + } + propertyGroup.Alias = alias; + propertyGroup.SortOrder = container.SortOrder; + + IPropertyType[] properties = model + .Properties + .Where(property => property.ContainerKey == container.Key) + .Select(property => MapProperty(contentType, property, propertyGroup, dataTypesByKey)) + .ToArray(); + + if (properties.Any() is false && parentContainerNamesById.ContainsKey(container.Key) is false) + { + // FIXME: if at all possible, retain empty containers (bad DX to remove stuff that's been attempted saved) + return null; + } + + if (propertyGroup.PropertyTypes == null || propertyGroup.PropertyTypes.SequenceEqual(properties) is false) + { + propertyGroup.PropertyTypes = new PropertyTypeCollection(SupportsPublishing, properties); + } + + return propertyGroup; + }) + .WhereNotNull() + .ToArray(); + + if (contentType.PropertyGroups.SequenceEqual(propertyGroups) is false) + { + contentType.PropertyGroups = new PropertyGroupCollection(propertyGroups); + } + + // handle orphaned properties + IEnumerable orphanedPropertyTypeModels = model.Properties.Where (x => x.ContainerKey is null).ToArray(); + IPropertyType[] orphanedPropertyTypes = orphanedPropertyTypeModels.Select(property => MapProperty(contentType, property, null, dataTypesByKey)).ToArray(); + if (contentType.NoGroupPropertyTypes.SequenceEqual(orphanedPropertyTypes) is false) + { + contentType.NoGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, orphanedPropertyTypes); + } + } + + private string PropertyGroupAlias(string? containerName) + { + if (containerName.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Container name cannot be empty", nameof(containerName)); + } + + var parts = containerName.Split(Constants.CharArrays.Space); + return $"{parts.First().ToFirstLowerInvariant()}{string.Join(string.Empty, parts.Skip(1).Select(part => part.ToFirstUpperInvariant()))}"; + } + + private IPropertyType MapProperty( + TContentType contentType, + TPropertyTypeModel property, + PropertyGroup? propertyGroup, + IDictionary dataTypesByKey) + { + // get the selected data type + // NOTE: this only works because we already ensured that the data type is present in the dataTypesByKey dictionary + if (dataTypesByKey.TryGetValue(property.DataTypeKey, out IDataType? dataType) is false) + { + throw new ArgumentException("One or more data types could not be found", nameof(dataTypesByKey)); + } + + // get the current property type (if it exists) + IPropertyType propertyType = contentType.PropertyTypes.FirstOrDefault(pt => pt.Key == property.Key) + ?? new PropertyType(_shortStringHelper, dataType); + + // We are demanding a property type key in the model, so we should probably ensure that it's the on that's actually used. + propertyType.Key = property.Key; + propertyType.Name = property.Name; + propertyType.DataTypeId = dataType.Id; + propertyType.DataTypeKey = dataType.Key; + propertyType.Mandatory = property.Validation.Mandatory; + propertyType.MandatoryMessage = property.Validation.MandatoryMessage; + propertyType.ValidationRegExp = property.Validation.RegularExpression; + propertyType.ValidationRegExpMessage = property.Validation.RegularExpressionMessage; + propertyType.SetVariesBy(ContentVariation.Culture, property.VariesByCulture); + propertyType.SetVariesBy(ContentVariation.Segment, property.VariesBySegment); + propertyType.Alias = property.Alias; + propertyType.Description = property.Description; + propertyType.SortOrder = property.SortOrder; + propertyType.LabelOnTop = property.Appearance.LabelOnTop; + + if (propertyGroup is not null) + { + propertyType.PropertyGroupId = new Lazy(() => propertyGroup.Id, false); + } + + return propertyType; + } + + private void UpdateCompositions( + TContentType contentType, + ContentTypeEditingModelBase model, + IContentTypeComposition[] allContentTypeCompositions) + { + // Updates compositions + // We don't actually have to worry about alias collision here because that's also checked in the service + // We'll probably want to refactor this to be able to return a proper ContentTypeOperationStatus. + // In the mapping step here we only really care about the most immediate ancestors. + // We only really have to care about removing when updating + Guid[] currentKeys = contentType.ContentTypeComposition.Select(x => x.Key).ToArray(); + Guid[] targetCompositionKeys = model.Compositions.Select(x => x.Key).ToArray(); + + // We want to remove all of those that are in current, but not in targetCompositionKeys + Guid[] remove = currentKeys.Except(targetCompositionKeys).ToArray(); + IEnumerable add = targetCompositionKeys.Except(currentKeys).ToArray(); + + foreach (Guid key in remove) + { + contentType.RemoveContentType(key); + } + + // We have to look up the content types we want to add to composition, since we keep a full reference. + if (add.Any()) + { + IContentTypeComposition[] contentTypesToAdd = allContentTypeCompositions.Where(c => add.Contains(c.Key)).ToArray(); + foreach (IContentTypeComposition contentTypeToAdd in contentTypesToAdd) + { + contentType.AddContentType(contentTypeToAdd); + } + } + } + + private void UpdateParentContentType( + TContentType contentType, + ContentTypeEditingModelBase model, + IContentTypeComposition[] allContentTypeCompositions) + { + // at this point, we should have already validated that there is at most one and that it exists if it there + Guid parentContentTypeKey = KeysForCompositionTypes(model, CompositionType.Inheritance).FirstOrDefault(); + if (parentContentTypeKey != Guid.Empty) + { + IContentTypeComposition parentContentType = allContentTypeCompositions.FirstOrDefault(c => c.Key == parentContentTypeKey) + ?? throw new ArgumentException("Parent content type could not be found", nameof(model)); + contentType.SetParent(parentContentType); + } + } + + #endregion + + #region Shared between model validation and model update + + private Guid[] GetDataTypeKeys(ContentTypeEditingModelBase model) + => model.Properties.Select(property => property.DataTypeKey).Distinct().ToArray(); + + private async Task GetDataTypesAsync(ContentTypeEditingModelBase model) + => (await _dataTypeService.GetAllAsync(GetDataTypeKeys(model))).ToArray(); + + private int? GetParentId(ContentTypeEditingModelBase model, Guid? containerKey) + { + Guid[] inheritedKeys = KeysForCompositionTypes(model, CompositionType.Inheritance); + + // figure out the content type parent; it is either + // - the specified composition of type inheritance (the content type has a parent content type) + // - the specified parent ID (the content type is placed in a container/folder) + // - root if none of the above + if (inheritedKeys.Any()) + { + Attempt parentContentTypeIdAttempt = _entityService.GetId(inheritedKeys.First(), ContentTypeObjectType); + return parentContentTypeIdAttempt.Success ? parentContentTypeIdAttempt.Result : null; + } + + if (containerKey.HasValue) + { + Attempt containerIdAttempt = _entityService.GetId(containerKey.Value, ContainerObjectType); + return containerIdAttempt.Success ? containerIdAttempt.Result : null; + } + + return Constants.System.Root; + } + + private Guid[] KeysForCompositionTypes(ContentTypeEditingModelBase model, params CompositionType[] compositionTypes) + => model.Compositions + .Where(c => compositionTypes.Contains(c.CompositionType)) + .Select(c => c.Key) + .ToArray(); + + private IContentTypeComposition[] GetAllContentTypeCompositions() + // NOTE: using Cast here is OK, because we implicitly enforce the constraint TContentType : IContentTypeComposition + => _concreteContentTypeService.GetAll().Cast().ToArray(); + + #endregion +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/IContentTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/IContentTypeEditingService.cs new file mode 100644 index 0000000000..9494f99527 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/IContentTypeEditingService.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +public interface IContentTypeEditingService +{ + Task> CreateAsync(ContentTypeCreateModel model, Guid userKey); + + Task> UpdateAsync(IContentType contentType, ContentTypeUpdateModel model, Guid userKey); +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs new file mode 100644 index 0000000000..b2b43814a5 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +public interface IMediaTypeEditingService +{ + Task> CreateAsync(MediaTypeCreateModel model, Guid userKey); + + Task> UpdateAsync(IMediaType mediaType, MediaTypeUpdateModel model, Guid userKey); +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs new file mode 100644 index 0000000000..edf9afe8a9 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs @@ -0,0 +1,56 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Core.Services.ContentTypeEditing; + +internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase, IMediaTypeEditingService +{ + private readonly IMediaTypeService _mediaTypeService; + + public MediaTypeEditingService( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IDataTypeService dataTypeService, + IEntityService entityService, + IShortStringHelper shortStringHelper) + : base(contentTypeService, mediaTypeService, dataTypeService, entityService, shortStringHelper) + => _mediaTypeService = mediaTypeService; + + public async Task> CreateAsync(MediaTypeCreateModel model, Guid userKey) + { + Attempt result = await ValidateAndMapForCreationAsync(model, model.Key, model.ParentKey); + if (result.Success) + { + IMediaType mediaType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForCreationAsync)} succeeded but did not yield any result"); + await _mediaTypeService.SaveAsync(mediaType, userKey); + } + + return result; + } + + public async Task> UpdateAsync(IMediaType mediaType, MediaTypeUpdateModel model, Guid userKey) + { + Attempt result = await ValidateAndMapForUpdateAsync(mediaType, model); + if (result.Success) + { + mediaType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result"); + await _mediaTypeService.SaveAsync(mediaType, userKey); + } + + return result; + } + + protected override Guid[] GetAvailableCompositionKeys(IContentTypeComposition? source, IContentTypeComposition[] allContentTypes, bool isElement) + => Array.Empty(); + + protected override IMediaType CreateContentType(IShortStringHelper shortStringHelper, int parentId) + => new MediaType(shortStringHelper, parentId); + + protected override bool SupportsPublishing => false; + + protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.MediaType; + + protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.MediaTypeContainer; +} diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 0e729da8e7..17ac6929e6 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -1,4 +1,6 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -24,10 +26,44 @@ public class ContentTypeService : ContentTypeServiceBase + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) + : base( + provider, + loggerFactory, + eventMessagesFactory, + repository, + auditRepository, + entityContainerRepository, + entityRepository, + eventAggregator, + userIdKeyResolver) => ContentService = contentService; + [Obsolete("Use the ctor specifying all dependencies instead")] + public ContentTypeService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IContentService contentService, + IContentTypeRepository repository, + IAuditRepository auditRepository, + IDocumentTypeContainerRepository entityContainerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator) + : this( + provider, + loggerFactory, + eventMessagesFactory, + contentService, + repository, + auditRepository, + entityContainerRepository, + entityRepository, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) + { } + // beware! order is important to avoid deadlocks protected override int[] ReadLockIds { get; } = { Constants.Locks.ContentTypes }; diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 7cf63445a9..c6b840d4ac 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -1,5 +1,7 @@ using System.Globalization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; @@ -9,6 +11,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -21,6 +24,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe private readonly IEntityContainerRepository _containerRepository; private readonly IEntityRepository _entityRepository; private readonly IEventAggregator _eventAggregator; + private readonly IUserIdKeyResolver _userIdKeyResolver; protected ContentTypeServiceBase( ICoreScopeProvider provider, @@ -30,7 +34,8 @@ public abstract class ContentTypeServiceBase : ContentTypeSe IAuditRepository auditRepository, IEntityContainerRepository containerRepository, IEntityRepository entityRepository, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) : base(provider, loggerFactory, eventMessagesFactory) { Repository = repository; @@ -38,6 +43,30 @@ public abstract class ContentTypeServiceBase : ContentTypeSe _containerRepository = containerRepository; _entityRepository = entityRepository; _eventAggregator = eventAggregator; + _userIdKeyResolver = userIdKeyResolver; + } + + [Obsolete("Use the ctor specifying all dependencies instead")] + protected ContentTypeServiceBase( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + TRepository repository, + IAuditRepository auditRepository, + IEntityContainerRepository containerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator) + : this( + provider, + loggerFactory, + eventMessagesFactory, + repository, + auditRepository, + containerRepository, + entityRepository, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) + { } protected TRepository Repository { get; } @@ -307,6 +336,9 @@ public abstract class ContentTypeServiceBase : ContentTypeSe return Repository.Get(id); } + /// + public Task GetAsync(Guid guid) => Task.FromResult(Get(guid)); + public IEnumerable GetAll(params int[] ids) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); @@ -462,6 +494,12 @@ public abstract class ContentTypeServiceBase : ContentTypeSe #region Save + public async Task SaveAsync(TItem item, Guid performingUserKey) + { + var userId = await _userIdKeyResolver.GetAsync(performingUserKey); + Save(item, userId); + } + public void Save(TItem? item, int userId = Constants.Security.SuperUserId) { if (item is null) @@ -570,6 +608,25 @@ public abstract class ContentTypeServiceBase : ContentTypeSe #region Delete + public async Task DeleteAsync(Guid key, Guid performingUserKey) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + int performingUserId = await _userIdKeyResolver.GetAsync(performingUserKey); + + TItem? item = await GetAsync(key); + + if (item is null) + { + return ContentTypeOperationStatus.NotFound; + } + + Delete(item, performingUserId); + + scope.Complete(); + return ContentTypeOperationStatus.Success; + } + public void Delete(TItem item, int userId = Constants.Security.SuperUserId) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -601,10 +658,10 @@ public abstract class ContentTypeServiceBase : ContentTypeSe DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id)); // Next find all other document types that have a reference to this content type - IEnumerable referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes?.Any(p=>p.Id.Value==item.Id) ?? false); + IEnumerable referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes?.Any(p => p.Key == item.Key) ?? false); foreach (TItem reference in referenceToAllowedContentTypes) { - reference.AllowedContentTypes = reference.AllowedContentTypes?.Where(p => p.Id.Value != item.Id); + reference.AllowedContentTypes = reference.AllowedContentTypes?.Where(p => p.Key != item.Key); var changedRef = new List>() { new ContentTypeChange(reference, ContentTypeChangeTypes.RefreshMain) }; // Fire change event scope.Notifications.Publish(GetContentTypeChangedNotification(changedRef, eventMessages)); diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 071262128f..fcb401640b 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -272,6 +272,22 @@ namespace Umbraco.Cms.Core.Services.Implement return await Task.FromResult(dataType); } + /// + public Task> GetAllAsync(params Guid[] keys) + { + // Nothing requested, return nothing + if (keys.Any() is false) + { + return Task.FromResult(Enumerable.Empty()); + } + + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + + IDataType[] dataTypes = _dataTypeRepository.Get(Query().Where(x => keys.Contains(x.Key))).ToArray(); + ConvertMissingEditorsOfDataTypesToLabels(dataTypes); + return Task.FromResult>(dataTypes); + } + /// /// Gets a by its Id /// diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index 8e67c78a20..372ff5b15b 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; @@ -31,6 +32,13 @@ public interface IContentTypeBaseService : IContentTypeBaseService, IServ /// TItem? Get(Guid key); + /// + /// Gets a content type. + /// + /// The key of the content type. + /// The found content type, null if none was found. + Task GetAsync(Guid guid); + /// /// Gets a content type. /// @@ -61,10 +69,24 @@ public interface IContentTypeBaseService : IContentTypeBaseService, IServ void Save(TItem? item, int userId = Constants.Security.SuperUserId); + Task SaveAsync(TItem item, Guid performingUserKey) + { + Save(item); + return Task.CompletedTask; + } + void Save(IEnumerable items, int userId = Constants.Security.SuperUserId); void Delete(TItem item, int userId = Constants.Security.SuperUserId); + /// + /// Deletes an item + /// + /// The item to delete. + /// + /// + Task DeleteAsync(Guid key, Guid performingUserKey); + void Delete(IEnumerable item, int userId = Constants.Security.SuperUserId); Attempt ValidateComposition(TItem? compo); diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index b9b39c9933..cd2a8b71a8 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -100,6 +100,13 @@ public interface IDataTypeService : IService /// Task GetAsync(Guid id); + /// + /// Gets multiple objects by their unique keys. + /// + /// The keys to get datatypes by. + /// An attempt with the requested data types. + Task> GetAllAsync(params Guid[] keys); + /// /// Gets all objects or those with the ids passed in /// diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs index eff6ba0fba..b7e59b1320 100644 --- a/src/Umbraco.Core/Services/MediaTypeService.cs +++ b/src/Umbraco.Core/Services/MediaTypeService.cs @@ -1,4 +1,6 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -10,6 +12,29 @@ namespace Umbraco.Cms.Core.Services; public class MediaTypeService : ContentTypeServiceBase, IMediaTypeService { + public MediaTypeService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IMediaService mediaService, + IMediaTypeRepository mediaTypeRepository, + IAuditRepository auditRepository, + IMediaTypeContainerRepository entityContainerRepository, + IEntityRepository entityRepository, + IEventAggregator eventAggregator, + IUserIdKeyResolver userIdKeyResolver) + : base( + provider, + loggerFactory, + eventMessagesFactory, + mediaTypeRepository, + auditRepository, + entityContainerRepository, + entityRepository, + eventAggregator, + userIdKeyResolver) => MediaService = mediaService; + + [Obsolete("Use the constructor with all dependencies instead")] public MediaTypeService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -20,7 +45,20 @@ public class MediaTypeService : ContentTypeServiceBase MediaService = mediaService; + : this( + provider, + loggerFactory, + eventMessagesFactory, + mediaService, + mediaTypeRepository, + auditRepository, + entityContainerRepository, + entityRepository, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + // beware! order is important to avoid deadlocks protected override int[] ReadLockIds { get; } = { Constants.Locks.MediaTypes }; diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index c4b12d9858..72f4cde792 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -15,7 +15,7 @@ public class MemberTypeService : ContentTypeServiceBase(), + entityContainerRepository, entityRepository, - eventAggregator) + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -47,7 +49,8 @@ public class MemberTypeService : ContentTypeServiceBase(() => Convert.ToInt32(source.Id)); } // Umbraco.Code.MapAll -Trashed diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 2f0350f5f5..9c9c058ee8 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1171,12 +1171,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging continue; } - if (allowedChildren?.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id) ?? false) + if (allowedChildren?.Any(x => x.Key == allowedChild.Key) ?? false) { continue; } - allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), allowedChild.Key, sortOrder, allowedChild.Alias)); + allowedChildren?.Add(new ContentTypeSort(allowedChild.Key, sortOrder, allowedChild.Alias)); sortOrder++; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index ae7e875337..edb5d9e617 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -128,7 +128,6 @@ internal class ContentTypeCommonRepository : IContentTypeCommonRepository } allowedContentTypes.Add(new ContentTypeSort( - new Lazy(() => allowedDto.AllowedId), key, allowedDto.SortOrder, alias!)); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 8b36a46224..f9a6af1603 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -246,15 +246,16 @@ AND umbracoNode.nodeObjectType = @objectType", } } - if (entity.AllowedContentTypes is not null) + (TEntity Entity, int SortOrder)[] allowedContentTypes = GetAllowedContentTypes(entity); + if (allowedContentTypes.Any()) { // Insert collection of allowed content types - foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) + foreach ((TEntity Entity, int SortOrder) allowedContentType in allowedContentTypes) { Database.Insert(new ContentTypeAllowedContentTypeDto { Id = entity.Id, - AllowedId = allowedContentType.Id.Value, + AllowedId = allowedContentType.Entity.Id, SortOrder = allowedContentType.SortOrder, }); } @@ -401,14 +402,16 @@ AND umbracoNode.id <> @id", // delete the allowed content type entries before re-inserting the collection of allowed content types Database.Delete("WHERE Id = @Id", new { entity.Id }); - if (entity.AllowedContentTypes is not null) + + (TEntity Entity, int SortOrder)[] allowedContentTypes = GetAllowedContentTypes(entity); + if (allowedContentTypes.Any()) { - foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) + foreach ((TEntity Entity, int SortOrder) allowedContentType in allowedContentTypes) { Database.Insert(new ContentTypeAllowedContentTypeDto { Id = entity.Id, - AllowedId = allowedContentType.Id.Value, + AllowedId = allowedContentType.Entity.Id, SortOrder = allowedContentType.SortOrder, }); } @@ -1537,6 +1540,26 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT return list; } + private (TEntity Entity, int SortOrder)[] GetAllowedContentTypes(IContentTypeBase contentTypeBase) + { + if (contentTypeBase.AllowedContentTypes?.Any() is not true) + { + return Array.Empty<(TEntity, int)>(); + } + + Guid[] allowedContentTypeKeys = contentTypeBase + .AllowedContentTypes + .OrderBy(c => c.SortOrder) + .Select(c => c.Key) + .ToArray(); + + // NOTE: we're efficiently discarding the input sort order here in favor of a "0 to n" sorting (which is the correct sort order). + // i.e. if the input sort orders are [5, 3, 17] they will be come [1, 0, 2] (in effect though they will also be sorted by + // the 0 based sort order, so they will be returned as [0, 1, 2]). + return PerformGetAll(allowedContentTypeKeys)?.Select(c => (c, allowedContentTypeKeys.IndexOf(c.Key))).ToArray() + ?? Array.Empty<(TEntity, int)>(); + } + private class NameCompareDto { public int NodeId { get; set; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 6f3cc8ebea..da06e8043e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2649,8 +2649,8 @@ public class ContentController : ContentControllerBase IContentType? parentContentType = _contentTypeService.Get(parent.ContentTypeId); //check if the item is allowed under this one - if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) + if (parentContentType?.AllowedContentTypes?.Select(x => x.Key).ToArray() + .Any(x => x == toMove.ContentType.Key) == false) { return ValidationProblem( _localizedTextService.Localize("moveOrCopy", "notAllowedByContentType")); diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index b43787c910..10e845c9b2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -414,9 +414,9 @@ public class ContentTypeController : ContentTypeControllerBase [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public ActionResult PostSave(DocumentTypeSave contentTypeSave) { - //Before we send this model into this saving/mapping pipeline, we need to do some cleanup on variations. - //If the doc type does not allow content variations, we need to update all of it's property types to not allow this either - //else we may end up with ysods. I'm unsure if the service level handles this but we'll make sure it is updated here + // Before we send this model into this saving/mapping pipeline, we need to do some cleanup on variations. + // If the doc type does not allow content variations, we need to update all of it's property types to not allow this either + // else we may end up with ysods. I'm unsure if the service level handles this but we'll make sure it is updated here if (!contentTypeSave.AllowCultureVariant) { foreach (PropertyTypeBasic prop in contentTypeSave.Groups.SelectMany(x => x.Properties)) @@ -590,14 +590,14 @@ public class ContentTypeController : ContentTypeControllerBase } IContentTypeComposition? contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(contentItem); - var ids = contentType?.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); + var keys = contentType?.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Key).ToArray(); - if (ids is null || ids.Any() == false) + if (keys is null || keys.Any() == false) { return Enumerable.Empty(); } - types = _contentTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); + types = _contentTypeService.GetAll(keys).OrderBy(c => keys.IndexOf(c.Key)).ToList(); } var basics = types.Where(type => type.IsElement == false) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs index a50d2c7bb1..b73a022eeb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs @@ -309,7 +309,7 @@ public abstract class ContentTypeControllerBase : BackOfficeNotifi return NotFound(); } - //Validate that there's no other ct with the same alias + // Validate that there's no other ct with the same alias // it in fact cannot be the same as any content type alias (member, content or media) because // this would interfere with how ModelsBuilder works and also how many of the published caches // works since that is based on aliases. @@ -427,7 +427,7 @@ public abstract class ContentTypeControllerBase : BackOfficeNotifi { newCt.AllowedContentTypes = newCt.AllowedContentTypes?.Union( - new[] { new ContentTypeSort(newCt.Id, allowIfselfAsChildSortOrder) }); + new[] { new ContentTypeSort(newCt.Key, allowIfselfAsChildSortOrder, newCt.Alias) }); saveContentType(newCt); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 2cf2392700..995fc8563e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -700,8 +700,8 @@ public class MediaController : ContentControllerBase //check if the item is allowed under this one IMediaType? parentContentType = _mediaTypeService.Get(parent.ContentTypeId); - if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) + if (parentContentType?.AllowedContentTypes?.Select(x => x.Key).ToArray() + .Any(x => x == toMove.ContentType.Key) == false) { var notificationModel = new SimpleNotificationModel(); notificationModel.AddErrorNotification( diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index 09b24b11e4..b3e1d515c9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -397,14 +397,14 @@ public class MediaTypeController : ContentTypeControllerBase } IMediaType? contentType = _mediaTypeService.Get(contentItem.ContentTypeId); - var ids = contentType?.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); + var keys = contentType?.AllowedContentTypes?.OrderBy(c => c.SortOrder).Select(x => x.Key).ToArray(); - if (ids is null || ids.Any() == false) + if (keys is null || keys.Any() == false) { return Enumerable.Empty(); } - types = _mediaTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); + types = _mediaTypeService.GetAll(keys).OrderBy(c => keys.IndexOf(c.Key)).ToList(); } var basics = types.Select(_umbracoMapper.Map).WhereNotNull().ToList(); diff --git a/tests/Umbraco.TestData/LoadTestController.cs b/tests/Umbraco.TestData/LoadTestController.cs index 835723f38e..cb4a8d28bb 100644 --- a/tests/Umbraco.TestData/LoadTestController.cs +++ b/tests/Umbraco.TestData/LoadTestController.cs @@ -217,7 +217,7 @@ public class LoadTestController : Controller }; containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[] { - new ContentTypeSort(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) + new ContentTypeSort(contentType.Key, 0, contentType.Alias) }); containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate }); containerType.SetDefaultTemplate(containerTemplate); diff --git a/tests/Umbraco.TestData/UmbracoTestDataController.cs b/tests/Umbraco.TestData/UmbracoTestDataController.cs index 37c9b28ab4..5a7e59b32c 100644 --- a/tests/Umbraco.TestData/UmbracoTestDataController.cs +++ b/tests/Umbraco.TestData/UmbracoTestDataController.cs @@ -276,7 +276,7 @@ public class UmbracoTestDataController : SurfaceController new PropertyType(_shortStringHelper, GetOrCreateMediaPicker(), "media") { Name = "Media" }); docType.AddPropertyType(new PropertyType(_shortStringHelper, GetOrCreateText(), "desc") { Name = "Description" }); Services.ContentTypeService.Save(docType); - docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Key, 0, docType.Alias) }; Services.ContentTypeService.Save(docType); return docType; } diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs index 68fb1eb916..7a4deca5f7 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs @@ -10,12 +10,12 @@ namespace Umbraco.Cms.Tests.Common.Builders; public class ContentTypeSortBuilder : ChildBuilderBase, - IWithIdBuilder, + IWithKeyBuilder, IWithAliasBuilder, IWithSortOrderBuilder { + private Guid? _key; private string _alias; - private int? _id; private int? _sortOrder; public ContentTypeSortBuilder() @@ -34,10 +34,10 @@ public class ContentTypeSortBuilder set => _alias = value; } - int? IWithIdBuilder.Id + Guid? IWithKeyBuilder.Key { - get => _id; - set => _id = value; + get => _key; + set => _key = value; } int? IWithSortOrderBuilder.SortOrder @@ -48,11 +48,10 @@ public class ContentTypeSortBuilder public override ContentTypeSort Build() { - var id = _id ?? 1; var alias = _alias ?? Guid.NewGuid().ToString().ToCamelCase(); var sortOrder = _sortOrder ?? 0; - var key = Guid.NewGuid(); + var key = _key ?? Guid.NewGuid(); - return new ContentTypeSort(new Lazy(() => id), key, sortOrder, alias); + return new ContentTypeSort(key, sortOrder, alias); } } diff --git a/tests/Umbraco.Tests.Common/Builders/Extensions/ContentTypeBuilderExtensions.cs b/tests/Umbraco.Tests.Common/Builders/Extensions/ContentTypeBuilderExtensions.cs index 67ad5e45f6..27ba5fd18a 100644 --- a/tests/Umbraco.Tests.Common/Builders/Extensions/ContentTypeBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Common/Builders/Extensions/ContentTypeBuilderExtensions.cs @@ -56,12 +56,12 @@ public static class ContentTypeBuilderExtensions .Done() .WithDefaultTemplateId(200) .AddAllowedContentType() - .WithId(888) + .WithKey(new Guid("0793C350-04C4-475C-AC09-1D4B7CFD4DF4")) .WithAlias("sub") .WithSortOrder(8) .Done() .AddAllowedContentType() - .WithId(889) + .WithKey(new Guid("9A485133-BCD8-4996-8D1C-64B5600B6A19")) .WithAlias("sub2") .WithSortOrder(9) .Done() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/ContentTypeModelMappingTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/ContentTypeModelMappingTests.cs index c7cb3d091d..89bb0473f6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/ContentTypeModelMappingTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/ContentTypeModelMappingTests.cs @@ -81,12 +81,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest Assert.AreEqual(propTypes.ElementAt(j).IsSensitiveData, result.IsSensitiveProperty(result.PropertyTypes.ElementAt(j).Alias)); } } - - Assert.AreEqual(display.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < display.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(display.AllowedContentTypes.ElementAt(i), result.AllowedContentTypes.ElementAt(i).Id.Value); - } } [Test] @@ -130,12 +124,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest Assert.AreEqual(propTypes.ElementAt(j).DataTypeId, result.PropertyTypes.ElementAt(j).DataTypeId); } } - - Assert.AreEqual(display.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < display.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(display.AllowedContentTypes.ElementAt(i), result.AllowedContentTypes.ElementAt(i).Id.Value); - } } [Test] @@ -203,12 +191,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest { Assert.AreEqual(display.AllowedTemplates.ElementAt(i), result.AllowedTemplates.ElementAt(i).Alias); } - - Assert.AreEqual(display.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < display.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(display.AllowedContentTypes.ElementAt(i), result.AllowedContentTypes.ElementAt(i).Id.Value); - } } [Test] @@ -295,10 +277,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest } Assert.AreEqual(memberType.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < memberType.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(memberType.AllowedContentTypes.ElementAt(i).Id.Value, result.AllowedContentTypes.ElementAt(i)); - } } [Test] @@ -340,10 +318,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest } Assert.AreEqual(mediaType.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < mediaType.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(mediaType.AllowedContentTypes.ElementAt(i).Id.Value, result.AllowedContentTypes.ElementAt(i)); - } } [Test] @@ -397,10 +371,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest } Assert.AreEqual(contentType.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < contentType.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(contentType.AllowedContentTypes.ElementAt(i).Id.Value, result.AllowedContentTypes.ElementAt(i)); - } } [Test] @@ -678,10 +648,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest result.Groups.SelectMany(x => x.ParentTabContentTypes).ContainsAll(new[] { ctMain.Id, ctChild1.Id })); Assert.AreEqual(contentType.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < contentType.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(contentType.AllowedContentTypes.ElementAt(i).Id.Value, result.AllowedContentTypes.ElementAt(i)); - } } [Test] @@ -771,10 +737,6 @@ public class ContentTypeModelMappingTests : UmbracoIntegrationTest } Assert.AreEqual(contentType.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); - for (var i = 0; i < contentType.AllowedContentTypes.Count(); i++) - { - Assert.AreEqual(contentType.AllowedContentTypes.ElementAt(i).Id.Value, result.AllowedContentTypes.ElementAt(i)); - } } [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs new file mode 100644 index 0000000000..94f80d497a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs @@ -0,0 +1,1060 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class ContentTypeEditingServiceTests +{ + [TestCase(false)] + [TestCase(true)] + public async Task Can_Create_With_All_Basic_Settings(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + createModel.Description = "This is the Test description"; + createModel.Icon = "icon icon-something"; + createModel.AllowedAsRoot = true; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual("test", contentType.Alias); + Assert.AreEqual("Test", contentType.Name); + Assert.AreEqual(result.Result.Id, contentType.Id); + Assert.AreEqual(result.Result.Key, contentType.Key); + Assert.AreEqual("This is the Test description", contentType.Description); + Assert.AreEqual("icon icon-something", contentType.Icon); + Assert.IsTrue(contentType.AllowedAsRoot); + } + + [TestCase(false, false)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(true, true)] + public async Task Can_Create_With_Variation(bool variesByCulture, bool variesBySegment) + { + var createModel = CreateCreateModel("Test", "test"); + createModel.VariesByCulture = variesByCulture; + createModel.VariesBySegment = variesBySegment; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + Assert.AreEqual(variesByCulture, contentType.VariesByCulture()); + Assert.AreEqual(variesBySegment, contentType.VariesBySegment()); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Create_In_A_Folder(bool isElement) + { + var containerResult = ContentTypeService.CreateContainer(Constants.System.Root, Guid.NewGuid(), "Test folder"); + Assert.IsTrue(containerResult.Success); + var container = containerResult.Result?.Entity; + Assert.IsNotNull(container); + + var createModel = CreateCreateModel("Test", "test", isElement: isElement, parentKey: container.Key); + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted in the folder + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + Assert.AreEqual(container.Id, contentType.ParentId); + Assert.AreEqual(isElement, contentType.IsElement); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Create_With_Properties_In_A_Container(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container = CreateContainer(); + createModel.Containers = new[] { container }; + + var propertyType = CreatePropertyType("Test Property", "testProperty", containerKey: container.Key); + createModel.Properties = new[] { propertyType }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(1, contentType.PropertyGroups.Count); + Assert.AreEqual(1, contentType.PropertyTypes.Count()); + Assert.AreEqual(1, contentType.PropertyGroups.First().PropertyTypes!.Count); + Assert.AreEqual("testProperty", contentType.PropertyTypes.First().Alias); + Assert.AreEqual("testProperty", contentType.PropertyGroups.First().PropertyTypes!.First().Alias); + Assert.IsEmpty(contentType.NoGroupPropertyTypes); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Create_With_Orphaned_Properties(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + + var propertyType = CreatePropertyType("Test Property", "testProperty"); + createModel.Properties = new[] { propertyType }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.IsEmpty(contentType.PropertyGroups); + Assert.AreEqual(1, contentType.PropertyTypes.Count()); + Assert.AreEqual("testProperty", contentType.PropertyTypes.First().Alias); + Assert.AreEqual(1, contentType.NoGroupPropertyTypes.Count()); + Assert.AreEqual("testProperty", contentType.NoGroupPropertyTypes.First().Alias); + } + + [Test] + public async Task Can_Specify_Key() + { + var key = new Guid("33C326F6-CB5E-43D6-9730-E946AA5F9C7B"); + var createModel = CreateCreateModel(key: key); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.IsNotNull(contentType); + Assert.AreEqual(key, contentType.Key); + }); + } + + [Test] + public async Task Can_Specify_PropertyType_Key() + { + var propertyTypeKey = new Guid("82DDEBD8-D2CA-423E-B88D-6890F26152B4"); + + var propertyTypeContainer = CreateContainer(); + var propertyTypeCreateModel = CreatePropertyType(key: propertyTypeKey, containerKey: propertyTypeContainer.Key); + + var createModel = CreateCreateModel( + propertyTypes: new[] { propertyTypeCreateModel }, + containers: new[] { propertyTypeContainer }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.IsNotNull(contentType); + var propertyType = contentType.PropertyGroups.FirstOrDefault()?.PropertyTypes?.FirstOrDefault(); + Assert.IsNotNull(propertyType); + Assert.AreEqual(propertyTypeKey, propertyType.Key); + }); + } + + [Test] + public async Task Can_Assign_Allowed_Types() + { + var allowedOne = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed One", "allowedOne"), Constants.Security.SuperUserKey)).Result; + var allowedTwo = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed Two", "allowedTwo"), Constants.Security.SuperUserKey)).Result; + Assert.IsNotNull(allowedOne); + Assert.IsNotNull(allowedTwo); + + var createModel = CreateCreateModel("Test", "test"); + createModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 10, allowedOne.Alias), + new ContentTypeSort(allowedTwo.Key, 20, allowedTwo.Alias), + }; + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + var contentType = await ContentTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(contentType); + + var allowedContentTypes = contentType.AllowedContentTypes?.ToArray(); + Assert.IsNotNull(allowedContentTypes); + Assert.AreEqual(2, allowedContentTypes.Length); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedOne.Key && c.SortOrder == 0 && c.Alias == allowedOne.Alias)); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 1 && c.Alias == allowedTwo.Alias)); + } + + [Test] + public async Task Can_Assign_History_Cleanup() + { + var createModel = CreateCreateModel("Test", "test"); + createModel.Cleanup = new ContentTypeCleanup + { + PreventCleanup = true, KeepAllVersionsNewerThanDays = 123, KeepLatestVersionPerDayForDays = 456 + }; + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + var contentType = await ContentTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(contentType); + Assert.IsNotNull(contentType.HistoryCleanup); + Assert.IsTrue(contentType.HistoryCleanup.PreventCleanup); + Assert.AreEqual(123, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays); + Assert.AreEqual(456, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays); + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, false)] + // Wondering where the last case is? Look at the test below. + public async Task Can_Create_Composite(bool compositionIsElement, bool contentTypeIsElement) + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase", + isElement: compositionIsElement); + + // Let's add a property to ensure that it passes through + var container = CreateContainer(); + compositionBase.Containers = new[] { container }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: container.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + isElement: contentTypeIsElement, + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, + Key = compositionType.Key, + }, + } + ); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.Multiple(() => + { + Assert.AreEqual(1, contentType.ContentTypeComposition.Count()); + Assert.AreEqual(compositionType.Key, contentType.ContentTypeComposition.First().Key); + Assert.AreEqual(1, compositionType.CompositionPropertyGroups.Count()); + Assert.AreEqual(container.Key, compositionType.CompositionPropertyGroups.First().Key); + Assert.AreEqual(1, compositionType.CompositionPropertyTypes.Count()); + Assert.AreEqual(compositionProperty.Key, compositionType.CompositionPropertyTypes.First().Key); + }); + } + + [Test] + public async Task Can_Create_Property_Container_Structure_Matching_Composition_Container_Structure() + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase"); + + // Let's add a property to ensure that it passes through + var compositionTab = CreateContainer("Composition Tab", type: TabContainerType); + var compositionGroup = CreateContainer("Composition Group", type: GroupContainerType); + compositionGroup.ParentKey = compositionTab.Key; + compositionBase.Containers = new[] { compositionTab, compositionGroup }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: compositionGroup.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition { CompositionType = CompositionType.Composition, Key = compositionType.Key, }, + } + ); + + var tab = CreateContainer("Composition Tab", type: TabContainerType); + var group = CreateContainer("Composition Group", type: GroupContainerType); + group.ParentKey = tab.Key; + createModel.Containers = new[] { tab, group }; + + var property = CreatePropertyType("My Property", "myProperty", containerKey: group.Key); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.AreEqual(2, contentType.PropertyGroups.Count); + var contentTypeTab = contentType.PropertyGroups.First(g => g.Name == "Composition Tab"); + Assert.AreEqual(tab.Key, contentTypeTab.Key); + Assert.AreEqual(PropertyGroupType.Tab, contentTypeTab.Type); + var contentTypeGroup = contentType.PropertyGroups.First(g => g.Name == "Composition Group"); + Assert.AreEqual(group.Key, contentTypeGroup.Key); + Assert.AreEqual(PropertyGroupType.Group, contentTypeGroup.Type); + var propertyTypeKeys = contentType.CompositionPropertyTypes.Select(t => t.Key).ToArray(); + Assert.AreEqual(2, propertyTypeKeys.Length); + Assert.IsTrue(propertyTypeKeys.Contains(compositionProperty.Key)); + Assert.IsTrue(propertyTypeKeys.Contains(property.Key)); + Assert.IsTrue(contentTypeGroup.PropertyTypes?.Contains("myProperty")); + Assert.IsFalse(contentTypeGroup.PropertyTypes?.Contains("compositionProperty")); + } + + [Test] + public async Task Property_Container_Aliases_Are_CamelCased_Names() + { + var createModel = CreateCreateModel("Test", "test"); + var tab = CreateContainer("My Tab", type: TabContainerType); + var group1 = CreateContainer("My Group", type: GroupContainerType); + group1.ParentKey = tab.Key; + var group2 = CreateContainer("AnotherGroup", type: GroupContainerType); + createModel.Containers = new[] { tab, group1, group2 }; + var property = CreatePropertyType("My Property", "myProperty", containerKey: group1.Key); + // assign some properties to the groups to make sure they're not cleaned out as "empty groups" + createModel.Properties = new[] + { + CreatePropertyType("My Property 1", "myProperty1", containerKey: group1.Key), + CreatePropertyType("My Property 2", "myProperty2", containerKey: group2.Key) + }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + var contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.AreEqual(3, contentType.PropertyGroups.Count); + Assert.AreEqual("myTab", contentType.PropertyGroups.First(g => g.Name == "My Tab").Alias); + Assert.AreEqual("myTab/myGroup", contentType.PropertyGroups.First(g => g.Name == "My Group").Alias); + Assert.AreEqual("anotherGroup", contentType.PropertyGroups.First(g => g.Name == "AnotherGroup").Alias); + } + + [Test] + public async Task Element_Types_Must_Not_Be_Composed_By_non_element_type() + { + // This is a pretty interesting one, since it actually seems to be broken in the old backoffice, + // since the client will always send the isElement flag as false to the GetAvailableCompositeContentTypes endpoint + // Even if it's an element type, however if we look at the comment in GetAvailableCompositeContentTypes + // We see that element types are not suppose to be allowed to be composed by non-element types. + // Since this breaks models builder evidently. + var compositionBase = CreateCreateModel( + "Composition Base", + isElement: false); + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + var createModel = CreateCreateModel( + "Content Type Using Composition", + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, + Key = compositionType.Key, + }, + }, + isElement: true); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); + Assert.IsNull(result.Result); + }); + } + + [Test] + public async Task ContentType_Containing_Composition_Cannot_Be_Used_As_Composition() + { + var compositionBase = CreateCreateModel("CompositionBase"); + + var baseResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(baseResult.Success); + + var composition = CreateCreateModel( + "Composition", + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, Key = baseResult.Result!.Key + } + }); + + var compositionResult = await ContentTypeEditingService.CreateAsync(composition, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + + // This is not allowed because the composition also has a composition (compositionBase). + var invalidComposition = CreateCreateModel( + "Invalid", + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, + Key = compositionResult.Result!.Key + }, + }); + + var invalidAttempt = await ContentTypeEditingService.CreateAsync(invalidComposition, Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsFalse(invalidAttempt.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, invalidAttempt.Status); + Assert.IsNull(invalidAttempt.Result); + }); + } + + [Test] + public async Task Can_Create_Child() + { + + var parentProperty = CreatePropertyType("Parent Property", "parentProperty"); + var parentModel = CreateCreateModel( + "Parent", + propertyTypes: new[] { parentProperty }); + + var parentResult = await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey); + Assert.IsTrue(parentResult.Success); + + var childProperty = CreatePropertyType("Child Property", "childProperty"); + var parentKey = parentResult.Result!.Key; + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = parentKey, + }, + }; + + var childModel = CreateCreateModel( + "Child", + propertyTypes: new[] { childProperty }, + compositions: composition); + + var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + + Assert.Multiple(() => + { + var contentType = result.Result!; + Assert.AreEqual(parentResult.Result.Id, contentType.ParentId); + Assert.AreEqual(1, contentType.ContentTypeComposition.Count()); + Assert.AreEqual(parentResult.Result.Key, contentType.ContentTypeComposition.FirstOrDefault()?.Key); + Assert.AreEqual(2, contentType.CompositionPropertyTypes.Count()); + Assert.IsTrue(contentType.CompositionPropertyTypes.Any(x => x.Alias == parentProperty.Alias)); + Assert.IsTrue(contentType.CompositionPropertyTypes.Any(x => x.Alias == childProperty.Alias)); + }); + } + + // Unlike compositions, it is allowed to inherit on multiple levels + [Test] + public async Task Can_Create_Grandchild() + { + var rootProperty = CreatePropertyType("Root property"); + ContentTypeCreateModel rootModel = CreateCreateModel( + "Root", + propertyTypes: new[] { rootProperty }); + + var rootResult = await ContentTypeEditingService.CreateAsync(rootModel, Constants.Security.SuperUserKey); + Assert.IsTrue(rootResult.Success); + + var childProperty = CreatePropertyType("Child Property", "childProperty"); + var rootKey = rootResult.Result!.Key; + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = rootKey, + }, + }; + + var childModel = CreateCreateModel( + "Child", + propertyTypes: new[] { childProperty }, + compositions: composition); + + var childResult = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + Assert.IsTrue(childResult.Success); + + var grandchildProperty = CreatePropertyType("Grandchild Property", "grandchildProperty"); + var childKey = childResult.Result!.Key; + Composition[] grandchildComposition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = childKey, + }, + }; + + var grandchildModel = CreateCreateModel( + "Grandchild", + propertyTypes: new[] { grandchildProperty }, + compositions: grandchildComposition); + + var grandchildResult = await ContentTypeEditingService.CreateAsync(grandchildModel, Constants.Security.SuperUserKey); + Assert.IsTrue(grandchildResult.Success); + + var root = rootResult.Result!; + var child = childResult.Result!; + IContentType grandchild = grandchildResult.Result!; + Assert.Multiple(() => + { + // Write asserts for this test + Assert.AreEqual(-1, root.ParentId); + Assert.AreEqual(root.Id, child.ParentId); + Assert.AreEqual(child.Id, grandchild.ParentId); + + // We only have the immediate parent as a composition + Assert.AreEqual(1, grandchild.ContentTypeComposition.Count()); + Assert.AreEqual(child.Key, grandchild.ContentTypeComposition.FirstOrDefault()?.Key); + + // But all the property types are there since we crawl up the chain in CompositionPropertyTypes + Assert.AreEqual(3, grandchild.CompositionPropertyTypes.Count()); + Assert.IsTrue(grandchild.CompositionPropertyTypes.Any(x => x.Alias == rootProperty.Alias)); + Assert.IsTrue(grandchild.CompositionPropertyTypes.Any(x => x.Alias == childProperty.Alias)); + Assert.IsTrue(grandchild.CompositionPropertyTypes.Any(x => x.Alias == grandchildProperty.Alias)); + }); + } + + [Test] + public async Task Cannot_Be_Both_Parent_And_Composition() + { + var compositionBase = CreateCreateModel("CompositionBase"); + + var baseResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(baseResult.Success); + + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, Key = baseResult.Result!.Key + }, + new Composition + { + CompositionType = CompositionType.Inheritance, Key = baseResult.Result!.Key + }, + }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + }); + } + + [Test] + public async Task Cannot_Have_Multiple_Inheritance() + { + var parentModel1 = CreateCreateModel("Parent1"); + var parentModel2 = CreateCreateModel("Parent2"); + + var parentKey1 = (await ContentTypeEditingService.CreateAsync(parentModel1, Constants.Security.SuperUserKey)).Result?.Key; + Assert.IsTrue(parentKey1.HasValue); + var parentKey2 = (await ContentTypeEditingService.CreateAsync(parentModel2, Constants.Security.SuperUserKey)).Result?.Key; + Assert.IsTrue(parentKey2.HasValue); + + var childProperty = CreatePropertyType("Child Property", "childProperty"); + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = parentKey1.Value, + }, + new() + { + CompositionType = CompositionType.Inheritance, Key = parentKey2.Value, + }, + }; + + var childModel = CreateCreateModel( + "Child", + propertyTypes: new[] { childProperty }, + compositions: composition); + + var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } + + [Test] + public async Task Cannot_Specify_Duplicate_PropertyType_Alias_From_Compositions() + { + var propertyTypeAlias = "testproperty"; + var compositionPropertyType = CreatePropertyType("Test Property", propertyTypeAlias); + var compositionBase = CreateCreateModel( + "CompositionBase", + propertyTypes: new[] { compositionPropertyType }); + + var compositionBaseResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionBaseResult.Success); + + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, Key = compositionBaseResult.Result!.Key + }, + }, + propertyTypes: new[] + { + CreatePropertyType("Test Property", propertyTypeAlias) + }); + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DuplicatePropertyTypeAlias, result.Status); + }); + } + + [Test] + public async Task Cannot_Specify_Non_Existent_DocType_As_Composition() + { + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition + { + CompositionType = CompositionType.Composition, Key = Guid.NewGuid() + }, + }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); + }); + } + + [Test] + public async Task Cannot_Mix_Inheritance_And_ParentKey() + { + var parentModel = CreateCreateModel("Parent"); + var parentKey = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result?.Key; + Assert.IsTrue(parentKey.HasValue); + + var containerResult = ContentTypeService.CreateContainer(Constants.System.Root, Guid.NewGuid(), "Test folder"); + Assert.IsTrue(containerResult.Success); + var container = containerResult.Result?.Entity; + Assert.IsNotNull(container); + + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = parentKey.Value, + } + }; + + var childModel = CreateCreateModel( + "Child", + parentKey: container.Key, + compositions: composition); + + var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); + } + + [Test] + public async Task Cannot_Use_As_ParentKey() + { + var parentModel = CreateCreateModel("Parent"); + var parentKey = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result?.Key; + Assert.IsTrue(parentKey.HasValue); + + var childModel = CreateCreateModel( + "Child", + parentKey: parentKey.Value); + + var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); + } + + // test some properties from IPublishedContent + [TestCase(nameof(IPublishedContent.Id))] + [TestCase(nameof(IPublishedContent.Name))] + [TestCase(nameof(IPublishedContent.SortOrder))] + // test some properties from IPublishedElement + [TestCase(nameof(IPublishedElement.Properties))] + [TestCase(nameof(IPublishedElement.ContentType))] + [TestCase(nameof(IPublishedElement.Key))] + // test some methods from IPublishedContent + [TestCase(nameof(IPublishedContent.IsDraft))] + [TestCase(nameof(IPublishedContent.IsPublished))] + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(".")] + [TestCase("-")] + [TestCase("!\"#¤%&/()=)?`")] + public async Task Cannot_Use_Invalid_PropertyType_Alias(string propertyTypeAlias) + { + // ensure that property casing is ignored when handling reserved property aliases + var propertyTypeAliases = new[] + { + propertyTypeAlias, propertyTypeAlias.ToLowerInvariant(), propertyTypeAlias.ToUpperInvariant() + }; + + foreach (var alias in propertyTypeAliases) + { + var propertyType = CreatePropertyType("Test Property", alias); + var createModel = CreateCreateModel("Test", propertyTypes: new[] { propertyType }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); + } + } + + [TestCase("testProperty", "testProperty")] + [TestCase("testProperty", "TestProperty")] + [TestCase("testProperty", "TESTPROPERTY")] + [TestCase("testProperty", "testproperty")] + public async Task Cannot_Use_Duplicate_PropertyType_Alias(string propertyTypeAlias1, string propertyTypeAlias2) + { + var propertyType1 = CreatePropertyType("Test Property", propertyTypeAlias1); + var propertyType2 = CreatePropertyType("Test Property", propertyTypeAlias2); + var createModel = CreateCreateModel("Test", propertyTypes: new[] { propertyType1, propertyType2 }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DuplicatePropertyTypeAlias, result.Status); + } + + [TestCase("testAlias", "testAlias")] + [TestCase("testAlias", "testalias")] + [TestCase("testAlias", "TESTALIAS")] + [TestCase("testAlias", "testAlias")] + [TestCase("testalias", "testAlias")] + [TestCase("TESTALIAS", "testAlias")] + public async Task Cannot_Use_Alias_As_PropertyType_Alias(string contentTypeAlias, string propertyTypeAlias) + { + var propertyType = CreatePropertyType("Test Property", propertyTypeAlias); + var createModel = CreateCreateModel("Test", contentTypeAlias, propertyTypes: new[] { propertyType }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); + } + + [Test] + public async Task Cannot_Use_Non_Existing_DataType_For_PropertyType() + { + var propertyType = CreatePropertyType("Test Property", "testProperty", dataTypeKey: Guid.NewGuid()); + var createModel = CreateCreateModel("Test", "test", propertyTypes: new[] { propertyType }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DataTypeNotFound, result.Status); + } + + [Test] + public async Task Cannot_Use_Empty_Alias_For_PropertyType() + { + var propertyType = CreatePropertyType("Test Property", string.Empty); + var createModel = CreateCreateModel("Test", "test", propertyTypes: new[] { propertyType }); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); + } + + [Test] + public async Task Cannot_Use_Empty_Name_For_PropertyType_Container() + { + var container = CreateContainer(string.Empty); + var propertyType = CreatePropertyType("Test Property", "testProperty", containerKey: container.Key); + var createModel = CreateCreateModel("Test", "test", propertyTypes: new[] { propertyType }); + createModel.Containers = new[] { container }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerName, result.Status); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(" ")] + [TestCase(".")] + [TestCase("-")] + [TestCase("!\"#¤%&/()=)?`")] + [TestCase("system")] + [TestCase("System")] + [TestCase("SYSTEM")] + public async Task Cannot_Use_Invalid_Alias(string contentTypeAlias) + { + var createModel = CreateCreateModel("Test", contentTypeAlias); + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidAlias, result.Status); + } + + [Test] + public async Task Cannot_Use_Existing_Alias() + { + var createModel = CreateCreateModel("Test", "test"); + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + createModel = CreateCreateModel("Test 2", "test"); + result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DuplicateAlias, result.Status); + } + + [Test] + public async Task Cannot_Add_Container_From_Composition() + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase"); + + // Let's add a property to ensure that it passes through + var compositionContainer = CreateContainer("Composition Tab"); + compositionBase.Containers = new[] { compositionContainer }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: compositionContainer.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition { CompositionType = CompositionType.Composition, Key = compositionType.Key, }, + } + ); + + // this is invalid; the model should not contain the composition container definitions (they will be resolved by ContentTypeEditingService) + createModel.Containers = new[] { compositionContainer }; + var property = CreatePropertyType("My Property", "myProperty", containerKey: compositionContainer.Key); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DuplicateContainer, result.Status); + } + + [Test] + public async Task Cannot_Duplicate_Container_Key_From_Composition() + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase"); + + var compositionContainer = CreateContainer("Composition Tab"); + compositionBase.Containers = new[] { compositionContainer }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: compositionContainer.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition { CompositionType = CompositionType.Composition, Key = compositionType.Key, }, + } + ); + + // this is invalid; cannot reuse the container key + var container = CreateContainer("My Group", type: GroupContainerType, key: compositionContainer.Key); + createModel.Containers = new[] { container }; + var property = CreatePropertyType("My Property", "myProperty", containerKey: container.Key); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DuplicateContainer, result.Status); + } + + [Test] + public async Task Cannot_Have_Duplicate_Container_Key() + { + // Create doc type using the composition + var createModel = CreateCreateModel("Test", "test"); + + // this is invalid; cannot reuse the container key + var containerKey = Guid.NewGuid(); + var container1 = CreateContainer("My Group 1", key: containerKey); + var container2 = CreateContainer("My Group 2", key: containerKey); + createModel.Containers = new[] { container1, container2 }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.DuplicateContainer, result.Status); + } + + [Test] + public async Task Cannot_Add_Property_To_Missing_Container() + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase"); + + var compositionContainer = CreateContainer("Composition Tab"); + compositionBase.Containers = new[] { compositionContainer }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: compositionContainer.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition { CompositionType = CompositionType.Composition, Key = compositionType.Key, }, + } + ); + + // this is invalid; cannot add properties to non-existing containers + var property = CreatePropertyType("My Property", "myProperty", containerKey: Guid.NewGuid()); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + } + + [Test] + public async Task Cannot_Add_Property_Container_To_Missing_Container() + { + // Create doc type using the composition + var createModel = CreateCreateModel(); + + var group = CreateContainer("My Group", type: GroupContainerType); + // this is invalid; a container cannot have a non-existing parent container key + group.ParentKey = Guid.NewGuid(); + createModel.Containers = new[] { group }; + + var property = CreatePropertyType("My Property", "myProperty", containerKey: group.Key); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + } + + [Test] + public async Task Cannot_Create_Property_In_Composition_Container() + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase"); + + // Let's add a property to ensure that it passes through + var compositionContainer = CreateContainer("Composition Tab"); + compositionBase.Containers = new[] { compositionContainer }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: compositionContainer.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition { CompositionType = CompositionType.Composition, Key = compositionType.Key, }, + } + ); + + // this is invalid; cannot add a property on a container that belongs to the composition (the container must be duplicated to the content type itself) + var property = CreatePropertyType("My Property", "myProperty", containerKey: compositionContainer.Key); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + } + + [Test] + public async Task Cannot_Create_Property_Container_In_Composition_Container() + { + var compositionBase = CreateCreateModel( + "Composition Base", + "compositionBase"); + + // Let's add a property to ensure that it passes through + var compositionContainer = CreateContainer("Composition Tab"); + compositionBase.Containers = new[] { compositionContainer }; + + var compositionProperty = CreatePropertyType("Composition Property", "compositionProperty", containerKey: compositionContainer.Key); + compositionBase.Properties = new[] { compositionProperty }; + + var compositionResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); + Assert.IsTrue(compositionResult.Success); + var compositionType = compositionResult.Result; + + // Create doc type using the composition + var createModel = CreateCreateModel( + compositions: new[] + { + new Composition { CompositionType = CompositionType.Composition, Key = compositionType.Key, }, + } + ); + + // this is invalid; cannot create a new container within a parent container that belongs to the composition (the parent container must be duplicated to the content type itself) + var container = CreateContainer("My Group", type: GroupContainerType); + container.ParentKey = compositionContainer.Key; + createModel.Containers = new[] { container }; + var property = CreatePropertyType("My Property", "myProperty", containerKey: container.Key); + createModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs new file mode 100644 index 0000000000..51dbe3c3ab --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs @@ -0,0 +1,805 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class ContentTypeEditingServiceTests +{ + [TestCase(false)] + [TestCase(true)] + public async Task Can_Update_All_Basic_Settings(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + createModel.Description = "This is the Test description"; + createModel.Icon = "icon icon-something"; + createModel.AllowedAsRoot = true; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test updated", "testUpdated", isElement: isElement); + updateModel.Description = "This is the Test description updated"; + updateModel.Icon = "icon icon-something-updated"; + updateModel.AllowedAsRoot = false; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual("testUpdated", contentType.Alias); + Assert.AreEqual("Test updated", contentType.Name); + Assert.AreEqual(result.Result.Id, contentType.Id); + Assert.AreEqual(result.Result.Key, contentType.Key); + Assert.AreEqual("This is the Test description updated", contentType.Description); + Assert.AreEqual("icon icon-something-updated", contentType.Icon); + Assert.IsFalse(contentType.AllowedAsRoot); + } + + [TestCase(false, false)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(true, true)] + public async Task Can_Update_Variation(bool variesByCulture, bool variesBySegment) + { + var createModel = CreateCreateModel("Test", "test"); + createModel.VariesByCulture = variesByCulture; + createModel.VariesBySegment = variesBySegment; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.VariesByCulture = !variesByCulture; + updateModel.VariesBySegment = !variesBySegment; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + Assert.AreEqual(!variesByCulture, contentType.VariesByCulture()); + Assert.AreEqual(!variesBySegment, contentType.VariesBySegment()); + } + + [Test] + public async Task Can_Add_Allowed_Types() + { + var allowedOne = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed One", "allowedOne"), Constants.Security.SuperUserKey)).Result!; + var allowedTwo = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed Two", "allowedTwo"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 10, allowedOne.Alias), + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 10, allowedOne.Alias), + new ContentTypeSort(allowedTwo.Key, 20, allowedTwo.Alias), + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + contentType = await ContentTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(contentType); + + var allowedContentTypes = contentType.AllowedContentTypes?.ToArray(); + Assert.IsNotNull(allowedContentTypes); + Assert.AreEqual(2, allowedContentTypes.Length); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedOne.Key && c.SortOrder == 0 && c.Alias == allowedOne.Alias)); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 1 && c.Alias == allowedTwo.Alias)); + } + + [Test] + public async Task Can_Remove_Allowed_Types() + { + var allowedOne = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed One", "allowedOne"), Constants.Security.SuperUserKey)).Result!; + var allowedTwo = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed Two", "allowedTwo"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 10, allowedOne.Alias), + new ContentTypeSort(allowedTwo.Key, 20, allowedTwo.Alias), + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.AllowedContentTypes = Array.Empty(); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + contentType = await ContentTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(contentType); + + var allowedContentTypes = contentType.AllowedContentTypes?.ToArray(); + Assert.IsNotNull(allowedContentTypes); + Assert.AreEqual(0, allowedContentTypes.Length); + } + + [Test] + public async Task Can_Rearrange_Allowed_Types() + { + var allowedOne = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed One", "allowedOne"), Constants.Security.SuperUserKey)).Result!; + var allowedTwo = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Allowed Two", "allowedTwo"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 0, allowedOne.Alias), + new ContentTypeSort(allowedTwo.Key, 1, allowedTwo.Alias), + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 1, allowedOne.Alias), + new ContentTypeSort(allowedTwo.Key, 0, allowedTwo.Alias), + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + contentType = await ContentTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(contentType); + + var allowedContentTypes = contentType.AllowedContentTypes?.ToArray(); + Assert.IsNotNull(allowedContentTypes); + Assert.AreEqual(2, allowedContentTypes.Length); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedOne.Key && c.SortOrder == 1 && c.Alias == allowedOne.Alias)); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 0 && c.Alias == allowedTwo.Alias)); + } + + [Test] + public async Task Can_Add_Self_To_Allowed_Types() + { + var createModel = CreateCreateModel("Test", "test"); + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + var id = contentType.Id; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.AllowedContentTypes = new[] + { + new ContentTypeSort(contentType.Key, 0, contentType.Alias) + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + contentType = await ContentTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(contentType); + + var allowedContentTypes = contentType.AllowedContentTypes?.ToArray(); + Assert.IsNotNull(allowedContentTypes); + Assert.AreEqual(1, allowedContentTypes.Length); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == contentType.Key && c.SortOrder == 0 && c.Alias == contentType.Alias)); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Add_Properties(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container = CreateContainer(); + createModel.Containers = new[] { container }; + + var propertyType = CreatePropertyType("Test Property", "testProperty", containerKey: container.Key); + createModel.Properties = new[] { propertyType }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + updateModel.Containers = new[] { container }; + var newPropertyType = CreatePropertyType("Test Property 2", "testProperty2", containerKey: container.Key); + newPropertyType.SortOrder = 0; + propertyType.SortOrder = 1; + updateModel.Properties = new[] { propertyType, newPropertyType }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(1, contentType.PropertyGroups.Count); + Assert.AreEqual(2, contentType.PropertyTypes.Count()); + Assert.AreEqual(2, contentType.PropertyGroups.First().PropertyTypes!.Count); + + var allPropertyTypes = contentType.PropertyTypes.OrderBy(p => p.SortOrder).ToArray(); + Assert.AreEqual("testProperty2", allPropertyTypes.First().Alias); + Assert.AreEqual("testProperty", allPropertyTypes.Last().Alias); + + var propertyTypesInContainer = contentType.PropertyGroups.First().PropertyTypes!.OrderBy(p => p.SortOrder).ToArray(); + Assert.AreEqual("testProperty2", propertyTypesInContainer.First().Alias); + Assert.AreEqual("testProperty", propertyTypesInContainer.Last().Alias); + + Assert.IsEmpty(contentType.NoGroupPropertyTypes); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Remove_Properties(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container = CreateContainer(); + createModel.Containers = new[] { container }; + + var propertyType = CreatePropertyType("Test Property", "testProperty", containerKey: container.Key); + createModel.Properties = new[] { propertyType }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + updateModel.Containers = new[] { container }; + updateModel.Properties = Array.Empty(); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(0, contentType.PropertyGroups.Count); + Assert.AreEqual(0, contentType.PropertyTypes.Count()); + + Assert.AreEqual(0, contentType.NoGroupPropertyTypes.Count()); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Edit_Properties(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var propertyType = CreatePropertyType("Test Property", "testProperty"); + propertyType.Description = "The description"; + createModel.Properties = new[] { propertyType }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + var originalPropertyTypeKey = contentType.PropertyTypes.First().Key; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + propertyType = CreatePropertyType("Test Property 2", "testProperty", key: originalPropertyTypeKey); + propertyType.Description = "The updated description"; + updateModel.Properties = new[] { propertyType }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(0, contentType.PropertyGroups.Count); + Assert.AreEqual(1, contentType.PropertyTypes.Count()); + + var property = contentType.PropertyTypes.First(); + Assert.AreEqual("Test Property 2", property.Name); + Assert.AreEqual("testProperty", property.Alias); + Assert.AreEqual("The updated description", property.Description); + Assert.AreEqual(originalPropertyTypeKey, property.Key); + + Assert.AreEqual(1, contentType.NoGroupPropertyTypes.Count()); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Move_Properties_To_Another_Container(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container1 = CreateContainer("One"); + createModel.Containers = new[] { container1 }; + + var propertyType1 = CreatePropertyType("Test Property 1", "testProperty1", containerKey: container1.Key); + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2", containerKey: container1.Key); + createModel.Properties = new[] { propertyType1, propertyType2 }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + var container2 = CreateContainer("Two"); + container2.SortOrder = 0; + container1.SortOrder = 1; + updateModel.Containers = new[] { container1, container2 }; + propertyType2.ContainerKey = container2.Key; + updateModel.Properties = new[] { propertyType1, propertyType2 }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(2, contentType.PropertyGroups.Count); + Assert.AreEqual(2, contentType.PropertyTypes.Count()); + Assert.AreEqual(1, contentType.PropertyGroups.First().PropertyTypes!.Count); + Assert.AreEqual(1, contentType.PropertyGroups.Last().PropertyTypes!.Count); + Assert.IsEmpty(contentType.NoGroupPropertyTypes); + + var sortedPropertyGroups = contentType.PropertyGroups.OrderBy(g => g.SortOrder).ToArray(); + Assert.AreEqual("testProperty2", sortedPropertyGroups.First().PropertyTypes!.Single().Alias); + Assert.AreEqual("testProperty1", sortedPropertyGroups.Last().PropertyTypes!.Single().Alias); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Rearrange_Containers(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container1 = CreateContainer("One"); + container1.SortOrder = 0; + var container2 = CreateContainer("Two"); + container2.SortOrder = 1; + createModel.Containers = new[] { container1, container2 }; + + var propertyType1 = CreatePropertyType("Test Property 1", "testProperty1", containerKey: container1.Key); + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2", containerKey: container2.Key); + createModel.Properties = new[] { propertyType1, propertyType2 }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + container2.SortOrder = 0; + container1.SortOrder = 1; + updateModel.Containers = new[] { container1, container2 }; + updateModel.Properties = new[] { propertyType1, propertyType2 }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(2, contentType.PropertyGroups.Count); + + var sortedPropertyGroups = contentType.PropertyGroups.OrderBy(g => g.SortOrder).ToArray(); + Assert.AreEqual("testProperty2", sortedPropertyGroups.First().PropertyTypes!.Single().Alias); + Assert.AreEqual("testProperty1", sortedPropertyGroups.Last().PropertyTypes!.Single().Alias); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Make_Properties_Orphaned(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container1 = CreateContainer("One"); + var container2 = CreateContainer("Two"); + createModel.Containers = new[] { container1, container2 }; + + var propertyType1 = CreatePropertyType("Test Property 1", "testProperty1", containerKey: container1.Key); + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2", containerKey: container2.Key); + createModel.Properties = new[] { propertyType1, propertyType2 }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + updateModel.Containers = new[] { container1 }; + propertyType2.ContainerKey = null; + updateModel.Properties = new[] { propertyType1, propertyType2 }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(1, contentType.PropertyGroups.Count); + Assert.AreEqual(2, contentType.PropertyTypes.Count()); + Assert.AreEqual(1, contentType.PropertyGroups.First().PropertyTypes!.Count); + Assert.AreEqual(1, contentType.NoGroupPropertyTypes.Count()); + + Assert.AreEqual("testProperty1", contentType.PropertyGroups.First().PropertyTypes!.Single().Alias); + Assert.AreEqual("testProperty2", contentType.NoGroupPropertyTypes.Single().Alias); + } + + [Test] + public async Task Can_Add_Compositions() + { + var propertyType1 = CreatePropertyType("Test Property 1", "testProperty1"); + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2"); + + var compositionCreateModel = CreateCreateModel("Composition", "composition"); + compositionCreateModel.Properties = new[] { propertyType1 }; + var compositionContentType = (await ContentTypeEditingService.CreateAsync(compositionCreateModel, Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.Properties = new[] { propertyType2 }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.Properties = new[] { propertyType2 }; + updateModel.Compositions = new[] + { + new Composition { Key = compositionContentType.Key, CompositionType = CompositionType.Composition } + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(1, contentType.ContentTypeComposition.Count()); + Assert.AreEqual(compositionContentType.Key, contentType.ContentTypeComposition.Single().Key); + var propertyTypeAliases = contentType.CompositionPropertyTypes.Select(c => c.Alias).ToArray(); + Assert.AreEqual(2, propertyTypeAliases.Length); + Assert.IsTrue(propertyTypeAliases.Contains("testProperty1")); + Assert.IsTrue(propertyTypeAliases.Contains("testProperty2")); + } + + [Test] + public async Task Can_Reapply_Compositions() + { + var propertyType1 = CreatePropertyType("Test Property 1", "testProperty1"); + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2"); + + var compositionCreateModel = CreateCreateModel("Composition", "composition"); + compositionCreateModel.Properties = new[] { propertyType1 }; + var compositionContentType = (await ContentTypeEditingService.CreateAsync(compositionCreateModel, Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.Properties = new[] { propertyType2 }; + createModel.Compositions = new[] + { + new Composition { Key = compositionContentType.Key, CompositionType = CompositionType.Composition } + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.Properties = new[] { propertyType2 }; + updateModel.Compositions = new[] + { + new Composition { Key = compositionContentType.Key, CompositionType = CompositionType.Composition } + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(1, contentType.ContentTypeComposition.Count()); + Assert.AreEqual(compositionContentType.Key, contentType.ContentTypeComposition.Single().Key); + var propertyTypeAliases = contentType.CompositionPropertyTypes.Select(c => c.Alias).ToArray(); + Assert.AreEqual(2, propertyTypeAliases.Length); + Assert.IsTrue(propertyTypeAliases.Contains("testProperty1")); + Assert.IsTrue(propertyTypeAliases.Contains("testProperty2")); + } + + [Test] + public async Task Can_Remove_Compositions() + { + var propertyType1 = CreatePropertyType("Test Property 1", "testProperty1"); + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2"); + + var compositionCreateModel = CreateCreateModel("Composition", "composition"); + compositionCreateModel.Properties = new[] { propertyType1 }; + var compositionContentType = (await ContentTypeEditingService.CreateAsync(compositionCreateModel, Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.Properties = new[] { propertyType2 }; + createModel.Compositions = new[] + { + new Composition { Key = compositionContentType.Key, CompositionType = CompositionType.Composition } + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.Properties = new[] { propertyType2 }; + updateModel.Compositions = Array.Empty(); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.IsEmpty(contentType.ContentTypeComposition); + Assert.AreEqual(1, contentType.CompositionPropertyTypes.Count()); + Assert.AreEqual("testProperty2", contentType.CompositionPropertyTypes.Single().Alias); + } + + [Test] + public async Task Can_Reapply_Inheritance() + { + var parentContentType = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key } + }); + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + var originalPath = contentType.Path; + + var updateModel = CreateUpdateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key } + }); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(1, contentType.ContentTypeComposition.Count()); + Assert.AreEqual(parentContentType.Id, contentType.ParentId); + Assert.AreEqual(originalPath, contentType.Path); + Assert.AreEqual($"-1,{parentContentType.Id},{contentType.Id}", contentType.Path); + } + + [Test] + public async Task Can_Update_History_Cleanup() + { + var createModel = CreateCreateModel("Test", "test"); + createModel.Cleanup = new ContentTypeCleanup + { + PreventCleanup = true, KeepAllVersionsNewerThanDays = 123, KeepLatestVersionPerDayForDays = 456 + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test updated", "testUpdated"); + updateModel.Cleanup = new ContentTypeCleanup + { + PreventCleanup = false, KeepAllVersionsNewerThanDays = 234, KeepLatestVersionPerDayForDays = 567 + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + Assert.IsNotNull(contentType.HistoryCleanup); + Assert.IsFalse(contentType.HistoryCleanup.PreventCleanup); + Assert.AreEqual(234, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays); + Assert.AreEqual(567, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Cannot_Move_Properties_To_Non_Existing_Containers(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container = CreateContainer("One"); + createModel.Containers = new[] { container }; + + var property = CreatePropertyType("Test Property", "testProperty", containerKey: container.Key); + createModel.Properties = new[] { property }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + property.ContainerKey = Guid.NewGuid(); + updateModel.Containers = new[] { container }; + updateModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Cannot_Move_Containers_To_Non_Existing_Containers(bool isElement) + { + var createModel = CreateCreateModel("Test", "test", isElement: isElement); + var container = CreateContainer("One"); + createModel.Containers = new[] { container }; + + var property = CreatePropertyType("Test Property", "testProperty", containerKey: container.Key); + createModel.Properties = new[] { property }; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test", isElement: isElement); + container.ParentKey = Guid.NewGuid(); + updateModel.Containers = new[] { container }; + updateModel.Properties = new[] { property }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + } + + [Test] + public async Task Cannot_Add_Self_As_Composition() + { + var property = CreatePropertyType("Test Property", "testProperty"); + var createModel = CreateCreateModel("Test", "test"); + createModel.Properties = new[] { property }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.Properties = new[] { property }; + updateModel.Compositions = new[] + { + new Composition { Key = contentType.Key, CompositionType = CompositionType.Composition } + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); + } + + [Test] + public async Task Cannot_Change_Inheritance() + { + var parentContentType1 = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent1"), Constants.Security.SuperUserKey)).Result!; + var parentContentType2 = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent2"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType1.Key } + }); + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType2.Key } + }); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } + + [Test] + public async Task Cannot_Add_Inheritance() + { + var parentContentType = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!; + var contentType = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Child"), Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key } + }); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } + + [Test] + public async Task Cannot_Add_Multiple_Inheritance() + { + var parentContentType1 = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent1"), Constants.Security.SuperUserKey)).Result!; + var parentContentType2 = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent2"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType1.Key } + }); + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType1.Key }, + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType2.Key } + }); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } + + [Test] + public async Task Cannot_Add_Self_As_Inheritance() + { + var createModel = CreateCreateModel("Test", "test"); + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.Compositions = new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = contentType.Key } + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } + + [Test] + public async Task Cannot_Add_Inheritance_When_Created_In_A_Folder() + { + EntityContainer container = ContentTypeService.CreateContainer(Constants.System.Root, Guid.NewGuid(), "Test folder").Result!.Entity; + + var parentContentType = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!; + var contentType = (await ContentTypeEditingService.CreateAsync(CreateCreateModel("Child", parentKey: container.Key), Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel( + "Child", + compositions: new Composition[] + { + new() { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key } + }); + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } + + [TestCase(CompositionType.Composition, CompositionType.Inheritance)] + [TestCase(CompositionType.Inheritance, CompositionType.Composition)] + public async Task Cannot_Change_Composition_To_Inheritance(CompositionType from, CompositionType to) + { + var compositionCreateModel = CreateCreateModel("Composition", "composition"); + var compositionContentType = (await ContentTypeEditingService.CreateAsync(compositionCreateModel, Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.Compositions = new[] + { + new Composition { Key = compositionContentType.Key, CompositionType = from } + }; + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.Compositions = new[] + { + new Composition { Key = compositionContentType.Key, CompositionType = to } + }; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs new file mode 100644 index 0000000000..12b16e9832 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs @@ -0,0 +1,92 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using PropertyTypeValidation = Umbraco.Cms.Core.Models.ContentTypeEditing.PropertyTypeValidation; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest( + Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true)] +public partial class ContentTypeEditingServiceTests : UmbracoIntegrationTest +{ + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private const string TabContainerType = "Tab"; + + private const string GroupContainerType = "Group"; + + private ContentTypeCreateModel CreateCreateModel( + string name = "Test", + string? alias = null, + Guid? key = null, + bool isElement = false, + Guid? parentKey = null, + IEnumerable? propertyTypes = null, + IEnumerable? containers = null, + IEnumerable? compositions = null) => + new() + { + Name = name, + Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name), + Key = key ?? Guid.NewGuid(), + ContainerKey = parentKey, + Properties = propertyTypes ?? Enumerable.Empty(), + Containers = containers ?? Enumerable.Empty(), + Compositions = compositions ?? Enumerable.Empty(), + IsElement = isElement + }; + + private ContentTypeUpdateModel CreateUpdateModel( + string name = "Test", + string? alias = null, + bool isElement = false, + IEnumerable? propertyTypes = null, + IEnumerable? containers = null, + IEnumerable? compositions = null) => + new() + { + Name = name, + Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name), + Properties = propertyTypes ?? Enumerable.Empty(), + Containers = containers ?? Enumerable.Empty(), + Compositions = compositions ?? Enumerable.Empty(), + IsElement = isElement + }; + + private ContentTypePropertyTypeModel CreatePropertyType( + string name = "Title", + string? alias = null, + Guid? key = null, + Guid? containerKey = null, + Guid? dataTypeKey = null) => + new() + { + Name = name, + Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name), + Key = key ?? Guid.NewGuid(), + ContainerKey = containerKey, + DataTypeKey = dataTypeKey ?? Constants.DataTypes.Guids.TextstringGuid, + Validation = new PropertyTypeValidation(), + Appearance = new PropertyTypeAppearance(), + }; + + private ContentTypePropertyContainerModel CreateContainer( + string name = "Container", + string type = TabContainerType, + Guid? key = null) => + new() + { + Name = name, + Type = type, + Key = key ?? Guid.NewGuid(), + }; +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Create.cs new file mode 100644 index 0000000000..66d4f30838 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Create.cs @@ -0,0 +1,117 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaTypeEditingServiceTests +{ + [Test] + public async Task Can_Create_With_All_Basic_Settings() + { + var createModel = CreateCreateModel("Test Media Type", "testMediaType"); + createModel.Description = "This is the Test description"; + createModel.Icon = "icon icon-something"; + createModel.AllowedAsRoot = true; + var result = await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + // Ensure it's actually persisted + var mediaType = await MediaTypeService.GetAsync(result.Result!.Key); + + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + Assert.IsNotNull(mediaType); + Assert.AreEqual("testMediaType", mediaType.Alias); + Assert.AreEqual("Test Media Type", mediaType.Name); + Assert.AreEqual(result.Result.Id, mediaType.Id); + Assert.AreEqual(result.Result.Key, mediaType.Key); + Assert.AreEqual("This is the Test description", mediaType.Description); + Assert.AreEqual("icon icon-something", mediaType.Icon); + Assert.IsTrue(mediaType.AllowedAsRoot); + }); + } + + [Test] + public async Task Can_Create_In_A_Folder() + { + var containerResult = MediaTypeService.CreateContainer(Constants.System.Root, Guid.NewGuid(), "Test folder"); + Assert.IsTrue(containerResult.Success); + var container = containerResult.Result?.Entity; + Assert.IsNotNull(container); + + var createModel = CreateCreateModel("Test", "test", parentKey: container.Key); + var result = await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted in the folder + var mediaType = await MediaTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(mediaType); + Assert.AreEqual(container.Id, mediaType.ParentId); + } + + [Test] + public async Task Can_Create_With_Properties_In_A_Container() + { + var createModel = CreateCreateModel("Test", "test"); + var container = CreateContainer(); + createModel.Containers = new[] { container }; + + var propertyType = CreatePropertyType(name: "Test Property", alias: "testProperty", containerKey: container.Key); + createModel.Properties = new[] { propertyType }; + + var result = await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + var mediaType = await MediaTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(mediaType); + Assert.AreEqual(1, mediaType.PropertyGroups.Count); + Assert.AreEqual(1, mediaType.PropertyTypes.Count()); + Assert.AreEqual(1, mediaType.PropertyGroups.First().PropertyTypes!.Count); + Assert.AreEqual("testProperty", mediaType.PropertyTypes.First().Alias); + Assert.AreEqual("testProperty", mediaType.PropertyGroups.First().PropertyTypes!.First().Alias); + Assert.IsEmpty(mediaType.NoGroupPropertyTypes); + } + + [Test] + public async Task Can_Create_As_Child() + { + var parentProperty = CreatePropertyType("Parent Property", "parentProperty"); + var parentModel = CreateCreateModel( + name: "Parent", + propertyTypes: new[] { parentProperty }); + + var parentResult = await MediaTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey); + Assert.IsTrue(parentResult.Success); + + var childProperty = CreatePropertyType("Child Property", "childProperty"); + var parentKey = parentResult.Result!.Key; + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = parentKey, + }, + }; + + var childModel = CreateCreateModel( + name: "Child", + propertyTypes: new[] { childProperty }, + compositions: composition); + + var result = await MediaTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + + var mediaType = result.Result!; + + Assert.AreEqual(parentResult.Result.Id, mediaType.ParentId); + Assert.AreEqual(1, mediaType.ContentTypeComposition.Count()); + Assert.AreEqual(parentResult.Result.Key, mediaType.ContentTypeComposition.FirstOrDefault()?.Key); + Assert.AreEqual(2, mediaType.CompositionPropertyTypes.Count()); + Assert.IsTrue(mediaType.CompositionPropertyTypes.Any(x => x.Alias == parentProperty.Alias)); + Assert.IsTrue(mediaType.CompositionPropertyTypes.Any(x => x.Alias == childProperty.Alias)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs new file mode 100644 index 0000000000..d9cc936f21 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs @@ -0,0 +1,116 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaTypeEditingServiceTests +{ + [Test] + public async Task Can_Update_All_Basic_Settings() + { + var createModel = CreateCreateModel("Test Media Type", "testMediaType"); + var mediaType = (await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test updated", "testUpdated"); + updateModel.Description = "This is the Test description updated"; + updateModel.Icon = "icon icon-something-updated"; + updateModel.AllowedAsRoot = false; + + var result = await MediaTypeEditingService.UpdateAsync(mediaType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + mediaType = await MediaTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(mediaType); + + Assert.AreEqual("testUpdated", mediaType.Alias); + Assert.AreEqual("Test updated", mediaType.Name); + Assert.AreEqual(result.Result.Id, mediaType.Id); + Assert.AreEqual(result.Result.Key, mediaType.Key); + Assert.AreEqual("This is the Test description updated", mediaType.Description); + Assert.AreEqual("icon icon-something-updated", mediaType.Icon); + Assert.IsFalse(mediaType.AllowedAsRoot); + } + + [Test] + public async Task Can_Add_Allowed_Types() + { + var allowedOne = (await MediaTypeEditingService.CreateAsync(CreateCreateModel("Allowed One", "allowedOne"), Constants.Security.SuperUserKey)).Result!; + var allowedTwo = (await MediaTypeEditingService.CreateAsync(CreateCreateModel("Allowed Two", "allowedTwo"), Constants.Security.SuperUserKey)).Result!; + var allowedThree = (await MediaTypeEditingService.CreateAsync(CreateCreateModel("Allowed Three", "allowedThree"), Constants.Security.SuperUserKey)).Result!; + + var createModel = CreateCreateModel("Test", "test"); + createModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedOne.Key, 10, allowedOne.Alias), + new ContentTypeSort(allowedTwo.Key, 20, allowedTwo.Alias), + }; + var mediaType = (await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + var updateModel = CreateUpdateModel("Test", "test"); + updateModel.AllowedContentTypes = new[] + { + new ContentTypeSort(allowedTwo.Key, 20, allowedTwo.Alias), + new ContentTypeSort(allowedThree.Key, 30, allowedThree.Alias), + }; + + var result = await MediaTypeEditingService.UpdateAsync(mediaType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + mediaType = await MediaTypeService.GetAsync(result.Result.Key); + Assert.IsNotNull(mediaType); + + var allowedContentTypes = mediaType.AllowedContentTypes?.ToArray(); + Assert.IsNotNull(allowedContentTypes); + Assert.AreEqual(2, allowedContentTypes.Length); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 0 && c.Alias == allowedTwo.Alias)); + Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedThree.Key && c.SortOrder == 1 && c.Alias == allowedThree.Alias)); + } + + [Test] + public async Task Can_Edit_Properties() + { + var createModel = CreateCreateModel("Test", "test"); + var propertyType = CreatePropertyType("Test Property", "testProperty"); + propertyType.Description = "The description"; + createModel.Properties = new[] { propertyType }; + + var mediaType = (await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + var originalPropertyTypeKey = mediaType.PropertyTypes.First().Key; + + var updateModel = CreateUpdateModel("Test", "test"); + propertyType = CreatePropertyType("Test Property Updated", "testProperty", key: originalPropertyTypeKey); + propertyType.Description = "The updated description"; + propertyType.SortOrder = 10; + var propertyType2 = CreatePropertyType("Test Property 2", "testProperty2"); + propertyType2.Description = "The description 2"; + propertyType2.SortOrder = 5; + updateModel.Properties = new[] { propertyType, propertyType2 }; + + var result = await MediaTypeEditingService.UpdateAsync(mediaType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + mediaType = await MediaTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(mediaType); + Assert.AreEqual(2, mediaType.PropertyTypes.Count()); + + var property1 = mediaType.PropertyTypes.First(); + Assert.AreEqual("Test Property 2", property1.Name); + Assert.AreEqual("testProperty2", property1.Alias); + Assert.AreEqual("The description 2", property1.Description); + Assert.AreEqual(5, property1.SortOrder); + var property2 = mediaType.PropertyTypes.Last(); + Assert.AreEqual("Test Property Updated", property2.Name); + Assert.AreEqual("testProperty", property2.Alias); + Assert.AreEqual("The updated description", property2.Description); + Assert.AreEqual(originalPropertyTypeKey, property2.Key); + Assert.AreEqual(10, property2.SortOrder); + + Assert.AreEqual(2, mediaType.NoGroupPropertyTypes.Count()); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.cs new file mode 100644 index 0000000000..575912dd7d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.cs @@ -0,0 +1,92 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using PropertyTypeValidation = Umbraco.Cms.Core.Models.ContentTypeEditing.PropertyTypeValidation; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +/// +/// Tests for the media type editing service. Please notice that a lot of functional test is covered by the content type +/// editing service tests, since these services share the same base implementation. +/// +[TestFixture] +[UmbracoTest( + Database = UmbracoTestOptions.Database.NewSchemaPerTest, + PublishedRepositoryEvents = true, + WithApplication = true)] +public partial class MediaTypeEditingServiceTests : UmbracoIntegrationTest +{ + private IMediaTypeEditingService MediaTypeEditingService => GetRequiredService(); + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private const string TabContainerType = "Tab"; + + private const string GroupContainerType = "Group"; + + private MediaTypeCreateModel CreateCreateModel( + string name = "Test", + string? alias = null, + Guid? key = null, + Guid? parentKey = null, + IEnumerable? propertyTypes = null, + IEnumerable? containers = null, + IEnumerable? compositions = null) => + new() + { + Name = name, + Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name), + Key = key ?? Guid.NewGuid(), + ParentKey = parentKey, + Properties = propertyTypes ?? Enumerable.Empty(), + Containers = containers ?? Enumerable.Empty(), + Compositions = compositions ?? Enumerable.Empty(), + }; + + private MediaTypeUpdateModel CreateUpdateModel( + string name = "Test", + string? alias = null, + IEnumerable? propertyTypes = null, + IEnumerable? containers = null, + IEnumerable? compositions = null) => + new() + { + Name = name, + Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name), + Properties = propertyTypes ?? Enumerable.Empty(), + Containers = containers ?? Enumerable.Empty(), + Compositions = compositions ?? Enumerable.Empty() + }; + + private MediaTypePropertyTypeModel CreatePropertyType( + string name = "Title", + string? alias = null, + Guid? key = null, + Guid? containerKey = null, + Guid? dataTypeKey = null) => + new() + { + Name = name, + Alias = alias ?? ShortStringHelper.CleanStringForSafeAlias(name), + Key = key ?? Guid.NewGuid(), + ContainerKey = containerKey, + DataTypeKey = dataTypeKey ?? Constants.DataTypes.Guids.TextstringGuid, + Validation = new PropertyTypeValidation(), + Appearance = new PropertyTypeAppearance(), + }; + + private MediaTypePropertyContainerModel CreateContainer( + string name = "Container", + string type = TabContainerType, + Guid? key = null) => + new() + { + Name = name, + Type = type, + Key = key ?? Guid.NewGuid(), + }; +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index 49ca4322d3..594b117d21 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -808,8 +808,8 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest var contentType = repository.Get(_simpleContentType.Id); contentType.AllowedContentTypes = new List { - new(new Lazy(() => subpageContentType.Id), subpageContentType.Key, 0, subpageContentType.Alias), - new(new Lazy(() => simpleSubpageContentType.Id), simpleSubpageContentType.Key, 1, simpleSubpageContentType.Alias) + new(subpageContentType.Key, 0, subpageContentType.Alias), + new(simpleSubpageContentType.Key, 1, simpleSubpageContentType.Alias) }; repository.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 2df2a7d68f..7939e39f75 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -822,7 +822,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest ContentTypeService.Save(variantCt); invariantCt.AllowedContentTypes = - new[] { new ContentTypeSort(invariantCt.Id, 0), new ContentTypeSort(variantCt.Id, 1) }; + new[] { new ContentTypeSort(invariantCt.Key, 0, invariantCt.Alias), new ContentTypeSort(variantCt.Key, 1, variantCt.Alias) }; ContentTypeService.Save(invariantCt); // Create content diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Create.cs index 79f6430c53..1813cdff8c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Create.cs @@ -79,7 +79,7 @@ public partial class ContentEditingServiceTests { rootContentType.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => childContentType.Id), childContentType.Key, 1, childContentType.Alias) + new ContentTypeSort(childContentType.Key, 1, childContentType.Alias) }; } ContentTypeService.Save(rootContentType); @@ -435,7 +435,7 @@ public partial class ContentEditingServiceTests contentType.AllowedAsRoot = true; contentType.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType.Id), contentType.Key, 1, contentType.Alias) + new ContentTypeSort(contentType.Key, 1, contentType.Alias) }; ContentTypeService.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs index 59d699e37b..7f67f2259d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.cs @@ -185,7 +185,7 @@ public partial class ContentEditingServiceTests : UmbracoIntegrationTestWithCont contentType.AllowedContentTypes = new List { - new (new Lazy(() => contentType.Id), contentType.Key, 1, contentType.Alias) + new (contentType.Key, 1, contentType.Alias) }; ContentTypeService.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs index d603ce7212..611ed082c3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs @@ -69,18 +69,18 @@ public class ContentServicePerformanceTest : UmbracoIntegrationTest ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 }); contentType1.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType2.Id), contentType2.Key, 0, contentType2.Alias), - new ContentTypeSort(new Lazy(() => contentType3.Id), contentType3.Key, 1, contentType3.Alias) + new ContentTypeSort(contentType2.Key, 0, contentType2.Alias), + new ContentTypeSort(contentType3.Key, 1, contentType3.Alias) }; contentType2.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType1.Id), contentType1.Key, 0, contentType1.Alias), - new ContentTypeSort(new Lazy(() => contentType3.Id), contentType3.Key, 1, contentType3.Alias) + new ContentTypeSort(contentType1.Key, 0, contentType1.Alias), + new ContentTypeSort(contentType3.Key, 1, contentType3.Alias) }; contentType3.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType1.Id), contentType1.Key, 0, contentType1.Alias), - new ContentTypeSort(new Lazy(() => contentType2.Id), contentType2.Key, 1, contentType2.Alias) + new ContentTypeSort(contentType1.Key, 0, contentType1.Alias), + new ContentTypeSort(contentType2.Key, 1, contentType2.Alias) }; ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs index 8d58f1dbd1..f331b5cc62 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs @@ -652,7 +652,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest CreateAndAddTagsPropertyType(contentType); ContentTypeService.Save(contentType); contentType.AllowedContentTypes = - new[] { new ContentTypeSort(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) }; + new[] { new ContentTypeSort(contentType.Key, 0, contentType.Alias) }; var content = ContentBuilder.CreateSimpleContent(contentType, "Tagged content"); content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index fe53f5b571..79526d12c2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -1844,7 +1844,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id); contentType.AllowedContentTypes = new List { - new(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) + new(contentType.Key, 0, contentType.Alias) }; ContentTypeService.Save(contentType); @@ -1884,7 +1884,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id); contentType.AllowedContentTypes = new List { - new(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) + new(contentType.Key, 0, contentType.Alias) }; ContentTypeService.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index e7a9c67f6c..e3f36c61b1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -90,5 +90,17 @@ UserServiceCrudTests.cs + + ContentTypeEditingServiceTests.cs + + + ContentTypeEditingServiceTests.cs + + + MediaTypeEditingServiceTests.cs + + + MediaTypeEditingServiceTests.cs + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TypeExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TypeExtensionsTests.cs new file mode 100644 index 0000000000..cd2658f574 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TypeExtensionsTests.cs @@ -0,0 +1,251 @@ +using NUnit.Framework; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions; + +[TestFixture] +public class TypeExtensionsTests +{ + private string[] PublicObjectMethodNames = { nameof(GetType), nameof(ToString), nameof(Equals), nameof(GetHashCode) }; + + private string[] NonPublicObjectMethodNames = { nameof(MemberwiseClone), "Finalize" }; + + [Test] + public void Can_Get_Public_Properties_Of_Interface() + { + var properties = typeof(ITheBaseThing).GetPublicProperties(); + Assert.AreEqual(1, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(ITheBaseThing.TheBaseThingProperty), propertyNames); + } + + [Test] + public void Get_Public_Properties_Of_Interface_Contains_Inherited_Properties() + { + var properties = typeof(ITheThing).GetPublicProperties(); + Assert.AreEqual(2, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(ITheBaseThing.TheBaseThingProperty), propertyNames); + Assert.Contains(nameof(ITheThing.TheThingProperty), propertyNames); + } + + [Test] + public void Can_Get_Public_Properties_Of_Class() + { + var properties = typeof(TheBaseThing).GetPublicProperties(); + Assert.AreEqual(1, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(TheBaseThing.TheBaseThingProperty), propertyNames); + } + + [Test] + public void Get_Public_Properties_Of_Class_Contains_Inherited_Properties() + { + var properties = typeof(TheThing).GetPublicProperties(); + Assert.AreEqual(3, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(TheBaseThing.TheBaseThingProperty), propertyNames); + Assert.Contains(nameof(TheThing.TheThingProperty), propertyNames); + Assert.Contains(nameof(TheThing.TheExtraProperty), propertyNames); + } + + [Test] + public void Get_All_Properties_Of_Class_Contains_Internal_Properties() + { + var properties = typeof(TheThing).GetAllProperties(); + Assert.AreEqual(4, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(TheBaseThing.TheBaseThingProperty), propertyNames); + Assert.Contains(nameof(TheThing.TheThingProperty), propertyNames); + Assert.Contains(nameof(TheThing.TheExtraProperty), propertyNames); + Assert.Contains(nameof(TheThing.TheInternalProperty), propertyNames); + } + + [Test] + public void Can_Get_Public_Methods_Of_Interface() + { + var methods = typeof(ITheBaseThing).GetPublicMethods(); + Assert.AreEqual(2, methods.Length); + + var methodNames = methods.Select(p => p.Name).ToArray(); + Assert.Contains( $"get_{nameof(ITheBaseThing.TheBaseThingProperty)}", methodNames); + Assert.Contains( nameof(ITheBaseThing.TheBaseThingMethod), methodNames); + } + + [Test] + public void Get_Public_Methods_Of_Interface_Contains_Inherited_Methods() + { + var methods = typeof(ITheThing).GetPublicMethods(); + Assert.AreEqual(5, methods.Length); + + var methodNames = methods.Select(p => p.Name).ToArray(); + Assert.Contains( $"get_{nameof(ITheBaseThing.TheBaseThingProperty)}", methodNames); + Assert.Contains( nameof(ITheBaseThing.TheBaseThingMethod), methodNames); + Assert.Contains( $"get_{nameof(ITheThing.TheThingProperty)}", methodNames); + Assert.Contains( $"set_{nameof(ITheThing.TheThingProperty)}", methodNames); + Assert.Contains( nameof(ITheThing.TheThingMethod), methodNames); + } + + [Test] + public void Can_Get_Public_Methods_Of_Class() + { + var methods = typeof(TheBaseThing).GetPublicMethods(); + Assert.AreEqual(3 + PublicObjectMethodNames.Length, methods.Length); + + var methodNames = methods.Select(p => p.Name).ToArray(); + Assert.Contains( $"get_{nameof(TheBaseThing.TheBaseThingProperty)}", methodNames); + Assert.Contains( nameof(TheBaseThing.TheBaseThingMethod), methodNames); + Assert.Contains( nameof(TheBaseThing.TheExtraMethod), methodNames); + Assert.IsTrue(methodNames.ContainsAll(PublicObjectMethodNames)); + } + + [Test] + public void Get_Public_Methods_Of_Class_Contains_Inherited_Methods() + { + var methods = typeof(TheThing).GetPublicMethods(); + Assert.AreEqual(7 + PublicObjectMethodNames.Length, methods.Length); + + var methodNames = methods.Select(p => p.Name).ToArray(); + Assert.Contains( $"get_{nameof(TheBaseThing.TheBaseThingProperty)}", methodNames); + Assert.Contains( nameof(TheBaseThing.TheBaseThingMethod), methodNames); + Assert.Contains( nameof(TheBaseThing.TheExtraMethod), methodNames); + Assert.Contains( $"get_{nameof(TheThing.TheThingProperty)}", methodNames); + Assert.Contains( $"set_{nameof(TheThing.TheThingProperty)}", methodNames); + Assert.Contains( $"get_{nameof(TheThing.TheExtraProperty)}", methodNames); + Assert.Contains( nameof(TheThing.TheThingMethod), methodNames); + Assert.IsTrue(methodNames.ContainsAll(PublicObjectMethodNames)); + } + + [Test] + public void Can_Get_All_Methods_Of_Class() + { + var methods = typeof(TheBaseThing).GetAllMethods(); + Assert.AreEqual(4 + PublicObjectMethodNames.Length + NonPublicObjectMethodNames.Length, methods.Length); + + var methodNames = methods.Select(p => p.Name).ToArray(); + Assert.Contains( $"get_{nameof(TheBaseThing.TheBaseThingProperty)}", methodNames); + Assert.Contains( nameof(TheBaseThing.TheBaseThingMethod), methodNames); + Assert.Contains( nameof(TheBaseThing.TheExtraMethod), methodNames); + Assert.Contains( nameof(TheBaseThing.TheInternalMethod), methodNames); + Assert.IsTrue(methodNames.ContainsAll(PublicObjectMethodNames)); + Assert.IsTrue(methodNames.ContainsAll(NonPublicObjectMethodNames)); + } + + [Test] + public void Get_All_Methods_Of_Class_Contains_Inherited_Methods() + { + var methods = typeof(TheThing).GetAllMethods(); + Assert.AreEqual(9 + PublicObjectMethodNames.Length + NonPublicObjectMethodNames.Length, methods.Length); + + var methodNames = methods.Select(p => p.Name).ToArray(); + Assert.Contains( $"get_{nameof(TheBaseThing.TheBaseThingProperty)}", methodNames); + Assert.Contains( nameof(TheBaseThing.TheBaseThingMethod), methodNames); + Assert.Contains( nameof(TheBaseThing.TheExtraMethod), methodNames); + Assert.Contains( nameof(TheBaseThing.TheInternalMethod), methodNames); + Assert.Contains( $"get_{nameof(TheThing.TheThingProperty)}", methodNames); + Assert.Contains( $"set_{nameof(TheThing.TheThingProperty)}", methodNames); + Assert.Contains( $"get_{nameof(TheThing.TheExtraProperty)}", methodNames); + Assert.Contains( $"get_{nameof(TheThing.TheInternalProperty)}", methodNames); + Assert.Contains( nameof(TheThing.TheThingMethod), methodNames); + Assert.IsTrue(methodNames.ContainsAll(PublicObjectMethodNames)); + Assert.IsTrue(methodNames.ContainsAll(NonPublicObjectMethodNames)); + } + + [Test] + public void Can_Get_Public_Properties_Of_Interface_With_Internal_Declarations() + { + var properties = typeof(ITheInterfaceWithInternalDeclarations).GetPublicProperties(); + Assert.AreEqual(1, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(ITheInterfaceWithInternalDeclarations.ThePublicProperty), propertyNames); + } + + [Test] + public void Can_Get_All_Properties_Of_Interface_With_Internal_Declarations() + { + var properties = typeof(ITheInterfaceWithInternalDeclarations).GetAllProperties(); + Assert.AreEqual(2, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains(nameof(ITheInterfaceWithInternalDeclarations.ThePublicProperty), propertyNames); + Assert.Contains(nameof(ITheInterfaceWithInternalDeclarations.TheInternalProperty), propertyNames); + } + + [Test] + public void Can_Get_Public_Methods_Of_Interface_With_Internal_Declarations() + { + var properties = typeof(ITheInterfaceWithInternalDeclarations).GetPublicMethods(); + Assert.AreEqual(2, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains($"get_{nameof(ITheInterfaceWithInternalDeclarations.ThePublicProperty)}", propertyNames); + Assert.Contains(nameof(ITheInterfaceWithInternalDeclarations.ThePublicMethod), propertyNames); + } + + [Test] + public void Can_Get_All_Methods_Of_Interface_With_Internal_Declarations() + { + var properties = typeof(ITheInterfaceWithInternalDeclarations).GetAllMethods(); + Assert.AreEqual(4, properties.Length); + + var propertyNames = properties.Select(p => p.Name).ToArray(); + Assert.Contains($"get_{nameof(ITheInterfaceWithInternalDeclarations.ThePublicProperty)}", propertyNames); + Assert.Contains($"get_{nameof(ITheInterfaceWithInternalDeclarations.TheInternalProperty)}", propertyNames); + Assert.Contains(nameof(ITheInterfaceWithInternalDeclarations.ThePublicMethod), propertyNames); + Assert.Contains(nameof(ITheInterfaceWithInternalDeclarations.TheInternalMethod), propertyNames); + } + + public interface ITheThing : ITheBaseThing + { + string TheThingProperty { get; set; } + + int TheThingMethod(int input); + } + + public interface ITheBaseThing + { + string TheBaseThingProperty { get; } + + int TheBaseThingMethod(); + } + + public class TheThing : TheBaseThing, ITheThing + { + public string TheThingProperty { get; set; } + + public int TheThingMethod(int input) => throw new NotImplementedException(); + + public bool TheExtraProperty { get; } + + internal decimal TheInternalProperty { get; } + } + + public class TheBaseThing : ITheBaseThing + { + public string TheBaseThingProperty { get; } + + public int TheBaseThingMethod() => throw new NotImplementedException(); + + public void TheExtraMethod() => throw new NotImplementedException(); + + internal void TheInternalMethod() => throw new NotImplementedException(); + } + + // it's not pretty, but it is possible to declare internal properties and methods in a public interface... we need to test those as well :/ + public interface ITheInterfaceWithInternalDeclarations + { + public int ThePublicProperty { get; } + + internal int TheInternalProperty { get; } + + public string ThePublicMethod(); + + internal string TheInternalMethod(); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeTests.cs index 5bc8715c18..abd384a19a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeTests.cs @@ -55,7 +55,7 @@ public class ContentTypeTests var clone = (ContentTypeSort)contentType.DeepClone(); Assert.AreNotSame(clone, contentType); Assert.AreEqual(clone, contentType); - Assert.AreEqual(clone.Id.Value, contentType.Id.Value); + Assert.AreEqual(clone.Key, contentType.Key); Assert.AreEqual(clone.SortOrder, contentType.SortOrder); Assert.AreEqual(clone.Alias, contentType.Alias); } @@ -64,7 +64,7 @@ public class ContentTypeTests { var builder = new ContentTypeSortBuilder(); return builder - .WithId(3) + .WithKey(new Guid("4CAE063E-0BE1-4972-B10C-A3D9BB7DE856")) .WithSortOrder(4) .WithAlias("test") .Build(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/AllowedContentTypeDetail.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/AllowedContentTypeDetail.cs index e423444132..e048845dad 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/AllowedContentTypeDetail.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/AllowedContentTypeDetail.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders; public class AllowedContentTypeDetail { - public int Id { get; set; } + public Guid Key { get; set; } public string Alias { get; set; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/ContentTypeBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/ContentTypeBuilderTests.cs index b23cd5209e..e6313fd081 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/ContentTypeBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/ContentTypeBuilderTests.cs @@ -39,8 +39,8 @@ public class ContentTypeBuilderTests new PropertyTypeDetail { Alias = "bodyText", Name = "Body Text", SortOrder = 2, DataTypeId = -87 }; var testTemplate1 = new TemplateDetail { Id = 200, Alias = "template1", Name = "Template 1" }; var testTemplate2 = new TemplateDetail { Id = 201, Alias = "template2", Name = "Template 2" }; - var testAllowedContentType1 = new AllowedContentTypeDetail { Id = 300, Alias = "subType1", SortOrder = 1 }; - var testAllowedContentType2 = new AllowedContentTypeDetail { Id = 301, Alias = "subType2", SortOrder = 2 }; + var testAllowedContentType1 = new AllowedContentTypeDetail { Key = new Guid("72EC4F7B-ACF0-43AA-AD92-0EC878223485"), Alias = "subType1", SortOrder = 1 }; + var testAllowedContentType2 = new AllowedContentTypeDetail { Key = new Guid("68FE62F0-95A9-471E-839F-F5A6B9CCA7A9"), Alias = "subType2", SortOrder = 2 }; var builder = new ContentTypeBuilder(); @@ -91,12 +91,12 @@ public class ContentTypeBuilderTests .Done() .WithDefaultTemplateId(testTemplate1.Id) .AddAllowedContentType() - .WithId(testAllowedContentType1.Id) + .WithKey(testAllowedContentType1.Key) .WithAlias(testAllowedContentType1.Alias) .WithSortOrder(testAllowedContentType1.SortOrder) .Done() .AddAllowedContentType() - .WithId(testAllowedContentType2.Id) + .WithKey(testAllowedContentType2.Key) .WithAlias(testAllowedContentType2.Alias) .WithSortOrder(testAllowedContentType2.SortOrder) .Done() @@ -134,7 +134,7 @@ public class ContentTypeBuilderTests var allowedContentTypes = contentType.AllowedContentTypes.ToList(); Assert.AreEqual(2, allowedContentTypes.Count); - Assert.AreEqual(testAllowedContentType1.Id, allowedContentTypes[0].Id.Value); + Assert.AreEqual(testAllowedContentType1.Key, allowedContentTypes[0].Key); Assert.AreEqual(testAllowedContentType1.Alias, allowedContentTypes[0].Alias); Assert.AreEqual(testAllowedContentType1.SortOrder, allowedContentTypes[0].SortOrder); }