CRUD API for media + get-by-id for media type (#13934)

* CRUD API for media + get by ID for media types

* A little housekeeping for documents (align with media)

* Update Open API json

* Add messages to NotFound results (both content and media)

* Review changes; use same model for content and media URLs + return bad request when trying to move something to trash that is already in trash

* Fix bad merge + rename base (response) classes appropriately between both media and content types
This commit is contained in:
Kenn Jacobsen
2023-03-13 15:02:30 +01:00
committed by GitHub
parent 8df3bca5fe
commit c0975f341c
51 changed files with 1192 additions and 146 deletions

View File

@@ -32,6 +32,10 @@ public class ContentControllerBase : ManagementApiControllerBase
.WithDetail("The selected template was not allowed for the operation.")
.Build()),
ContentEditingOperationStatus.PropertyTypeNotFound => NotFound("One or more property types could not be found"),
ContentEditingOperationStatus.InTrash => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Content is in the recycle bin")
.WithDetail("Could not perform the operation because the targeted content was in the recycle bin.")
.Build()),
ContentEditingOperationStatus.Unknown => StatusCode(StatusCodes.Status500InternalServerError, "Unknown error. Please see the log for more details."),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status.")
};

View File

@@ -27,7 +27,7 @@ public class ByKeyDocumentController : DocumentControllerBase
IContent? content = await _contentEditingService.GetAsync(key);
if (content == null)
{
return NotFound();
return DocumentNotFound();
}
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content);

View File

@@ -11,4 +11,5 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))]
public abstract class DocumentControllerBase : ContentControllerBase
{
protected IActionResult DocumentNotFound() => NotFound("The requested Document could not be found");
}

View File

@@ -13,18 +13,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
public class UpdateDocumentController : DocumentControllerBase
{
private readonly IContentService _contentService;
private readonly IContentEditingService _contentEditingService;
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public UpdateDocumentController(
IContentService contentService,
IContentEditingService contentEditingService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentService = contentService;
_contentEditingService = contentEditingService;
_documentEditingPresentationFactory = documentEditingPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
@@ -37,10 +34,10 @@ public class UpdateDocumentController : DocumentControllerBase
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid key, UpdateDocumentRequestModel requestModel)
{
IContent? content = _contentService.GetById(key);
IContent? content = await _contentEditingService.GetAsync(key);
if (content == null)
{
return NotFound();
return DocumentNotFound();
}
ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel);

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Media;
public class ByKeyMediaController : MediaControllerBase
{
private readonly IMediaEditingService _mediaEditingService;
private readonly IMediaPresentationModelFactory _mediaPresentationModelFactory;
public ByKeyMediaController(IMediaEditingService mediaEditingService, IMediaPresentationModelFactory mediaPresentationModelFactory)
{
_mediaEditingService = mediaEditingService;
_mediaPresentationModelFactory = mediaPresentationModelFactory;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid key)
{
IMedia? media = await _mediaEditingService.GetAsync(key);
if (media == null)
{
return MediaNotFound();
}
MediaResponseModel model = await _mediaPresentationModelFactory.CreateResponseModelAsync(media);
return Ok(model);
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Media;
public class CreateMediaController : MediaControllerBase
{
private readonly IMediaEditingService _mediaEditingService;
private readonly IMediaEditingPresentationFactory _mediaEditingPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public CreateMediaController(IMediaEditingService mediaEditingService, IMediaEditingPresentationFactory mediaEditingPresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_mediaEditingService = mediaEditingService;
_mediaEditingPresentationFactory = mediaEditingPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CreateMediaRequestModel createRequestModel)
{
MediaCreateModel model = _mediaEditingPresentationFactory.MapCreateModel(createRequestModel);
Attempt<IMedia?, ContentEditingOperationStatus> result = await _mediaEditingService.CreateAsync(model, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtAction<ByKeyMediaController>(controller => nameof(controller.ByKey), result.Result!.Key)
: ContentEditingOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Content;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Media;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.Media)]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))]
public class MediaControllerBase : ContentControllerBase
{
protected IActionResult MediaNotFound() => NotFound("The requested Media could not be found");
}

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Media;
public class MoveToRecycleBinMediaController : MediaControllerBase
{
private readonly IMediaEditingService _mediaEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public MoveToRecycleBinMediaController(IMediaEditingService mediaEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_mediaEditingService = mediaEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpDelete("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> MoveToRecycleBin(Guid key)
{
Attempt<IMedia?, ContentEditingOperationStatus> result = await _mediaEditingService.MoveToRecycleBinAsync(key, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,50 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Media;
public class UpdateMediaController : MediaControllerBase
{
private readonly IMediaEditingService _mediaEditingService;
private readonly IMediaEditingPresentationFactory _mediaEditingPresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public UpdateMediaController(
IMediaEditingService mediaEditingService,
IMediaEditingPresentationFactory mediaEditingPresentationFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_mediaEditingService = mediaEditingService;
_mediaEditingPresentationFactory = mediaEditingPresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPut("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid key, UpdateMediaRequestModel updateRequestModel)
{
IMedia? media = await _mediaEditingService.GetAsync(key);
if (media == null)
{
return MediaNotFound();
}
MediaUpdateModel model = _mediaEditingPresentationFactory.MapUpdateModel(updateRequestModel);
Attempt<IMedia, ContentEditingOperationStatus> result = await _mediaEditingService.UpdateAsync(media, model, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
public class ByKeyMediaTypeController : MediaTypeControllerBase
{
private readonly IMediaTypeService _mediaTypeService;
private readonly IUmbracoMapper _umbracoMapper;
public ByKeyMediaTypeController(IMediaTypeService mediaTypeService, IUmbracoMapper umbracoMapper)
{
_mediaTypeService = mediaTypeService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(MediaTypeResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid key)
{
// FIXME: create and use an async get method here.
IMediaType? mediaType = _mediaTypeService.Get(key);
if (mediaType == null)
{
return NotFound();
}
MediaTypeResponseModel model = _umbracoMapper.Map<MediaTypeResponseModel>(mediaType)!;
return await Task.FromResult(Ok(model));
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.MediaType)]
[ApiExplorerSettings(GroupName = "Media Type")]
public abstract class MediaTypeControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Media;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class MediaBuilderExtensions
{
internal static IUmbracoBuilder AddMedia(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IMediaPresentationModelFactory, MediaPresentationModelFactory>();
builder.Services.AddTransient<IMediaEditingPresentationFactory, MediaEditingPresentationFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MediaMapDefinition>();
return builder;
}
}

View File

@@ -0,0 +1,15 @@
using Umbraco.Cms.Api.Management.Mapping.MediaType;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class MediaTypeBuilderExtensions
{
internal static IUmbracoBuilder AddMediaTypes(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MediaTypeMapDefinition>();
return builder;
}
}

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IMediaEditingPresentationFactory
{
MediaCreateModel MapCreateModel(CreateMediaRequestModel createRequestModel);
MediaUpdateModel MapUpdateModel(UpdateMediaRequestModel updateRequestModel);
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IMediaPresentationModelFactory
{
Task<MediaResponseModel> CreateResponseModelAsync(IMedia media);
}

View File

@@ -0,0 +1,23 @@
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Api.Management.Factories;
internal sealed class MediaEditingPresentationFactory : ContentEditingPresentationFactory<MediaValueModel, MediaVariantRequestModel>, IMediaEditingPresentationFactory
{
public MediaCreateModel MapCreateModel(CreateMediaRequestModel createRequestModel)
{
MediaCreateModel model = MapContentEditingModel<MediaCreateModel>(createRequestModel);
model.ContentTypeKey = createRequestModel.ContentTypeKey;
model.ParentKey = createRequestModel.ParentKey;
return model;
}
public MediaUpdateModel MapUpdateModel(UpdateMediaRequestModel updateRequestModel)
{
MediaUpdateModel model = MapContentEditingModel<MediaUpdateModel>(updateRequestModel);
return model;
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
public class MediaPresentationModelFactory : IMediaPresentationModelFactory
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly ContentSettings _contentSettings;
private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
public MediaPresentationModelFactory(IUmbracoMapper umbracoMapper, IOptions<ContentSettings> contentSettings, MediaUrlGeneratorCollection mediaUrlGenerators)
{
_umbracoMapper = umbracoMapper;
_contentSettings = contentSettings.Value;
_mediaUrlGenerators = mediaUrlGenerators;
}
public Task<MediaResponseModel> CreateResponseModelAsync(IMedia media)
{
MediaResponseModel responseModel = _umbracoMapper.Map<MediaResponseModel>(media)!;
responseModel.Urls = media
.GetUrls(_contentSettings, _mediaUrlGenerators)
.WhereNotNull()
.Select(mediaUrl => new ContentUrlInfo
{
Culture = null,
Url = mediaUrl
})
.ToArray();
return Task.FromResult(responseModel);
}
}

View File

@@ -28,6 +28,8 @@ public class ManagementApiComposer : IComposer
.AddAuditLogs()
.AddDocuments()
.AddDocumentTypes()
.AddMedia()
.AddMediaTypes()
.AddLanguages()
.AddDictionary()
.AddFileUpload()

View File

@@ -2,15 +2,16 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
using ContentTypeSort = Umbraco.Cms.Api.Management.ViewModels.ContentType.ContentTypeSort;
namespace Umbraco.Cms.Api.Management.Mapping.ContentType;
public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewModel, TPropertyTypeContainerViewModel>
public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeResponseModel, TPropertyTypeContainerResponseModel>
where TContentType : IContentTypeBase
where TPropertyTypeViewModel : PropertyTypePresentationBase, new()
where TPropertyTypeContainerViewModel : PropertyTypeContainerPresentationBase, new()
where TPropertyTypeResponseModel : PropertyTypeResponseModelBase, new()
where TPropertyTypeContainerResponseModel : PropertyTypeContainerResponseModelBase, new()
{
protected IEnumerable<TPropertyTypeViewModel> MapPropertyTypes(TContentType source)
protected IEnumerable<TPropertyTypeResponseModel> MapPropertyTypes(TContentType source)
{
// create a mapping table between properties and their associated groups
var groupKeysByPropertyKeys = source
@@ -20,7 +21,7 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewMo
.ToDictionary(map => map.PropertyTypeKey, map => map.GroupKey);
return source.PropertyTypes.Select(propertyType =>
new TPropertyTypeViewModel
new TPropertyTypeResponseModel
{
Key = propertyType.Key,
ContainerKey = groupKeysByPropertyKeys.ContainsKey(propertyType.Key)
@@ -47,7 +48,7 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewMo
.ToArray();
}
protected IEnumerable<TPropertyTypeContainerViewModel> MapPropertyTypeContainers(TContentType source)
protected IEnumerable<TPropertyTypeContainerResponseModel> MapPropertyTypeContainers(TContentType source)
{
// create a mapping table between property group aliases and keys
var groupKeysByGroupAliases = source
@@ -65,7 +66,7 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewMo
return source
.PropertyGroups
.Select(propertyGroup =>
new TPropertyTypeContainerViewModel
new TPropertyTypeContainerResponseModel
{
Key = propertyGroup.Key,
ParentKey = ParentGroupKey(propertyGroup),
@@ -75,4 +76,17 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewMo
})
.ToArray();
}
protected IEnumerable<ContentTypeSort> MapAllowedContentTypes(TContentType source)
=> source.AllowedContentTypes?.Select(contentTypeSort => new ContentTypeSort { Key = contentTypeSort.Key, SortOrder = contentTypeSort.SortOrder }).ToArray()
?? Array.Empty<ContentTypeSort>();
protected IEnumerable<ContentTypeComposition> MapCompositions(TContentType source, IEnumerable<IContentTypeComposition> contentTypeComposition)
=> contentTypeComposition.Select(contentType => new ContentTypeComposition
{
Key = contentType.Key,
CompositionType = contentType.Id == source.ParentId
? ContentTypeCompositionType.Inheritance
: ContentTypeCompositionType.Composition
}).ToArray();
}

View File

@@ -4,11 +4,10 @@ using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
using ContentTypeSort = Umbraco.Cms.Api.Management.ViewModels.ContentType.ContentTypeSort;
namespace Umbraco.Cms.Api.Management.Mapping.DocumentType;
public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType, DocumentTypePropertyTypePresentationBase, DocumentTypePropertyTypeContainerPresentationBase>, IMapDefinition
public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType, DocumentTypePropertyTypeResponseModel, DocumentTypePropertyTypeContainerResponseModel>, IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IContentType, DocumentTypeResponseModel>((_, _) => new DocumentTypeResponseModel(), Map);
@@ -27,12 +26,8 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType,
target.IsElement = source.IsElement;
target.Containers = MapPropertyTypeContainers(source);
target.Properties = MapPropertyTypes(source);
if (source.AllowedContentTypes != null)
{
target.AllowedContentTypes = source.AllowedContentTypes.Select(contentTypeSort
=> new ContentTypeSort { Key = contentTypeSort.Key, SortOrder = contentTypeSort.SortOrder });
}
target.AllowedContentTypes = MapAllowedContentTypes(source);
target.Compositions = MapCompositions(source, source.ContentTypeComposition);
if (source.AllowedTemplates != null)
{
@@ -50,13 +45,5 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType,
KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays
};
}
target.Compositions = source.ContentTypeComposition.Select(contentType => new ContentTypeComposition
{
Key = contentType.Key,
CompositionType = contentType.Id == source.ParentId
? ContentTypeCompositionType.Inheritance
: ContentTypeCompositionType.Composition
}).ToArray();
}
}

View File

@@ -0,0 +1,27 @@
using Umbraco.Cms.Api.Management.Mapping.Content;
using Umbraco.Cms.Api.Management.ViewModels.Media;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Api.Management.Mapping.Media;
public class MediaMapDefinition : ContentMapDefinition<IMedia, MediaValueModel, MediaVariantResponseModel>, IMapDefinition
{
public MediaMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
{
}
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IMedia, MediaResponseModel>((_, _) => new MediaResponseModel(), Map);
// Umbraco.Code.MapAll -Urls
private void Map(IMedia source, MediaResponseModel target, MapperContext context)
{
target.Key = source.Key;
target.ContentTypeKey = source.ContentType.Key;
target.Values = MapValueViewModels(source);
target.Variants = MapVariantViewModels(source);
}
}

View File

@@ -0,0 +1,31 @@
using Umbraco.Cms.Api.Management.Mapping.ContentType;
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.MediaType;
public class MediaTypeMapDefinition : ContentTypeMapDefinition<IMediaType, MediaTypePropertyTypeResponseModel, MediaTypePropertyTypeContainerResponseModel>, IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IMediaType, MediaTypeResponseModel>((_, _) => new MediaTypeResponseModel(), Map);
// Umbraco.Code.MapAll
private void Map(IMediaType source, MediaTypeResponseModel target, MapperContext context)
{
target.Key = source.Key;
target.Alias = source.Alias;
target.Name = source.Name ?? string.Empty;
target.Description = source.Description;
target.Icon = source.Icon ?? string.Empty;
target.AllowedAsRoot = source.AllowedAsRoot;
target.VariesByCulture = source.VariesByCulture();
target.VariesBySegment = source.VariesBySegment();
target.IsElement = source.IsElement;
target.Containers = MapPropertyTypeContainers(source);
target.Properties = MapPropertyTypes(source);
target.AllowedContentTypes = MapAllowedContentTypes(source);
target.Compositions = MapCompositions(source, source.ContentTypeComposition);
}
}

View File

@@ -3127,6 +3127,44 @@
}
}
},
"/umbraco/management/api/v1/media-type/{key}": {
"get": {
"tags": [
"Media Type"
],
"operationId": "GetMediaTypeByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTypeResponseModel"
}
]
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
},
"/umbraco/management/api/v1/tree/media-type/children": {
"get": {
"tags": [
@@ -3274,6 +3312,155 @@
}
}
},
"/umbraco/management/api/v1/media": {
"post": {
"tags": [
"Media"
],
"operationId": "PostMedia",
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/CreateMediaRequestModel"
}
]
}
}
}
},
"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"
},
"404": {
"description": "Not Found"
}
}
}
},
"/umbraco/management/api/v1/media/{key}": {
"get": {
"tags": [
"Media"
],
"operationId": "GetMediaByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentResponseModel"
}
]
}
}
}
},
"404": {
"description": "Not Found"
}
}
},
"delete": {
"tags": [
"Media"
],
"operationId": "DeleteMediaByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Not Found"
}
}
},
"put": {
"tags": [
"Media"
],
"operationId": "PutMediaByKey",
"parameters": [
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpdateMediaRequestModel"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Not Found"
}
}
}
},
"/umbraco/management/api/v1/recycle-bin/media/children": {
"get": {
"tags": [
@@ -6560,37 +6747,6 @@
},
"additionalProperties": false
},
"ContentCreateRequestModelBaseDocumentValueModelDocumentVariantRequestModel": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantRequestModel"
}
]
}
},
"parentKey": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
},
"ContentResponseModelBaseDocumentValueModelDocumentVariantResponseModel": {
"type": "object",
"properties": {
@@ -6705,7 +6861,7 @@
"type": "integer",
"format": "int32"
},
"ContentTypePresentationBaseDocumentTypePropertyTypePresentationBaseDocumentTypePropertyTypeContainerPresentationBaseModel": {
"ContentTypeResponseModelBaseDocumentTypePropertyTypeResponseModelDocumentTypePropertyTypeContainerResponseModel": {
"type": "object",
"properties": {
"key": {
@@ -6742,7 +6898,7 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypePropertyTypePresentationBaseModel"
"$ref": "#/components/schemas/DocumentTypePropertyTypeResponseModel"
}
]
}
@@ -6752,7 +6908,7 @@
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypePropertyTypeContainerPresentationBaseModel"
"$ref": "#/components/schemas/DocumentTypePropertyTypeContainerResponseModel"
}
]
}
@@ -6776,13 +6932,81 @@
}
]
}
}
},
"additionalProperties": false
},
"ContentTypeResponseModelBaseMediaTypePropertyTypeResponseModelMediaTypePropertyTypeContainerResponseModel": {
"type": "object",
"properties": {
"key": {
"type": "string",
"format": "uuid"
},
"cleanup": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeCleanupModel"
}
]
"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/MediaTypePropertyTypeResponseModel"
}
]
}
},
"containers": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTypePropertyTypeContainerResponseModel"
}
]
}
},
"allowedContentTypes": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeSortModel"
}
]
}
},
"compositions": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeCompositionModel"
}
]
}
}
},
"additionalProperties": false
@@ -6801,32 +7025,6 @@
},
"additionalProperties": false
},
"ContentUpdateRequestModelBaseDocumentValueModelDocumentVariantRequestModel": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantRequestModel"
}
]
}
}
},
"additionalProperties": false
},
"ContentUrlInfoModel": {
"type": "object",
"properties": {
@@ -6851,6 +7049,68 @@
},
"additionalProperties": false
},
"CreateContentRequestModelBaseDocumentValueModelDocumentVariantRequestModel": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantRequestModel"
}
]
}
},
"parentKey": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
},
"CreateContentRequestModelBaseMediaValueModelMediaVariantRequestModel": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaVariantRequestModel"
}
]
}
},
"parentKey": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
},
"CreateDataTypeRequestModel": {
"type": "object",
"allOf": [
@@ -6887,7 +7147,7 @@
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ContentCreateRequestModelBaseDocumentValueModelDocumentVariantRequestModel"
"$ref": "#/components/schemas/CreateContentRequestModelBaseDocumentValueModelDocumentVariantRequestModel"
}
],
"properties": {
@@ -6933,6 +7193,21 @@
},
"additionalProperties": false
},
"CreateMediaRequestModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/CreateContentRequestModelBaseMediaValueModelMediaVariantRequestModel"
}
],
"properties": {
"contentTypeKey": {
"type": "string",
"format": "uuid"
}
},
"additionalProperties": false
},
"CreatePackageRequestModel": {
"type": "object",
"allOf": [
@@ -7349,20 +7624,20 @@
}
}
},
"DocumentTypePropertyTypeContainerPresentationBaseModel": {
"DocumentTypePropertyTypeContainerResponseModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/PropertyTypeContainerPresentationBaseModel"
"$ref": "#/components/schemas/PropertyTypeContainerResponseModelBaseModel"
}
],
"additionalProperties": false
},
"DocumentTypePropertyTypePresentationBaseModel": {
"DocumentTypePropertyTypeResponseModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/PropertyTypePresentationBaseModel"
"$ref": "#/components/schemas/PropertyTypeResponseModelBaseModel"
}
],
"additionalProperties": false
@@ -7371,7 +7646,7 @@
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ContentTypePresentationBaseDocumentTypePropertyTypePresentationBaseDocumentTypePropertyTypeContainerPresentationBaseModel"
"$ref": "#/components/schemas/ContentTypeResponseModelBaseDocumentTypePropertyTypeResponseModelDocumentTypePropertyTypeContainerResponseModel"
}
],
"properties": {
@@ -7386,6 +7661,13 @@
"type": "string",
"format": "uuid",
"nullable": true
},
"cleanup": {
"oneOf": [
{
"$ref": "#/components/schemas/ContentTypeCleanupModel"
}
]
}
},
"additionalProperties": false
@@ -8099,6 +8381,102 @@
},
"additionalProperties": false
},
"MediaTypePropertyTypeContainerResponseModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/PropertyTypeContainerResponseModelBaseModel"
}
],
"additionalProperties": false
},
"MediaTypePropertyTypeResponseModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/PropertyTypeResponseModelBaseModel"
}
],
"additionalProperties": false
},
"MediaTypeResponseModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ContentTypeResponseModelBaseMediaTypePropertyTypeResponseModelMediaTypePropertyTypeContainerResponseModel"
}
],
"additionalProperties": false
},
"MediaValueModel": {
"required": [
"$type"
],
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ValueModelBaseModel"
}
],
"properties": {
"$type": {
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaValueModel": "#/components/schemas/MediaValueModel"
}
}
},
"MediaVariantRequestModel": {
"required": [
"$type"
],
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/VariantModelBaseModel"
}
],
"properties": {
"$type": {
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaVariantRequestModel": "#/components/schemas/MediaVariantRequestModel"
}
}
},
"MediaVariantResponseModel": {
"required": [
"$type"
],
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/VariantResponseModelBaseModel"
}
],
"properties": {
"$type": {
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaVariantResponseModel": "#/components/schemas/MediaVariantResponseModel"
}
}
},
"ModelsBuilderResponseModel": {
"type": "object",
"properties": {
@@ -9139,7 +9517,7 @@
},
"additionalProperties": false
},
"PropertyTypeContainerPresentationBaseModel": {
"PropertyTypeContainerResponseModelBaseModel": {
"type": "object",
"properties": {
"key": {
@@ -9165,7 +9543,7 @@
},
"additionalProperties": false
},
"PropertyTypePresentationBaseModel": {
"PropertyTypeResponseModelBaseModel": {
"type": "object",
"properties": {
"key": {
@@ -9843,6 +10221,58 @@
},
"additionalProperties": false
},
"UpdateContentRequestModelBaseDocumentValueModelDocumentVariantRequestModel": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentVariantRequestModel"
}
]
}
}
},
"additionalProperties": false
},
"UpdateContentRequestModelBaseMediaValueModelMediaVariantRequestModel": {
"type": "object",
"properties": {
"values": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaValueModel"
}
]
}
},
"variants": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaVariantRequestModel"
}
]
}
}
},
"additionalProperties": false
},
"UpdateDataTypeRequestModel": {
"type": "object",
"allOf": [
@@ -9865,7 +10295,7 @@
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/ContentUpdateRequestModelBaseDocumentValueModelDocumentVariantRequestModel"
"$ref": "#/components/schemas/UpdateContentRequestModelBaseDocumentValueModelDocumentVariantRequestModel"
}
],
"properties": {
@@ -9895,6 +10325,15 @@
],
"additionalProperties": false
},
"UpdateMediaRequestModel": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/UpdateContentRequestModelBaseMediaValueModelMediaVariantRequestModel"
}
],
"additionalProperties": false
},
"UpdatePackageRequestModel": {
"type": "object",
"allOf": [
@@ -10131,6 +10570,7 @@
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaValueModel": "#/components/schemas/MediaValueModel",
"DocumentValueModel": "#/components/schemas/DocumentValueModel"
}
}
@@ -10160,6 +10600,8 @@
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaVariantRequestModel": "#/components/schemas/MediaVariantRequestModel",
"MediaVariantResponseModel": "#/components/schemas/MediaVariantResponseModel",
"DocumentVariantRequestModel": "#/components/schemas/DocumentVariantRequestModel",
"DocumentVariantResponseModel": "#/components/schemas/DocumentVariantResponseModel"
}
@@ -10198,6 +10640,7 @@
"discriminator": {
"propertyName": "$type",
"mapping": {
"MediaVariantResponseModel": "#/components/schemas/MediaVariantResponseModel",
"DocumentVariantResponseModel": "#/components/schemas/DocumentVariantResponseModel"
}
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ContentCreateRequestModelBase<TValueModel, TVariantModel>
public abstract class CreateContentRequestModelBase<TValueModel, TVariantModel>
: ContentModelBase<TValueModel, TVariantModel>
where TValueModel : ValueModelBase
where TVariantModel : VariantModelBase

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ContentUpdateRequestModelBase<TValueModel, TVariantModel>
public abstract class UpdateContentRequestModelBase<TValueModel, TVariantModel>
: ContentModelBase<TValueModel, TVariantModel>
where TValueModel : ValueModelBase
where TVariantModel : VariantModelBase

View File

@@ -1,8 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class ContentTypePresentationBase<TPropertyType, TPropertyTypeContainer>
where TPropertyType : PropertyTypePresentationBase
where TPropertyTypeContainer : PropertyTypeContainerPresentationBase
public abstract class ContentTypeResponseModelBase<TPropertyType, TPropertyTypeContainer>
where TPropertyType : PropertyTypeResponseModelBase
where TPropertyTypeContainer : PropertyTypeContainerResponseModelBase
{
public Guid Key { get; set; }
@@ -29,6 +29,4 @@ public abstract class ContentTypePresentationBase<TPropertyType, TPropertyTypeCo
public IEnumerable<ContentTypeSort> AllowedContentTypes { get; set; } = Array.Empty<ContentTypeSort>();
public IEnumerable<ContentTypeComposition> Compositions { get; set; } = Array.Empty<ContentTypeComposition>();
public ContentTypeCleanup Cleanup { get; set; } = new();
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class PropertyTypeContainerPresentationBase
public abstract class PropertyTypeContainerResponseModelBase
{
public Guid Key { get; set; }

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class PropertyTypePresentationBase
public abstract class PropertyTypeResponseModelBase
{
public Guid Key { get; set; }

View File

@@ -2,7 +2,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class CreateDocumentRequestModel : ContentCreateRequestModelBase<DocumentValueModel, DocumentVariantRequestModel>
public class CreateDocumentRequestModel : CreateContentRequestModelBase<DocumentValueModel, DocumentVariantRequestModel>
{
public Guid ContentTypeKey { get; set; }

View File

@@ -2,7 +2,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class UpdateDocumentRequestModel : ContentUpdateRequestModelBase<DocumentValueModel, DocumentVariantRequestModel>
public class UpdateDocumentRequestModel : UpdateContentRequestModelBase<DocumentValueModel, DocumentVariantRequestModel>
{
public Guid? TemplateKey { get; set; }
}

View File

@@ -2,6 +2,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypePropertyTypeContainerPresentationBase : PropertyTypeContainerPresentationBase
public class DocumentTypePropertyTypeContainerResponseModel : PropertyTypeContainerResponseModelBase
{
}

View File

@@ -2,6 +2,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypePropertyTypePresentationBase : PropertyTypePresentationBase
public class DocumentTypePropertyTypeResponseModel : PropertyTypeResponseModelBase
{
}

View File

@@ -2,9 +2,11 @@
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypeResponseModel : ContentTypePresentationBase<DocumentTypePropertyTypePresentationBase, DocumentTypePropertyTypeContainerPresentationBase>
public class DocumentTypeResponseModel : ContentTypeResponseModelBase<DocumentTypePropertyTypeResponseModel, DocumentTypePropertyTypeContainerResponseModel>
{
public IEnumerable<Guid> AllowedTemplateKeys { get; set; } = Array.Empty<Guid>();
public Guid? DefaultTemplateKey { get; set; }
public ContentTypeCleanup Cleanup { get; set; } = new();
}

View File

@@ -0,0 +1,8 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
public class CreateMediaRequestModel : CreateContentRequestModelBase<MediaValueModel, MediaVariantRequestModel>
{
public Guid ContentTypeKey { get; set; }
}

View File

@@ -0,0 +1,8 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
public class MediaResponseModel : ContentResponseModelBase<MediaValueModel, MediaVariantResponseModel>
{
public IEnumerable<ContentUrlInfo> Urls { get; set; } = Array.Empty<ContentUrlInfo>();
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
public class MediaValueModel : ValueModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
public class MediaVariantRequestModel : VariantModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
public class MediaVariantResponseModel : VariantResponseModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Media;
public class UpdateMediaRequestModel : UpdateContentRequestModelBase<MediaValueModel, MediaVariantRequestModel>
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
public class MediaTypePropertyTypeContainerResponseModel : PropertyTypeContainerResponseModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
public class MediaTypePropertyTypeResponseModel : PropertyTypeResponseModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.MediaType;
public class MediaTypeResponseModel : ContentTypeResponseModelBase<MediaTypePropertyTypeResponseModel, MediaTypePropertyTypeContainerResponseModel>
{
}

View File

@@ -299,6 +299,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IContentVersionCleanupPolicy, DefaultContentVersionCleanupPolicy>();
Services.AddUnique<IMemberService, MemberService>();
Services.AddUnique<IMediaService, MediaService>();
Services.AddUnique<IMediaEditingService, MediaEditingService>();
Services.AddUnique<IContentTypeService, ContentTypeService>();
Services.AddUnique<IContentTypeBaseServiceProvider, ContentTypeBaseServiceProvider>();
Services.AddUnique<IMediaTypeService, MediaTypeService>();

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class MediaCreateModel : ContentCreationModelBase
{
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Core.Models.ContentEditing;
public class MediaUpdateModel : ContentEditingModelBase
{
}

View File

@@ -12,7 +12,6 @@ internal sealed class ContentEditingService
{
private readonly ITemplateService _templateService;
private readonly ILogger<ContentEditingService> _logger;
private readonly ICoreScopeProvider _scopeProvider;
public ContentEditingService(
IContentService contentService,
@@ -22,11 +21,10 @@ internal sealed class ContentEditingService
ITemplateService templateService,
ILogger<ContentEditingService> logger,
ICoreScopeProvider scopeProvider)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider)
{
_templateService = templateService;
_logger = logger;
_scopeProvider = scopeProvider;
}
public async Task<IContent?> GetAsync(Guid id)
@@ -77,10 +75,10 @@ internal sealed class ContentEditingService
}
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> MoveToRecycleBinAsync(Guid id, int userId = Constants.Security.SuperUserId)
=> await HandleDeletionAsync(id, content => ContentService.MoveToRecycleBin(content, userId));
=> await HandleDeletionAsync(id, content => ContentService.MoveToRecycleBin(content, userId), false);
public async Task<Attempt<IContent?, ContentEditingOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId)
=> await HandleDeletionAsync(id, content => ContentService.Delete(content, userId));
=> await HandleDeletionAsync(id, content => ContentService.Delete(content, userId), true);
protected override IContent Create(string? name, int parentId, IContentType contentType) => new Content(name, parentId, contentType);
@@ -130,26 +128,4 @@ internal sealed class ContentEditingService
return ContentEditingOperationStatus.Unknown;
}
}
// helper method to perform move-to-recycle-bin and delete for content as they are very much handled in the same way
private async Task<Attempt<IContent?, ContentEditingOperationStatus>> HandleDeletionAsync(Guid id, Func<IContent, OperationResult> performDelete)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete:true);
IContent? content = ContentService.GetById(id);
if (content == null)
{
return await Task.FromResult(Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, content));
}
OperationResult deleteResult = performDelete(content);
return deleteResult.Result switch
{
// these are the only result states currently expected from Delete
OperationResultType.Success => Attempt.SucceedWithStatus<IContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.Success, content),
OperationResultType.FailedCancelledByEvent => Attempt.FailWithStatus<IContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.CancelledByNotification, content),
// for any other state we'll return "unknown" so we know that we need to amend this
_ => Attempt.FailWithStatus<IContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.Unknown, content)
};
}
}

View File

@@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -20,16 +21,20 @@ public abstract class ContentEditingServiceBase<TContent, TContentType, TContent
private readonly ILogger<ContentEditingServiceBase<TContent, TContentType, TContentService, TContentTypeService>> _logger;
private readonly ICoreScopeProvider _scopeProvider;
protected ContentEditingServiceBase(
TContentService contentService,
TContentTypeService contentTypeService,
PropertyEditorCollection propertyEditorCollection,
IDataTypeService dataTypeService,
ILogger<ContentEditingServiceBase<TContent, TContentType, TContentService, TContentTypeService>> logger)
ILogger<ContentEditingServiceBase<TContent, TContentType, TContentService, TContentTypeService>> logger,
ICoreScopeProvider scopeProvider)
{
_propertyEditorCollection = propertyEditorCollection;
_dataTypeService = dataTypeService;
_logger = logger;
_scopeProvider = scopeProvider;
ContentService = contentService;
ContentTypeService = contentTypeService;
}
@@ -77,6 +82,33 @@ public abstract class ContentEditingServiceBase<TContent, TContentType, TContent
return Attempt.Succeed(ContentEditingOperationStatus.Success);
}
// helper method to perform move-to-recycle-bin and delete for content as they are very much handled in the same way
protected async Task<Attempt<TContent?, ContentEditingOperationStatus>> HandleDeletionAsync(Guid id, Func<TContent, OperationResult?> performDelete, bool allowForTrashed)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete:true);
TContent? content = ContentService.GetById(id);
if (content == null)
{
return await Task.FromResult(Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, content));
}
if (content.Trashed && allowForTrashed is false)
{
return await Task.FromResult(Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.InTrash, content));
}
OperationResult? deleteResult = performDelete(content);
return deleteResult?.Result switch
{
// these are the only result states currently expected from Delete
OperationResultType.Success => Attempt.SucceedWithStatus<TContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.Success, content),
OperationResultType.FailedCancelledByEvent => Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.CancelledByNotification, content),
// for any other state we'll return "unknown" so we know that we need to amend this
_ => Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(ContentEditingOperationStatus.Unknown, content)
};
}
private TContentType? TryGetAndValidateContentType(Guid contentTypeKey, ContentEditingModelBase contentEditingModelBase, out ContentEditingOperationStatus operationStatus)
{
TContentType? contentType = ContentTypeService.Get(contentTypeKey);

View File

@@ -0,0 +1,18 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
public interface IMediaEditingService
{
Task<IMedia?> GetAsync(Guid id);
Task<Attempt<IMedia?, ContentEditingOperationStatus>> CreateAsync(MediaCreateModel createModel, int userId = Constants.Security.SuperUserId);
Task<Attempt<IMedia, ContentEditingOperationStatus>> UpdateAsync(IMedia content, MediaUpdateModel updateModel, int userId = Constants.Security.SuperUserId);
Task<Attempt<IMedia?, ContentEditingOperationStatus>> MoveToRecycleBinAsync(Guid id, int userId = Constants.Security.SuperUserId);
Task<Attempt<IMedia?, ContentEditingOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId);
}

View File

@@ -0,0 +1,91 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
internal sealed class MediaEditingService
: ContentEditingServiceBase<IMedia, IMediaType, IMediaService, IMediaTypeService>, IMediaEditingService
{
private readonly ILogger<ContentEditingServiceBase<IMedia, IMediaType, IMediaService, IMediaTypeService>> _logger;
public MediaEditingService(
IMediaService contentService,
IMediaTypeService contentTypeService,
PropertyEditorCollection propertyEditorCollection,
IDataTypeService dataTypeService,
ILogger<ContentEditingServiceBase<IMedia, IMediaType, IMediaService, IMediaTypeService>> logger,
ICoreScopeProvider scopeProvider)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider) =>
_logger = logger;
public async Task<IMedia?> GetAsync(Guid id)
{
IMedia? media = ContentService.GetById(id);
return await Task.FromResult(media);
}
public async Task<Attempt<IMedia?, ContentEditingOperationStatus>> CreateAsync(MediaCreateModel createModel, int userId = Constants.Security.SuperUserId)
{
Attempt<IMedia?, ContentEditingOperationStatus> result = await MapCreate(createModel);
if (result.Success == false)
{
return result;
}
IMedia media = result.Result!;
ContentEditingOperationStatus operationStatus = Save(media, userId);
return operationStatus == ContentEditingOperationStatus.Success
? Attempt.SucceedWithStatus<IMedia?, ContentEditingOperationStatus>(ContentEditingOperationStatus.Success, media)
: Attempt.FailWithStatus<IMedia?, ContentEditingOperationStatus>(operationStatus, media);
}
public async Task<Attempt<IMedia, ContentEditingOperationStatus>> UpdateAsync(IMedia content, MediaUpdateModel updateModel, int userId = Constants.Security.SuperUserId)
{
Attempt<ContentEditingOperationStatus> result = await MapUpdate(content, updateModel);
if (result.Success == false)
{
return Attempt.FailWithStatus(result.Result, content);
}
ContentEditingOperationStatus operationStatus = Save(content, userId);
return operationStatus == ContentEditingOperationStatus.Success
? Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, content)
: Attempt.FailWithStatus(operationStatus, content);
}
public async Task<Attempt<IMedia?, ContentEditingOperationStatus>> MoveToRecycleBinAsync(Guid id, int userId = Constants.Security.SuperUserId)
=> await HandleDeletionAsync(id, media => ContentService.MoveToRecycleBin(media, userId).Result, false);
public async Task<Attempt<IMedia?, ContentEditingOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId)
=> await HandleDeletionAsync(id, media => ContentService.Delete(media, userId).Result, true);
protected override IMedia Create(string? name, int parentId, IMediaType contentType)
=> new Models.Media(name, parentId, contentType);
private ContentEditingOperationStatus Save(IMedia media, int userId)
{
try
{
Attempt<OperationResult?> saveResult = ContentService.Save(media, userId);
return saveResult.Result?.Result switch
{
// these are the only result states currently expected from Save
OperationResultType.Success => ContentEditingOperationStatus.Success,
OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification,
// for any other state we'll return "unknown" so we know that we need to amend this
_ => ContentEditingOperationStatus.Unknown
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Media save operation failed");
return ContentEditingOperationStatus.Unknown;
}
}
}

View File

@@ -12,5 +12,6 @@ public enum ContentEditingOperationStatus
TemplateNotFound,
TemplateNotAllowed,
PropertyTypeNotFound,
InTrash,
Unknown
}