diff --git a/Directory.Packages.props b/Directory.Packages.props index 5909b9f25be7..090afcd2160d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,23 +12,23 @@ - + - - - - + + + + - + - - + + @@ -46,10 +46,10 @@ - - + + - + @@ -57,9 +57,9 @@ - - - + + + @@ -74,17 +74,17 @@ - + - + - + - + \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Item/DatatypeItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Item/DatatypeItemControllerBase.cs index ab7881f7a438..f082e86b3ff5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Item/DatatypeItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Item/DatatypeItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.DataType}")] [ApiExplorerSettings(GroupName = "Data Type")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)] public class DatatypeItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Item/DictionaryItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Item/DictionaryItemControllerBase.cs index c64f67572b04..0859d28fa57c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Item/DictionaryItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Item/DictionaryItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/dictionary")] [ApiExplorerSettings(GroupName = "Dictionary")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessDictionary)] public class DictionaryItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/DocumentItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/DocumentItemControllerBase.cs index 700a0d4734c9..04a08643e30d 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/DocumentItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/DocumentItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Document.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Document}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))] -[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] public class DocumentItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Item/DocumentBlueprintItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Item/DocumentBlueprintItemControllerBase.cs index 4e204aa91885..61c65268f8a3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Item/DocumentBlueprintItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentBlueprint/Item/DocumentBlueprintItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.DocumentBlueprint}")] [ApiExplorerSettings(GroupName = "Document Blueprint")] -[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] public class DocumentBlueprintItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/DocumentTypeItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/DocumentTypeItemControllerBase.cs index 16e2a3af9baf..b9e6e9873b78 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/DocumentTypeItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/DocumentTypeItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Item; [VersionedApiBackOfficeRoute( $"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.DocumentType}")] [ApiExplorerSettings(GroupName = "Document Type")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public class DocumentTypeItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Language/Item/LanguageItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Language/Item/LanguageItemControllerBase.cs index 1e416bdee637..bdb89a129073 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Language/Item/LanguageItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Language/Item/LanguageItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Language.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Language}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Language))] -[Authorize(Policy = AuthorizationPolicies.TreeAccessLanguages)] public class LanguageItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/MediaItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/MediaItemControllerBase.cs index ad773156756f..f2cce0a87df5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/MediaItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/MediaItemControllerBase.cs @@ -1,15 +1,11 @@ -using Asp.Versioning; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Media.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Media}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))] -[Authorize(Policy = AuthorizationPolicies.SectionAccessForMediaTree)] public class MediaItemControllerBase : ManagementApiControllerBase { } 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 000000000000..970ed0af2809 --- /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/Controllers/MediaType/Item/MediaTypeItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/MediaTypeItemControllerBase.cs index 3ed737b93268..d19be41c40c6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/MediaTypeItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/MediaTypeItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.MediaType}")] [ApiExplorerSettings(GroupName = "Media Type")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] public class MediaTypeItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs index 87d1c7d201d4..641ca0924598 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree; @@ -38,6 +39,7 @@ protected override MediaTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? if (mediaTypes.TryGetValue(entity.Id, out IMediaType? mediaType)) { responseModel.Icon = mediaType.Icon ?? responseModel.Icon; + responseModel.IsDeletable = mediaType.IsSystemMediaType() is false; } return responseModel; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/MemberItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/MemberItemControllerBase.cs index fba91bd72ea1..8a653f57ad10 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/MemberItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/Item/MemberItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Member.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Member}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Member))] -[Authorize(Policy = AuthorizationPolicies.SectionAccessForMemberTree)] public class MemberItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Item/MemberGroupItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Item/MemberGroupItemControllerBase.cs index 926677156c1a..c6116e9a66dd 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Item/MemberGroupItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberGroup/Item/MemberGroupItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.MemberGroup.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.MemberGroup}")] [ApiExplorerSettings(GroupName = "Member Group")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberGroups)] public class MemberGroupItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Item/PartialViewItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Item/PartialViewItemControllerBase.cs index 1276370095b4..286a4ac1cd01 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Item/PartialViewItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Item/PartialViewItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.PartialView}")] [ApiExplorerSettings(GroupName = "Partial View")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)] public class PartialViewItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/RelationType/Item/RelationTypeItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/RelationType/Item/RelationTypeItemControllerBase.cs index 9daa3ac10e4e..5ca45c513523 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/RelationType/Item/RelationTypeItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/RelationType/Item/RelationTypeItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.RelationType.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.RelationType}")] [ApiExplorerSettings(GroupName = "Relation Type")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessRelationTypes)] public class RelationTypeItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Item/ScriptItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Item/ScriptItemControllerBase.cs index 9da3dea41129..51c0c52456fb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Item/ScriptItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Item/ScriptItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Script.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Script}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Script))] -[Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)] public class ScriptItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Item/StylesheetItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Item/StylesheetItemControllerBase.cs index 17a7f01125d7..ea16eca4401a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Item/StylesheetItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Item/StylesheetItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Stylesheet}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Stylesheet))] -[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)] public class StylesheetItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Item/TemplateItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Item/TemplateItemControllerBase.cs index 19941bd6d7fa..acb531401c2c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Item/TemplateItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Item/TemplateItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Template.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Template}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Template))] -[Authorize(Policy = AuthorizationPolicies.TreeAccessTemplates)] public class TemplateItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/Item/UserGroupItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/Item/UserGroupItemControllerBase.cs index 70664a5d1996..7c3a60454a9c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/Item/UserGroupItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/Item/UserGroupItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.UserGroup.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/user-group")] [ApiExplorerSettings(GroupName = "User Group")] -[Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] public class UserGroupItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookItemControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookItemControllerBase.cs index 30b8ef5ac7f0..1a6d41d43f7e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookItemControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Webhook/Item/WebhookItemControllerBase.cs @@ -1,14 +1,11 @@ -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; -using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Item; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.Webhook}")] [ApiExplorerSettings(GroupName = "Webhook")] -[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)] public class WebhookItemControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs index aa6b150a94bb..25382c72093a 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs @@ -47,8 +47,13 @@ public IEnumerable CreateDataTypeReferenceViewMo IEnumerable propertyAliases = propertyAliasesByGuid[contentType.Key]; yield return new DataTypeReferenceResponseModel { - Id = contentType.Key, - Type = usagesByEntityType.Key, + ContentType = new DataTypeContentTypeReferenceModel + { + Id = contentType.Key, + Name = contentType.Name, + Icon = contentType.Icon, + Type = usagesByEntityType.Key, + }, Properties = contentType .PropertyTypes .Where(propertyType => propertyAliases.InvariantContains(propertyType.Alias)) diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs index 543968b73e68..32fdf427b865 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs @@ -48,6 +48,8 @@ private void Map(IMediaType source, MediaTypeResponseModel target, MapperContext MediaType = referenceByIdModel, CompositionType = compositionType, }); + target.IsDeletable = source.IsSystemMediaType() is false; + target.AliasCanBeChanged = source.IsSystemMediaType() is false; } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 3da37e114958..6cd20dc3ce41 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": [ @@ -35344,6 +35399,34 @@ ], "type": "string" }, + "DataTypeContentTypeReferenceModel": { + "required": [ + "icon", + "id", + "name", + "type" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "DataTypeItemResponseModel": { "required": [ "id", @@ -35402,18 +35485,17 @@ }, "DataTypeReferenceResponseModel": { "required": [ - "id", - "properties", - "type" + "contentType", + "properties" ], "type": "object", "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string" + "contentType": { + "oneOf": [ + { + "$ref": "#/components/schemas/DataTypeContentTypeReferenceModel" + } + ] }, "properties": { "type": "array", @@ -38587,12 +38669,14 @@ "MediaTypeResponseModel": { "required": [ "alias", + "aliasCanBeChanged", "allowedAsRoot", "allowedMediaTypes", "compositions", "containers", "icon", "id", + "isDeletable", "isElement", "name", "properties", @@ -38680,6 +38764,12 @@ } ] } + }, + "isDeletable": { + "type": "boolean" + }, + "aliasCanBeChanged": { + "type": "boolean" } }, "additionalProperties": false @@ -38710,6 +38800,7 @@ "hasChildren", "icon", "id", + "isDeletable", "isFolder", "name" ], @@ -38738,6 +38829,9 @@ }, "icon": { "type": "string" + }, + "isDeletable": { + "type": "boolean" } }, "additionalProperties": false @@ -45027,4 +45121,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeContentTypeReferenceModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeContentTypeReferenceModel.cs new file mode 100644 index 000000000000..d89033584d5b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeContentTypeReferenceModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DataType; + +public class DataTypeContentTypeReferenceModel +{ + public required Guid Id { get; set; } + + public required string? Type { get; set; } + + public required string? Name { get; set; } + + public required string? Icon { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeReferenceResponseModel.cs index 7c9c54a72d4a..2aa7dc39d511 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeReferenceResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeReferenceResponseModel.cs @@ -2,9 +2,7 @@ public class DataTypeReferenceResponseModel { - public required Guid Id { get; init; } - - public required string Type { get; init; } + public required DataTypeContentTypeReferenceModel ContentType { get; init; } public required IEnumerable Properties { get; init; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeResponseModel.cs index 19112c6a877b..552e899e256a 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/MediaType/MediaTypeResponseModel.cs @@ -7,4 +7,8 @@ public class MediaTypeResponseModel : ContentTypeResponseModelBase AllowedMediaTypes { get; set; } = Enumerable.Empty(); public IEnumerable Compositions { get; set; } = Enumerable.Empty(); + + public bool IsDeletable { get; set; } + + public bool AliasCanBeChanged { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTypeTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTypeTreeItemResponseModel.cs index ccb0fed5e2a9..f1f8f55c38ea 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTypeTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/MediaTypeTreeItemResponseModel.cs @@ -3,4 +3,6 @@ public class MediaTypeTreeItemResponseModel : FolderTreeItemResponseModel { public string Icon { get; set; } = string.Empty; + + public bool IsDeletable { get; set; } } diff --git a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs index c71243a199a2..d46a22ac44cf 100644 --- a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs @@ -23,7 +23,15 @@ public static void RefreshPublicAccess(this DistributedCache dc) #region UserCacheRefresher public static void RemoveUserCache(this DistributedCache dc, IEnumerable users) - => dc.Remove(UserCacheRefresher.UniqueId, users.Select(x => x.Id).Distinct().ToArray()); + { + IEnumerable payloads = users.Select(x => new UserCacheRefresher.JsonPayload() + { + Id = x.Id, + Key = x.Key, + }); + + dc.RefreshByPayload(UserCacheRefresher.UniqueId, payloads); + } public static void RefreshUserCache(this DistributedCache dc, IEnumerable users) { diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 3d10a6b2675a..e10356fd96ec 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -20,8 +20,8 @@ public class ContentSettings internal const string StaticDisallowedUploadFiles = "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx"; internal const bool StaticShowDeprecatedPropertyEditors = false; internal const string StaticLoginBackgroundImage = "login/login.jpg"; - internal const string StaticLoginLogoImage = "login/logo_dark.svg"; - internal const string StaticLoginLogoImageAlternative = "login/logo_light.svg"; + internal const string StaticLoginLogoImage = "login/logo_light.svg"; + internal const string StaticLoginLogoImageAlternative = "login/logo_dark.svg"; internal const bool StaticHideBackOfficeLogo = false; internal const bool StaticDisableDeleteWhenReferenced = false; internal const bool StaticDisableUnpublishWhenReferenced = false; @@ -80,8 +80,8 @@ public class ContentSettings /// of a light background (e.g. in mobile resolutions). /// /// This is the alternative version to the regular logo found at . - [DefaultValue(StaticLoginLogoImage)] - public string LoginLogoImageAlternative { get; set; } = StaticLoginLogoImage; + [DefaultValue(StaticLoginLogoImageAlternative)] + public string LoginLogoImageAlternative { get; set; } = StaticLoginLogoImageAlternative; /// /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. diff --git a/src/Umbraco.Core/Configuration/Models/PackageManifestSettings.cs b/src/Umbraco.Core/Configuration/Models/PackageManifestSettings.cs deleted file mode 100644 index f353281ddde9..000000000000 --- a/src/Umbraco.Core/Configuration/Models/PackageManifestSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Umbraco.Cms.Core.Configuration.Models; - -/// -/// Typed configuration options for package manifest settings. -/// -[UmbracoOptions(Constants.Configuration.ConfigPackageManifests)] -public class PackageManifestSettings -{ - public TimeSpan CacheTimeout { get; set; } = TimeSpan.FromMinutes(10); -} diff --git a/src/Umbraco.Core/Constants-Validation.cs b/src/Umbraco.Core/Constants-Validation.cs index 9024730337b9..98aa2fc67eba 100644 --- a/src/Umbraco.Core/Constants-Validation.cs +++ b/src/Umbraco.Core/Constants-Validation.cs @@ -8,11 +8,11 @@ public static class ErrorMessages { public static class Properties { - public const string Missing = "#validation.invalidNull"; + public const string Missing = "#validation_invalidNull"; - public const string Empty = "#validation.invalidEmpty"; + public const string Empty = "#validation_invalidEmpty"; - public const string PatternMismatch = "#validation.invalidPattern"; + public const string PatternMismatch = "#validation_invalidPattern"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index f11aa5d982dc..6832bbe78971 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -85,7 +85,6 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder) .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions() .AddUmbracoOptions(); // Configure connection string and ensure it's updated when the configuration changes diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index 4ff3782019a5..38f9bf15ffe3 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -65,7 +65,7 @@ public static class ClaimsIdentityExtensions if (identity is ClaimsIdentity claimsIdentity) { userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier) - ?? claimsIdentity.FindFirstValue("sub"); + ?? claimsIdentity.FindFirstValue(Constants.Security.OpenIdDictSubClaimType); } return userId; @@ -88,7 +88,7 @@ public static class ClaimsIdentityExtensions string? userKey = null; if (identity is ClaimsIdentity claimsIdentity) { - userKey = claimsIdentity.FindFirstValue("sub"); + userKey = claimsIdentity.FindFirstValue(Constants.Security.OpenIdDictSubClaimType); } return Guid.TryParse(userKey, out Guid result) diff --git a/src/Umbraco.Core/Models/MediaType.cs b/src/Umbraco.Core/Models/MediaType.cs index 64683ae462a9..7e5e572cd406 100644 --- a/src/Umbraco.Core/Models/MediaType.cs +++ b/src/Umbraco.Core/Models/MediaType.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models; @@ -45,6 +46,21 @@ public MediaType(IShortStringHelper shortStringHelper, IMediaType parent, string /// public override ISimpleContentType ToSimple() => new SimpleContentType(this); + /// + public override string Alias + { + get => base.Alias; + set + { + if (this.IsSystemMediaType() && value != Alias) + { + throw new InvalidOperationException("Cannot change the alias of a system media type"); + } + + base.Alias = value; + } + } + /// IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => (IMediaType)DeepCloneWithResetIdentities(newAlias); diff --git a/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs index d628537ab32b..ade1da8b8af2 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockGridConfiguration.cs @@ -26,6 +26,10 @@ public class BlockGridBlockConfiguration : IBlockConfiguration public Guid ContentElementTypeKey { get; set; } public Guid? SettingsElementTypeKey { get; set; } + + public bool AllowAtRoot { get; set; } + + public bool AllowInAreas { get; set; } } public class NumberRange diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs b/src/Umbraco.Core/Services/ContentTypeEditing/IMediaTypeEditingService.cs index 36b9ef40e2f3..e712fc66a2c1 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 47fc1b8f7a12..11def4a3338b 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/MediaTypeEditingService.cs @@ -42,6 +42,11 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase> UpdateAsync(IMediaType mediaType, MediaTypeUpdateModel model, Guid userKey) { + if (mediaType.IsSystemMediaType() && mediaType.Alias != model.Alias) + { + return Attempt.FailWithStatus(ContentTypeOperationStatus.NotAllowed, null); + } + Attempt result = await ValidateAndMapForUpdateAsync(mediaType, model); if (result.Success) { @@ -102,6 +107,35 @@ public async Task> GetMediaTypesForFileExtensionAsync(str } + public Task> 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/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index f44ebb3a28f0..8340f3c58478 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -620,6 +620,11 @@ public async Task DeleteAsync(Guid key, Guid perform return ContentTypeOperationStatus.NotFound; } + if (CanDelete(item) is false) + { + return ContentTypeOperationStatus.NotAllowed; + } + Delete(item, performingUserId); scope.Complete(); @@ -628,6 +633,11 @@ public async Task DeleteAsync(Guid key, Guid perform public void Delete(TItem item, int userId = Constants.Security.SuperUserId) { + if (CanDelete(item) is false) + { + throw new InvalidOperationException("The item was not allowed to be deleted"); + } + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { EventMessages eventMessages = EventMessagesFactory.Get(); @@ -695,6 +705,10 @@ public void Delete(TItem item, int userId = Constants.Security.SuperUserId) public void Delete(IEnumerable items, int userId = Constants.Security.SuperUserId) { TItem[] itemsA = items.ToArray(); + if (itemsA.All(CanDelete) is false) + { + throw new InvalidOperationException("One or more items were not allowed to be deleted"); + } using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { @@ -750,6 +764,8 @@ public void Delete(IEnumerable items, int userId = Constants.Security.Sup protected abstract void DeleteItemsOfTypes(IEnumerable typeIds); + protected virtual bool CanDelete(TItem item) => true; + #endregion #region Copy diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index b22fcfd46ec6..872b6b686857 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -211,6 +211,7 @@ public XElement Serialize(IDataType dataType) // The 'ID' when exporting is actually the property editor alias (in pre v7 it was the IDataType GUID id) xml.Add(new XAttribute("Id", dataType.EditorAlias)); + xml.Add(new XAttribute("EditorUiAlias", dataType.EditorUiAlias ?? dataType.EditorAlias)); xml.Add(new XAttribute("Definition", dataType.Key)); xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString())); xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.ConfigurationObject))); diff --git a/src/Umbraco.Core/Services/MediaTypeService.cs b/src/Umbraco.Core/Services/MediaTypeService.cs index cc914e43bc66..359b4a99a946 100644 --- a/src/Umbraco.Core/Services/MediaTypeService.cs +++ b/src/Umbraco.Core/Services/MediaTypeService.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Locking; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -77,6 +78,9 @@ protected override void DeleteItemsOfTypes(IEnumerable typeIds) } } + protected override bool CanDelete(IMediaType item) + => item.IsSystemMediaType() is false; + #region Notifications protected override SavingNotification GetSavingNotification( diff --git a/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs b/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs index e161a7171a3e..fbf00b7fa323 100644 --- a/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs +++ b/src/Umbraco.Infrastructure/Manifest/PackageManifestService.cs @@ -10,16 +10,18 @@ internal sealed class PackageManifestService : IPackageManifestService { private readonly IEnumerable _packageManifestReaders; private readonly IAppPolicyCache _cache; - private readonly PackageManifestSettings _packageManifestSettings; + private RuntimeSettings _runtimeSettings; + public PackageManifestService( IEnumerable packageManifestReaders, AppCaches appCaches, - IOptions packageManifestSettings) + IOptionsMonitor runtimeSettingsOptionsMonitor) { _packageManifestReaders = packageManifestReaders; - _packageManifestSettings = packageManifestSettings.Value; _cache = appCaches.RuntimeCache; + _runtimeSettings = runtimeSettingsOptionsMonitor.CurrentValue; + runtimeSettingsOptionsMonitor.OnChange(runtimeSettings => _runtimeSettings = runtimeSettings); } public async Task> GetAllPackageManifestsAsync() @@ -34,7 +36,9 @@ public async Task> GetAllPackageManifestsAsync() return tasks.SelectMany(x => x.Result); }, - _packageManifestSettings.CacheTimeout) + _runtimeSettings.Mode == RuntimeMode.Production + ? TimeSpan.FromDays(30) + : TimeSpan.FromSeconds(10)) ?? Array.Empty(); public async Task> GetPublicPackageManifestsAsync() diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 3e9bd6ffdc28..009811bc7d48 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1062,6 +1062,7 @@ private void CreateContentTypeData() Thumbnail = Constants.Icons.MediaFolder, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing, + ListView = Constants.DataTypes.Guids.ListViewMediaGuid }); } @@ -1886,11 +1887,13 @@ void InsertDataTypeDto(int id, string editorAlias, string editorUiAlias, string } // layouts for the list view - const string cardLayout = - "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": true, \"selected\": true}"; - const string listLayout = - "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": true,\"selected\": true}"; - const string layouts = "[" + cardLayout + "," + listLayout + "]"; + string TableCollectionView(string collectionViewType) => + $"{{\"name\": \"List\",\"collectionView\": \"Umb.CollectionView.{collectionViewType}.Table\", \"icon\": \"icon-list\", \"isSystem\": true, \"selected\": true}}"; + + string GridCollectionView(string collectionViewType) => + $"{{\"name\": \"Grid\",\"collectionView\": \"Umb.CollectionView.{collectionViewType}.Grid\",\"icon\": \"icon-thumbnails-small\", \"isSystem\": true,\"selected\": true}}"; + + string Layouts(string collectionViewType) => $"[{GridCollectionView(collectionViewType)},{TableCollectionView(collectionViewType)}]"; // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration // of data types to create). @@ -2094,7 +2097,7 @@ void InsertDataTypeDto(int id, string editorAlias, string editorUiAlias, string DbType = "Nvarchar", Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + - layouts + + Layouts("Document") + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":true},{\"alias\":\"creator\",\"header\":\"Updated by\",\"isSystem\":true}]}", }); } @@ -2113,7 +2116,7 @@ void InsertDataTypeDto(int id, string editorAlias, string editorUiAlias, string DbType = "Nvarchar", Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + - layouts + + Layouts("Media") + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":true},{\"alias\":\"creator\",\"header\":\"Updated by\",\"isSystem\":true}]}", }); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs index ab6d079a96de..cbdccc1ca420 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs @@ -91,6 +91,11 @@ protected MigrationBase(IMigrationContext context) /// public bool RebuildCache { get; set; } + /// + /// If this is set to true, all backoffice client tokens will be revoked upon successful completion of the migration. + /// + public bool InvalidateBackofficeUserAccess { get; set; } + /// /// Runs the migration. /// diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index 31a9a3fa5f5c..bf2d73430541 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -1,11 +1,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using Org.BouncyCastle.Utilities; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Scoping; @@ -42,10 +46,12 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IKeyValueService _keyValueService; + private readonly IServiceScopeFactory _serviceScopeFactory; private readonly DistributedCache _distributedCache; private readonly IScopeAccessor _scopeAccessor; private readonly ICoreScopeProvider _scopeProvider; private bool _rebuildCache; + private bool _invalidateBackofficeUserAccess; public MigrationPlanExecutor( ICoreScopeProvider scopeProvider, @@ -55,7 +61,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor IUmbracoDatabaseFactory databaseFactory, IPublishedSnapshotService publishedSnapshotService, DistributedCache distributedCache, - IKeyValueService keyValueService) + IKeyValueService keyValueService, + IServiceScopeFactory serviceScopeFactory) { _scopeProvider = scopeProvider; _scopeAccessor = scopeAccessor; @@ -64,6 +71,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor _databaseFactory = databaseFactory; _publishedSnapshotService = publishedSnapshotService; _keyValueService = keyValueService; + _serviceScopeFactory = serviceScopeFactory; _distributedCache = distributedCache; _logger = _loggerFactory.CreateLogger(); } @@ -85,7 +93,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -103,8 +112,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService() - ) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -135,6 +144,12 @@ public ExecutedMigrationPlan ExecutePlan(MigrationPlan plan, string fromState) RebuildCache(); } + // If any completed migration requires us to sign out the user we'll do that. + if (_invalidateBackofficeUserAccess) + { + RevokeBackofficeTokens().GetAwaiter().GetResult(); // should async all the way up at some point + } + return result; } @@ -320,6 +335,11 @@ private void RunMigration(Type migrationType, MigrationContext context) { _rebuildCache = true; } + + if (migration.InvalidateBackofficeUserAccess) + { + _invalidateBackofficeUserAccess = true; + } } private void RebuildCache() @@ -327,4 +347,31 @@ private void RebuildCache() _publishedSnapshotService.RebuildAll(); _distributedCache.RefreshAllPublishedSnapshot(); } + + private async Task RevokeBackofficeTokens() + { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + + IOpenIddictApplicationManager openIddictApplicationManager = scope.ServiceProvider.GetRequiredService(); + var backOfficeClient = await openIddictApplicationManager.FindByClientIdAsync(Constants.OAuthClientIds.BackOffice); + if (backOfficeClient is null) + { + _logger.LogWarning("Could not get the openIddict Application for {backofficeClientId}. Canceling token revocation. Users might have to manually log out to get proper access to the backoffice", Constants.OAuthClientIds.BackOffice); + return; + } + + var backOfficeClientId = await openIddictApplicationManager.GetIdAsync(backOfficeClient); + if (backOfficeClientId is null) + { + _logger.LogWarning("Could not extract the clientId from the openIddict backofficelient Application. Canceling token revocation. Users might have to manually log out to get proper access to the backoffice", Constants.OAuthClientIds.BackOffice); + return; + } + + IOpenIddictTokenManager tokenManager = scope.ServiceProvider.GetRequiredService(); + var tokens = await tokenManager.FindByApplicationIdAsync(backOfficeClientId).ToArrayAsync(); + foreach (var token in tokens) + { + await tokenManager.DeleteAsync(token); + } + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs index e5edce8e870c..fe730fd2b83c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs @@ -26,6 +26,7 @@ public AddGuidsToUsers(IMigrationContext context, IScopeProvider scopeProvider) protected override void Migrate() { + InvalidateBackofficeUserAccess = true; using IScope scope = _scopeProvider.CreateScope(); using IDisposable notificationSuppression = scope.Notifications.Suppress(); ScopeDatabase(scope); diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 32be1b1cc1ed..a6c3ea453e38 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1257,12 +1257,15 @@ public IReadOnlyList ImportDataTypes(IReadOnlyCollection da editor = new VoidEditor(_dataValueEditorFactory) {Alias = editorAlias ?? string.Empty}; } + var editorUiAlias = dataTypeElement.Attribute("EditorUiAlias")?.Value?.Trim() ?? editorAlias; + var dataType = new DataType(editor, _serializer) { Key = dataTypeDefinitionId, Name = dataTypeDefinitionName, DatabaseType = databaseType, - ParentId = parentId + ParentId = parentId, + EditorUiAlias = editorUiAlias, }; var configurationAttributeValue = dataTypeElement.Attribute("Configuration")?.Value; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 23ae35cb406b..c24a982e008e 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -157,8 +157,9 @@ public async Task ValidateSessionIdAsync(string? userId, string? sessionId throw new DataException("Could not create the user, check logs for details"); } - // re-assign id + // re-assign id and key user.Id = UserIdToString(userEntity.Id); + user.Key = userEntity.Key; if (isLoginsPropertyDirty) { diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index fb470a9625b7..6abe0e675e4d 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit fb470a9625b7a04ef1a33322fa36c3b5480a11e9 +Subproject commit 6abe0e675e4d41570e9df980f44041051f3c8950 diff --git a/src/Umbraco.Web.UI.Login/index.html b/src/Umbraco.Web.UI.Login/index.html index ba0c0f314187..db8045b5dee3 100644 --- a/src/Umbraco.Web.UI.Login/index.html +++ b/src/Umbraco.Web.UI.Login/index.html @@ -48,7 +48,7 @@

{ const input = document.createElement('input'); input.type = opts.type; @@ -28,6 +29,7 @@ const createInput = (opts: { input.required = true; input.inputMode = opts.inputmode; input.ariaLabel = opts.label; + input.autofocus = opts.autofocus || false; return input; }; @@ -171,6 +173,7 @@ export default class UmbAuthElement extends UmbLitElement { autocomplete: 'username', label: labelUsername, inputmode: this.usernameIsEmail ? 'email' : '', + autofocus: true, }); this._passwordInput = createInput({ id: 'password-input', diff --git a/src/Umbraco.Web.UI.New/umbraco/Data/Umbraco.sqlite.db b/src/Umbraco.Web.UI.New/umbraco/Data/Umbraco.sqlite.db deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 78cb5a571ddb..4fce9e86f31e 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -5,9 +5,9 @@ - + - + @@ -17,7 +17,7 @@ - + diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 98ae006aec9b..00eec9388acb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.5", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.46", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.49", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", @@ -146,9 +146,9 @@ "integrity": "sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==" }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.46", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.46.tgz", - "integrity": "sha512-SJKmKO/84QFnCy0j7fGEYbRtbLZKC/k1xlyUKrkZpzVekVIS6gSki5ECWu4LiJnfmo+yhxGBsA2l3iLZfL1gow==", + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.49.tgz", + "integrity": "sha512-fmEJjuawY8QEHLQ9xozp83GozkVFFwEAHMYnNENYt98XSyu55OA7sRxIXUabKPOUGzgcGkiSceicKG7JXBoofw==", "dependencies": { "@umbraco/json-models-builders": "2.0.6", "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 3cc0dfdc902e..baabb52f3f09 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.5", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.46", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.49", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts index e512b25d68fd..c0b1d0710713 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 3 : 2, + retries: process.env.CI ? 2 : 1, // We don't want to run parallel, as tests might differ in state workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts new file mode 100644 index 000000000000..e12749b1cd43 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentType.spec.ts @@ -0,0 +1,150 @@ +import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from '@playwright/test'; + +const documentTypeName = 'TestDocumentType'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can create a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.clickActionsMenuAtRoot(); + await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateDocumentTypeButton(); + await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + await umbracoUi.documentType.reloadTree('Document Types'); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentTypeName); +}); + +test('can create a document type with a template', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoApi.template.ensureNameNotExists(documentTypeName); + + // Act + await umbracoUi.documentType.clickActionsMenuAtRoot(); + await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateDocumentTypeWithTemplateButton(); + await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + // Checks if both the success notification for document Types and teh template are visible + await umbracoUi.documentType.doesSuccessNotificationsHaveCount(2); + // Checks if the documentType contains the template + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + const templateData = await umbracoApi.template.getByName(documentTypeName); + expect(documentTypeData.allowedTemplates[0].id).toEqual(templateData.id); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + + // Clean + await umbracoApi.template.ensureNameNotExists(documentTypeName); +}); + +test('can create a element type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.clickActionsMenuAtRoot(); + await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateElementTypeButton(); + await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + // Checks if the isElement is true + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.isElement).toBeTruthy(); +}); + +test('can rename a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'NotADocumentTypeName'; + await umbracoApi.documentType.ensureNameNotExists(wrongName); + await umbracoApi.documentType.createDefaultDocumentType(wrongName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(wrongName); + await umbracoUi.documentType.enterDocumentTypeName(documentTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + await umbracoUi.documentType.isDocumentTreeItemVisible(wrongName, false); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentTypeName); +}); + +test('can update the alias for a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldAlias = AliasHelper.toAlias(documentTypeName); + const newAlias = 'newDocumentTypeAlias'; + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + const documentTypeDataOld = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeDataOld.alias).toBe(oldAlias); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.enterAliasName(newAlias); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentTypeName, true); + const documentTypeDataNew = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeDataNew.alias).toBe(newAlias); +}); + +test('can add an icon for a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const bugIcon = 'icon-bug'; + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.updateIcon(bugIcon); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.icon).toBe(bugIcon); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentTypeName, true); +}); + +test('can delete a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + + // Act + await umbracoUi.documentType.clickRootFolderCaretButton(); + await umbracoUi.documentType.clickActionsMenuForDocumentType(documentTypeName); + await umbracoUi.documentType.clickDeleteExactButton(); + await umbracoUi.documentType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeFalsy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts new file mode 100644 index 000000000000..9254db4605b7 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeDesignTab.spec.ts @@ -0,0 +1,431 @@ +import {ConstantHelper, test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const documentTypeName = 'TestDocumentType'; +const dataTypeName = 'Approved Color'; +const groupName = 'TestGroup'; +const tabName = 'TestTab'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add a property to a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickAddGroupButton(); + await umbracoUi.documentType.addPropertyEditor(dataTypeName); + await umbracoUi.documentType.enterGroupName(groupName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + const dataType = await umbracoApi.dataType.getByName(dataTypeName); + // Checks if the correct property was added to the document type + expect(documentTypeData.properties[0].dataType.id).toBe(dataType.id); +}); + +test('can update a property in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const newDataTypeName = 'Image Media Picker'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.updatePropertyEditor(newDataTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + const dataType = await umbracoApi.dataType.getByName(newDataTypeName); + // Checks if the correct property was added to the document type + expect(documentTypeData.properties[0].dataType.id).toBe(dataType.id); +}); + +test('can update group name in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const newGroupName = 'UpdatedGroupName'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.enterGroupName(newGroupName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.containers[0].name).toBe(newGroupName); +}); + +test('can delete a group in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.deleteGroup(groupName, true); + await umbracoUi.documentType.clickConfirmToDeleteButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.containers.length).toBe(0); + expect(documentTypeData.properties.length).toBe(0); +}); + +// TODO: Currently I am getting an error If I delete a tab that contains children. The children are not cleaned up when deleting the tab. +test.skip('can delete a tab in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditorInTab(documentTypeName, dataTypeName, dataTypeData.id, tabName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickRemoveTabWithName(tabName); + await umbracoUi.documentType.clickConfirmToDeleteButton(); + await umbracoUi.documentType.clickSaveButton(); + + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); +}); + +test('can delete a property editor in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.deletePropertyEditorWithName(dataTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties.length).toBe(0); +}); + +test('can create a document type with a property in a tab', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickAddTabButton(); + await umbracoUi.documentType.enterTabName(tabName); + await umbracoUi.documentType.clickAddGroupButton(); + await umbracoUi.documentType.addPropertyEditor(dataTypeName, 1); + await umbracoUi.documentType.enterGroupName(groupName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(await umbracoApi.documentType.doesTabContainCorrectPropertyEditorInGroup(documentTypeName, dataTypeName, documentTypeData.properties[0].dataType.id, tabName, groupName)).toBeTruthy(); +}); + +test('can create a document type with multiple groups', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondDataTypeName = 'Image Media Picker'; + const secondGroupName = 'TesterGroup'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName); + const secondDataType = await umbracoApi.dataType.getByName(secondDataTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickAddGroupButton(); + await umbracoUi.documentType.enterGroupName(secondGroupName, 1); + await umbracoUi.documentType.addPropertyEditor(secondDataTypeName, 1); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + expect(await umbracoApi.documentType.doesGroupContainCorrectPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName)).toBeTruthy(); + expect(await umbracoApi.documentType.doesGroupContainCorrectPropertyEditor(documentTypeName, secondDataTypeName, secondDataType.id, secondGroupName)).toBeTruthy(); +}); + +test('can create a document type with multiple tabs', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondDataTypeName = 'Image Media Picker'; + const secondGroupName = 'TesterGroup'; + const secondTabName = 'SecondTab'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditorInTab(documentTypeName, dataTypeName, dataTypeData.id, tabName, groupName); + const secondDataType = await umbracoApi.dataType.getByName(secondDataTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickAddTabButton(); + await umbracoUi.documentType.enterTabName(secondTabName); + await umbracoUi.documentType.clickAddGroupButton(); + await umbracoUi.documentType.enterGroupName(secondGroupName); + await umbracoUi.documentType.addPropertyEditor(secondDataTypeName, 1); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy(); + expect(await umbracoApi.documentType.doesTabContainCorrectPropertyEditorInGroup(documentTypeName, dataTypeName, dataTypeData.id, tabName, groupName)).toBeTruthy(); + expect(await umbracoApi.documentType.doesTabContainCorrectPropertyEditorInGroup(documentTypeName, secondDataTypeName, secondDataType.id, secondTabName, secondGroupName)).toBeTruthy(); +}); + +test('can create a document type with a composition', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const compositionDocumentTypeName = 'CompositionDocumentType'; + await umbracoApi.documentType.ensureNameNotExists(compositionDocumentTypeName); + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const compositionDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(compositionDocumentTypeName, dataTypeName, dataTypeData.id, groupName); + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickCompositionsButton(); + await umbracoUi.documentType.clickButtonWithName(compositionDocumentTypeName); + await umbracoUi.documentType.clickSubmitButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(umbracoUi.documentType.doesGroupHaveValue(groupName)).toBeTruthy(); + // Checks if the composition in the document type is correct + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.compositions[0].documentType.id).toBe(compositionDocumentTypeId); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(compositionDocumentTypeName); +}); + +test('can remove a composition form a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const compositionDocumentTypeName = 'CompositionDocumentType'; + await umbracoApi.documentType.ensureNameNotExists(compositionDocumentTypeName); + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const compositionDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(compositionDocumentTypeName, dataTypeName, dataTypeData.id, groupName); + await umbracoApi.documentType.createDocumentTypeWithAComposition(documentTypeName, compositionDocumentTypeId); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickCompositionsButton(); + await umbracoUi.documentType.clickButtonWithName(compositionDocumentTypeName); + await umbracoUi.documentType.clickSubmitButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoUi.documentType.doesGroupHaveValue(groupName)).toBeFalsy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.compositions).toEqual([]); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(compositionDocumentTypeName); +}); + +test('can reorder groups in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondGroupName = 'SecondGroup'; + await umbracoApi.documentType.createDocumentTypeWithTwoGroups(documentTypeName, dataTypeName, dataTypeData.id, groupName, secondGroupName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.goToDocumentType(documentTypeName); + + // Act + await umbracoUi.documentType.clickReorderButton(); + const groupValues = await umbracoUi.documentType.reorderTwoGroups(); + const firstGroupValue = groupValues.firstGroupValue; + const secondGroupValue = groupValues.secondGroupValue; + await umbracoUi.documentType.clickIAmDoneReorderingButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + // Since we swapped sorting order, the firstGroupValue should have sortOrder 1 and the secondGroupValue should have sortOrder 0 + expect(await umbracoApi.documentType.doesDocumentTypeGroupNameContainCorrectSortOrder(documentTypeName, secondGroupValue, 0)).toBeTruthy(); + expect(await umbracoApi.documentType.doesDocumentTypeGroupNameContainCorrectSortOrder(documentTypeName, firstGroupValue, 1)).toBeTruthy(); +}); + +// TODO: Unskip when it works. Sometimes the properties are not dragged correctly. +test.skip('can reorder properties in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const dataTypeNameTwo = "Second Color Picker"; + await umbracoApi.documentType.createDocumentTypeWithTwoPropertyEditors(documentTypeName, dataTypeName, dataTypeData.id, dataTypeNameTwo, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickReorderButton(); + // Drag and Drop + await umbracoUi.waitForTimeout(5000); + const dragFromLocator = umbracoUi.documentType.getTextLocatorWithName(dataTypeNameTwo); + const dragToLocator = umbracoUi.documentType.getTextLocatorWithName(dataTypeName); + await umbracoUi.documentType.dragAndDrop(dragFromLocator, dragToLocator, 0, 0, 5); + await umbracoUi.documentType.clickIAmDoneReorderingButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties[0].name).toBe(dataTypeNameTwo); + expect(documentTypeData.properties[1].name).toBe(dataTypeName); +}); + +// TODO: Unskip when the frontend does not give the secondTab -1 as the sortOrder +test.skip('can reorder tabs in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondTabName = 'SecondTab'; + await umbracoApi.documentType.createDocumentTypeWithTwoTabs(documentTypeName, dataTypeName, dataTypeData.id, tabName, secondTabName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.goToDocumentType(documentTypeName); + + // Act + const dragToLocator = umbracoUi.documentType.getTabLocatorWithName(tabName); + const dragFromLocator = umbracoUi.documentType.getTabLocatorWithName(secondTabName); + await umbracoUi.documentType.clickReorderButton(); + await umbracoUi.documentType.dragAndDrop(dragFromLocator, dragToLocator, 0, 0, 10); + await umbracoUi.documentType.clickIAmDoneReorderingButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + expect(await umbracoApi.documentType.doesDocumentTypeTabNameContainCorrectSortOrder(documentTypeName, secondTabName, 0)).toBeTruthy(); + expect(await umbracoApi.documentType.doesDocumentTypeTabNameContainCorrectSortOrder(documentTypeName, tabName, 1)).toBeTruthy(); +}); + +test('can add a description to a property in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const descriptionText = 'This is a property'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickEditorSettingsButton(); + await umbracoUi.documentType.enterPropertyEditorDescription(descriptionText); + await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + await expect(umbracoUi.documentType.enterDescriptionTxt).toBeVisible(); + expect(umbracoUi.documentType.doesDescriptionHaveValue(descriptionText)).toBeTruthy(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties[0].description).toBe(descriptionText); +}); + +test('can set is mandatory for a property in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickEditorSettingsButton(); + await umbracoUi.documentType.clickMandatorySlider(); + await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties[0].validation.mandatory).toBeTruthy(); +}); + +test('can enable validation for a property in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const regex = '^[a-zA-Z0-9]*$'; + const regexMessage = 'Only letters and numbers are allowed'; + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickEditorSettingsButton(); + await umbracoUi.documentType.selectValidationOption(''); + await umbracoUi.documentType.enterRegEx(regex); + await umbracoUi.documentType.enterRegExMessage(regexMessage); + await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties[0].validation.regEx).toBe(regex); + expect(documentTypeData.properties[0].validation.regExMessage).toBe(regexMessage); +}); + +test('can allow vary by culture for a property in a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName, true); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickEditorSettingsButton(); + await umbracoUi.documentType.clickVaryByCultureSlider(); + await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties[0].variesByCulture).toBeTruthy(); +}); + +test('can set appearance to label on top for a property in a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickEditorSettingsButton(); + await umbracoUi.documentType.clickLabelOnTopButton(); + await umbracoUi.documentType.clickUpdateButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.properties[0].appearance.labelOnTop).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts new file mode 100644 index 000000000000..b6a8660ad9d2 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeFolder.spec.ts @@ -0,0 +1,128 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from '@playwright/test'; + +const documentFolderName = 'TestFolder'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentFolderName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentFolderName); +}); + +test('can create a empty document type folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.clickActionsMenuForName('Document Types'); + await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateDocumentFolderButton(); + await umbracoUi.documentType.enterFolderName(documentFolderName); + await umbracoUi.documentType.clickCreateFolderButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const folder = await umbracoApi.documentType.getByName(documentFolderName); + expect(folder.name).toBe(documentFolderName); + // Checks if the folder is in the root + await umbracoUi.documentType.reloadTree('Document Types'); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentFolderName); +}); + +test('can delete a document type folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createFolder(documentFolderName); + + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.clickRootFolderCaretButton(); + await umbracoUi.documentType.clickActionsMenuForName(documentFolderName); + await umbracoUi.documentType.deleteFolder(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + await umbracoApi.documentType.doesNameExist(documentFolderName); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentFolderName, false); +}); + +test('can rename a document type folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldFolderName = 'OldName'; + await umbracoApi.documentType.createFolder(oldFolderName); + + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.clickRootFolderCaretButton(); + await umbracoUi.documentType.clickActionsMenuForName(oldFolderName); + await umbracoUi.documentType.clickRenameFolderButton(); + await umbracoUi.documentType.enterFolderName(documentFolderName); + await umbracoUi.documentType.clickUpdateFolderButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const folder = await umbracoApi.documentType.getByName(documentFolderName); + expect(folder.name).toBe(documentFolderName); + await umbracoUi.documentType.isDocumentTreeItemVisible(oldFolderName, false); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentFolderName); +}); + +test('can create a document type folder in a folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childFolderName = 'ChildFolder'; + await umbracoApi.documentType.ensureNameNotExists(childFolderName); + const parentFolderId = await umbracoApi.documentType.createFolder(documentFolderName); + + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.clickRootFolderCaretButton(); + await umbracoUi.documentType.clickActionsMenuForName(documentFolderName); + await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateDocumentFolderButton(); + await umbracoUi.documentType.enterFolderName(childFolderName); + await umbracoUi.documentType.clickCreateFolderButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const folder = await umbracoApi.documentType.getByName(childFolderName); + expect(folder.name).toBe(childFolderName); + // Checks if the parentFolder contains the ChildFolder as a child + const parentFolder = await umbracoApi.documentType.getChildren(parentFolderId); + expect(parentFolder[0].name).toBe(childFolderName); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(childFolderName); +}); + +test('can create a folder in a folder in a folder', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const grandParentFolderName = 'TheGrandFolder'; + const parentFolderName = 'TheParentFolder'; + await umbracoApi.documentType.ensureNameNotExists(grandParentFolderName); + await umbracoApi.documentType.ensureNameNotExists(parentFolderName); + const grandParentFolderId = await umbracoApi.documentType.createFolder(grandParentFolderName); + const parentFolderId = await umbracoApi.documentType.createFolder(parentFolderName, grandParentFolderId); + + // Act + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + await umbracoUi.documentType.clickRootFolderCaretButton(); + await umbracoUi.documentType.clickCaretButtonForName(grandParentFolderName); + await umbracoUi.documentType.clickActionsMenuForName(parentFolderName); + await umbracoUi.documentType.clickCreateButton(); + await umbracoUi.documentType.clickCreateDocumentFolderButton(); + await umbracoUi.documentType.enterFolderName(documentFolderName); + await umbracoUi.documentType.clickCreateFolderButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + await umbracoUi.documentType.reloadTree(parentFolderName); + await umbracoUi.documentType.isDocumentTreeItemVisible(documentFolderName); + const grandParentChildren = await umbracoApi.documentType.getChildren(grandParentFolderId); + expect(grandParentChildren[0].name).toBe(parentFolderName); + const parentChildren = await umbracoApi.documentType.getChildren(parentFolderId); + expect(parentChildren[0].name).toBe(documentFolderName); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(grandParentFolderName); + await umbracoApi.documentType.ensureNameNotExists(parentFolderName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts new file mode 100644 index 000000000000..7463ff804b5a --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeSettingsTab.spec.ts @@ -0,0 +1,84 @@ +import {ConstantHelper, test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const documentTypeName = 'TestDocumentType'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add allow vary by culture for a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickDocumentTypeSettingsTab(); + await umbracoUi.documentType.clickVaryByCultureButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.variesByCulture).toBeTruthy(); +}); + +test('can add allow segmentation for a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickDocumentTypeSettingsTab(); + await umbracoUi.documentType.clickVaryBySegmentsButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.variesBySegment).toBeTruthy(); +}); + +test('can set is an element type for a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickDocumentTypeSettingsTab(); + await umbracoUi.documentType.clickTextButtonWithName('Element type'); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.isElement).toBeTruthy(); +}); + +// TODO: Unskip. Currently The cleanup is not updated upon save +test.skip('can disable history cleanup for a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + // Is needed + await umbracoUi.waitForTimeout(200); + await umbracoUi.documentType.clickDocumentTypeSettingsTab(); + await umbracoUi.documentType.clickAutoCleanupButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.cleanup.preventCleanup).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts new file mode 100644 index 000000000000..2bf10c9157b7 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeStructureTab.spec.ts @@ -0,0 +1,97 @@ +import {ConstantHelper, test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const documentTypeName = 'TestDocumentType'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add allow as root to a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickStructureTab(); + await umbracoUi.documentType.clickAllowAtRootButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.allowedAsRoot).toBeTruthy(); +}); + +test('can add an allowed child node to a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickStructureTab(); + await umbracoUi.documentType.clickChooseButton(); + await umbracoUi.documentType.clickButtonWithName(documentTypeName); + await umbracoUi.documentType.clickAllowedChildNodesButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.allowedDocumentTypes[0].documentType.id).toBe(documentTypeData.id); +}); + +test('can remove an allowed child node from a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childDocumentTypeName = 'ChildDocumentType'; + await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName); + const childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName); + await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickStructureTab(); + await umbracoUi.documentType.clickRemoveButtonForName(childDocumentTypeName); + await umbracoUi.documentType.clickConfirmRemoveButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.allowedDocumentTypes.length).toBe(0); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(childDocumentTypeName); +}); + +test('can configure a collection for a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const collectionDataTypeName = 'TestCollection'; + await umbracoApi.dataType.ensureNameNotExists(collectionDataTypeName); + const collectionDataTypeId = await umbracoApi.dataType.create(collectionDataTypeName, 'Umbraco.ListView', [], null, 'Umb.PropertyEditorUi.CollectionView'); + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickStructureTab(); + await umbracoUi.documentType.clickConfigureAsACollectionButton(); + await umbracoUi.documentType.clickTextButtonWithName(collectionDataTypeName); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.collection.id).toEqual(collectionDataTypeId); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(collectionDataTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts new file mode 100644 index 000000000000..1390bbdc4f65 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentType/DocumentTypeTemplatesTab.spec.ts @@ -0,0 +1,78 @@ +import {ConstantHelper, test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const documentTypeName = 'TestDocumentType'; +const templateName = 'TestTemplate'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoUi.goToBackOffice(); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add an allowed template to a document type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.documentType.createDefaultDocumentType(documentTypeName); + await umbracoApi.template.ensureNameNotExists(templateName); + const templateId = await umbracoApi.template.createDefaultTemplate(templateName); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickDocumentTypeTemplatesTab(); + await umbracoUi.documentType.clickAddButton(); + await umbracoUi.documentType.clickLabelWithName(templateName); + await umbracoUi.documentType.clickChooseButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.allowedTemplates[0].id).toBe(templateId); + + // Clean + await umbracoApi.template.ensureNameNotExists(templateName); +}); + +test('can set an allowed template as default for document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.template.ensureNameNotExists(templateName); + const templateId = await umbracoApi.template.createDefaultTemplate(templateName); + await umbracoApi.documentType.createDocumentTypeWithAllowedTemplate(documentTypeName, templateId); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickDocumentTypeTemplatesTab(); + await umbracoUi.documentType.clickDefaultTemplateButton(); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.allowedTemplates[0].id).toBe(templateId); + expect(documentTypeData.defaultTemplate.id).toBe(templateId); +}); + +// When removing a template, the defaultTemplateId is set to "" which is not correct +test.skip('can remove an allowed template from a document type', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.template.ensureNameNotExists(templateName); + const templateId = await umbracoApi.template.createDefaultTemplate(templateName); + await umbracoApi.documentType.createDocumentTypeWithAllowedTemplate(documentTypeName, templateId); + await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings); + + // Act + await umbracoUi.documentType.goToDocumentType(documentTypeName); + await umbracoUi.documentType.clickDocumentTypeTemplatesTab(); + await umbracoUi.documentType.clickRemoveWithName(templateName, true); + await umbracoUi.documentType.clickSaveButton(); + + // Assert + await umbracoUi.documentType.isSuccessNotificationVisible(); + const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName); + expect(documentTypeData.allowedTemplates).toHaveLength(0); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts index 53d7efd18bd1..4586122d50e3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/Language/Language.spec.ts @@ -1,4 +1,4 @@ -import {test} from '@umbraco/playwright-testhelpers'; +import {test} from '@umbraco/playwright-testhelpers'; import {expect} from "@playwright/test"; const languageName = 'Arabic'; @@ -15,7 +15,7 @@ test.afterEach(async ({umbracoApi}) => { await umbracoApi.language.ensureNameNotExists(languageName); }); -test('can add language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { +test.skip('can add language', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoUi.language.goToSettingsTreeItem('Language'); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts new file mode 100644 index 000000000000..96d562206eba --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaType.spec.ts @@ -0,0 +1,94 @@ +import {expect} from "@playwright/test"; +import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +const mediaTypeName = 'TestMediaType'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.mediaType.goToSection(ConstantHelper.sections.settings); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeName); +}); + +test('can create a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Act + await umbracoUi.mediaType.clickActionsMenuForName('Media Types'); + await umbracoUi.mediaType.clickCreateButton(); + await umbracoUi.mediaType.clickNewMediaTypeButton(); + await umbracoUi.mediaType.enterMediaTypeName(mediaTypeName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeTruthy(); +}); + +test('can rename a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'NotAMediaTypeName'; + await umbracoApi.mediaType.ensureNameNotExists(wrongName); + await umbracoApi.mediaType.createDefaultMediaType(wrongName); + + // Act + await umbracoUi.mediaType.goToMediaType(wrongName); + await umbracoUi.mediaType.enterMediaTypeName(mediaTypeName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeTruthy(); +}); + +test('can update the alias for a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldAlias = AliasHelper.toAlias(mediaTypeName); + const updatedAlias = 'TestMediaAlias'; + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + const mediaTypeDataOld = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeDataOld.alias).toBe(oldAlias); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.enterAliasName(updatedAlias); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.alias).toBe(updatedAlias); +}); + +test('can add an icon for a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const bugIcon = 'icon-bug'; + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.updateIcon(bugIcon); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.icon).toBe(bugIcon); + await umbracoUi.mediaType.isTreeItemVisible(mediaTypeName, true); +}); + +test('can delete a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.clickRootFolderCaretButton(); + await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeName); + await umbracoUi.mediaType.clickDeleteButton(); + await umbracoUi.mediaType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeFalsy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts new file mode 100644 index 000000000000..d344210334aa --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeDesignTab.spec.ts @@ -0,0 +1,363 @@ +import {ConstantHelper, test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const mediaTypeName = 'TestMediaType'; +const dataTypeName = 'Upload File'; +const groupName = 'TestGroup'; +const tabName = 'TestTab'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.mediaType.goToSection(ConstantHelper.sections.settings); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeName); +}); + +test('can create a media type with a property', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickAddGroupButton(); + await umbracoUi.mediaType.addPropertyEditor(dataTypeName); + await umbracoUi.mediaType.enterGroupName(groupName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeTruthy(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + const dataType = await umbracoApi.dataType.getByName(dataTypeName); + // Checks if the correct property was added to the media type + expect(mediaTypeData.properties[0].dataType.id).toBe(dataType.id); +}); + +test('can update a property in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const newDataTypeName = 'Image Media Picker'; + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.updatePropertyEditor(newDataTypeName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + const dataType = await umbracoApi.dataType.getByName(newDataTypeName); + // Checks if the correct property was added to the media type + expect(mediaTypeData.properties[0].dataType.id).toBe(dataType.id); +}); + +test('can update group name in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const updatedGroupName = 'UpdatedGroupName'; + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id, groupName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.enterGroupName(updatedGroupName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.containers[0].name).toBe(updatedGroupName); +}); + +test('can delete a property in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id, groupName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.deletePropertyEditorWithName(dataTypeName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.properties.length).toBe(0); +}); + +test('can add a description to property in a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const descriptionText = 'Test Description'; + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id, groupName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickEditorSettingsButton(); + await umbracoUi.mediaType.enterPropertyEditorDescription(descriptionText); + await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + await expect(umbracoUi.mediaType.enterDescriptionTxt).toBeVisible(); + expect(umbracoUi.mediaType.doesDescriptionHaveValue(descriptionText)).toBeTruthy(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.properties[0].description).toBe(descriptionText); +}); + +test('can set a property as mandatory in a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickEditorSettingsButton(); + await umbracoUi.mediaType.clickMandatorySlider(); + await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.properties[0].validation.mandatory).toBeTruthy(); +}); + +test('can set up validation for a property in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const regex = '^[a-zA-Z0-9]*$'; + const regexMessage = 'Only letters and numbers are allowed'; + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickEditorSettingsButton(); + await umbracoUi.mediaType.selectValidationOption(''); + await umbracoUi.mediaType.enterRegEx(regex); + await umbracoUi.mediaType.enterRegExMessage(regexMessage); + await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.properties[0].validation.regEx).toBe(regex); + expect(mediaTypeData.properties[0].validation.regExMessage).toBe(regexMessage); +}); + +test('can set appearance as label on top for property in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickEditorSettingsButton(); + await umbracoUi.mediaType.clickLabelOnTopButton(); + await umbracoUi.mediaType.clickUpdateButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.properties[0].appearance.labelOnTop).toBeTruthy(); +}); + +test('can delete a group in a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id, groupName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.deleteGroup(groupName, true); + await umbracoUi.mediaType.clickConfirmToDeleteButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.containers.length).toBe(0); + expect(mediaTypeData.properties.length).toBe(0); +}); + +test('can create a media type with a property in a tab', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickAddTabButton(); + await umbracoUi.mediaType.enterTabName(tabName); + await umbracoUi.mediaType.addPropertyEditor(dataTypeName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + // Checks if the media type has the correct tab and property + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(await umbracoApi.mediaType.doesTabContainerCorrectPropertyEditor(mediaTypeName, tabName, mediaTypeData.properties[0].dataType.id)).toBeTruthy(); +}); + +test('can create a media type with multiple groups', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondDataTypeName = 'Image Media Picker'; + await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id, groupName); + const secondDataType = await umbracoApi.dataType.getByName(secondDataTypeName); + const secondGroupName = 'TesterGroup'; + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickAddGroupButton(); + await umbracoUi.mediaType.addPropertyEditor(secondDataTypeName, 1); + await umbracoUi.mediaType.enterGroupName(secondGroupName, 1); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeTruthy(); + expect(await umbracoApi.mediaType.doesGroupContainCorrectPropertyEditor(mediaTypeName, dataTypeName, dataTypeData.id, groupName)).toBeTruthy(); + expect(await umbracoApi.mediaType.doesGroupContainCorrectPropertyEditor(mediaTypeName, secondDataTypeName, secondDataType.id, secondGroupName)).toBeTruthy(); +}); + +test('can create a media type with multiple tabs', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondDataTypeName = 'Image Media Picker'; + const secondGroupName = 'TesterGroup'; + const secondTabName = 'SecondTab'; + await umbracoApi.mediaType.createMediaTypeWithPropertyEditorInTab(mediaTypeName, dataTypeName, dataTypeData.id, tabName, groupName); + const secondDataType = await umbracoApi.dataType.getByName(secondDataTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickAddTabButton(); + await umbracoUi.mediaType.enterTabName(secondTabName); + await umbracoUi.mediaType.clickAddGroupButton(); + await umbracoUi.mediaType.enterGroupName(secondGroupName); + await umbracoUi.mediaType.addPropertyEditor(secondDataTypeName, 1); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeTruthy(); + expect(await umbracoApi.mediaType.doesTabContainCorrectPropertyEditorInGroup(mediaTypeName, dataTypeName, dataTypeData.id, tabName, groupName)).toBeTruthy(); + expect(await umbracoApi.mediaType.doesTabContainCorrectPropertyEditorInGroup(mediaTypeName, secondDataTypeName, secondDataType.id, secondTabName, secondGroupName)).toBeTruthy(); +}); + +test('can delete a tab from a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + await umbracoApi.mediaType.createMediaTypeWithPropertyEditorInTab(mediaTypeName, dataTypeName, dataTypeData.id, tabName, groupName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickRemoveTabWithName(tabName); + await umbracoUi.mediaType.clickConfirmToDeleteButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeName)).toBeTruthy(); +}); + +// TODO: Currently there is no composition button, which makes it impossible to test +test.skip('can create a media type with a composition', async ({umbracoApi, umbracoUi}) => { + // Arrange + const compositionMediaTypeName = 'CompositionMediaType'; + await umbracoApi.mediaType.ensureNameNotExists(compositionMediaTypeName); + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const compositionMediaTypeId = await umbracoApi.mediaType.createMediaTypeWithPropertyEditor(compositionMediaTypeName, dataTypeName, dataTypeData.id, groupName); + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickCompositionsButton(); + await umbracoUi.mediaType.clickButtonWithName(compositionMediaTypeName); + await umbracoUi.mediaType.clickSubmitButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(umbracoUi.mediaType.doesGroupHaveValue(groupName)).toBeTruthy(); + // Checks if the composition in the media type is correct + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.compositions[0].mediaType.id).toBe(compositionMediaTypeId); + + // Clean + await umbracoApi.mediaType.ensureNameNotExists(compositionMediaTypeName); +}); + +test('can reorder groups in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondGroupName = 'SecondGroup'; + await umbracoApi.mediaType.createMediaTypeWithTwoGroups(mediaTypeName, dataTypeName, dataTypeData.id, groupName, secondGroupName); + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.clickReorderButton(); + const groupValues = await umbracoUi.mediaType.reorderTwoGroups(); + const firstGroupValue = groupValues.firstGroupValue; + const secondGroupValue = groupValues.secondGroupValue; + await umbracoUi.mediaType.clickIAmDoneReorderingButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + // Since we swapped sorting order, the firstGroupValue should have sortOrder 1 and the secondGroupValue should have sortOrder 0 + expect(await umbracoApi.mediaType.doesMediaTypeGroupNameContainCorrectSortOrder(mediaTypeName, secondGroupValue, 0)).toBeTruthy(); + expect(await umbracoApi.mediaType.doesMediaTypeGroupNameContainCorrectSortOrder(mediaTypeName, firstGroupValue, 1)).toBeTruthy(); +}); + +// TODO: Unskip when it works. Sometimes the properties are not dragged correctly. +test.skip('can reorder properties in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const dataTypeNameTwo = "Upload Second File"; + await umbracoApi.mediaType.createMediaTypeWithTwoPropertyEditors(mediaTypeName, dataTypeName, dataTypeData.id, dataTypeNameTwo, dataTypeData.id); + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.clickReorderButton(); + // Drag and Drop + const dragFromLocator = umbracoUi.mediaType.getTextLocatorWithName(dataTypeNameTwo); + const dragToLocator = umbracoUi.mediaType.getTextLocatorWithName(dataTypeName); + await umbracoUi.mediaType.dragAndDrop(dragFromLocator, dragToLocator, -10, 0, 5); + await umbracoUi.waitForTimeout(200); + await umbracoUi.mediaType.clickIAmDoneReorderingButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.properties[0].name).toBe(dataTypeNameTwo); +}); + +// TODO: Unskip when the frontend does not give the secondTab -1 as the sortOrder +test.skip('can reorder tabs in a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + const secondTabName = 'SecondTab'; + await umbracoApi.mediaType.createMediaTypeWithTwoTabs(mediaTypeName, dataTypeName, dataTypeData.id, tabName, secondTabName); + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + + // Act + const dragToLocator = umbracoUi.mediaType.getTabLocatorWithName(tabName); + const dragFromLocator = umbracoUi.mediaType.getTabLocatorWithName(secondTabName); + await umbracoUi.mediaType.clickReorderButton(); + await umbracoUi.mediaType.dragAndDrop(dragFromLocator, dragToLocator, 0, 0, 10); + await umbracoUi.mediaType.clickIAmDoneReorderingButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesMediaTypeTabNameContainCorrectSortOrder(mediaTypeName, secondTabName, 0)).toBeTruthy(); + expect(await umbracoApi.mediaType.doesMediaTypeTabNameContainCorrectSortOrder(mediaTypeName, tabName, 1)).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts new file mode 100644 index 000000000000..7605e1eb7822 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeFolder.spec.ts @@ -0,0 +1,109 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const mediaTypeFolderName = 'TestMediaTypeFolder'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeFolderName); + await umbracoUi.goToBackOffice(); + await umbracoUi.mediaType.goToSection(ConstantHelper.sections.settings); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeFolderName); +}); + +test('can create a empty media type folder', async ({umbracoApi, umbracoUi}) => { + // Act + await umbracoUi.mediaType.clickActionsMenuForName('Media Types'); + await umbracoUi.mediaType.createFolder(mediaTypeFolderName); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const folder = await umbracoApi.mediaType.getByName(mediaTypeFolderName); + expect(folder.name).toBe(mediaTypeFolderName); + // Checks if the folder is in the root + await umbracoUi.mediaType.clickRootFolderCaretButton(); + await umbracoUi.mediaType.isTreeItemVisible(mediaTypeFolderName, true); +}); + +test('can delete a media type folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.mediaType.createFolder(mediaTypeFolderName); + + // Act + await umbracoUi.mediaType.clickRootFolderCaretButton(); + await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeFolderName); + await umbracoUi.mediaType.deleteFolder(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesNameExist(mediaTypeFolderName)).toBeFalsy(); +}); + +test('can rename a media type folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldFolderName = 'OldName'; + await umbracoApi.mediaType.createFolder(oldFolderName); + + // Act + await umbracoUi.mediaType.clickRootFolderCaretButton(); + await umbracoUi.mediaType.clickActionsMenuForName(oldFolderName); + await umbracoUi.mediaType.clickRenameFolderButton(); + await umbracoUi.mediaType.enterFolderName(mediaTypeFolderName); + await umbracoUi.mediaType.clickUpdateFolderButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const folder = await umbracoApi.mediaType.getByName(mediaTypeFolderName); + expect(folder.name).toBe(mediaTypeFolderName); +}); + +test('can create a media type folder in a folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const childFolderName = 'ChildFolder'; + await umbracoApi.mediaType.ensureNameNotExists(childFolderName); + const parentFolderId = await umbracoApi.mediaType.createFolder(mediaTypeFolderName); + + // Act + await umbracoUi.mediaType.clickRootFolderCaretButton(); + await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeFolderName); + await umbracoUi.mediaType.createFolder(childFolderName); + + // Assert + await umbracoUi.mediaType.clickCaretButtonForName(mediaTypeFolderName); + await umbracoUi.mediaType.isTreeItemVisible(childFolderName, true); + const parentFolderChildren = await umbracoApi.mediaType.getChildren(parentFolderId); + expect(parentFolderChildren[0].name).toBe(childFolderName); + + // Clean + await umbracoApi.mediaType.ensureNameNotExists(childFolderName); +}); + +test('can create a media type folder in a folder in a folder', async ({umbracoApi, umbracoUi}) => { + // Arrange + const grandparentFolderName = 'GrandparentFolder'; + const childFolderName = 'ChildFolder'; + await umbracoApi.mediaType.ensureNameNotExists(childFolderName); + await umbracoApi.mediaType.ensureNameNotExists(grandparentFolderName); + const grandParentFolderId = await umbracoApi.mediaType.createFolder(grandparentFolderName); + const parentFolderId = await umbracoApi.mediaType.createFolder(mediaTypeFolderName, grandParentFolderId); + + // Act + await umbracoUi.mediaType.clickRootFolderCaretButton(); + await umbracoUi.mediaType.clickCaretButtonForName(grandparentFolderName); + await umbracoUi.mediaType.clickActionsMenuForName(mediaTypeFolderName); + await umbracoUi.mediaType.createFolder(childFolderName); + + // Assert + await umbracoUi.mediaType.clickCaretButtonForName(mediaTypeFolderName); + await umbracoUi.mediaType.isTreeItemVisible(childFolderName, true); + const grandParentFolderChildren = await umbracoApi.mediaType.getChildren(grandParentFolderId); + expect(grandParentFolderChildren[0].name).toBe(mediaTypeFolderName); + const parentFolderChildren = await umbracoApi.mediaType.getChildren(parentFolderId); + expect(parentFolderChildren[0].name).toBe(childFolderName); + + // Clean + await umbracoApi.mediaType.ensureNameNotExists(childFolderName); + await umbracoApi.mediaType.ensureNameNotExists(grandparentFolderName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts new file mode 100644 index 000000000000..fda909044323 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/MediaType/MediaTypeStructureTab.spec.ts @@ -0,0 +1,118 @@ +import {ConstantHelper, test} from "@umbraco/playwright-testhelpers"; +import {expect} from "@playwright/test"; + +const mediaTypeName = 'TestMediaType'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.mediaType.goToSection(ConstantHelper.sections.settings); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.mediaType.ensureNameNotExists(mediaTypeName); +}); + +test('can create a media type with allow at root enabled', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickStructureTab(); + await umbracoUi.mediaType.clickAllowAtRootButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.allowedAsRoot).toBeTruthy(); +}); + +test('can create a media type with an allowed child node type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickStructureTab(); + await umbracoUi.mediaType.clickChooseButton(); + await umbracoUi.mediaType.clickButtonWithName(mediaTypeName); + await umbracoUi.mediaType.clickAllowedChildNodesButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.allowedMediaTypes[0].mediaType.id).toBe(mediaTypeData.id); +}); + +test('can create a media type with multiple allowed child nodes types', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeId = await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + const secondMediaTypeName = 'SecondMediaType'; + await umbracoApi.mediaType.ensureNameNotExists(secondMediaTypeName); + const secondMediaTypeId = await umbracoApi.mediaType.createDefaultMediaType(secondMediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickStructureTab(); + await umbracoUi.mediaType.clickChooseButton(); + await umbracoUi.mediaType.clickButtonWithName(mediaTypeName); + await umbracoUi.mediaType.clickButtonWithName(secondMediaTypeName); + await umbracoUi.mediaType.clickAllowedChildNodesButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + expect(await umbracoApi.mediaType.doesMediaTypeContainAllowedChildNodeIds(mediaTypeName, [mediaTypeId, secondMediaTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.mediaType.ensureNameNotExists(secondMediaTypeName); +}); + +test('can delete an allowed child note from a media type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const childNodeName = 'MediaChildNode'; + await umbracoApi.mediaType.ensureNameNotExists(childNodeName); + const childNodeId = await umbracoApi.mediaType.createDefaultMediaType(childNodeName); + await umbracoApi.mediaType.createMediaTypeWithAllowedChildNode(mediaTypeName, childNodeId); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickStructureTab(); + await umbracoUi.mediaType.clickRemoveButtonForName(childNodeName); + await umbracoUi.mediaType.clickConfirmRemoveButton(); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(childNodeName); + expect(mediaTypeData.allowedMediaTypes.length).toBe(0); + + // Clean + await umbracoApi.mediaType.ensureNameNotExists(childNodeName); +}); + +test('can configure a collection for a media type', async ({umbracoApi, umbracoUi}) => { + // Arrange + const collectionDataTypeName = 'TestCollection'; + await umbracoApi.dataType.ensureNameNotExists(collectionDataTypeName); + const collectionDataTypeId = await umbracoApi.dataType.create(collectionDataTypeName, 'Umbraco.ListView', [], null, 'Umb.PropertyEditorUi.CollectionView'); + await umbracoApi.mediaType.createDefaultMediaType(mediaTypeName); + + // Act + await umbracoUi.mediaType.goToMediaType(mediaTypeName); + await umbracoUi.mediaType.clickStructureTab(); + await umbracoUi.mediaType.clickConfigureAsACollectionButton(); + await umbracoUi.mediaType.clickTextButtonWithName(collectionDataTypeName); + await umbracoUi.mediaType.clickSaveButton(); + + // Assert + await umbracoUi.mediaType.isSuccessNotificationVisible(); + const mediaTypeData = await umbracoApi.mediaType.getByName(mediaTypeName); + expect(mediaTypeData.collection.id).toEqual(collectionDataTypeId); + + // Clean + await umbracoApi.dataType.ensureNameNotExists(collectionDataTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts index 6349efbd99c8..93c8c20c43fb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/auth.setup.ts @@ -10,6 +10,7 @@ setup('authenticate', async ({page}) => { await umbracoUi.login.enterEmail(process.env.UMBRACO_USER_LOGIN); await umbracoUi.login.enterPassword(process.env.UMBRACO_USER_PASSWORD); await umbracoUi.login.clickLoginButton(); + await page.waitForTimeout(5000); await umbracoUi.login.goToSection(ConstantHelper.sections.settings); await umbracoUi.page.context().storageState({path: STORAGE_STATE}); }); 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 000000000000..a86900e76672 --- /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.Core/Services/MediaTypeEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs index a2ea9ad916d5..8b1b7cfc7866 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaTypeEditingServiceTests.Update.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -113,4 +114,22 @@ public async Task Can_Edit_Properties() Assert.AreEqual(2, mediaType.NoGroupPropertyTypes.Count()); } + + [TestCase(Constants.Conventions.MediaTypes.File)] + [TestCase(Constants.Conventions.MediaTypes.Folder)] + [TestCase(Constants.Conventions.MediaTypes.Image)] + public async Task Cannot_Change_Alias_Of_System_Media_Type(string mediaTypeAlias) + { + var mediaType = MediaTypeService.Get(mediaTypeAlias); + Assert.IsNotNull(mediaType); + + var updateModel = MediaTypeUpdateModel(mediaTypeAlias, $"{mediaTypeAlias}_updated"); + var result = await MediaTypeEditingService.UpdateAsync(mediaType, updateModel, Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.NotAllowed, result.Status); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index 084efb2371bf..cd138283ebf1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -35,6 +36,7 @@ public class AdvancedMigrationTests : UmbracoIntegrationTest private IMigrationBuilder MigrationBuilder => GetRequiredService(); private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService(); private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); + private IServiceScopeFactory ServiceScopeFactory => GetRequiredService(); private DistributedCache DistributedCache => GetRequiredService(); private IMigrationPlanExecutor MigrationPlanExecutor => new MigrationPlanExecutor( CoreScopeProvider, @@ -44,7 +46,8 @@ public class AdvancedMigrationTests : UmbracoIntegrationTest UmbracoDatabaseFactory, PublishedSnapshotService, DistributedCache, - Mock.Of()); + Mock.Of(), + ServiceScopeFactory); [Test] public void CreateTableOfTDto() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaTypeServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaTypeServiceTests.cs index 6c779a3c6e83..8fa1a658c13e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaTypeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaTypeServiceTests.cs @@ -1,10 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -222,6 +220,34 @@ public void Can_Copy_MediaType_To_New_Parent_By_Performing_Clone() originalMediaType.PropertyGroups.First(x => x.Name.StartsWith("Media")).Id); } + [TestCase(Constants.Conventions.MediaTypes.File)] + [TestCase(Constants.Conventions.MediaTypes.Folder)] + [TestCase(Constants.Conventions.MediaTypes.Image)] + public void Cannot_Delete_System_Media_Type(string mediaTypeAlias) + { + // Arrange + // Act + var mediaType = MediaTypeService.Get(mediaTypeAlias); + Assert.IsNotNull(mediaType); + + // Assert + Assert.Throws(() => MediaTypeService.Delete(mediaType)); + } + + [TestCase(Constants.Conventions.MediaTypes.File)] + [TestCase(Constants.Conventions.MediaTypes.Folder)] + [TestCase(Constants.Conventions.MediaTypes.Image)] + public void Cannot_Change_Alias_Of_System_Media_Type(string mediaTypeAlias) + { + // Arrange + // Act + var mediaType = MediaTypeService.Get(mediaTypeAlias); + Assert.IsNotNull(mediaType); + + // Assert + Assert.Throws(() => mediaType.Alias += "_updated"); + } + public class ContentNotificationHandler : INotificationHandler { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 2ffdc7c97658..b7ab21b0628d 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 + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestServiceTests.cs index 2c30dc6214cd..9bba7839e8f9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/PackageManifestServiceTests.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Options; -using Moq; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Infrastructure.Manifest; +using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Manifest; @@ -32,7 +32,10 @@ public void SetUp() NoAppCache.Instance, new IsolatedCaches(type => NoAppCache.Instance)); - _service = new PackageManifestService(new[] { _readerMock.Object }, appCaches, new OptionsWrapper(new PackageManifestSettings())); + _service = new PackageManifestService( + new[] { _readerMock.Object }, + appCaches, + new TestOptionsMonitor(new RuntimeSettings { Mode = RuntimeMode.Production })); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index fd279b5b3a88..b70d03622796 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; @@ -76,7 +77,7 @@ public void CanExecute() loggerFactory, migrationBuilder, databaseFactory, - Mock.Of(), distributedCache, Mock.Of()); + Mock.Of(), distributedCache, Mock.Of(), Mock.Of()); var plan = new MigrationPlan("default") .From(string.Empty)