diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/FolderMediaTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/FolderMediaTypeItemController.cs new file mode 100644 index 0000000000..970ed0af28 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/FolderMediaTypeItemController.cs @@ -0,0 +1,37 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.MediaType.Item; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.ContentTypeEditing; + +namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Item; + +[ApiVersion("1.0")] +public class FolderMediaTypeItemController : MediaTypeItemControllerBase +{ + private readonly IMediaTypeEditingService _mediaTypeEditingService; + private readonly IUmbracoMapper _mapper; + + public FolderMediaTypeItemController(IMediaTypeEditingService mediaTypeEditingService, IUmbracoMapper mapper) + { + _mediaTypeEditingService = mediaTypeEditingService; + _mapper = mapper; + } + + [HttpGet("folders")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] + public async Task Folders(CancellationToken cancellationToken, int skip = 0, int take = 100) + { + PagedModel mediaTypes = await _mediaTypeEditingService.GetFolderMediaTypes(skip, take); + + var result = new PagedModel + { + Items = _mapper.MapEnumerable(mediaTypes.Items), + Total = mediaTypes.Total + }; + return Ok(result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 3da37e1149..537fd63637 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -13217,6 +13217,61 @@ ] } }, + "/umbraco/management/api/v1/item/media-type/folders": { + "get": { + "tags": [ + "Media Type" + ], + "operationId": "GetItemMediaTypeFolders", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedModelMediaTypeItemResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/item/media-type/search": { "get": { "tags": [ @@ -45027,4 +45082,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs index 36b9ef40e2..e712fc66a2 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs @@ -16,4 +16,6 @@ public interface IMediaTypeEditingService IEnumerable currentPropertyAliases); Task> GetMediaTypesForFileExtensionAsync(string fileExtension, int skip, int take); + + Task> GetFolderMediaTypes(int skip, int take); } diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs index 47fc1b8f7a..86b9989ef8 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs @@ -102,6 +102,35 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase> GetFolderMediaTypes(int skip, int take) + { + // we'll consider it a "folder" media type if it: + // - does not contain an umbracoFile property + // - has any allowed types below itself + var folderMediaTypes = _mediaTypeService + .GetAll() + .Where(mt => + mt.CompositionPropertyTypes.Any(pt => pt.Alias == Constants.Conventions.Media.File) is false + && mt.AllowedContentTypes?.Any() is true) + .ToList(); + + // as a special case, the "Folder" system media type must always be included + if (folderMediaTypes.Any(mediaType => mediaType.Alias == Constants.Conventions.MediaTypes.Folder) is false) + { + IMediaType? defaultFolderMediaType = _mediaTypeService.Get(Constants.Conventions.MediaTypes.Folder); + if (defaultFolderMediaType is not null) + { + folderMediaTypes.Add(defaultFolderMediaType); + } + } + + return Task.FromResult(new PagedModel + { + Items = folderMediaTypes.Skip(skip).Take(take), + Total = folderMediaTypes.Count + }); + } + protected override IMediaType CreateContentType(IShortStringHelper shortStringHelper, int parentId) => new MediaType(shortStringHelper, parentId); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.GetFolderMediaTypes.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.GetFolderMediaTypes.cs new file mode 100644 index 0000000000..a86900e766 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.GetFolderMediaTypes.cs @@ -0,0 +1,84 @@ +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_Get_Default_Folder_Media_Type() + { + var folderMediaTypes = await MediaTypeEditingService.GetFolderMediaTypes( 0, 100); + Assert.AreEqual(1, folderMediaTypes.Total); + Assert.AreEqual(Constants.Conventions.MediaTypes.Folder, folderMediaTypes.Items.First().Alias); + } + + [Test] + public async Task Can_Yield_Multiple_Folder_Media_Types() + { + var imageMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Image); + + var createModel = MediaTypeCreateModel("Test Media Type", "testMediaType"); + createModel.Description = "This is the Test description"; + createModel.Icon = "icon icon-something"; + createModel.AllowedAsRoot = true; + createModel.Properties = []; + createModel.AllowedContentTypes = new[] + { + new ContentTypeSort { Alias = imageMediaType.Alias, Key = imageMediaType.Key } + }; + + await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + var folderMediaTypes = await MediaTypeEditingService.GetFolderMediaTypes( 0, 100); + Assert.AreEqual(2, folderMediaTypes.Total); + Assert.Multiple(() => + { + var aliases = folderMediaTypes.Items.Select(i => i.Alias).ToArray(); + Assert.IsTrue(aliases.Contains(Constants.Conventions.MediaTypes.Folder)); + Assert.IsTrue(aliases.Contains("testMediaType")); + }); + } + + [Test] + public async Task System_Folder_Media_Type_Is_Always_Included() + { + // update the system "Folder" media type so it does not pass the conventions for a "folder" media type + // - remove all allowed child content types + // - add an "umbracoFile" property + var systemFolderMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Folder)!; + var updateModel = MediaTypeUpdateModel(Constants.Conventions.MediaTypes.Folder, Constants.Conventions.MediaTypes.Folder); + updateModel.Properties = new[] + { + MediaTypePropertyTypeModel("Test Property", Constants.Conventions.Media.File) + }; + updateModel.AllowedContentTypes = []; + + var updateResult = await MediaTypeEditingService.UpdateAsync(systemFolderMediaType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(updateResult.Success); + + // despite the system "Folder" media type no longer living up to the "folder" media type requirements, + // it should still be considered a "folder" + var folderMediaTypes = await MediaTypeEditingService.GetFolderMediaTypes( 0, 100); + Assert.AreEqual(1, folderMediaTypes.Total); + Assert.AreEqual(Constants.Conventions.MediaTypes.Folder, folderMediaTypes.Items.First().Alias); + } + + [Test] + public async Task Folder_Media_Types_Must_Have_Allowed_Content_Types() + { + var createModel = MediaTypeCreateModel("Test Media Type", "testMediaType"); + createModel.Description = "This is the Test description"; + createModel.Icon = "icon icon-something"; + createModel.AllowedAsRoot = true; + createModel.Properties = []; + createModel.AllowedContentTypes = []; + + await MediaTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + var folderMediaTypes = await MediaTypeEditingService.GetFolderMediaTypes( 0, 100); + Assert.AreEqual(1, folderMediaTypes.Total); + Assert.AreEqual(Constants.Conventions.MediaTypes.Folder, folderMediaTypes.Items.First().Alias); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 2ffdc7c976..b7ab21b062 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -148,6 +148,12 @@ ContentBlueprintEditingServiceTests.cs + + MediaTypeEditingServiceTests.cs + + + MediaTypeEditingServiceTests.cs +